This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/tools/re-capture/ob_hunt.py
Sylpheed RE agent abd72a2c24 tools: read REMAINING OB off the HUD, so the scan can follow the counter
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
2026-08-23 18:19:29 +00:00

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())