Files
Sylpheed/tools/re-capture/ring_row.py
sylph-decoder 8ebbfc70c1 re: the focus reader was two items out -- initial focus is NEW GAME, and my labels were wrong
menu_focus.py's row centres are design-space rows from screenshot output; my
probes fed it whole-display x11grab frames carrying Xenia's chrome and a surface
scaled 1.060. Caught by ground truth, not by a control: the probe announced 'on
EXTRAS', pressed A, and opened OPTIONS.

Measured directly with ring_row.py: initial focus on a fresh boot is NEW GAME,
2/2 fresh boots, both the first menu entry. That agrees with boot_menu.sh's own
line and menu-state-in-memory.md's four-downs, and withdraws this page's
'TUTORIAL 2/2' as the outlier.

Persistence stands and is now geometry-free -- 384.0 vs 385.5, 1.5 px apart. An
equality test is immune to a constant offset, which is why the conclusion survived
a broken reader when the published item names did not.

The control was structurally blind: 'two DOWNs move two items' tests relative
motion, and a constant offset preserves it exactly.

EXTRAS remains unmeasured; that run navigated to OPTIONS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-30 21:54:32 +00:00

79 lines
2.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Locate the menu focus ring by MEASUREMENT, in whatever frame you have.
🔴 Why this exists. `menu_focus.py`'s row centres [166,241,315,390,465] are
DESIGN-SPACE rows read off `screenshot` output. Feeding it an ffmpeg x11grab
frame of the whole X display silently reads the wrong rows: the frame carries
Xenia's title bar and menu bar, and the game surface is scaled. On 2026-08-30 a
probe announced "on EXTRAS", pressed Ⓐ, and opened OPTIONS -- two items out.
⚠️ AND THE CONTROL COULD NOT CATCH IT. "Two DOWNs must move the cursor two
items" tests RELATIVE motion, which a constant offset preserves exactly. It
passed on a reader that was two items wrong. So this returns the ring's measured
ROW, and callers compare rows; naming an item needs a calibration, below.
Measured on the x11grab frames of 2026-08-30:
spacing 79.25 px per item (design spacing 74.75 -> surface scaled 1.060)
capture_y = 49.5 + 1.060 * design_y
Checks: design 241 -> 305 predicted vs 304.75 measured; 315 -> 383.4 vs 384.0.
ring_row.py FRAME.png [FRAME.png ...]
"""
import sys
import numpy as np
from PIL import Image
GUTTER = (500, 542)
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
def ring_row(img):
"""The ring's y centre in THIS frame's own pixels, or None."""
g = np.asarray(img.convert("L"), dtype=float)
col = g[:, GUTTER[0]:GUTTER[1]].max(axis=1)
col[:DECOR_ROWS] = 0
ys = np.nonzero(col > 150)[0]
if len(ys) == 0:
return None
groups, cur = [], [int(ys[0])]
for y in ys[1:]:
if y - cur[-1] <= 4:
cur.append(int(y))
else:
groups.append(cur)
cur = [int(y)]
groups.append(cur)
groups = [g_ for g_ in groups if len(g_) >= 5 and g_[0] < FOOTER_Y]
if not groups:
return None
b = max(groups, key=len)
return (b[0] + b[-1]) / 2
def main_menu_item(y):
"""Item index on the MAIN MENU, from the measured calibration.
Only valid for the five-item main menu in an x11grab frame. Returns None if
the row is not within half a step of a row centre -- refusing is the point,
since a wrong name is what this file exists to prevent.
"""
if y is None:
return None
i = round((y - ROW0) / SPACING)
if not (0 <= i < len(NAMES)):
return None
if abs(y - (ROW0 + i * SPACING)) > SPACING * 0.4:
return None
return i
if __name__ == "__main__":
for p in sys.argv[1:]:
y = ring_row(Image.open(p))
i = main_menu_item(y)
nm = NAMES[i] if i is not None else "<not a main-menu row>"
print(f"{p.split('/')[-1]:22} ring y = {y} -> {nm}")