tools+docs: the bit-level OB test, and a liveness hole - GAME OVER animates
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
This commit is contained in:
@@ -208,3 +208,46 @@ by position changing, so a sample that lands on a load or loses the race with
|
||||
another scanner returns nothing — and a zero-entity sample silently yields zero
|
||||
candidates and a run that proves nothing. Seen exactly that way once, with the
|
||||
following sample finding 81 entities moments later.
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-24 — the bit-level test exists; three runs, no verification, and one hole closed
|
||||
|
||||
[`ob_bitflag.py`](../../tools/re-capture/ob_bitflag.py) is the follow-on the
|
||||
word-level refutation named: for every 4-byte offset in the window (now
|
||||
`-0x400 … +0xC00`) and every one of its 32 bits, count how many entities have it
|
||||
**set**, keep the `(offset, bit)` pairs whose count is exactly the counter, and
|
||||
require the survivors to match again after the counter moves. Both polarities are
|
||||
counted, because an objective could equally be marked by a bit that is *clear* on
|
||||
it.
|
||||
|
||||
It ran three times and **verified nothing**. What each attempt cost is the useful
|
||||
part:
|
||||
|
||||
| run | sample A | outcome |
|
||||
|---|---|---|
|
||||
| 1 | counter 4, 29 entities → **187** set-polarity + **33** clear-polarity candidates | "counter never moved" for the whole 700 s window — **and the mission had ended in GAME OVER partway through** |
|
||||
| 2 | — | HUD unreadable: still the GAME OVER screen |
|
||||
| 3 (fresh boot) | — | the counter was at a **different address**; the guard refused to run. `ob_hunt` got one filter (`008 → 012`, 4 survivors including `0xbdb58668`) and then the guest froze |
|
||||
|
||||
### 🔴 The hole run 1 exposed: "animating" is not "still in a mission"
|
||||
|
||||
`frozen.py` asks whether the guest is advancing. The **GAME OVER screen animates
|
||||
perfectly happily** — mean colour (114, 22, 63), the magenta plate — so every
|
||||
liveness check passed while the mission was over, and the tool reported *"the
|
||||
counter never moved"*, which reads as a fact about the game and was a fact about
|
||||
a finished one.
|
||||
|
||||
Closed: `frozen.in_flight()` classifies the screen with `screen_id.py`, and
|
||||
`ob_hunt.py` / `ob_flag.py` / `ob_bitflag.py` now abort with **`NO LONGER IN
|
||||
FLIGHT`** instead of waiting out their windows. That is the second time a
|
||||
confident negative in this investigation turned out to be about a dead world —
|
||||
the first was the freeze — and the pattern is worth naming: *before believing
|
||||
"X never happened", show that the thing that would produce X was still running.*
|
||||
|
||||
### Attrition is now the dominant cost
|
||||
|
||||
Of the Stage 02 runs that have reached flight, roughly half end early — a freeze,
|
||||
or a GAME OVER when the escorted ACROPOLIS or the craft is lost — and the scan
|
||||
needs the run to survive **two** counter transitions. Nothing here fixes that; it
|
||||
is why three attempts produced one filter between them.
|
||||
|
||||
@@ -55,6 +55,28 @@ def compare(pa, pb):
|
||||
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])
|
||||
|
||||
174
tools/re-capture/ob_bitflag.py
Executable file
174
tools/re-capture/ob_bitflag.py
Executable file
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Is `REMAINING OB` the population of a per-entity *bit*?
|
||||
|
||||
[`ob_flag.py`](ob_flag.py) refuted the word-level version: no 4-byte word near an
|
||||
entity's position holds a common value on exactly the objective entities, and its
|
||||
two candidates both died on the `12 → 11` transition. But that test asks which
|
||||
entities share an **exact 32-bit value**, and it names its own blind spot — a
|
||||
single bit ORed into a word that also carries health, a timer or a state machine
|
||||
would never produce a shared value and could not be seen.
|
||||
|
||||
This is that test at bit granularity: for every 4-byte offset in the window and
|
||||
every one of its 32 bits, count how many entities have the bit SET, keep the
|
||||
(offset, bit) pairs whose count is exactly the counter, and then require the
|
||||
survivors to match again after the counter moves — the same "verify across a
|
||||
transition you did not select on" rule, because there will be thousands of
|
||||
coincidences at N alone.
|
||||
|
||||
Both polarities are counted: an objective might be marked by a bit that is set on
|
||||
it, or by one that is CLEAR on it and set on everything else.
|
||||
|
||||
🔴 Same limits as the word-level test, and they still bound the answer:
|
||||
`entities2.typed` types entities by their position CHANGING, so a stationary
|
||||
objective is invisible; and anything outside the window is untested.
|
||||
|
||||
Usage: ob_bitflag.py <out-dir> [timeout_s]
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import entities2 # noqa: E402
|
||||
import frozen # noqa: E402
|
||||
import gmem # noqa: E402
|
||||
import gworld # noqa: E402
|
||||
import ob_read # noqa: E402
|
||||
|
||||
VA = int(os.environ.get("OB_VA", "0xBDB59668"), 0)
|
||||
LO, HI = -0x400, 0xC00 # window around the position triple
|
||||
DELTA = 0x130
|
||||
|
||||
|
||||
def counter(fd):
|
||||
return struct.unpack(">I", os.pread(fd, 4, gmem.va_to_off(VA)))[0]
|
||||
|
||||
|
||||
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 sample(w, defs, tag):
|
||||
"""(counter, entity names, words[n_entities, n_words] as uint32)."""
|
||||
fd = w.fd
|
||||
n = counter(fd)
|
||||
movers = entities2.moving(fd, w.size)
|
||||
ents = entities2.typed(fd, defs, movers, DELTA)
|
||||
uniq = {}
|
||||
for off, nm, pos, sp in ents:
|
||||
uniq.setdefault((nm, tuple(round(c, 1) for c in pos)), (off, nm))
|
||||
ents = list(uniq.values())
|
||||
rows, names = [], []
|
||||
for off, nm in ents:
|
||||
blob = os.pread(fd, HI - LO, off + LO)
|
||||
if len(blob) != HI - LO:
|
||||
continue
|
||||
rows.append(np.frombuffer(blob, dtype=">u4"))
|
||||
names.append(nm)
|
||||
print(f"[{tag}] counter={n} entities={len(rows)}", flush=True)
|
||||
return n, names, (np.array(rows, dtype=np.uint32) if rows else np.zeros((0, 0), np.uint32))
|
||||
|
||||
|
||||
def bit_counts(words):
|
||||
"""counts[bit, word] = how many entities have that bit set."""
|
||||
out = np.zeros((32, words.shape[1]), dtype=np.int32)
|
||||
for b in range(32):
|
||||
out[b] = ((words >> np.uint32(b)) & np.uint32(1)).sum(axis=0)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
out = sys.argv[1]
|
||||
deadline = time.time() + (float(sys.argv[2]) if len(sys.argv) > 2 else 700)
|
||||
os.makedirs(out, exist_ok=True)
|
||||
w = gworld.World()
|
||||
defs = entities2.definitions(w)
|
||||
|
||||
v = None
|
||||
for _ in range(8):
|
||||
v = hud(f"{out}/a.png")
|
||||
n0 = counter(w.fd)
|
||||
if v is not None:
|
||||
break
|
||||
time.sleep(3)
|
||||
print(f"HUD={v} RAM={n0}", flush=True)
|
||||
if v is None or v != n0:
|
||||
print("HUD and RAM disagree (or unreadable) — re-scan with ob_hunt.py",
|
||||
flush=True)
|
||||
return 2
|
||||
|
||||
for _ in range(6):
|
||||
nA, namesA, wordsA = sample(w, defs, "A")
|
||||
if len(namesA) >= 10:
|
||||
break
|
||||
print("[A] too few entities — retrying", flush=True)
|
||||
time.sleep(5)
|
||||
if len(namesA) < 10:
|
||||
print("[A] never got a usable entity sample", flush=True)
|
||||
return 4
|
||||
|
||||
cA = bit_counts(wordsA)
|
||||
nA_ents = len(namesA)
|
||||
setA = np.argwhere(cA == nA) # bit set on exactly N entities
|
||||
clrA = np.argwhere(cA == nA_ents - nA) # bit CLEAR on exactly N
|
||||
print(f"[A] (offset,bit) pairs set on exactly {nA}: {len(setA)}; "
|
||||
f"clear on exactly {nA}: {len(clrA)}", flush=True)
|
||||
|
||||
stuck = 0
|
||||
while time.time() < deadline:
|
||||
time.sleep(5)
|
||||
if counter(w.fd) != nA:
|
||||
break
|
||||
stuck += 1
|
||||
if stuck % 12 == 0:
|
||||
# Two different ways the wait can be pointless, and only one of them
|
||||
# is a freeze: a run that ends in GAME OVER keeps ANIMATING, so
|
||||
# frozen() is happy while the mission is over. One window was spent
|
||||
# reporting "the counter never moved" about exactly that.
|
||||
if frozen.frozen(6.0)[0]:
|
||||
print("GUEST FROZEN — the counter was never going to move",
|
||||
flush=True)
|
||||
return 3
|
||||
if not frozen.in_flight():
|
||||
print("NO LONGER IN FLIGHT — the mission ended; this window "
|
||||
"proves nothing", flush=True)
|
||||
return 5
|
||||
nB = counter(w.fd)
|
||||
if nB == nA:
|
||||
print("counter never moved — no verification possible", flush=True)
|
||||
return 1
|
||||
vb = hud(f"{out}/b.png")
|
||||
print(f"counter {nA} -> {nB} (HUD {vb})", flush=True)
|
||||
|
||||
nB2, namesB, wordsB = sample(w, defs, "B")
|
||||
cB = bit_counts(wordsB)
|
||||
nB_ents = len(namesB)
|
||||
surv_set = [(int(b), int(o)) for b, o in setA if cB[b, o] == nB2]
|
||||
surv_clr = [(int(b), int(o)) for b, o in clrA if cB[b, o] == nB_ents - nB2]
|
||||
print(f"[B] survivors — set-polarity {len(surv_set)}, "
|
||||
f"clear-polarity {len(surv_clr)}", flush=True)
|
||||
rows = ([{"polarity": "set", "offset": LO + o * 4, "bit": b} for b, o in surv_set]
|
||||
+ [{"polarity": "clear", "offset": LO + o * 4, "bit": b} for b, o in surv_clr])
|
||||
json.dump({"nA": nA, "nB": nB2, "hud_a": v, "hud_b": vb,
|
||||
"entities_a": nA_ents, "entities_b": nB_ents,
|
||||
"candidates_a": {"set": len(setA), "clear": len(clrA)},
|
||||
"survivors": rows}, open(f"{out}/bitflag.json", "w"), indent=1)
|
||||
for r in rows[:40]:
|
||||
print(f" {r['polarity']:5} pos{r['offset']:+#07x} bit {r['bit']:2d}",
|
||||
flush=True)
|
||||
if len(rows) > 40:
|
||||
print(f" ... and {len(rows)-40} more (see bitflag.json)", flush=True)
|
||||
print(f"wrote {out}/bitflag.json", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -150,10 +150,16 @@ def main():
|
||||
if counter(w.fd) != nA:
|
||||
break
|
||||
stuck += 1
|
||||
if stuck % 12 == 0 and frozen.frozen(6.0)[0]:
|
||||
print("GUEST FROZEN — the world stopped advancing, so the counter "
|
||||
"was never going to move; this run proves nothing", flush=True)
|
||||
if stuck % 12 == 0:
|
||||
if frozen.frozen(6.0)[0]:
|
||||
print("GUEST FROZEN — the world stopped advancing, so the "
|
||||
"counter was never going to move; this run proves "
|
||||
"nothing", flush=True)
|
||||
return 3
|
||||
if not frozen.in_flight():
|
||||
print("NO LONGER IN FLIGHT — the mission ended; this window "
|
||||
"proves nothing", flush=True)
|
||||
return 5
|
||||
nB = counter(w.fd)
|
||||
if nB == nA:
|
||||
print("counter never moved — no verification possible", flush=True)
|
||||
|
||||
@@ -78,9 +78,13 @@ def main():
|
||||
# world stopped ~10 s into flight and everything downstream --
|
||||
# screen_id, the liveness check, the CPU -- stayed happy.
|
||||
stuck += 1
|
||||
if stuck % 12 == 0 and frozen.frozen(6.0)[0]:
|
||||
if stuck % 12 == 0:
|
||||
if frozen.frozen(6.0)[0]:
|
||||
log("GUEST FROZEN — the world stopped advancing; abandoning")
|
||||
break
|
||||
if not frozen.in_flight():
|
||||
log("NO LONGER IN FLIGHT — the mission ended; abandoning")
|
||||
break
|
||||
continue
|
||||
stuck = 0
|
||||
# Confirm the new value before spending a filter on it.
|
||||
|
||||
Reference in New Issue
Block a user