The briefing map is cyan and satisfies every clause of the menu rule (b-r > 30, r < 45, little white), with no earlier rule claiming it -- so it was labelled `menu`. That made wait_screen.sh report NEVER REACHED READY ROOM on a run that had successfully done LOAD GAME -> slot 01 -> YES and was three screens further on: a working route scored as a failed one, pointing the next debugging step at an input path that was fine. Cyan has b and g nearly equal (b-g ~ 5) where the menu's blue leads its green (b-g ~ 32), so `r < 20 and g > 30 and b - g < 20` separates them; the r floor keeps the title screen out. The file's own docstring already carried the briefing's mean as an aside -- it just never had a class. Verified against all eight signatures the file documents (2 menu variants, title, ready room, flight, 3 briefing measurements): no regressions, and the captured briefing image now reads `briefing`.
130 lines
5.7 KiB
Python
Executable File
130 lines
5.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Identify which game screen a screenshot shows, without fixed pixel positions.
|
|
|
|
The nav scripts used to test named pixels ("648,221 is white"). That works only
|
|
while the game image sits at a known place on the root window, and it does not:
|
|
xenia's GTK window has a menu bar, so on some displays the image is ~25 px lower
|
|
and every constant reads the wrong row. The failure is silent and expensive — a
|
|
run sat on a plainly visible MAIN MENU for 300 s reporting "no main menu", and a
|
|
later one missed the title screen so the attract movie looped for ten minutes.
|
|
|
|
So identify screens by WHOLE-IMAGE statistics instead, which no vertical shift
|
|
(or scale, or letterbox) can move:
|
|
|
|
green fraction of pixels that are the game's green UI text/HUD colour
|
|
white fraction of near-white pixels
|
|
mean per-channel mean
|
|
|
|
Measured on known-good captures of each screen (1280x720, no menu bar):
|
|
|
|
title green 0.11% white 7.4% mean (62, 75, 84) blue-ish, bright
|
|
main menu green 0.04% white 3.5% mean (25, 38, 70) dark, strongly blue
|
|
in flight green 1.2% white 3.4% mean (54, 39, 37) green HUD everywhere
|
|
movie green 0% white varies no green at all
|
|
|
|
Usage: screen_id.py <png> [--json]
|
|
Prints one of: title | menu | briefing | readyroom | flight | other, plus the features.
|
|
"""
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
|
|
# Downscaled first: the features are area fractions, so 320x180 gives the same
|
|
# answer for a fraction of the work (a full-size pure-python pass costs seconds,
|
|
# and these oracles are polled once a second).
|
|
W, H = 320, 180
|
|
|
|
|
|
def features(path):
|
|
raw = subprocess.run(
|
|
["convert", path, "-alpha", "off", "-resize", f"{W}x{H}!", "-depth", "8",
|
|
"rgb:-"], capture_output=True).stdout
|
|
n = len(raw) // 3
|
|
if not n:
|
|
return None
|
|
green = white = 0
|
|
sr = sg = sb = 0
|
|
for i in range(0, n * 3, 3):
|
|
r, g, b = raw[i], raw[i + 1], raw[i + 2]
|
|
sr += r; sg += g; sb += b
|
|
if g > 130 and g - r > 45 and g - b > 45:
|
|
green += 1
|
|
if r > 230 and g > 230 and b > 230:
|
|
white += 1
|
|
return {"green": green / n, "white": white / n,
|
|
"r": sr / n, "g": sg / n, "b": sb / n}
|
|
|
|
|
|
def classify(f):
|
|
if f is None:
|
|
return "none"
|
|
# Flight first: the HUD paints far more green than any menu.
|
|
# (READY ROOM is checked before the menu rule because it is also dark-ish
|
|
# and strongly blue, and would otherwise be swallowed by it.)
|
|
if f["green"] > 0.004:
|
|
return "flight"
|
|
# The READY ROOM is far the brightest blue of any 2D screen: measured
|
|
# (12.7, 36.5, 133.5) on two independent captures, byte-identical because
|
|
# the screen is static. The next-bluest thing on the load path is the save
|
|
# list at (16.7, 41.7, 81.0), and the mission briefing is cyan rather than
|
|
# blue at (4.6, 47.7, 54.5), so a floor of b > 110 separates all three with
|
|
# room to spare. Worth having as its own class: `launch_mission.sh` used a
|
|
# fixed `sleep 28` for this transition and its next press was swallowed,
|
|
# which put one run in OPTIONS and the next in BRIEFINGS.
|
|
if f["b"] > 110 and f["r"] < 40 and f["b"] - f["g"] > 60:
|
|
return "readyroom"
|
|
# The mission BRIEFING map is cyan, not blue, and without this rule it falls
|
|
# straight through to the menu test below (dark, b - r > 30, r < 45, little
|
|
# white -- all true of it). MEASURED 2026-08-26: that mislabel made
|
|
# `wait_screen.sh readyroom` report NEVER REACHED READY ROOM on a run that
|
|
# had in fact navigated LOAD GAME -> slot 01 -> YES and was sitting on the
|
|
# briefing three screens further on, which is the worst kind of failure --
|
|
# a successful route scored as a failed one.
|
|
#
|
|
# What separates them is that cyan has blue and green nearly equal, while
|
|
# the menu's blue runs far ahead of its green:
|
|
#
|
|
# briefing (1.4, 45.2, 49.6) (5.5, 49.1, 54.3) (4.6, 47.7, 54.5) b-g ~ 5
|
|
# main menu (25, 38, 70) (13, 26, 59) b-g ~ 32
|
|
#
|
|
# The r < 20 floor keeps the title screen (52.8, 66.1, 74.5, b-g 8.4) out;
|
|
# the ready room (b-g ~ 97) and flight (green rule) are already claimed above.
|
|
if f["r"] < 20 and f["g"] > 30 and f["b"] - f["g"] < 20:
|
|
return "briefing"
|
|
# The main menu is dark and strongly blue. TWO measured signatures, not one:
|
|
#
|
|
# white 3.5% mean (25, 38, 70) the 2026-07 reference at the top
|
|
# white 0.03% mean (13, 26, 59) measured 2026-08-18, twice, on the menu
|
|
# reached from the boot title
|
|
#
|
|
# The second one classified as "other" under the original rule's
|
|
# `0.015 < white` floor, which is worse than it sounds: a script that waits
|
|
# for "menu" then never sees it reports the navigation as FAILED while the
|
|
# menu is plainly on screen. Both variants are dark, strongly blue and
|
|
# essentially green-free, so key on that and let the near-white fraction be
|
|
# anything below the title's 3.8%.
|
|
if f["b"] - f["r"] > 30 and f["r"] < 45 and f["white"] < 0.075:
|
|
return "menu"
|
|
# The title is brighter, still blue-ish, and carries the green PRESS A text.
|
|
if f["green"] > 0.0004 and f["b"] > 60 and f["white"] > 0.03:
|
|
return "title"
|
|
return "other"
|
|
|
|
|
|
def main():
|
|
path = sys.argv[1]
|
|
f = features(path)
|
|
name = classify(f)
|
|
if "--json" in sys.argv:
|
|
print(json.dumps({"screen": name, **(f or {})}))
|
|
elif f:
|
|
print(f"{name} green={f['green']:.4f} white={f['white']:.4f} "
|
|
f"mean=({f['r']:.1f},{f['g']:.1f},{f['b']:.1f})")
|
|
else:
|
|
print(name)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|