From 14ec1c1df8d8542e682f00c6c2f1b268a1364979 Mon Sep 17 00:00:00 2001 From: Sylpheed port agent Date: Sat, 29 Aug 2026 17:29:48 +0000 Subject: [PATCH] port: which-focus -- a focus detector for the Decoder, with the control wired in S00A is blocked on knowing which button a screenshot has focused. `newgame_path.sh` assumed NEW GAME at boot, drove on it, and landed in a tutorial mission -- HANDOFF Q5 measured focus as UNSTABLE across boots. Counting presses cannot substitute: up from the first item wraps to the last, so no fixed number of presses lands on a known item from an unknown start. The Decoder's own attempt, a per-row brightness statistic, FAILED the control -- it picked NEW GAME on the capture whose filename says OPTIONS. The render-difference method passes it, so this packages it as a script. IT RUNS THE CONTROL ON EVERY INVOCATION, not once when it was written, and refuses to report anything if the control fails. live-main-menu-options-focused KNOWN ANSWER OPTIONS 4.7x live-main-menu the question NEW GAME 11.4x live-extras KNOWN from corpus MISSION SELECT 4.2x live-title-press-a no menu at all refuses 1.0x The extras row is a second known answer I did not plant -- authored/flow.json already records "MEASURED: EXTRAS opens focused on MISSION SELECT (live-extras.png)" -- and the tool reaches it independently. The title row is the negative control. AND THE REFUSAL NOW CARRIES A NON-ZERO EXIT CODE. The first version printed "do not act on this" and exited 0, so a caller scripting it -- which is the entire point -- would have read a refusal as an answer. Same defect as a checker claiming a check it skipped, and the fifth instance of that shape this session. What it is not: it identifies focus in ONE FRAME and says nothing about what selects focus. Q5's instability stands. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF --- docs/port/DECISIONS.md | 51 +++++++++++++++++++ tools/port/which-focus | 108 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100755 tools/port/which-focus diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index 8cce0eeb..8bdf6f90 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -3804,3 +3804,54 @@ Re-deriving the transfer curve on the correctly-posed pair gives 1.20 / 1.26 / So the focus-state contamination it flagged really did not move the trend. Its reproduction stands as a second opinion after all, and that is now a measurement rather than a hope. + +## `tools/port/which-focus` — the Decoder asked for a detector, and it carries its own control + +`S00A` is blocked on knowing which button a screenshot has focused. +`newgame_path.sh` assumed NEW GAME is focused at boot, drove on that assumption, +and landed in a **tutorial mission** — because HANDOFF Q5 measured focus as +*unstable across boots*. And counting presses cannot substitute: ⬆ from the first +item wraps to the last, so no fixed number of presses lands on a known item from +an unknown start. + +The Decoder's own attempt — a per-row brightness statistic — **failed the +control**, picking NEW GAME on the capture whose filename says OPTIONS. The +render-difference method passes it, so it is now a script that agent can run. + +### It runs the control on every invocation, not once when it was written + +``` +control -- live-main-menu-options-focused.png (answer is in the filename): + OPTIONS 1285 <- picked + EXTRAS 6073 + ... + -> OPTIONS, margin 4.7x CONTROL PASSED +``` + +If that fails, the tool **refuses to report a result at all**. A control that +does not execute is not a control, and this one cannot be skipped. + +### Three checks, and one of them independently reproduces a corpus measurement + +| input | verdict | margin | +|---|---|---| +| `live-main-menu-options-focused` — **known answer** | OPTIONS | 4.7× | +| `live-main-menu` — the question | **NEW GAME** | 11.4× | +| `live-extras` — **known from the corpus** | MISSION SELECT | 4.2× | +| `live-title-press-a` — **no menu at all** | *refuses* | 1.0× | + +The `extras` row is a second known answer I did not plant: `authored/flow.json` +already records *"MEASURED: EXTRAS opens focused on MISSION SELECT +(live-extras.png)"*, and the tool reaches it independently. + +The title row is the negative control. A frame with no menu in it gives a margin +of 1.0× and the tool says *"this frame does not decide it. Do not act on this."* + +⚠️ **And that refusal now carries a non-zero exit code.** The first version +printed the warning and exited 0 — so a caller scripting it, which is the entire +point, would have read a refusal as an answer. That is the same defect as a +checker claiming a check it skipped, and it is the fifth instance of that shape +between the two of us this session. + +**What it is not:** it identifies focus in *one frame*. It says nothing about +what *selects* focus; Q5's instability stands. diff --git a/tools/port/which-focus b/tools/port/which-focus new file mode 100755 index 00000000..23427fad --- /dev/null +++ b/tools/port/which-focus @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# Which button is focused in a screenshot of the real game? +# +# tools/port/which-focus SHOT.png # main_menu (5 buttons) +# tools/port/which-focus SHOT.png extras # extras (3 buttons) +# +# Renders the port's own screen with each button focused in turn and reports +# which one differs least from the shot. Answers a question the Decoder needs to +# drive the game -- `newgame_path.sh` assumed NEW GAME is focused at boot, drove +# on that, and landed in a tutorial mission, because HANDOFF Q5 measured focus as +# UNSTABLE across boots. Counting presses cannot substitute: up from the first +# item wraps to the last, so no fixed number of presses lands on a known item +# from an unknown start. +# +# ⚠️ IT RUNS ITS OWN CONTROL FIRST AND REFUSES TO ANSWER IF THE CONTROL FAILS. +# `docs/re/captures/title-builds/live-main-menu-options-focused.png` has the +# answer in its filename, so the method can be tested on every invocation rather +# than once when it was written. A brightness-per-row detector was tried for this +# job and picked NEW GAME on that capture; this method picks OPTIONS by 4.7x. +# A control that does not execute is not a control. +# +# WHAT IT IS NOT. It identifies the focus in ONE FRAME. It says nothing about +# what selects focus -- Q5's four boots gave TUTORIAL, TUTORIAL, NEW GAME, NEW +# GAME and that instability stands. +set -euo pipefail +cd "${PROJECT_DIR:-/work}" +export DISPLAY="${DISPLAY:-:97}" +shot="${1:?usage: which-focus SHOT.png [screen]}" +screen="${2:-main_menu}" +OUT="${OUT:-$(mktemp -d)}"; mkdir -p "$OUT" +CAPS=docs/re/captures/title-builds + +# Buttons, in the order ui_down walks them. +case "$screen" in + main_menu) BUTTONS=(ptbtn01:NEW_GAME ptbtn02:LOAD_GAME ptbtn03:TUTORIAL ptbtn04:OPTIONS ptbtn05:EXTRAS) ;; + extras) BUTTONS=(ptbtn11:MISSION_SELECT ptbtn12:MOVIE_THEATER ptbtn13:THIRD) ;; + *) echo "which-focus: no button list for $screen" >&2; exit 2 ;; +esac +n=${#BUTTONS[@]} +downs=$(python3 -c "print(','.join(['down']*($n-1)))") + +render_all() { # render_all + godot --path port --resolution 1280x720 -- "--menu=$screen" "--script=$downs" \ + "--shots=$OUT/$1" >"$OUT/$1.log" 2>&1 || true +} +# Normalise any input to the captures' 1279x675 top-left crop. A 1280x720 guest +# frame and a 1279x675 screenshot are the same pixels; the difference is the +# crop the screenshot tool applies, not a scale. +norm() { convert "$1" -crop 1279x675+0+0 +repage "$2"; } + +score() { # score ; prints "