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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
This commit is contained in:
@@ -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.
|
||||
|
||||
108
tools/port/which-focus
Executable file
108
tools/port/which-focus
Executable file
@@ -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 <tag>
|
||||
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 <shot> ; prints "<idx> <label> <pixels>" per candidate
|
||||
local s="$1" i=0 f
|
||||
for f in "$OUT"/r_*.png; do
|
||||
[ -f "$f" ] || continue
|
||||
norm "$f" "$OUT/cand.png"
|
||||
local d
|
||||
d=$(convert "$OUT/cand.png" "$s" -compose difference -composite \
|
||||
-colorspace Gray -threshold 25% -format "%[fx:mean*w*h]" info:)
|
||||
echo "$i ${BUTTONS[$i]#*:} $d"
|
||||
i=$((i+1))
|
||||
done
|
||||
}
|
||||
|
||||
render_all r
|
||||
# Godot names the shots `<tag>_00_start.png`, `<tag>_01_down.png`, ... -- rename
|
||||
# to a sortable form so the candidate order is the ui_down order and not glob luck.
|
||||
i=0
|
||||
for f in "$OUT"/r_0*.png; do mv "$f" "$OUT/r_$(printf '%02d' $i).png"; i=$((i+1)); done
|
||||
[ "$i" = "$n" ] || { echo "which-focus: rendered $i of $n focus states -- see $OUT" >&2; exit 3; }
|
||||
|
||||
verdict() { # verdict <shot> <expected-or-empty>
|
||||
local s="$1" expect="${2:-}"
|
||||
norm "$s" "$OUT/shot.png"
|
||||
mapfile -t rows < <(score "$OUT/shot.png" | sort -k3 -n)
|
||||
local best_lbl best_px second_px
|
||||
best_lbl=$(echo "${rows[0]}" | awk '{print $2}')
|
||||
best_px=$(echo "${rows[0]}" | awk '{print $3}')
|
||||
second_px=$(echo "${rows[1]}" | awk '{print $3}')
|
||||
local margin
|
||||
margin=$(python3 -c "print('%.1f' % ($second_px/max($best_px,1)))")
|
||||
for r in "${rows[@]}"; do printf ' %-16s %8s\n' "$(echo "$r"|awk '{print $2}')" "$(echo "$r"|awk '{print $3}')"; done
|
||||
echo " -> $best_lbl, margin ${margin}x"
|
||||
if [ -n "$expect" ]; then
|
||||
if [ "$best_lbl" = "$expect" ]; then echo " CONTROL PASSED (expected $expect)"; return 0
|
||||
else echo " 🔴 CONTROL FAILED: expected $expect, got $best_lbl"; return 1; fi
|
||||
fi
|
||||
# A thin margin means the frame does not decide it. 2x is below the 4.7x the
|
||||
# control achieves and well above 1.0; a shot that cannot beat it should be
|
||||
# re-taken rather than guessed at.
|
||||
python3 -c "import sys; sys.exit(0 if $margin >= 2.0 else 1)" || {
|
||||
echo " ⚠️ margin under 2x -- this frame does not decide it. Do not act on this."; return 1; }
|
||||
}
|
||||
|
||||
if [ "$screen" = main_menu ]; then
|
||||
echo "control -- $CAPS/live-main-menu-options-focused.png (answer is in the filename):"
|
||||
verdict "$CAPS/live-main-menu-options-focused.png" OPTIONS || {
|
||||
echo "refusing to report a result from a method that just failed its control." >&2; exit 1; }
|
||||
echo
|
||||
fi
|
||||
# The exit code must carry the refusal. An earlier version printed "do not act on
|
||||
# this" and exited 0, so a caller scripting this -- which is the entire point,
|
||||
# the Decoder runs it between drive steps -- would have read a refusal as an
|
||||
# answer. That is the same defect as a checker claiming a check it skipped.
|
||||
echo "$shot:"
|
||||
rc=0
|
||||
verdict "$shot" || rc=$?
|
||||
echo "artifacts in $OUT"
|
||||
exit $rc
|
||||
Reference in New Issue
Block a user