diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 978b13c3..1e317a48 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -382,8 +382,17 @@ search cannot find a *schedule*. probe duration.** 🔴 Harness bug found+fixed: a `sed` had stripped the probe args, so every derived probe ran on defaults — the previous "cut by the turn timeout" claim was wrong (it hit its own 240 s default). - **Next: a pilot that kills AND survives** (hunt turrets but keep evade/retire; - keep-out between 600 and 2500) — tuning, not discovery. + ✅🔴 **DONE + REFUTED (2026-08-24)**: `SYLPH_KEEPOUT=1400` gives a pilot that + kills and survives (hull 1500, asset 100 %, 8 losses). An enemy squadron was + **eliminated** — `e007 Turret 2→0` at t=163 s, `deployed` 41→40, first ever — + and **no arrival followed** in ~90 s of valid observation (two further losses in + that window prove the mission was live). ⇒ *"a wave is released when a squadron + is wiped out"* is **refuted**; event-gating generally is not (thresholds, + objectives, specific squadrons untested). 🔴 **Guest stalled at ~t=255 s** — + last 400 pilot lines have 1 distinct speed vs 236 in the first 400 — so trailing + flat samples in ANY earlier run may be a frozen guest, not a quiet mission. + Probe now carries a frame-rate tick witness printing `*** GUEST STALLED ***` + (implemented, **not yet run**). ⚠️ The ~210 s title movie at boot is the binding constraint on observable game time per turn. Earlier framing: diff --git a/docs/re/mission-arrival-watch.md b/docs/re/mission-arrival-watch.md index 1349ac71..4381ad27 100644 --- a/docs/re/mission-arrival-watch.md +++ b/docs/re/mission-arrival-watch.md @@ -200,3 +200,75 @@ extremes: survival mode kills nothing in 240 s, hunt mode kills steadily and die at 83 s. A middle setting — hunt turrets but keep the evade/retire behaviour, or a keep-out between 600 and 2500 — is the obvious next step, and it is a tuning change rather than a new discovery. + +--- + +# ✅ The elimination test ran — and refutes the elimination trigger (2026-08-24) + +Status: ✅ an enemy squadron was wiped out and observed; 🔴 **no arrival +followed**, so "a wave is released when a squadron is eliminated" is refuted for +this case; ✅ the keep-out tuning gave a surviving pilot that still kills; +🔴 the guest stalled late in the run, which bounds the valid window and is now +detected automatically. + +## ✅ The tuning worked + +`SYLPH_KEEPOUT` makes the hunt keep-out a knob rather than a hard-coded 600. +At **1400** the pilot both kills and survives — hull 1500 and escorted asset +100 % for the entire run, `ENGAGE` throughout, 8 loss events against 7 in the +run where it died at t = 83 s. + +## ✅ An enemy squadron reached zero + +``` +t= 30s loss UN_e007_ADAN_Turret 18 -> 10 +t= 44s loss UN_e007_ADAN_Turret 10 -> 8 +t= 96s loss UN_e007_ADAN_Turret 8 -> 4 +t=124s loss UN_e007_ADAN_Turret 4 -> 2 +t=150s loss UN_e010_ADAN_Attacker_S 8 -> 6 +t=163s loss UN_e007_ADAN_Turret 2 -> 0 <-- ELIMINATED, deployed 41 -> 40 +t=176s loss UN_f106_TCAF_Destroyer 4 -> 2 +t=202s loss UN_e007_ADAN_Turret 18 -> 16 +``` + +An enemy squadron destroyed outright, for the first time in nine runs. + +## 🔴 No arrival followed — the elimination trigger is refuted + +`ARRIVALS=0` at every sample, including all of those after t = 163. The +hypothesis from the previous iteration — that a wave is released when a squadron +is *wiped out* rather than merely damaged — does not survive its first test. + +**The window matters and is smaller than it looks:** valid observation after the +elimination is about **90 seconds**, not the 143 s the log appears to show, for +the reason below. Within that 90 s two further losses occurred (t = 176, t = 202), +so the mission was demonstrably still live and still processing kills — it simply +produced no arrival. + +This refutes elimination-of-one-squadron as *the* trigger. It does not refute +event-gating generally: a threshold across several squadrons, an objective +completion, or a specific squadron could all still be the gate. + +## 🔴 The guest stalled at ~t = 255 s, and flat samples look identical to a quiet mission + +The pilot's own telemetry gives it away: the **last 400 log lines contain one +distinct speed value, against 236 in the first 400**, with no timestamp gaps. +The process kept logging; the game stopped advancing. + +Every sample after roughly t = 255 s is therefore a frozen guest, not a quiet +mission — and nothing in the probe's output distinguished the two. The same +ambiguity affects any earlier run's trailing flat samples. + +**Fixed:** `wave6_probe.py` now locates a counter that advances at frame rate +(the technique from [mission-clock-advances.md](mission-clock-advances.md)), +samples it every tick, and prints `*** GUEST STALLED ***` when it fails to +advance. This makes every future run self-validating. It is implemented but has +**not yet run**, so it is unverified. + +## What is still open + +* Whether any event gates arrivals — elimination of a single squadron is out, + but thresholds, objectives and specific squadrons are untested. +* Whether an arrival is observable at all. Nine runs, zero `0 → n`. +* Re-examining earlier "flat" results now that a stall and a quiet mission are + known to look the same. diff --git a/tools/re-capture/pilot.py b/tools/re-capture/pilot.py index 9a0ad3b5..d1e9c483 100644 --- a/tools/re-capture/pilot.py +++ b/tools/re-capture/pilot.py @@ -123,8 +123,12 @@ class Pilot: CONE_MIN = math.radians(2.0) CONE_MAX = math.radians(25.0) # close-in the target subtends a lot; let it # Keep-out from things that shoot back. Under SYLPH_HUNT the turrets ARE the - # targets, so standing off 2.5 km from them just guarantees no kills. - TURRET_KEEPOUT = 600.0 if HUNT else 2500.0 + # targets, so standing off 2.5 km from them guarantees no kills -- but 600 + # cost the pilot its life at t=83 s and froze the mission for the remaining + # 220 s of the run (mission-arrival-watch.md). The usable observation window + # is player survival, so this is a real trade and not a free knob. Tunable. + TURRET_KEEPOUT = float(os.environ.get( + "SYLPH_KEEPOUT", "1400" if HUNT else "2500")) EVADE_QUIET = 5.0 # seconds without damage before re-engaging RETIRE_FRAC = 0.30 # hull fraction that sends us home HZ = 8.0 diff --git a/tools/re-capture/wave6_probe.py b/tools/re-capture/wave6_probe.py index 92b6b16e..525bca73 100755 --- a/tools/re-capture/wave6_probe.py +++ b/tools/re-capture/wave6_probe.py @@ -19,6 +19,11 @@ _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) +# A guest that has stalled produces flat samples that read exactly like a quiet +# mission -- that happened at t~255 in the keep-out run and was only caught by +# eyeballing the pilot log afterwards. timer_probe found counters advancing at +# frame rate; sampling one of them makes every run self-validating. +TICK_LO, TICK_HI = 0xBC000000, 0xBE000000 DELTA, WIN, LINK = 0x130, 0x400, 0x08 BASELINE = 116 @@ -36,6 +41,20 @@ def scan_vt(fd, size, vt): pos += m return sorted(out) +def find_tick(fd, dt=3.0): + """One word that advances steadily: the guest-is-running witness.""" + lo, hi = gmem.va_to_off(TICK_LO), gmem.va_to_off(TICK_LO + 0x400000) + a = os.pread(fd, hi - lo, lo) + time.sleep(dt) + b = os.pread(fd, hi - lo, lo) + best = None + for k in range(0, min(len(a), len(b)) - 3, 4): + va = struct.unpack_from('>I', a, k)[0] + vb = struct.unpack_from('>I', b, k)[0] + if va < vb and (vb - va) / dt > 5 and (vb - va) < 1 << 20: + best = (lo + k, (vb - va) / dt); break + return best + def sample(fd, defs, want): lo, hi = gmem.va_to_off(entities2.ENT_VA_LO), gmem.va_to_off(entities2.ENT_VA_HI) per = collections.Counter(); tot = 0; pos = lo @@ -72,6 +91,10 @@ def main(): if va is None: continue label[o] = wave3.resolve_id(f, w.size, o)[0] or '?' want[va + LINK] = o + tick = find_tick(fd) + if tick: print('tick witness at %#x, ~%.1f/s' % (tick[0], tick[1])) + else: print('WARNING: no advancing counter found; stalls cannot be detected') + last_tick = struct.unpack('>I', os.pread(fd, 4, tick[0]))[0] if tick else 0 log = open('/tmp/wave6.tsv', 'w'); log.write('t\tkind\toff\tunit\tfrom\tto\n') prev = None; t0 = time.time(); arr_n = loss_n = 0 while time.time() - t0 < secs: @@ -85,8 +108,14 @@ def main(): arr = [(o, per[o]) for o in per if prev.get(o, 0) == 0 < per[o]] los = [(o, prev[o], per.get(o, 0)) for o in prev if per.get(o, 0) < prev[o]] arr_n += len(arr); loss_n += len(los) - print('t=%4ds craft=%d deployed=%d ARRIVALS=%d losses=%d (cum %d/%d)' - % (el, tot, len(per), len(arr), len(los), arr_n, loss_n), flush=True) + stalled = '' + if tick: + now = struct.unpack('>I', os.pread(fd, 4, tick[0]))[0] + if now <= last_tick: stalled = ' *** GUEST STALLED ***' + last_tick = now + print('t=%4ds craft=%d deployed=%d ARRIVALS=%d losses=%d (cum %d/%d)%s' + % (el, tot, len(per), len(arr), len(los), arr_n, loss_n, stalled), + flush=True) for o, c in arr: print(' ARRIVAL %-30s 0 -> %d' % (label.get(o, '?'), c), flush=True) log.write('%d\tarrival\t%#x\t%s\t0\t%d\n' % (el, o, label.get(o, '?'), c)) diff --git a/tools/re-capture/wave6_session.sh b/tools/re-capture/wave6_session.sh index d4a2497c..053f582f 100755 --- a/tools/re-capture/wave6_session.sh +++ b/tools/re-capture/wave6_session.sh @@ -7,7 +7,7 @@ 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" \ + SYLPH_HUNT="$HUNT" SYLPH_KILL_TURRETS=1 SYLPH_KEEPOUT="${SYLPH_KEEPOUT:-1400}" nohup python3 "$SD/pilot.py" "$CFG" "$SECS" \ /tmp/live-pilot.log 2>&1 & PILOT=$!; echo "--- pilot (SYLPH_HUNT=$HUNT)" else PILOT=""; echo "--- BIND FAILED, no pilot"; fi