diff --git a/tools/re-capture/ring_row.py b/tools/re-capture/ring_row.py index defe7b80..5536f4a1 100755 --- a/tools/re-capture/ring_row.py +++ b/tools/re-capture/ring_row.py @@ -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) diff --git a/tools/re-capture/submenu_focus_sweep.py b/tools/re-capture/submenu_focus_sweep.py index f69cdff3..5c9f3384 100644 --- a/tools/re-capture/submenu_focus_sweep.py +++ b/tools/re-capture/submenu_focus_sweep.py @@ -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"):