Refutation attempt, per the adversarial duty. Target: this page's own row "B on the main menu goes to the title". Chosen because it is one of only two Q5 rows with an empty evidence cell, and because it is the only exit from the main menu, so the port will build on it. Whole-frame colour test for the pad-glyph discs. The main menu carries ZERO red-B pixels anywhere in the frame, on two independent captures, while the same unchanged detector finds 514 on EXTRAS and 518 on DIFFICULTY. The control passes twice: the A glyph reads 438/438/440/438 across all four screens, so it is one asset at one size and a B of that family could not have slipped under a threshold. The main menu's legend is "Select / OK"; every submenu adds "Back". The claim SURVIVES -- a legend is not behaviour, and an absent glyph cannot refute an observed press -- but it is downgraded to amber. The observation is uncited and single, it is now the only Q5 row the game's own text contradicts, and there is a named confound: the title-side screens auto-return after ~8-10 s idle, which looks exactly like what was described. Reading 0x828A690C while pressing B would separate them in one run; that run needs a disc this container does not have. Second finding, same method. MISSION SELECT's "sixteen d-pad presses never left Stage 01" was a LOCKED stage list, not a broken one. The labels have three brightnesses, not two -- locked 104, unlocked 183, focused 254 -- and the all-story-unlocked capture is the control that separates the lower two while holding row 1 at an identical 254. On that save the cursor reaches Stage16 at the bottom of a scrolled list. The list is 16 long and shows 8 at a time. Regenerator committed beside the finding; it reads only files already in git. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UxPvE5cz7zekXBKi7Xw2r
93 lines
3.1 KiB
Python
Executable File
93 lines
3.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Re-measure two menu facts from the COMMITTED oracle captures — no disc needed.
|
|
|
|
1. Which screens advertise Ⓑ in their footer legend.
|
|
The pad glyphs are saturated green (Ⓐ) and red (Ⓑ) discs on a blue field,
|
|
so a colour test finds them without knowing where the footer is.
|
|
|
|
2. Whether a dim MISSION SELECT row is LOCKED or merely UNFOCUSED.
|
|
Three brightness levels discriminate; the all-unlocked capture is the
|
|
control that separates them.
|
|
|
|
Usage: python3 tools/re-capture/footer_and_locked_rows.py [repo-root]
|
|
"""
|
|
import sys
|
|
import pathlib
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
ROOT = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
|
|
CAP = ROOT / "docs/re/captures"
|
|
|
|
|
|
def glyph_masks(rgb):
|
|
r, g, b = rgb[:, :, 0], rgb[:, :, 1], rgb[:, :, 2]
|
|
green = (g > 110) & (g > r + 45) & (g > b + 45)
|
|
red = (r > 110) & (r > g + 45) & (r > b + 45)
|
|
return green, red
|
|
|
|
|
|
def blobs(mask, gap=20):
|
|
ys, xs = np.nonzero(mask)
|
|
if len(xs) == 0:
|
|
return []
|
|
o = np.argsort(xs)
|
|
xs, ys = xs[o], ys[o]
|
|
out, start = [], 0
|
|
for i in range(1, len(xs) + 1):
|
|
if i == len(xs) or xs[i] - xs[i - 1] > gap:
|
|
s = slice(start, i)
|
|
out.append((int(xs[s].min()), int(xs[s].max()),
|
|
int(ys[s].min()), int(ys[s].max()), i - start))
|
|
start = i
|
|
return out
|
|
|
|
|
|
def footers():
|
|
print("== 1. footer legends: does the screen advertise Ⓑ? ==")
|
|
print(f"{'capture':46} {'Ⓐ px':>7} {'Ⓑ px':>7} verdict")
|
|
shots = [
|
|
("title-builds/live-main-menu.png", "main menu"),
|
|
("title-builds/live-main-menu-options-focused.png", "main menu (OPTIONS focused)"),
|
|
("title-builds/live-extras.png", "EXTRAS"),
|
|
("difficulty-screen.png", "DIFFICULTY"),
|
|
]
|
|
for rel, _name in shots:
|
|
p = CAP / rel
|
|
if not p.exists():
|
|
print(f"{rel:46} MISSING")
|
|
continue
|
|
a = np.asarray(Image.open(p).convert("RGB")).astype(int)
|
|
g, r = glyph_masks(a) # WHOLE frame, not a guessed band
|
|
verdict = "no Ⓑ anywhere in frame" if r.sum() == 0 else f"Ⓑ at {blobs(r)[0][:2]}"
|
|
print(f"{rel:46} {g.sum():7d} {r.sum():7d} {verdict}")
|
|
|
|
|
|
ROW_Y0, ROW_PITCH, ROW_X = 201, 50, (190, 320)
|
|
|
|
|
|
def stage_rows():
|
|
print("\n== 2. MISSION SELECT rows: locked, or just unfocused? ==")
|
|
shots = [
|
|
("mission-select-stage01-only.png", "save with only Stage01 cleared"),
|
|
("mission-select-all-story-unlocked.png", "save with the story unlocked"),
|
|
("mission-select-ends-at-stage16.png", "unlocked, scrolled to the end"),
|
|
]
|
|
for rel, note in shots:
|
|
p = CAP / rel
|
|
if not p.exists():
|
|
print(f"{rel:44} MISSING")
|
|
continue
|
|
a = np.asarray(Image.open(p).convert("L")).astype(float)
|
|
p95 = []
|
|
for i in range(8):
|
|
y = ROW_Y0 + ROW_PITCH * i
|
|
p95.append(np.percentile(a[y - 14:y + 14, ROW_X[0]:ROW_X[1]], 95))
|
|
print(f"{rel:44} {note}")
|
|
print(" row p95: " + " ".join(f"{v:5.0f}" for v in p95))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
footers()
|
|
stage_rows()
|