Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -70,126 +70,377 @@ def _sym(grid,axis):
|
|
| 70 |
s[y,:]=(grid[y-r:y,:]==grid[y+1:y+r+1,:][::-1,:]).mean()
|
| 71 |
return s
|
| 72 |
|
| 73 |
-
# ββ Im
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
return
|
| 110 |
-
|
| 111 |
-
def
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
return
|
| 138 |
-
|
| 139 |
-
def
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
(
|
| 155 |
-
|
| 156 |
-
(
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
(
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
if not diffs: return None
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
pool=
|
| 191 |
return pool[np.random.randint(len(pool))]
|
| 192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
# ββ Feature extractor βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 194 |
|
| 195 |
def extract_features(grid,num_colours=10):
|
|
@@ -337,17 +588,12 @@ class TinyAgent:
|
|
| 337 |
feat=extract_features(grid).to(self.device)
|
| 338 |
cur_state=str(state) if state else None
|
| 339 |
|
| 340 |
-
# ββ Im
|
| 341 |
-
|
| 342 |
-
best_name,best_cand,best_conf=(candidates[0] if candidates
|
| 343 |
-
else ('none',grid,0.0))
|
| 344 |
|
| 345 |
# Candidate proximity bonus
|
| 346 |
-
if
|
| 347 |
-
|
| 348 |
-
candidates,
|
| 349 |
-
key=lambda c:(grid!=c[1]).mean() if grid.shape==c[1].shape else 1.0)
|
| 350 |
-
curr_dist=(grid!=nn_cand).mean() if grid.shape==nn_cand.shape else 1.0
|
| 351 |
if curr_dist==0.0:
|
| 352 |
cand_bonus=self.candidate_win_reward
|
| 353 |
elif curr_dist<self.prev_candidate_dist:
|
|
@@ -356,7 +602,7 @@ class TinyAgent:
|
|
| 356 |
cand_bonus=0.0
|
| 357 |
self.prev_candidate_dist=curr_dist
|
| 358 |
else:
|
| 359 |
-
cand_bonus=0.0
|
| 360 |
|
| 361 |
# Store shaped experience
|
| 362 |
if self.prev_feat is not None:
|
|
@@ -386,21 +632,23 @@ class TinyAgent:
|
|
| 386 |
if self.step_count%10==0 and len(self.buf)>=16:
|
| 387 |
self._train()
|
| 388 |
|
| 389 |
-
# ββ Im β Re bridge:
|
| 390 |
analytic_action=None; analytic_meta={}
|
| 391 |
-
if
|
| 392 |
-
diffs=pixel_diff(grid,
|
| 393 |
if diffs:
|
| 394 |
-
cell=most_urgent_diff(grid,
|
| 395 |
if cell is not None:
|
| 396 |
r,c,tgt_color=cell
|
| 397 |
H,W=grid.shape
|
| 398 |
gy=min(63,max(0,int(r*64/H+32/H)))
|
| 399 |
gx=min(63,max(0,int(c*64/W+32/W)))
|
| 400 |
analytic_action=6
|
|
|
|
| 401 |
analytic_meta={'x':gx,'y':gy,'cell':(r,c,tgt_color),
|
| 402 |
-
'hypothesis':
|
| 403 |
-
'n_diffs':len(diffs),
|
|
|
|
| 404 |
|
| 405 |
# ββ CNN fallback ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 406 |
with torch.no_grad():
|
|
|
|
| 70 |
s[y,:]=(grid[y-r:y,:]==grid[y+1:y+r+1,:][::-1,:]).mean()
|
| 71 |
return s
|
| 72 |
|
| 73 |
+
# ββ Re/Im board reader (inlined from arc_solver.py) βββββββββββββββββββββββββ
|
| 74 |
+
|
| 75 |
+
"""
|
| 76 |
+
arc_solver.py β Re/Im board reader for ARC-AGI-3
|
| 77 |
+
=================================================
|
| 78 |
+
|
| 79 |
+
The board IS a complex object M = Re(M) + iΒ·Im(M).
|
| 80 |
+
|
| 81 |
+
Re(M) = multiplicative structure: what colors exist, how many pixels,
|
| 82 |
+
where objects are, their bounding boxes, centroids, density.
|
| 83 |
+
|
| 84 |
+
Im(M) = additive structure: symmetry axes, boundary contour, gradient
|
| 85 |
+
flow direction, winding/curl β the "where pointed" information.
|
| 86 |
+
|
| 87 |
+
log separates them. iΒ· swaps them.
|
| 88 |
+
|
| 89 |
+
The answer is what you get by applying iΒ· to the board:
|
| 90 |
+
"Read the Im side of the board β that tells you what the Re side
|
| 91 |
+
of the answer must look like β find the cells that need to change."
|
| 92 |
+
|
| 93 |
+
Pipeline:
|
| 94 |
+
read_board(grid) β (re, im, answer_grid, confidence, reasoning)
|
| 95 |
+
pixel_diff(current, answer) β list of (r, c, target_color)
|
| 96 |
+
most_urgent_diff(current, answer) β single most important cell (Re coords)
|
| 97 |
+
try_analytic_action(frame, available) β (action_id, data, name, confidence)
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
import numpy as np
|
| 101 |
+
from typing import Optional, Tuple, List, Dict
|
| 102 |
+
|
| 103 |
+
# ββ Primitives ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 104 |
+
|
| 105 |
+
def _sobel(f):
|
| 106 |
+
p = np.pad(f, 1, mode='edge')
|
| 107 |
+
gx = (-p[:-2,:-2]-2*p[1:-1,:-2]-p[2:,:-2]+p[:-2,2:]+2*p[1:-1,2:]+p[2:,2:])/8
|
| 108 |
+
gy = (-p[:-2,:-2]-2*p[:-2,1:-1]-p[:-2,2:]+p[2:,:-2]+2*p[2:,1:-1]+p[2:,2:])/8
|
| 109 |
+
return gx, gy
|
| 110 |
+
|
| 111 |
+
def _boundary(grid):
|
| 112 |
+
"""Color-change boundary (Im side)."""
|
| 113 |
+
p = np.pad(grid, 1, mode='edge')
|
| 114 |
+
return ((p[1:-1,1:-1]!=p[:-2,1:-1])|(p[1:-1,1:-1]!=p[2:,1:-1])|
|
| 115 |
+
(p[1:-1,1:-1]!=p[1:-1,:-2])|(p[1:-1,1:-1]!=p[1:-1,2:])).astype(np.float32)
|
| 116 |
+
|
| 117 |
+
def _perimeter(grid):
|
| 118 |
+
"""
|
| 119 |
+
Object perimeter β cells at the edge of nonzero regions.
|
| 120 |
+
Handles solid blocks (all same color, no color-change boundary).
|
| 121 |
+
This is the Cauchy contour for the Re side.
|
| 122 |
+
"""
|
| 123 |
+
H, W = grid.shape
|
| 124 |
+
p = np.zeros((H,W), dtype=np.float32)
|
| 125 |
+
mask = grid > 0
|
| 126 |
+
if not mask.any(): return p
|
| 127 |
+
padded = np.pad(mask.astype(int), 1, constant_values=0)
|
| 128 |
+
for dy,dx in [(-1,0),(1,0),(0,-1),(0,1)]:
|
| 129 |
+
shifted = padded[1+dy:H+1+dy, 1+dx:W+1+dx]
|
| 130 |
+
p[mask & (shifted==0)] = 1
|
| 131 |
+
# Solid block: no cell has a zero neighbor β use outer ring
|
| 132 |
+
if p.sum() == 0:
|
| 133 |
+
p[0,:] = mask[0,:] .astype(float)
|
| 134 |
+
p[-1,:] = mask[-1,:].astype(float)
|
| 135 |
+
p[:,0] = mask[:,0] .astype(float)
|
| 136 |
+
p[:,-1] = mask[:,-1].astype(float)
|
| 137 |
+
return p
|
| 138 |
+
|
| 139 |
+
def _cc(mask):
|
| 140 |
+
labels = np.zeros_like(mask, dtype=np.int32); cur = 0; H,W = mask.shape
|
| 141 |
+
for r in range(H):
|
| 142 |
+
for c in range(W):
|
| 143 |
+
if mask[r,c] and labels[r,c]==0:
|
| 144 |
+
cur+=1; q=[(r,c)]; labels[r,c]=cur
|
| 145 |
+
while q:
|
| 146 |
+
y,x=q.pop()
|
| 147 |
+
for dy,dx in [(-1,0),(1,0),(0,-1),(0,1)]:
|
| 148 |
+
ny,nx=y+dy,x+dx
|
| 149 |
+
if 0<=ny<H and 0<=nx<W and mask[ny,nx] and labels[ny,nx]==0:
|
| 150 |
+
labels[ny,nx]=cur; q.append((ny,nx))
|
| 151 |
+
return labels
|
| 152 |
+
|
| 153 |
+
def _h_sym_at(grid, x):
|
| 154 |
+
r = min(x, grid.shape[1]-1-x)
|
| 155 |
+
if r <= 0: return 0.0
|
| 156 |
+
return float((grid[:, x-r:x] == grid[:, x+1:x+r+1][:,::-1]).mean())
|
| 157 |
+
|
| 158 |
+
def _v_sym_at(grid, y):
|
| 159 |
+
r = min(y, grid.shape[0]-1-y)
|
| 160 |
+
if r <= 0: return 0.0
|
| 161 |
+
return float((grid[y-r:y, :] == grid[y+1:y+r+1, :][::-1, :]).mean())
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
# ββ Re/Im board reader ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 165 |
+
|
| 166 |
+
def read_board(grid: np.ndarray):
|
| 167 |
+
"""
|
| 168 |
+
Read the board as a complex object.
|
| 169 |
+
|
| 170 |
+
Returns
|
| 171 |
+
-------
|
| 172 |
+
re : dict β Re-side structure per color
|
| 173 |
+
im : dict β Im-side structure (symmetry, boundary, flow, curl)
|
| 174 |
+
answer : np.ndarray β derived answer grid (or None)
|
| 175 |
+
confidence : float 0-1
|
| 176 |
+
reasoning : list of strings, one per signal that fired
|
| 177 |
+
"""
|
| 178 |
+
H, W = grid.shape
|
| 179 |
+
gx, gy = _sobel(grid.astype(np.float32) / 9)
|
| 180 |
+
bound = _boundary(grid)
|
| 181 |
+
|
| 182 |
+
# ββ Re side βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 183 |
+
colors = [c for c in range(1, 10) if (grid == c).any()]
|
| 184 |
+
re = {}
|
| 185 |
+
for color in colors:
|
| 186 |
+
mask = (grid == color)
|
| 187 |
+
ys, xs = np.where(mask)
|
| 188 |
+
labels = _cc(mask)
|
| 189 |
+
re[color] = {
|
| 190 |
+
'count': int(mask.sum()),
|
| 191 |
+
'objects': int(labels.max()),
|
| 192 |
+
'centroid': (float(ys.mean()), float(xs.mean())),
|
| 193 |
+
'bbox': (int(ys.min()),int(xs.min()),int(ys.max()),int(xs.max())),
|
| 194 |
+
}
|
| 195 |
+
total_px = int((grid > 0).sum())
|
| 196 |
+
|
| 197 |
+
# ββ Im side βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 198 |
+
# Symmetry axes
|
| 199 |
+
h_scores = [(x, _h_sym_at(grid, x)) for x in range(1, W-1)]
|
| 200 |
+
v_scores = [(y, _v_sym_at(grid, y)) for y in range(1, H-1)]
|
| 201 |
+
best_h = max(h_scores, key=lambda x:x[1]) if h_scores else (W//2, 0.0)
|
| 202 |
+
best_v = max(v_scores, key=lambda x:x[1]) if v_scores else (H//2, 0.0)
|
| 203 |
+
|
| 204 |
+
# Boundary (Cauchy contour)
|
| 205 |
+
b_px = int(bound.sum())
|
| 206 |
+
b_ratio = b_px / max(total_px, 1)
|
| 207 |
+
|
| 208 |
+
# Gradient field
|
| 209 |
+
gx_mag = float(np.abs(gx).mean())
|
| 210 |
+
gy_mag = float(np.abs(gy).mean())
|
| 211 |
+
|
| 212 |
+
# Suspended pixels (Re gives count, Im gives direction)
|
| 213 |
+
suspended = sum(
|
| 214 |
+
1 for c in range(W)
|
| 215 |
+
for r in np.where(grid[:, c] > 0)[0]
|
| 216 |
+
if r < H-1 and grid[r+1, c] == 0
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
# Winding / curl
|
| 220 |
+
p_gx = np.pad(gx,1,mode='edge'); p_gy = np.pad(gy,1,mode='edge')
|
| 221 |
+
curl = ((p_gy[1:-1,2:]-p_gy[1:-1,:-2])/2 -
|
| 222 |
+
(p_gx[2:,1:-1]-p_gx[:-2,1:-1])/2)
|
| 223 |
+
curl_max = float(np.abs(curl).max())
|
| 224 |
+
|
| 225 |
+
im = {
|
| 226 |
+
'best_h': best_h, # (x, score)
|
| 227 |
+
'best_v': best_v, # (y, score)
|
| 228 |
+
'b_ratio': b_ratio,
|
| 229 |
+
'b_px': b_px,
|
| 230 |
+
'gx_mag': gx_mag,
|
| 231 |
+
'gy_mag': gy_mag,
|
| 232 |
+
'suspended': suspended,
|
| 233 |
+
'curl_max': curl_max,
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
# ββ iΒ· column swap: apply Im β derive answer (Re coords) ββββββββββββββ
|
| 237 |
+
reasoning = []
|
| 238 |
+
answer = None
|
| 239 |
+
confidence = 0.0
|
| 240 |
+
|
| 241 |
+
# ββ Signal 1: One dominant color fills everything β check boundary first
|
| 242 |
+
# Re: single color, high density β Im: boundary ratio is the key signal
|
| 243 |
+
if len(colors) == 1 and total_px > 0:
|
| 244 |
+
single_color = colors[0]
|
| 245 |
+
filled_ratio = total_px / (H * W)
|
| 246 |
+
# Check if grid is already hollow (has interior zeros)
|
| 247 |
+
interior = (grid == 0) & (_perimeter(grid) == 0)
|
| 248 |
+
already_hollow = interior.any()
|
| 249 |
+
if filled_ratio > 0.5 and not already_hollow:
|
| 250 |
+
# Solid block β Im says extract the perimeter
|
| 251 |
+
perim = _perimeter(grid)
|
| 252 |
+
answer = np.zeros_like(grid)
|
| 253 |
+
answer[perim > 0] = grid[perim > 0]
|
| 254 |
+
perim_ratio = float(perim.sum()) / max(total_px, 1)
|
| 255 |
+
confidence = filled_ratio * 0.88 # solid fill is the signal
|
| 256 |
+
reasoning.append(
|
| 257 |
+
f"Re: 1 color={single_color}, fill={filled_ratio:.2f} "
|
| 258 |
+
f"Im: perimeter={int(perim.sum())}px (ratio={perim_ratio:.2f}) "
|
| 259 |
+
f"iΒ·: Cauchy perimeter IS the answer")
|
| 260 |
+
|
| 261 |
+
# ββ Signal 2: Strong H-symmetry + asymmetric Re mass
|
| 262 |
+
if confidence < 0.75:
|
| 263 |
+
hx, hs = best_h
|
| 264 |
+
if hs > 0.65:
|
| 265 |
+
left_px = int((grid[:, :hx] > 0).sum())
|
| 266 |
+
right_px = int((grid[:, hx:] > 0).sum())
|
| 267 |
+
asymmetry = abs(left_px - right_px) / max(left_px + right_px, 1)
|
| 268 |
+
if asymmetry > 0.25:
|
| 269 |
+
answer = grid.copy()
|
| 270 |
+
if left_px > right_px:
|
| 271 |
+
# Mirror left β right (fill empty right side)
|
| 272 |
+
for c in range(hx):
|
| 273 |
+
mir = W - 1 - c
|
| 274 |
+
if 0 <= mir < W:
|
| 275 |
+
mask = answer[:, mir] == 0
|
| 276 |
+
answer[mask, mir] = grid[mask, c]
|
| 277 |
+
else:
|
| 278 |
+
# Mirror right β left
|
| 279 |
+
for c in range(hx+1, W):
|
| 280 |
+
mir = W - 1 - c
|
| 281 |
+
if 0 <= mir < W:
|
| 282 |
+
mask = answer[:, mir] == 0
|
| 283 |
+
answer[mask, mir] = grid[mask, c]
|
| 284 |
+
confidence = hs * asymmetry * 0.95
|
| 285 |
+
reasoning.append(
|
| 286 |
+
f"Im H-sym={hs:.2f} at x={hx} "
|
| 287 |
+
f"Re left={left_px} right={right_px} (asym={asymmetry:.2f}) "
|
| 288 |
+
f"iΒ·: complete H mirror")
|
| 289 |
+
|
| 290 |
+
# ββ Signal 3: Strong V-symmetry + asymmetric top/bottom
|
| 291 |
+
if confidence < 0.55:
|
| 292 |
+
vy, vs = best_v
|
| 293 |
+
if vs > 0.65:
|
| 294 |
+
top_px = int((grid[:vy, :] > 0).sum())
|
| 295 |
+
bot_px = int((grid[vy:, :] > 0).sum())
|
| 296 |
+
asymmetry = abs(top_px - bot_px) / max(top_px + bot_px, 1)
|
| 297 |
+
if asymmetry > 0.25:
|
| 298 |
+
answer = grid.copy()
|
| 299 |
+
if top_px > bot_px:
|
| 300 |
+
for r in range(vy):
|
| 301 |
+
mir = H - 1 - r
|
| 302 |
+
if 0 <= mir < H:
|
| 303 |
+
mask = answer[mir, :] == 0
|
| 304 |
+
answer[mir, mask] = grid[r, mask]
|
| 305 |
+
else:
|
| 306 |
+
for r in range(vy+1, H):
|
| 307 |
+
mir = H - 1 - r
|
| 308 |
+
if 0 <= mir < H:
|
| 309 |
+
mask = answer[mir, :] == 0
|
| 310 |
+
answer[mir, mask] = grid[r, mask]
|
| 311 |
+
confidence = vs * asymmetry * 0.90
|
| 312 |
+
reasoning.append(
|
| 313 |
+
f"Im V-sym={vs:.2f} at y={vy} "
|
| 314 |
+
f"Re top={top_px} bot={bot_px} (asym={asymmetry:.2f}) "
|
| 315 |
+
f"iΒ·: complete V mirror")
|
| 316 |
+
|
| 317 |
+
# ββ Signal 4: Hollow Re structure + unfilled interior
|
| 318 |
+
if confidence < 0.5:
|
| 319 |
+
interior = (grid == 0) & (bound == 0)
|
| 320 |
+
if interior.any() and total_px > 0:
|
| 321 |
+
dom = np.argmax(np.bincount(grid[grid>0].flatten(),minlength=10)[1:])+1
|
| 322 |
+
answer = grid.copy()
|
| 323 |
+
answer[interior] = dom
|
| 324 |
+
conf = interior.sum() / max(1, (grid==0).sum()) * 0.80
|
| 325 |
+
if conf > confidence:
|
| 326 |
+
confidence = conf
|
| 327 |
+
reasoning.append(
|
| 328 |
+
f"Re: hollow object, dom_color={dom} "
|
| 329 |
+
f"Im: interior={int(interior.sum())}px unfilled "
|
| 330 |
+
f"iΒ·: fill interior β Im interior β Re color")
|
| 331 |
+
|
| 332 |
+
# ββ Signal 5: Suspended pixels + Im gradient direction β gravity
|
| 333 |
+
if confidence < 0.45 and suspended > 0:
|
| 334 |
+
direction = 'down' if gy_mag >= gx_mag else 'right'
|
| 335 |
+
answer = np.zeros_like(grid)
|
| 336 |
+
if direction == 'down':
|
| 337 |
+
for c in range(W):
|
| 338 |
+
vals = grid[:, c][grid[:, c] > 0]
|
| 339 |
+
if len(vals): answer[H-len(vals):H, c] = vals
|
| 340 |
+
else:
|
| 341 |
+
for r in range(H):
|
| 342 |
+
vals = grid[r, :][grid[r, :] > 0]
|
| 343 |
+
if len(vals): answer[r, W-len(vals):W] = vals
|
| 344 |
+
confidence = min(0.80, suspended / max(total_px, 1) * 2.5)
|
| 345 |
+
reasoning.append(
|
| 346 |
+
f"Re suspended={suspended}px "
|
| 347 |
+
f"Im gy_mag={gy_mag:.3f} gx_mag={gx_mag:.3f} "
|
| 348 |
+
f"iΒ·: gravity {direction}")
|
| 349 |
+
|
| 350 |
+
# ββ Signal 6: Color shift (Re count ratio between colors suggests increment)
|
| 351 |
+
if confidence < 0.35 and len(colors) > 0:
|
| 352 |
+
if all(v < 9 for v in colors):
|
| 353 |
+
answer = grid.copy()
|
| 354 |
+
mask = grid > 0
|
| 355 |
+
answer[mask] = ((grid[mask] - 1 + 1) % 9) + 1
|
| 356 |
+
confidence = 0.35
|
| 357 |
+
reasoning.append(
|
| 358 |
+
f"Re: colors {colors} all < 9 "
|
| 359 |
+
f"Im: uniform distribution suggests ReβIm shift "
|
| 360 |
+
f"iΒ·: increment all colors +1")
|
| 361 |
+
|
| 362 |
+
return re, im, answer, confidence, reasoning
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
# ββ Re-side: exact cell targeting ββββββββββββββββββββββββββββββββββββββββββββ
|
| 366 |
+
|
| 367 |
+
def pixel_diff(current: np.ndarray, target: np.ndarray):
|
| 368 |
+
"""All differing cells: [(r, c, target_color), ...]"""
|
| 369 |
+
if current.shape != target.shape: return []
|
| 370 |
+
return [(int(r), int(c), int(target[r,c]))
|
| 371 |
+
for r in range(current.shape[0])
|
| 372 |
+
for c in range(current.shape[1])
|
| 373 |
+
if current[r,c] != target[r,c]]
|
| 374 |
+
|
| 375 |
+
def most_urgent_diff(current: np.ndarray, target: np.ndarray):
|
| 376 |
+
"""
|
| 377 |
+
Re-side: find the single most important cell to fix.
|
| 378 |
+
Cauchy boundary-first: the boundary determines the interior,
|
| 379 |
+
so fix boundary cells before interior cells.
|
| 380 |
+
"""
|
| 381 |
+
diffs = pixel_diff(current, target)
|
| 382 |
if not diffs: return None
|
| 383 |
+
bound = _boundary(current)
|
| 384 |
+
boundary_diffs = [(r,c,v) for r,c,v in diffs if bound[r,c] > 0]
|
| 385 |
+
pool = boundary_diffs if boundary_diffs else diffs
|
| 386 |
return pool[np.random.randint(len(pool))]
|
| 387 |
|
| 388 |
+
|
| 389 |
+
# ββ Main entry point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 390 |
+
|
| 391 |
+
CONFIDENCE_THRESHOLD = 0.38
|
| 392 |
+
|
| 393 |
+
def try_analytic_action(
|
| 394 |
+
frame_2d: np.ndarray,
|
| 395 |
+
available_actions,
|
| 396 |
+
) -> Tuple[Optional[int], Optional[dict], str, float]:
|
| 397 |
+
"""
|
| 398 |
+
Read the board as a complex object, derive the answer via iΒ· column swap,
|
| 399 |
+
then use the Re-side diff to find the exact cell to click.
|
| 400 |
+
|
| 401 |
+
Returns (action_id, action_data, solver_name, confidence)
|
| 402 |
+
"""
|
| 403 |
+
if frame_2d is None: return None, None, 'none', 0.0
|
| 404 |
+
|
| 405 |
+
avail_ids = set(
|
| 406 |
+
int(a.value if hasattr(a,'value') else a)
|
| 407 |
+
for a in (available_actions or range(1,7))
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
# Read the board
|
| 411 |
+
re, im, answer, confidence, reasoning = read_board(frame_2d)
|
| 412 |
+
|
| 413 |
+
if answer is None or confidence < CONFIDENCE_THRESHOLD:
|
| 414 |
+
return None, None, 'low_confidence', confidence
|
| 415 |
+
|
| 416 |
+
# Find the diff (Re side: exact wrong cells)
|
| 417 |
+
diffs = pixel_diff(frame_2d, answer)
|
| 418 |
+
if not diffs:
|
| 419 |
+
return None, None, 'already_matches', confidence
|
| 420 |
+
|
| 421 |
+
solver_name = reasoning[0].split('β')[0].strip() if reasoning else 're_im'
|
| 422 |
+
|
| 423 |
+
# Emit ACTION6 at the most urgent Re-side coordinate
|
| 424 |
+
if 6 in avail_ids:
|
| 425 |
+
cell = most_urgent_diff(frame_2d, answer)
|
| 426 |
+
if cell is not None:
|
| 427 |
+
r, c, _ = cell
|
| 428 |
+
H, W = frame_2d.shape
|
| 429 |
+
game_y = min(63, max(0, int(r * 64 / H + 32 / H)))
|
| 430 |
+
game_x = min(63, max(0, int(c * 64 / W + 32 / W)))
|
| 431 |
+
return 6, {'x': game_x, 'y': game_y}, solver_name, confidence
|
| 432 |
+
|
| 433 |
+
# Fallback: button actions
|
| 434 |
+
BUTTONS = {'h_mirror': 1, 'v_mirror': 2, 'rotate': 3, 'gravity': 4}
|
| 435 |
+
for key, a_id in BUTTONS.items():
|
| 436 |
+
if any(key in r for r in reasoning) and a_id in avail_ids:
|
| 437 |
+
return a_id, None, solver_name, confidence
|
| 438 |
+
|
| 439 |
+
return None, None, 'no_action_mapping', confidence
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
|
| 444 |
# ββ Feature extractor βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 445 |
|
| 446 |
def extract_features(grid,num_colours=10):
|
|
|
|
| 588 |
feat=extract_features(grid).to(self.device)
|
| 589 |
cur_state=str(state) if state else None
|
| 590 |
|
| 591 |
+
# ββ Re/Im: read the board as a complex object ββββββββββββββββββββ
|
| 592 |
+
re,im,cand_answer,cand_conf,cand_reasoning=read_board(grid)
|
|
|
|
|
|
|
| 593 |
|
| 594 |
# Candidate proximity bonus
|
| 595 |
+
if cand_answer is not None and cand_conf>=0.35:
|
| 596 |
+
curr_dist=(grid!=cand_answer).mean() if grid.shape==cand_answer.shape else 1.0
|
|
|
|
|
|
|
|
|
|
| 597 |
if curr_dist==0.0:
|
| 598 |
cand_bonus=self.candidate_win_reward
|
| 599 |
elif curr_dist<self.prev_candidate_dist:
|
|
|
|
| 602 |
cand_bonus=0.0
|
| 603 |
self.prev_candidate_dist=curr_dist
|
| 604 |
else:
|
| 605 |
+
cand_bonus=0.0; cand_answer=None
|
| 606 |
|
| 607 |
# Store shaped experience
|
| 608 |
if self.prev_feat is not None:
|
|
|
|
| 632 |
if self.step_count%10==0 and len(self.buf)>=16:
|
| 633 |
self._train()
|
| 634 |
|
| 635 |
+
# ββ Im β Re bridge: read board β derive answer β click exact cell ββ
|
| 636 |
analytic_action=None; analytic_meta={}
|
| 637 |
+
if cand_answer is not None and cand_conf>=CONF_THRESHOLD:
|
| 638 |
+
diffs=pixel_diff(grid,cand_answer)
|
| 639 |
if diffs:
|
| 640 |
+
cell=most_urgent_diff(grid,cand_answer)
|
| 641 |
if cell is not None:
|
| 642 |
r,c,tgt_color=cell
|
| 643 |
H,W=grid.shape
|
| 644 |
gy=min(63,max(0,int(r*64/H+32/H)))
|
| 645 |
gx=min(63,max(0,int(c*64/W+32/W)))
|
| 646 |
analytic_action=6
|
| 647 |
+
reasoning_str=' | '.join(cand_reasoning[:2]) if cand_reasoning else 'read_board'
|
| 648 |
analytic_meta={'x':gx,'y':gy,'cell':(r,c,tgt_color),
|
| 649 |
+
'hypothesis':reasoning_str,'conf':cand_conf,
|
| 650 |
+
'n_diffs':len(diffs),
|
| 651 |
+
'candidates':[(reasoning_str,cand_answer,cand_conf)]}
|
| 652 |
|
| 653 |
# ββ CNN fallback ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 654 |
with torch.no_grad():
|