Both hand attempts at this address managed exactly one filter and then lost the counter, because every reading of the HUD costs a human round trip. ob_read.py does it by template correlation over three fixed digit cells, and ob_hunt.py uses that to run the whole method unattended: confirm the HUD on both sides of the 0.9 s scan, then filter on each following transition, labelling the first as the one it selected on and the rest as verification. Two things measured rather than assumed: * Normalisation is the point, not a nicety. The plate is translucent, so an explosion turns it orange - on the 008 reference frame a cyan-stroke mask finds 6 of ~70 stroke columns and would silently read nothing. Per-cell mean/std normalisation reads it correctly at 0.843. * The accept rule is two-sided because the margin is narrow: over 12 cells from four frames of known value the correct digit scores 0.843..1.000 and the best WRONG digit reaches 0.789 (0 and 8 are similar outlines). So a floor of 0.80 AND a 0.05 margin over the runner-up, against a smallest observed correct margin of 0.093. A bare threshold fitted between those two numbers would be fitted to twelve samples. Templates exist for 0 1 2 4 8 - the digits actually seen. Anything else reads as "?" and callers must treat a "?" as unknown, never as a value. Rejects both negatives tested: GAME OVER scores 0.05-0.18, the main menu 0.07-0.14. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
100 lines
3.6 KiB
Python
Executable File
100 lines
3.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Find `REMAINING OB` in RAM by value-scanning against the HUD, automatically.
|
|
|
|
The method is `ob_scan.py`'s, and the two traps it records are what this
|
|
automates away, because both were paid twice by hand:
|
|
|
|
* **Confirm the HUD on BOTH sides of the scan.** The scan itself takes ~0.9 s,
|
|
but the counter climbs 004 -> 008 -> 012 within the first minutes of Stage 02,
|
|
and a scan taken across a step selects nothing. A pass whose before- and
|
|
after-readings disagree is thrown away and retried rather than used.
|
|
* **Verify across a transition you did NOT select on.** The first filter only
|
|
narrows; the SECOND one is the evidence. Two hand attempts each managed one
|
|
filter and then lost the counter — once because the pilot stopped killing
|
|
anything, once because the mission ended.
|
|
|
|
The HUD is read by `ob_read.py`, which returns `?` rather than a wrong digit, so
|
|
an unreadable frame (explosion, GAME OVER) stalls the hunt instead of poisoning
|
|
it.
|
|
|
|
Usage: ob_hunt.py <out-dir> [transitions] [timeout_s]
|
|
"""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import gmem # noqa: E402
|
|
import ob_read # noqa: E402
|
|
import ob_scan # noqa: E402
|
|
|
|
|
|
def hud(shot):
|
|
subprocess.run(["screenshot", shot], stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL)
|
|
txt, _ = ob_read.read(shot)
|
|
return int(txt) if txt.isdigit() else None
|
|
|
|
|
|
def main():
|
|
out = sys.argv[1]
|
|
want = int(sys.argv[2]) if len(sys.argv) > 2 else 2
|
|
deadline = time.time() + (float(sys.argv[3]) if len(sys.argv) > 3 else 900)
|
|
os.makedirs(out, exist_ok=True)
|
|
mem = gmem.mem_path()
|
|
log = lambda *a: print(*a, flush=True)
|
|
|
|
# --- the scan, with the HUD confirmed on both sides -----------------------
|
|
cands = None
|
|
while cands is None and time.time() < deadline:
|
|
v0 = hud(f"{out}/scan-before.png")
|
|
if v0 is None:
|
|
time.sleep(3)
|
|
continue
|
|
offs = ob_scan.scan(mem, v0)
|
|
v1 = hud(f"{out}/scan-after.png")
|
|
if v1 != v0:
|
|
log(f"scan at {v0} discarded: HUD moved to {v1} across it")
|
|
continue
|
|
cands = offs
|
|
log(f"scanned at {v0:03d}: {len(cands)} candidates")
|
|
if cands is None:
|
|
log("never got a clean scan")
|
|
return 1
|
|
|
|
# --- filter on each following transition ---------------------------------
|
|
prev, done, results = v0, 0, []
|
|
while done < want and time.time() < deadline:
|
|
time.sleep(5)
|
|
v = hud(f"{out}/poll.png")
|
|
if v is None or v == prev:
|
|
continue
|
|
# Confirm the new value before spending a filter on it.
|
|
time.sleep(1.5)
|
|
if hud(f"{out}/poll2.png") != v:
|
|
continue
|
|
vals = ob_scan.read_at(mem, cands)
|
|
cands = [o for o, x in vals.items() if x == v]
|
|
done += 1
|
|
kind = "SELECTED ON" if done == 1 else "verification (unselected)"
|
|
log(f"{prev:03d} -> {v:03d} [{kind}] {len(cands)} survive")
|
|
results.append({"from": prev, "to": v, "survivors": len(cands)})
|
|
prev = v
|
|
if not cands:
|
|
log("no survivors — the scan value or a reading was wrong")
|
|
break
|
|
|
|
payload = {"scan_value": v0, "transitions": results,
|
|
"survivors": [{"off": o, "va": gmem.primary_va(o)} for o in sorted(cands)]}
|
|
json.dump(payload, open(f"{out}/hunt.json", "w"), indent=1)
|
|
for o in sorted(cands):
|
|
log(f" va {gmem.primary_va(o):#x}")
|
|
log(f"wrote {out}/hunt.json")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|