re: refutation attempt on Q2's 'shipped twice' -- it survives, with one real caveat
The doubt was my own artefact: the entry dump printed only the first two sprite names in HashMap order, making 11 and 14 look like different studios. Full sets are identical. 7 of 8 pairs declare identical sprite sets, control included. 4/7 does not: entry 7 carries nine sprites entry 4 lacks, including ptlogo_jp and ptlogo_jpeff, so the Japanese title is a different element inventory rather than the same screen localised. That matches the JP capture work from the other side. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
@@ -1,27 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Does the EXTRAS submenu remember its cursor across leave -> re-enter?
|
||||
|
||||
sylpheed-port's contract-check asserted that EXTRAS does NOT persist. Nothing
|
||||
measured that. The corpus has EXTRAS' initial focus as MISSION SELECT from a
|
||||
SINGLE entry, and Ⓑ-from-a-submenu restoring the PARENT's focus 4/4 -- neither
|
||||
says what a submenu's own cursor does on re-entry. Their label
|
||||
`initial_focus: ptbtn11, kind: "measured"` is exposed to the same history problem
|
||||
raised for the main menu: taken on one entry, it may be reading history.
|
||||
sylpheed-port's contract-check asserts that EXTRAS does NOT persist. Nothing
|
||||
measured that: the corpus has EXTRAS' initial focus from a SINGLE entry, and
|
||||
Ⓑ-from-a-submenu restoring the PARENT's focus 4/4 -- neither says what a
|
||||
submenu's own cursor does on re-entry. The main menu was measured to PERSIST, so
|
||||
the question is live in both directions, and their `kind: "measured"` label on
|
||||
`initial_focus: ptbtn11` turns on it.
|
||||
|
||||
The main menu was measured to PERSIST (focus-persists-across-title.txt), so the
|
||||
question is live in both directions.
|
||||
🔴 RUN 1 (2026-08-30) WAS VOID AND ITS FAILURES SHAPE THIS FILE:
|
||||
* it navigated to OPTIONS believing it was EXTRAS -- the focus reader used
|
||||
design-space rows against x11grab frames (menu-focus-reader-offset.txt);
|
||||
* its screen detector could not tell the main menu from a submenu, because
|
||||
both sit inside the glyph 250..420 window (main menu 327, OPTIONS 317);
|
||||
* its control checked only that the ring MOVED, which a wrong origin passes.
|
||||
|
||||
⚠️ NO HAND-GUESSED GEOMETRY. menu_focus.py's ys are the five-item main menu and
|
||||
do not transfer to a three-item submenu, and the only EXTRAS capture in the
|
||||
corpus is a 562x182 crop. So the ring is located by DIFFERENCE instead: the
|
||||
pixels that change when the cursor moves ARE the ring's footprint. Persistence is
|
||||
then decided inside that mask, which needs no coordinates at all.
|
||||
|
||||
🔴 CONTROL, and the run stops on it: the E1->E2 difference must be ONE COMPACT
|
||||
CLUSTER. If moving the cursor changes the whole frame, the mask is not the ring
|
||||
and nothing after it may be read. (sylpheed-port measured one 46x44 cluster,
|
||||
0.155 % of frame, across a full ring cycle on their renderer -- an independent
|
||||
expectation for what this should look like.)
|
||||
So this run: absolute row checks against a measured calibration, screen identity
|
||||
against a REFERENCE FRAME captured in this same run, and raw ring ROWS compared
|
||||
inside the submenu -- no submenu geometry is assumed or needed.
|
||||
|
||||
extras_focus_persistence.py LOG OUTDIR [wait_s]
|
||||
"""
|
||||
@@ -29,13 +25,13 @@ import os, re, subprocess, sys, time
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from ring_row import ring_row, main_menu_item, NAMES, ROW0, SPACING
|
||||
|
||||
LOG, OUT = sys.argv[1], sys.argv[2]
|
||||
WAIT = float(sys.argv[3]) if len(sys.argv) > 3 else 600
|
||||
W, H = 1280, 720
|
||||
MENU_LO, MENU_HI, MENU_HOLD = 250, 420, 6
|
||||
PAD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pad.py")
|
||||
NAMES = ["NEW GAME", "LOAD GAME", "TUTORIAL", "OPTIONS", "EXTRAS"]
|
||||
YS = [166, 241, 315, 390, 465]
|
||||
T0 = time.time()
|
||||
|
||||
|
||||
@@ -56,7 +52,7 @@ def press(btn, vk, tries=5):
|
||||
if deliveries(vk) > before:
|
||||
print(f"[{time.time()-T0:7.1f}s] {btn} delivered", flush=True)
|
||||
return True
|
||||
print(f"[{time.time()-T0:7.1f}s] {btn} NOT delivered (attempt {k+1})", flush=True)
|
||||
print(f"[{time.time()-T0:7.1f}s] 🔴 {btn} NEVER delivered", flush=True)
|
||||
return False
|
||||
|
||||
|
||||
@@ -69,117 +65,118 @@ def _open():
|
||||
|
||||
|
||||
def fresh():
|
||||
"""A CURRENT frame. Reading the pipe after a sleep returns a buffered one."""
|
||||
q = _open(); a = None
|
||||
for _ in range(3):
|
||||
buf = q.stdout.read(W * H * 3)
|
||||
if len(buf) == W * H * 3:
|
||||
a = np.frombuffer(buf, np.uint8).reshape(H, W, 3).astype(int)
|
||||
b = q.stdout.read(W * H * 3)
|
||||
if len(b) == W * H * 3:
|
||||
a = np.frombuffer(b, np.uint8).reshape(H, W, 3).astype(int)
|
||||
q.kill()
|
||||
return a
|
||||
|
||||
|
||||
def img(a):
|
||||
return Image.fromarray(a.astype(np.uint8))
|
||||
|
||||
|
||||
def differs(a, b):
|
||||
return float((np.abs(a - b).max(axis=2) > 24).mean())
|
||||
|
||||
|
||||
def glyph(a):
|
||||
r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2]
|
||||
return int(((g > 130) & (g - r > 45) & (g - b > 45)).sum())
|
||||
r, g, bl = a[:, :, 0], a[:, :, 1], a[:, :, 2]
|
||||
return int(((g > 130) & (g - r > 45) & (g - bl > 45)).sum())
|
||||
|
||||
|
||||
def mainfocus(a):
|
||||
g = np.asarray(Image.fromarray(a.astype(np.uint8)).convert("L"), dtype=float)
|
||||
v = [g[y - 20:y + 20, 500:542].max() for y in YS]
|
||||
return int(np.argmax(v)), v
|
||||
|
||||
|
||||
def wait_menu(limit=90):
|
||||
t = time.time(); streak = 0
|
||||
def wait_until(pred, what, limit=60):
|
||||
t = time.time()
|
||||
while time.time() - t < limit:
|
||||
a = fresh()
|
||||
if a is None:
|
||||
continue
|
||||
if MENU_LO <= glyph(a) <= MENU_HI:
|
||||
streak += 1
|
||||
if streak >= 2:
|
||||
return a
|
||||
else:
|
||||
streak = 0
|
||||
if a is not None and pred(a):
|
||||
print(f"[{time.time()-T0:7.1f}s] {what}", flush=True)
|
||||
return a
|
||||
print(f"[{time.time()-T0:7.1f}s] 🔴 TIMEOUT waiting for {what}", flush=True)
|
||||
return None
|
||||
|
||||
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
# ---- 1. on the main menu, walk to EXTRAS ------------------------------------
|
||||
a = wait_menu()
|
||||
if a is None:
|
||||
sys.exit("never saw the main menu")
|
||||
f, v = mainfocus(a)
|
||||
print(f"[{time.time()-T0:7.1f}s] MAIN MENU focus = {NAMES[f]} ring "
|
||||
+ " ".join(f"{x:5.0f}" for x in v), flush=True)
|
||||
need = (4 - f) % 5
|
||||
print(f" {need} DOWN to reach EXTRAS", flush=True)
|
||||
for _ in range(need):
|
||||
# ---- 1. main menu: reference frame + calibrated focus ------------------------
|
||||
MAIN = wait_until(lambda a: main_menu_item(ring_row(img(a))) is not None
|
||||
and 250 <= glyph(a) <= 420, "MAIN MENU reference captured", 120)
|
||||
if MAIN is None:
|
||||
sys.exit("never identified the main menu")
|
||||
img(MAIN).save(f"{OUT}/0-main-ref.png")
|
||||
f = main_menu_item(ring_row(img(MAIN)))
|
||||
print(f" focus = {NAMES[f]} (ring y {ring_row(img(MAIN))})", flush=True)
|
||||
|
||||
# ---- 2. walk to EXTRAS, checking the ABSOLUTE row after every press ----------
|
||||
for step in range((4 - f) % 5):
|
||||
if not press("DOWN", "5811"):
|
||||
sys.exit("a DOWN was never delivered")
|
||||
time.sleep(0.8)
|
||||
time.sleep(1.5)
|
||||
a = fresh(); f2, v2 = mainfocus(a)
|
||||
print(f" now on {NAMES[f2]} ring " + " ".join(f"{x:5.0f}" for x in v2), flush=True)
|
||||
if f2 != 4:
|
||||
sys.exit(f"CONTROL FAILED: wanted EXTRAS, on {NAMES[f2]}")
|
||||
print("✅ on EXTRAS", flush=True)
|
||||
time.sleep(1.2)
|
||||
a = fresh(); y = ring_row(img(a)); i = main_menu_item(y)
|
||||
want = (f + step + 1) % 5
|
||||
print(f" step {step+1}: ring y {y} -> {NAMES[i] if i is not None else '??'} "
|
||||
f"(want {NAMES[want]})", flush=True)
|
||||
if i != want:
|
||||
sys.exit(f"🔴 CONTROL FAILED: after {step+1} DOWN the ring is not on {NAMES[want]}")
|
||||
print("✅ on EXTRAS, verified by absolute row after every press", flush=True)
|
||||
|
||||
# ---- 2. enter EXTRAS, capture E1, move, capture E2 --------------------------
|
||||
# ---- 3. enter EXTRAS -- and prove we LEFT the main menu ----------------------
|
||||
if not press("A", "5800"):
|
||||
sys.exit("A never delivered")
|
||||
time.sleep(6.0)
|
||||
E1 = fresh(); Image.fromarray(E1.astype(np.uint8)).save(f"{OUT}/E1.png")
|
||||
print(f"[{time.time()-T0:7.1f}s] IN EXTRAS (glyph {glyph(E1)}) — E1 captured", flush=True)
|
||||
E1 = wait_until(lambda a: differs(a, MAIN) > 0.20, "left the main menu", 40)
|
||||
if E1 is None:
|
||||
sys.exit("A did not change the screen")
|
||||
time.sleep(3.0)
|
||||
E1 = fresh(); img(E1).save(f"{OUT}/E1.png")
|
||||
y1 = ring_row(img(E1))
|
||||
print(f"[{time.time()-T0:7.1f}s] E1 in the submenu: ring y = {y1}, "
|
||||
f"glyph {glyph(E1)}, {100*differs(E1, MAIN):.1f}% from main", flush=True)
|
||||
|
||||
# ---- 4. move the cursor, control on an ABSOLUTE change ----------------------
|
||||
if not press("DOWN", "5811"):
|
||||
sys.exit("a DOWN was never delivered inside EXTRAS")
|
||||
sys.exit("a DOWN was never delivered inside the submenu")
|
||||
time.sleep(2.5)
|
||||
E2 = fresh(); Image.fromarray(E2.astype(np.uint8)).save(f"{OUT}/E2.png")
|
||||
print(f"[{time.time()-T0:7.1f}s] E2 captured after 1 DOWN", flush=True)
|
||||
E2 = fresh(); img(E2).save(f"{OUT}/E2.png")
|
||||
y2 = ring_row(img(E2))
|
||||
print(f"[{time.time()-T0:7.1f}s] E2 after 1 DOWN: ring y = {y2}", flush=True)
|
||||
if y1 is None or y2 is None:
|
||||
sys.exit("🔴 CONTROL FAILED: no ring found in the submenu — this reader does not work here")
|
||||
if abs(y2 - y1) < 20:
|
||||
sys.exit(f"🔴 CONTROL FAILED: the ring did not move ({y1} -> {y2})")
|
||||
print(f"✅ CONTROL PASSED: the ring moved {y1} -> {y2} ({abs(y2-y1):.1f} px)", flush=True)
|
||||
|
||||
# ---- 3. CONTROL: the difference must be one compact cluster -----------------
|
||||
d = (np.abs(E1 - E2).max(axis=2) > 24)
|
||||
frac = d.mean()
|
||||
ys, xs = np.nonzero(d)
|
||||
if len(ys) == 0:
|
||||
sys.exit("🔴 CONTROL FAILED: the cursor did not move at all (E1 == E2)")
|
||||
bbox = (int(xs.min()), int(ys.min()), int(xs.max()), int(ys.max()))
|
||||
bw, bh = bbox[2] - bbox[0] + 1, bbox[3] - bbox[1] + 1
|
||||
dens = len(ys) / (bw * bh)
|
||||
print(f" diff: {100*frac:.3f}% of frame, bbox {bw}x{bh} at ({bbox[0]},{bbox[1]}), "
|
||||
f"fill {100*dens:.0f}%", flush=True)
|
||||
if frac > 0.05:
|
||||
sys.exit(f"🔴 CONTROL FAILED: {100*frac:.1f}% of the frame changed — that is not a ring")
|
||||
print("✅ CONTROL PASSED: the move changed one localised region", flush=True)
|
||||
|
||||
# ---- 4. leave, re-enter, capture E3 ----------------------------------------
|
||||
# ---- 5. leave, prove we are back on the main menu, re-enter -----------------
|
||||
if not press("B", "5801"):
|
||||
sys.exit("B never delivered")
|
||||
a = wait_menu()
|
||||
if a is None:
|
||||
sys.exit("B did not return to the main menu")
|
||||
f3, _ = mainfocus(a)
|
||||
print(f"[{time.time()-T0:7.1f}s] back on the MAIN MENU, focus = {NAMES[f3]}", flush=True)
|
||||
back = wait_until(lambda a: differs(a, MAIN) < 0.15, "back on the MAIN MENU (vs reference)", 60)
|
||||
if back is None:
|
||||
sys.exit("🔴 B did not return to the main menu — refusing to read E3")
|
||||
if not press("A", "5800"):
|
||||
sys.exit("A never delivered on re-entry")
|
||||
time.sleep(6.0)
|
||||
E3 = fresh(); Image.fromarray(E3.astype(np.uint8)).save(f"{OUT}/E3.png")
|
||||
print(f"[{time.time()-T0:7.1f}s] RE-ENTERED EXTRAS — E3 captured", flush=True)
|
||||
E3 = wait_until(lambda a: differs(a, MAIN) > 0.20, "left the main menu again", 40)
|
||||
if E3 is None:
|
||||
sys.exit("re-entry did not change the screen")
|
||||
time.sleep(3.0)
|
||||
E3 = fresh(); img(E3).save(f"{OUT}/E3.png")
|
||||
y3 = ring_row(img(E3))
|
||||
same_screen = differs(E3, E1) < 0.15
|
||||
print(f"[{time.time()-T0:7.1f}s] E3 on re-entry: ring y = {y3}, "
|
||||
f"{100*differs(E3, E1):.1f}% from E1 — same screen: {same_screen}", flush=True)
|
||||
|
||||
# ---- 5. decide, inside the ring mask only ----------------------------------
|
||||
m = d
|
||||
d1 = float(np.abs(E3 - E1).max(axis=2)[m].mean())
|
||||
d2 = float(np.abs(E3 - E2).max(axis=2)[m].mean())
|
||||
print(f"\n inside the ring mask ({int(m.sum())} px):")
|
||||
print(f" mean |E3 - E1| = {d1:6.2f} (E1 = where EXTRAS opened)")
|
||||
print(f" mean |E3 - E2| = {d2:6.2f} (E2 = where I left it)")
|
||||
if d2 < d1 * 0.5:
|
||||
print("\n=> EXTRAS PERSISTS: re-entry matches where I left the cursor")
|
||||
elif d1 < d2 * 0.5:
|
||||
print("\n=> EXTRAS RESETS: re-entry matches where it first opened")
|
||||
# ---- 6. decide ------------------------------------------------------------
|
||||
print(f"\n E1 (opened on) ring y = {y1}")
|
||||
print(f" E2 (left it on) ring y = {y2}")
|
||||
print(f" E3 (re-entered) ring y = {y3}")
|
||||
if not same_screen:
|
||||
print("\n=> VOID: re-entry is not the same screen; nothing measured")
|
||||
elif y3 is None:
|
||||
print("\n=> VOID: no ring on re-entry")
|
||||
elif abs(y3 - y2) < 20 and abs(y3 - y1) >= 20:
|
||||
print("\n=> EXTRAS PERSISTS: re-entry is where I left the cursor")
|
||||
elif abs(y3 - y1) < 20 and abs(y3 - y2) >= 20:
|
||||
print("\n=> EXTRAS RESETS: re-entry is where it first opened")
|
||||
else:
|
||||
print(f"\n=> UNDECIDED: {d1:.2f} vs {d2:.2f} are too close to separate")
|
||||
print(f"\n=> UNDECIDED: |E3-E1|={abs(y3-y1):.1f} |E3-E2|={abs(y3-y2):.1f}")
|
||||
print("EXTRAS FOCUS RUN DONE", flush=True)
|
||||
|
||||
Reference in New Issue
Block a user