tools: ring_row gets a self-test, which found that a TITLE frame reads as NEW GAME

sylpheed-port closed their last harness gap on a control that never asked whether
the measurement was LIVE -- an empty band list made identity and the real pair both
pass. Applying that to ring_row.py, which underpins every focus finding I have made
and had no self-test at all.

It found a real defect immediately: main_menu_item(ring_row(f)) is not None was
being used as a main-menu test, and on a TITLE frame the gutter carries a bright
cluster at y=243, inside tolerance of row 0, so the title reads as NEW GAME. Glyph
714 against the menu's 327 separates them cleanly; the ring row alone does not.

It never misfired in the sweeps, because B from a submenu goes to the menu rather
than the title -- the test was simply weaker than it was being trusted to be. Added
is_main_menu(), which requires the row AND the glyph signature, and the sweep's two
menu tests now use it.

The self-test asserts the defect it guards, and includes a liveness case: a blanked
frame must return None rather than a number.

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-31 02:00:07 +00:00
parent b4ed61373e
commit 1dfcdd81ee
2 changed files with 70 additions and 4 deletions

View File

@@ -41,6 +41,7 @@ DECOR_ROWS = 50 # window title bar + menu bar live above this
FOOTER_Y = 620 # the button-legend strip is bright in the gutter too
NAMES = ["NEW GAME", "LOAD GAME", "TUTORIAL", "OPTIONS", "EXTRAS"]
ROW0, SPACING = 225.5, 79.25 # measured, main menu, x11grab
MENU_GLYPH_LO, MENU_GLYPH_HI = 250, 420 # the glyph-327 menu detector
def ring_row(img):
@@ -83,7 +84,72 @@ def main_menu_item(y):
return i
def menu_glyph(img):
"""The green-(A)-glyph pixel count, is_title.py's counter."""
a = np.asarray(img.convert("RGB"), dtype=int)
r, g, b = a[:, :, 0], a[:, :, 1], a[:, :, 2]
return int(((g > 130) & (g - r > 45) & (g - b > 45)).sum())
def is_main_menu(img):
"""Is this frame the MAIN MENU? Row AND signature, not row alone.
🔴 `main_menu_item(ring_row(f)) is not None` is NOT a main-menu test, and I
used it as one. On a TITLE frame the gutter carries a bright cluster at
y=243, which is within tolerance of row 0, so the title reads as "NEW GAME"
(glyph 714 — the plate's pulse band — against the menu's 327). It never
misfired in the sweeps because Ⓑ from a submenu goes to the menu, not the
title; the test was simply weaker than it was being trusted to be.
Both conditions, so a screen must have a main-menu ROW and the menu's glyph
SIGNATURE.
"""
if main_menu_item(ring_row(img)) is None:
return False
return MENU_GLYPH_LO <= menu_glyph(img) <= MENU_GLYPH_HI
def _selftest():
"""The reader must find a ring where one is, refuse where none is, and REFUSE
when it is not measuring at all.
The last is sylpheed-port's: their band check passed identity and the real
pair with an EMPTY band list, because every comparison read 0.0 dB — three
controls running and none asking whether the measurement was live.
"""
import glob
ok = True
menu = sorted(glob.glob("/sylph-home/re/*/reach/1-F1.png"))
title = sorted(glob.glob("/sylph-home/re/*/reach/3-title.png"))
if not menu or not title:
print("🔴 SELFTEST UNAVAILABLE: no reference frames"); return 3
m, t = Image.open(menu[0]), Image.open(title[0])
cases = [("finds the ring on a main menu", ring_row(m) is not None, True),
("names it NEW GAME", main_menu_item(ring_row(m)) == 0, True),
("accepts the main menu", is_main_menu(m), True),
("REJECTS the title (row alone would accept)", is_main_menu(t), False),
("row-alone DOES accept the title — the defect this guards",
main_menu_item(ring_row(t)) is not None, True)]
for name, got, want in cases:
good = (got == want)
ok &= good
print(f" {'' if good else '🔴'} {name:52} {got} (want {want})")
# LIVENESS: a blanked gutter must return None, not a number.
import numpy as _np
blank = Image.fromarray(_np.zeros((720, 1280, 3), dtype=_np.uint8))
live = ring_row(blank) is None
ok &= live
print(f" {'' if live else '🔴'} refuses a blank frame (liveness) {not live}")
if not ok:
print("🔴 RING_ROW SELFTEST FAILED — nothing it reports can be trusted.")
return 2
print(" ✅ ring_row selftest passed")
return 0
if __name__ == "__main__":
if "--selftest" in sys.argv:
sys.exit(_selftest())
for p in sys.argv[1:]:
y = ring_row(Image.open(p))
i = main_menu_item(y)

View File

@@ -34,7 +34,7 @@ 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
from ring_row import ring_row, main_menu_item, is_main_menu, NAMES
LOG, OUT = sys.argv[1], sys.argv[2]
W, H = 1280, 720
@@ -158,7 +158,7 @@ _self_test()
if "--selftest-only" in sys.argv:
sys.exit(0)
MAIN = wait_until(lambda a: main_menu_item(ring_row(img(a))) is not None
MAIN = wait_until(lambda a: is_main_menu(img(a))
and 250 <= glyph(a) <= 420, "the main menu", 150)
if MAIN is None:
sys.exit("never identified the main menu")
@@ -173,7 +173,7 @@ for tgt in TARGETS:
# 🔴 NARROW test, not whole-frame. A crash dialog covering the screen centre
# made a whole-frame identity test unable to match ever again, while the ring
# column the dialog did not cover read correctly throughout.
a = wait_until(lambda a: main_menu_item(ring_row(img(a))) is not None,
a = wait_until(lambda a: is_main_menu(img(a)),
"the main menu (by ring row)", 60)
if a is None:
print(f" SKIP {name}: no main-menu ring row"); continue
@@ -221,7 +221,7 @@ for tgt in TARGETS:
if not press("B", "5801"):
results[name] = "skipped (B not delivered)"; continue
if wait_until(lambda x: main_menu_item(ring_row(img(x))) is not None,
if wait_until(lambda x: is_main_menu(img(x)),
"the main menu (by ring row)", 60) is None:
results[name] = "VOID: B did not return to the main menu"; continue
if not press("A", "5800"):