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:
@@ -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)
|
||||
|
||||
@@ -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"):
|
||||
|
||||
Reference in New Issue
Block a user