re: widen the OB scan to seven encodings; run inconclusive, probe lacked a witness

ob_by_hud.py now scans seven readings of the same bytes and keeps a separate
candidate set for each, as byte offsets: u32 big and little endian, u16 both
endiannesses at both alignments, and u8. The big-endian u32 reading had been
refuted, so widening rather than assuming is the point.

u32le is much the tightest at 154 candidates against u32be's 4452. That is a
hint about the encoding rather than a result, since a rarer bit pattern narrows
faster regardless of meaning.

The run is inconclusive. The HUD read 4 at every sample, so there was no second
value to collapse the sets against, and from t=136 the candidate counts are
byte-identical across five samples in all seven encodings, which is what a
frozen guest looks like -- nothing in 32 MB changed at all. The probe had no
stall witness, so the run cannot prove it either way. One is added now.

Worth stating plainly: this is the fourth probe written without a witness and
the third whose flat output could not be distinguished from a freeze. Each time
the fix gets applied to that one script. The durable fix is the shared probe
harness already noted in this file, and the lesson recurring four times is
itself the argument for building it.

What the hunt needs is unchanged: two HUD readings at different values in
non-stalled samples. The counter moves on kills, which lands back on the combat
limit, though the earlier 4 to 11 observation shows it does move.
This commit is contained in:
Sylpheed RE agent
2026-08-24 23:14:59 +00:00
parent 696119b9e6
commit a040cad0da
3 changed files with 117 additions and 12 deletions

View File

@@ -620,6 +620,16 @@ search cannot find a *schedule*.
(template strip covers only **0 1 2 4 8**; most samples read `00?`/`???`), or a (template strip covers only **0 1 2 4 8**; most samples read `00?`/`???`), or a
counter that can rise. **Next: widen the scan to u16/u8 and LE, and beyond the counter that can rise. **Next: widen the scan to u16/u8 and LE, and beyond the
heap** — one function, no combat cost. Also extend `ob_digits.png`. heap** — one function, no combat cost. Also extend `ob_digits.png`.
* 🔴 **(2026-08-24) Widened to 7 encodings; run inconclusive.** `ob_by_hud.py`
now keeps a candidate set per encoding (`u32be/le`, `u16be/le` at both
alignments, `u8`) as byte offsets. **`u32le` is tightest at 154** vs u32be's
4452 — a hint, not a result. 🔴 HUD read **4 at every sample**, so nothing
collapsed; and counts were **byte-identical across five samples in all seven
encodings** from t=136 s, which is what a freeze looks like. The probe had **no
witness** — now added. ⚠️ **Fourth probe written without one, third flat run
that cannot be told from a freeze**; the recurring fix is the shared harness
noted earlier. Needs two HUD readings at *different* values in non-stalled
samples.
* ~~🚧 BLOCKER: t=210/240 unreachable in one turn~~ — **superseded, see above**; * ~~🚧 BLOCKER: t=210/240 unreachable in one turn~~ — **superseded, see above**;
it rested on an untested assumption that a turn is one shell call. 595 s shell cap ~220 s boot (a ~190 s title movie that cannot be it rested on an untested assumption that a turn is one shell call. 595 s shell cap ~220 s boot (a ~190 s title movie that cannot be
tapped through) ~25 s startup = **~350 s observation ≈ 193 game-seconds**. tapped through) ~25 s startup = **~350 s observation ≈ 193 game-seconds**.

View File

@@ -215,3 +215,46 @@ kill-driven route it costs no combat.
Also worth extending `ob_digits.png` with the missing digits — most samples were Also worth extending `ob_digits.png` with the missing digits — most samples were
unreadable, which is why only two data points survived a 480 s run. unreadable, which is why only two data points survived a 480 s run.
---
# Widened to every plausible encoding (2026-08-24)
`ob_by_hud.py` now scans seven readings of the same bytes and keeps a separate
candidate set for each, expressed as byte offsets:
`u32be`, `u32le`, `u16be`, `u16be@1`, `u16le`, `u16le@1`, `u8`.
First run, HUD showing **4** throughout:
```
t= 7s u32be:6285 u32le:414 u16be:10668 u16be@1:2718 u16le:656 u16le@1:9012 u8:25559
t=110s u32be:4472 u32le:156 u16be: 8794 u16be@1:1386 u16le:286 u16le@1:8124 u8:20381
t=136s u32be:4452 u32le:154 u16be: 8773 u16be@1:1380 u16le:284 u16le@1:8119 u8:20346
t=162s .. t=267s identical to t=136 in every encoding
```
**`u32le` is much the tightest at 154**, an order of magnitude below `u32be`.
That is a hint about the encoding, not a result — a rarer bit pattern narrows
faster regardless of meaning.
## 🔴 Inconclusive, and probably a frozen guest
The HUD read **4** at every sample, so there was no second value to collapse the
sets against. Worse, from t = 136 s the candidate counts are **byte-identical
across five samples in all seven encodings**, which is what a frozen guest looks
like — nothing in 32 MB changed at all.
`ob_by_hud.py` had **no stall witness**, so the run cannot prove it. Added now.
**This is the fourth probe written without a witness and the third whose flat
output could not be told from a freeze.** Each time the fix is applied to that
one script. The durable fix is the shared harness already noted in this file, and
the fact that the lesson keeps recurring is itself the argument for building it.
## What is needed
Two HUD readings at **different** values, in non-stalled samples. The counter
changes on kills, so this lands back on the combat limit — unless a phase change
or another event moves it. The earlier 4 → 11 observation shows it does move,
which is what makes the approach worth continuing.

View File

@@ -24,7 +24,30 @@ def region(fd):
out, pos = bytearray(), lo out, pos = bytearray(), lo
while pos < hi: while pos < hi:
n = min(1 << 24, hi - pos); out += os.pread(fd, n, pos); pos += n n = min(1 << 24, hi - pos); out += os.pread(fd, n, pos); pos += n
return np.frombuffer(bytes(out), dtype='>u4'), lo return bytes(out), lo
# The big-endian u32 reading was refuted: no word held 4 then 11. Widen rather
# than assume -- the counter may be narrower, little-endian, or unaligned. Each
# encoding keeps its own candidate set, expressed as BYTE OFFSETS so the answer
# is directly usable whichever one wins.
ENCODINGS = [
('u32be', '>u4', 4, 0), ('u32le', '<u4', 4, 0),
('u16be', '>u2', 2, 0), ('u16be@1', '>u2', 2, 1),
('u16le', '<u2', 2, 0), ('u16le@1', '<u2', 2, 1),
('u8', 'u1', 1, 0),
]
def matches(buf, value):
"""byte offsets whose value equals `value`, per encoding"""
out = {}
for name, dt, w, skew in ENCODINGS:
if value > (1 << (8 * w)) - 1:
continue
n = (len(buf) - skew) // w * w
a = np.frombuffer(buf[skew:skew + n], dtype=dt)
idx = np.nonzero(a == value)[0]
out[name] = idx.astype(np.int64) * w + skew
return out
def hud_value(): def hud_value():
subprocess.run(['screenshot', SHOT], capture_output=True, timeout=60) subprocess.run(['screenshot', SHOT], capture_output=True, timeout=60)
@@ -40,6 +63,19 @@ def main():
secs = int(sys.argv[1]) if len(sys.argv) > 1 else 240 secs = int(sys.argv[1]) if len(sys.argv) > 1 else 240
every = int(sys.argv[2]) if len(sys.argv) > 2 else 25 every = int(sys.argv[2]) if len(sys.argv) > 2 else 25
w = gworld.World(); fd = w.fd w = gworld.World(); fd = w.fd
# Witness. This is the FOURTH probe written without one, and the third to
# produce a run whose flat output could not be distinguished from a freeze.
# The recurring fix is a shared harness; until that exists, carry it.
b0, _ = region(fd); time.sleep(3.0); b1, _ = region(fd)
a0 = np.frombuffer(b0, dtype='>u4').astype(np.int64)
a1 = np.frombuffer(b1, dtype='>u4').astype(np.int64)
d = a1 - a0
rates = collections.Counter((d[(d > 15) & (d < 600)] // 3).tolist())
band = [r for r in rates if 8 <= r <= 40]
pick = max(band, key=lambda r: rates[r]) if band else None
ticks = np.nonzero(d // 3 == pick)[0][:32] if pick is not None else np.array([], dtype=int)
print('tick witnesses: %d at %s/s' % (len(ticks), pick), flush=True)
last_t = a1[ticks] if len(ticks) else None
cand = None; base = None; seen = [] cand = None; base = None; seen = []
t0 = time.time() t0 = time.time()
while time.time() - t0 < secs: while time.time() - t0 < secs:
@@ -48,23 +84,39 @@ def main():
if v is None: if v is None:
print(' t=%4ds HUD unreadable (%s)' % (el, note), flush=True) print(' t=%4ds HUD unreadable (%s)' % (el, note), flush=True)
else: else:
r, base = region(fd) buf, base = region(fd)
hit = set(np.nonzero(r == v)[0].tolist()) hit = matches(buf, v)
cand = hit if cand is None else (cand & hit) if cand is None:
cand = hit
else:
cand = {k: np.intersect1d(cand[k], hit[k], assume_unique=True)
for k in cand if k in hit}
seen.append(v) seen.append(v)
print(' t=%4ds HUD=%-4d words==%d: %-8d -> candidates %d (%s)' st = ''
% (el, v, v, len(hit), len(cand), note), flush=True) if len(ticks):
if len(cand) <= 40 and len(set(seen)) >= 2: now_t = np.frombuffer(buf, dtype='>u4').astype(np.int64)[ticks]
if int((now_t > last_t).sum()) == 0: st = ' *** GUEST STALLED ***'
last_t = now_t
print(' t=%4ds HUD=%-4d %s%s (%s)'
% (el, v, ' '.join('%s:%d' % (k, len(cand[k])) for k in cand), st, note),
flush=True)
live = {k: c for k, c in cand.items() if len(c)}
if live and len(set(seen)) >= 2 and min(len(c) for c in live.values()) <= 40:
break break
time.sleep(every) time.sleep(every)
print('\nHUD values seen: %s' % sorted(set(seen))) print('\nHUD values seen: %s' % sorted(set(seen)))
if cand and base is not None: if cand and base is not None:
print('candidates: %d' % len(cand)) for k in sorted(cand, key=lambda k: len(cand[k])):
for i in sorted(cand)[:20]: c = cand[k]
va = gmem.primary_va(base + i * 4) print(' %-8s %d candidate(s)' % (k, len(c)))
print(' va %s' % (('%#010x' % va) if va else '?')) for off in c[:8]:
va = gmem.primary_va(base + int(off))
print(' va %s' % (('%#010x' % va) if va else '?'))
if all(len(c) == 0 for c in cand.values()):
print('\nEVERY encoding eliminated -- the counter is not in this region '
'in any of them, or a HUD reading is wrong.')
else: else:
print('no candidates (HUD never read, or value never matched a u32)') print('no candidates (HUD never read)')
return 0 return 0
if __name__ == '__main__': if __name__ == '__main__':