re: per-record strength measured, but the run does not reproduce

The link from the previous iteration gives a per-record live strength, and the
measurement is internally sound: 11 records at 2, 2 at 8 and 11 at 12 sums to
exactly the 170 craft counted, with 24 of 42 records deployed.

It does not reproduce. The previous run saw 116 roster records and 300 craft
with strengths 2/4/8/18; this one sees 42 and 170 with 2/8/12 -- same disc, same
save slot, same launch script, same stage.

Two explanations were checked and both fail. The save has not drifted:
game01/savedata is unmodified since 2026-08-23 and only the profile .gpd files
were written today. The guest was not frozen: the pilot's telemetry over the
same 190 s shows speed varying across dozens of values and a live engagement
with an e007 Turret at 259-680 m.

So the discrepancy is real and unexplained, and until it is understood
per-record counts cannot be used as a time series. Recorded as the blocker
rather than worked around. The same within-run-only discipline already noted for
the global craft count now applies one level down.

Also recorded: zero arrivals and zero losses across 190 s. That is weak evidence
against clock-driven arrivals at t = 90/120/170 seconds, since a 0 -> n
transition should have appeared and none did. It is no test of the event-gated
model at all, because nothing was killed -- fc=0, no record lost strength, and
the player's hull never moved off 1500. The pilot closes to 259 m and misses, so
the gap is accuracy rather than engagement.
This commit is contained in:
Sylpheed RE agent
2026-08-24 14:04:24 +00:00
parent 46e511e791
commit f137d3eae3
4 changed files with 205 additions and 2 deletions

View File

@@ -347,8 +347,17 @@ search cannot find a *schedule*.
Fan-in: only **41 of 116** records have craft (2×24, 4×1, 8×4, 18×12 = 300).
⇒ **an arrival is craft appearing for a record that had none; a kill is that
count decreasing — both attributable to a named squadron**, hence to a route.
**Next:** watch PER-RECORD craft counts over a mission (not the global total);
that is the kill-vs-no-kill test with squadron resolution.
🔴 **BLOCKER (2026-08-24): the per-record measurement does not reproduce.**
[`mission-per-record-strength.md`](mission-per-record-strength.md) — one run
gives 116 records/300 craft, the next 42/170, same disc, save and script. Save
drift is REFUTED (savedata untouched since 2026-08-23) and the guest was not
frozen (pilot telemetry shows live engagement). Also: **zero arrivals and zero
losses in 190 s of hunting** — weak evidence against clock arrivals at
t=90/120/170 s, and no test of event-gating at all because nothing was killed
(`fc=0`, hull never moved). **Next: (1)** sample the roster-record count
repeatedly WITHIN one run to tell a load race from a run-specific constant;
**(2)** get a confirmed kill — the pilot reaches 259 m and still misses, so the
gap is accuracy, not engagement.
* 🟡 **COMPETING MODEL (user, 2026-08-24): waves are event-gated, not
clock-driven** — released by kills/objectives rather than at a fixed time, with
the route's start time being a post-trigger delay. Fits the evidence better:

View File

@@ -0,0 +1,74 @@
# Per-record craft strength — the measurement works, the run does not reproduce
Status: ✅ the per-record measurement is internally consistent; 🔴 it does **not**
reproduce across runs, and that is now the blocker; 🔴 zero arrivals and zero
losses in 190 s of hunting, which does not test the event-gated model.
Follows [roster-to-craft-link.md](roster-to-craft-link.md): craft point at
`roster_base + 0x08`, so a member's live strength is how many craft point at it,
and an arrival should be a `0 → n` transition on one record.
## ✅ The measurement itself is sound
`tools/re-capture/wave5_probe.py` counted craft per roster record every ~20 s.
The first sample:
```
craft=170 deployed records=24/42 strength histogram [(2, 11), (8, 2), (12, 11)]
```
11·2 + 2·8 + 11·12 = **170**, exactly the craft total. The per-record decomposition
is internally consistent, and 24 of 42 records deployed with 18 holding nothing
is the same shape as the previous run's 41 of 116.
## 🔴 But the numbers do not reproduce across runs
| run | roster records | craft | deployed | strengths |
|---|---|---|---|---|
| link2 (earlier) | **116** | **300** | 41 | 2, 4, 8, **18** |
| wave5 (this one) | **42** | **170** | 24 | 2, 8, **12** |
Same disc, same save slot, same launch script, same stage.
**The save has not drifted** — that was the first suspicion and it is refuted:
`content/.../game01/savedata` is unmodified since 2026-08-23 16:37; only the
profile `.gpd` files were written today.
**The guest was not frozen** — the pilot's telemetry over the same 190 s shows
speed varying across dozens of values and a live engagement (`tgt=e007_ADAN_Turret`
at 259680 m), so the game was advancing normally.
So the discrepancy is real and unexplained. Candidates, none tested:
* the probe samples at a different point in the mission's deployment and the
roster-record set is still being built (argued against by the counts being
*completely static* for 190 s afterwards — if it were still loading, it should
have grown toward 116);
* `gmem.extents()` races the emulator's allocation, so the vtable scan sees a
different set of extents between runs;
* deployment strength genuinely varies per run (turret squadrons at 18 vs 12).
Until this is understood, **per-record counts cannot be used as a time series**,
and no cross-run comparison of them means anything. This is the same discipline
already recorded for the global craft count, now needed one level down.
## 🔴 Zero arrivals, zero losses in 190 s
Every sample was identical: 170 craft, 24 deployed, no `0 → n`, no `n → n1`.
* **Against the clock model**, weakly: if phase-1 arrivals were at t = 90/120/170
seconds, a `0 → n` should have appeared, and none did.
* **It does not test the event-gated model at all**, because nothing was killed.
The pilot engaged continuously — closing to 259 m on turrets — but `fc=0`, no
record lost strength, and the player's own hull never moved off 1500. It shot
and missed, which `pilot.py`'s own notes say has happened before.
## What is needed next
1. **Explain the 42-vs-116 discrepancy** before trusting any per-record series.
Cheapest test: sample the roster-record count repeatedly *within one run*,
starting the instant flight is detected. If it climbs, it is a load race; if
it is flat at a run-specific value, it is not.
2. **Get a confirmed kill.** Without one, the kill-versus-no-kill experiment
cannot run. The pilot reaches firing range and does not destroy anything, so
the gap is accuracy, not engagement.

104
tools/re-capture/wave5_probe.py Executable file
View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Per-record craft counts over a mission: catch arrivals and kills by squadron.
The link is a pointer at roster_base + 0x08, verified 300/300 by independent
unit-type agreement (roster-to-craft-link.md). So a roster member's live
strength is simply how many craft point at it:
0 -> n the member deployed (an ARRIVAL)
n -> n-1 one of its craft died (a KILL)
Both are attributable to a UnitGroup member, which is what ties an observation
back to Route_S<NN>.tbl. This watches those counts instead of any global total.
"""
import os, sys, time, struct, collections, importlib.util
SD = __file__.rsplit('/', 1)[0]
sys.path.insert(0, SD)
import gmem, gworld, entities2
_w3 = importlib.util.spec_from_file_location('w3', SD + '/wave3_probe.py')
wave3 = importlib.util.module_from_spec(_w3); _w3.loader.exec_module(wave3)
ROSTER_VT = struct.pack('>I', 0x820AF030)
DELTA, WIN, LINK = 0x130, 0x400, 0x08
def scan_vt(fd, size, vt):
out = []
for a, b in gmem.extents(fd, size):
pos = a
while pos < b:
n = min(1 << 24, b - pos)
blob = os.pread(fd, n, pos)
i = blob.find(vt)
while i != -1:
if (pos + i) % 4 == 0: out.append(pos + i)
i = blob.find(vt, i + 1)
pos += n
return sorted(out)
def counts(fd, defs, want):
"""craft per roster VA, this instant"""
lo, hi = gmem.va_to_off(entities2.ENT_VA_LO), gmem.va_to_off(entities2.ENT_VA_HI)
per = collections.Counter(); pos = lo; n = 0
while pos < hi:
m = min(1 << 24, hi - pos)
blob = os.pread(fd, m, pos)
for k in range(0, len(blob) - 3, 4):
if blob[k:k+4] in defs:
base = pos + k - DELTA
n += 1
head = os.pread(fd, WIN, base)
for j in range(0, len(head) - 3, 4):
(p,) = struct.unpack_from('>I', head, j)
if p in want:
per[want[p]] += 1
break
pos += m
return per, n
def main():
secs = int(sys.argv[1]) if len(sys.argv) > 1 else 200
every = int(sys.argv[2]) if len(sys.argv) > 2 else 15
w = gworld.World(); fd = w.fd
defs = entities2.definitions(w)
if not defs: print('NOT IN A MISSION'); return 2
roster = scan_vt(fd, w.size, ROSTER_VT)
f = os.fdopen(os.dup(fd), 'rb')
label, want = {}, {}
for o in roster:
va = gmem.primary_va(o)
if va is None: continue
label[va] = wave3.resolve_id(f, w.size, o)[0] or '?'
want[va + LINK] = va
print('roster records: %d' % len(label))
log = open('/tmp/wave5-per-record.tsv', 'w')
log.write('t\tva\tunit\tfrom\tto\n')
prev, total0 = None, None
t0 = time.time()
while time.time() - t0 < secs:
per, n = counts(fd, defs, want)
el = round(time.time() - t0)
if prev is None:
print('t=%4ds craft=%d deployed records=%d/%d strength histogram %s'
% (el, n, len(per), len(label),
sorted(collections.Counter(per.values()).items())))
else:
arr = [(va, per[va]) for va in per if prev.get(va, 0) == 0 and per[va] > 0]
kil = [(va, prev[va], per[va]) for va in prev
if per.get(va, 0) < prev[va]]
print('t=%4ds craft=%d deployed=%d ARRIVALS=%d losses=%d'
% (el, n, len(per), len(arr), len(kil)), flush=True)
for va, c in arr:
print(' ARRIVAL %#010x %-30s 0 -> %d' % (va, label.get(va, '?'), c))
log.write('%d\t%#x\t%s\t0\t%d\n' % (el, va, label.get(va, '?'), c))
for va, a, b in kil:
print(' loss %#010x %-30s %d -> %d' % (va, label.get(va, '?'), a, b))
log.write('%d\t%#x\t%s\t%d\t%d\n' % (el, va, label.get(va, '?'), a, b))
log.flush()
prev = per
time.sleep(max(0, every - (time.time() - t0 - el)))
log.close()
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -u
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
export PYTHONPATH=/sylph-home/.local/lib/python3.12/site-packages
SD="$(cd "$(dirname "$0")" && pwd)"
SECS="${1:-180}"; EVERY="${2:-10}"; HUNT="${3:-1}"
CFG=/tmp/nav-live.json
"$SD/launch_mission.sh" fly || { echo "BOOT FAILED"; exit 1; }
if python3 "$SD/entities2.py" self 0x130 "$CFG" >/dev/null 2>&1; then
SYLPH_HUNT="$HUNT" SYLPH_KILL_TURRETS=1 nohup python3 "$SD/pilot.py" "$CFG" "$SECS" \
</dev/null >/tmp/live-pilot.log 2>&1 &
PILOT=$!; echo "--- pilot (SYLPH_HUNT=$HUNT)"
else PILOT=""; echo "--- BIND FAILED, no pilot"; fi
python3 "$SD/wave5_probe.py" ; rc=$?
[ -n "$PILOT" ] && kill "$PILOT" 2>/dev/null
echo "LIVENESS DONE rc=$rc"