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:
sylph-decoder
2026-08-30 22:05:03 +00:00
parent 04e8c6d8a7
commit e35c56a5ed
3 changed files with 201 additions and 105 deletions

View File

@@ -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)