#!/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 -> 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)