ob_bitflag.py is the follow-on the word-level refutation named: for every 4-byte offset in the window and every one of its 32 bits, count how many entities have it set, keep the pairs whose count is exactly the counter, and require them to match again after a transition. Both polarities, since an objective could be marked by a bit that is CLEAR on it. Three runs, no verification, and the reasons are recorded: run 1 gave 187 + 33 candidates at counter 4 and then reported "the counter never moved" for 700 s - about a mission that had ENDED in GAME OVER partway through; run 2 hit the same dead mission; run 3 had the counter at a different address (the guard refused, correctly) and then froze after one filter. The hole is closed. frozen() asks whether the guest is ANIMATING, and the GAME OVER screen animates happily - mean colour (114,22,63) - so every liveness check passed while the mission was over. frozen.in_flight() classifies the screen with screen_id, and ob_hunt/ob_flag/ob_bitflag now abort with NO LONGER IN FLIGHT. That is the second confident negative in this investigation that was really about a dead world, so the rule is written down: before believing "X never happened", show that the thing that would produce X was still running. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
93 lines
3.7 KiB
Python
Executable File
93 lines
3.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Is the guest still ANIMATING, or has the mission frozen?
|
|
|
|
Measured 2026-08-23: a Stage 02 run entered flight, drew a correct HUD, and then
|
|
stopped advancing about ten seconds later. `screen_id.py` still said `flight`,
|
|
the emulator still burned 212 % CPU, `pilot.py` still logged 5 900 samples, and
|
|
every one of them carried the same speed, yaw and pitch — 724 s of identical
|
|
state. Two screenshots six seconds apart were **byte-identical**, max difference
|
|
0 over 863 325 pixels.
|
|
|
|
That is worth its own test because of what it costs when missed: an experiment
|
|
that waits for `REMAINING OB` to change will wait out its whole timeout and then
|
|
report "the counter never moved", which reads as a fact about the game and is
|
|
really a fact about a dead world. Nothing else in the toolkit notices — the
|
|
screen classifier, the liveness check and the CPU are all happy.
|
|
|
|
A frozen frame is EXACTLY identical, not merely similar: this is a stopped
|
|
simulation, not a still scene, so no tolerance is needed and none is used. The
|
|
one thing that must be excluded is a screenshot that failed.
|
|
|
|
Usage: frozen.py [gap_s] -> prints frozen|animating, exit 0 if FROZEN
|
|
frozen.py --pair <a.png> <b.png> -> same test on two saved frames, so the
|
|
NEGATIVE side can be checked without
|
|
a live game to be un-frozen in
|
|
"""
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
from PIL import Image, ImageChops
|
|
|
|
|
|
def grab(path):
|
|
subprocess.run(["screenshot", path], stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL)
|
|
return Image.open(path).convert("RGB")
|
|
|
|
|
|
def frozen(gap=6.0):
|
|
a = grab("/tmp/frz-a.png")
|
|
time.sleep(gap)
|
|
b = grab("/tmp/frz-b.png")
|
|
if a.size != b.size:
|
|
return None, -1
|
|
bbox = ImageChops.difference(a, b).getbbox()
|
|
px = list(ImageChops.difference(a.convert("L"), b.convert("L")).getdata())
|
|
return (bbox is None), max(px)
|
|
|
|
|
|
def compare(pa, pb):
|
|
a, b = Image.open(pa).convert("RGB"), Image.open(pb).convert("RGB")
|
|
if a.size != b.size:
|
|
return None, -1
|
|
px = list(ImageChops.difference(a.convert("L"), b.convert("L")).getdata())
|
|
return (ImageChops.difference(a, b).getbbox() is None), max(px)
|
|
|
|
|
|
def in_flight(shot="/tmp/frz-flight.png"):
|
|
"""Is the flight HUD still on screen?
|
|
|
|
`frozen()` answers "is the guest animating", and that is NOT the same
|
|
question: the GAME OVER screen animates happily, and a run that ends there
|
|
passes every liveness check while the mission is over. One bit-level
|
|
differential spent its whole 700 s window reporting "the counter never
|
|
moved" about a world that had stopped being a mission — mean colour
|
|
(114, 22, 63), the magenta GAME OVER plate, not flight.
|
|
|
|
Classified by `screen_id.py`'s whole-image statistics, for the reasons its
|
|
own header gives.
|
|
"""
|
|
import subprocess as _sp
|
|
import os as _os
|
|
_sp.run(["screenshot", shot], stdout=_sp.DEVNULL, stderr=_sp.DEVNULL)
|
|
here = _os.path.dirname(_os.path.abspath(__file__))
|
|
out = _sp.run(["python3", _os.path.join(here, "screen_id.py"), shot],
|
|
capture_output=True, text=True).stdout.split()
|
|
return bool(out) and out[0] == "flight"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) > 1 and sys.argv[1] == "--pair":
|
|
f, mx = compare(sys.argv[2], sys.argv[3])
|
|
gap = "pair"
|
|
else:
|
|
gap = float(sys.argv[1]) if len(sys.argv) > 1 else 6.0
|
|
f, mx = frozen(gap)
|
|
if f is None:
|
|
print("unknown (screenshot failed)")
|
|
sys.exit(2)
|
|
suffix = gap if gap == "pair" else f"{gap}s"
|
|
print(f"{'frozen' if f else 'animating'} max_pixel_delta={mx} gap={suffix}")
|
|
sys.exit(0 if f else 1)
|