diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 663694f2..13a300ba 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -332,8 +332,11 @@ search cannot find a *schedule*. moves in one and not the other. * 🔴 **Diffing inside the 116 records did not find the arrival flag** (2026-08-24, same doc). 10 of 116 records are dynamic, 106 never change a byte - in 170 s — more support for the pre-allocated roster — and STRONGER than - first written: the "turrets don't move" hedge was withdrawn after the user + in 170 s — 🔴 **the "10 of 116 dynamic" figure is WITHDRAWN** — a hunting + run measured 41–56 records changing per tick; I had changed two variables at + once (record bound and pilot) so the discrepancy is unattributed. The roster + identity is unaffected: it now rests on the 10/10 unit-composition match. + Earlier note, kept for history: the "turrets don't move" hedge was withdrawn after the user pointed out that early-mission "Turret" is a craft type, which the data confirms (`UN_e007_ADAN_Turret` flies under `AI_ADAN_CraftSquadron_*`, never `AI_Structure`). Lesson: check a unit's `AIID`, not its English name. No field transitions in groups of 3 at the diff --git a/docs/re/mission-wave-arrivals.md b/docs/re/mission-wave-arrivals.md index 62ca7801..00c30842 100644 --- a/docs/re/mission-wave-arrivals.md +++ b/docs/re/mission-wave-arrivals.md @@ -313,3 +313,76 @@ records it recurring in about 5 runs of 7), and this was one of the misses, so the run cannot say whether the pilot killed anything. Re-hunting the counter with `ob_hunt.py` is a precondition for the kill-versus-no-kill test, not an optional extra. + +--- + +# Hunting run (2026-08-24) — and a withdrawal + +Status: ✅ a hunting pilot exists and engages; 🔴 the "10 of 116 records are +dynamic" result is **withdrawn**; ❔ the wave question is still open, and the +entity scan is too noisy to settle it. + +The user asked for an actively hunting pilot, with attention to the objectives, +because the two previous runs used the *survival* pilot and a player who kills +nothing cannot trigger an event-gated wave. + +## ✅ `SYLPH_HUNT=1` + +`pilot.py` gained a hunt mode. The change that matters is which contacts ENGAGE +is allowed to shoot: it previously skipped every "hard" target with the comment +*"turrets and hulls are not the objective"*, and kept 2500 units away from +turrets. That was written on the assumption that an `e007` "Turret" is an +anti-aircraft mount on a capital ship. It is not — it is a craft, one of the main +enemy types of the first six missions +(`AI_ADAN_CraftSquadron_*`, `Type = Squad`), and at 100 HP the cheapest kill on +the field. Under `SYLPH_HUNT=1` turrets are targets and the keep-out drops to +600. + +The run confirms it flies and shoots: steady `ENGAGE`, `fire=1`, committed to +`e010_ADAN_Attacker_S` at ~2.2 km, hull 1500/1500 and escorted asset at 100 % +throughout 160 s. + +## 🔴 Withdrawn: "only 10 of 116 records are dynamic" + +The previous section reported that 10 of 116 records ever changed a byte in +170 s, and used it as supporting evidence for the pre-allocated roster. **This +run measured 41–56 records changing in every single 10 s tick.** The earlier +figure does not reproduce and is withdrawn. + +I cannot say why, because I changed two things at once — the record bound (fixed +`0x200` → bounded by the next record's address, capped at `0x400`) *and* the +pilot (survival → hunting). Either could explain it: the old window mis-framed +every record whose true size is not `0x200`, and a hunting pilot flies into +traffic that an evading one avoids. **That is a design error on my part**: a run +that changes two variables cannot attribute its own result, and the honest +outcome is that the old number is retracted without a replacement explanation. + +The conclusion the retracted number was supporting is unaffected — the +roster identity now rests on the exact 10-of-10 unit-composition match, which is +far stronger evidence and was measured independently. + +## ❔ The entity scan is too noisy to answer the wave question + +`pilot.py`'s own scanner reports a live entity count each tick. Over the run: + +``` +t=0s 168 (147 ADAN) t=56s 147 (131) t=101s 145 (129) +t=20s 171 (147) t=76s 155 (131) t=137s 151 (129) +t=30s 148 (132) t=86s 147 (131) t=157s 166 (142) +``` + +ADAN drifts 147 → 129 and back to 142. The late rise is the shape an arrival +would have, but the sample-to-sample swing is ±10 or more, which is the same +size as the effect. This is exactly the trap `docker/agent/AGENT.md` warns about +— polling faster than the guest updates manufactures a curve out of noise — so +**no wave conclusion is drawn from it.** + +Two things are needed before this run type can settle the question: + +1. A *stable* liveness signal — a per-record field that means alive/dead, read + from the 116 labelled records, rather than a re-scan whose population changes + between samples. +2. A kill count. `fc=0` in the pilot's telemetry and the asset at 100 % suggest + the hunt did not actually destroy anything in 160 s, in which case this run + does not test the event-gated model either. `REMAINING OB` at `0xbdb59668` + still did not read as a counter, so that check remains unavailable. diff --git a/tools/re-capture/pilot.py b/tools/re-capture/pilot.py index 980b08a1..9a0ad3b5 100644 --- a/tools/re-capture/pilot.py +++ b/tools/re-capture/pilot.py @@ -113,13 +113,18 @@ ASSET_NAME = os.environ.get("SYLPH_ASSET", "Acropolis") KILL_TURRETS = os.environ.get("SYLPH_KILL_TURRETS") == "1" +HUNT = os.environ.get("SYLPH_HUNT") == "1" + + class Pilot: KP, KD = 2.2, 0.45 FIRE_CONE = math.radians(9) # fallback only; the real gate is angular size FIRE_RANGE = SHELL_MAX_RANGE # the shells simply do not arrive past this CONE_MIN = math.radians(2.0) CONE_MAX = math.radians(25.0) # close-in the target subtends a lot; let it - TURRET_KEEPOUT = 2500.0 # ...and stay this far from things that shoot back + # 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 EVADE_QUIET = 5.0 # seconds without damage before re-engaging RETIRE_FRAC = 0.30 # hull fraction that sends us home HZ = 8.0 @@ -355,11 +360,20 @@ class Pilot: return tgt def pick(self, me_p, me_v, fwd, hos): - """Nearest *fighter*, weighted by how far off the nose it is.""" + """Nearest *fighter*, weighted by how far off the nose it is. + + SYLPH_HUNT=1 also lets ENGAGE take turrets. The exclusion below was + written believing an e007 "Turret" is an anti-aircraft mount bolted to a + capital ship. It is not -- it is a craft, one of the main enemy types of + the first six missions, flown by AI_ADAN_CraftSquadron_* (Type=Squad, + full manoeuvre-weight block) and never by AI_Structure. At 100 HP it is + also the cheapest kill on the field, which is what a run needs when the + question is whether kills release the next wave. + """ best, bestscore = None, 1e18 for off, nm, p, v, r, hard in hos: - if hard: - continue # turrets and hulls are not the objective + if hard and not HUNT: + continue # capital-ship hulls are not the objective rel = p - me_p d = float(np.linalg.norm(rel)) if d < 1e-3: diff --git a/tools/re-capture/wave4_probe.py b/tools/re-capture/wave4_probe.py new file mode 100755 index 00000000..7cf79dfc --- /dev/null +++ b/tools/re-capture/wave4_probe.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Hunting run: does killing things make more records go active? + +Uses the labelling and the MEASURED stride from wave3_probe.py -- each record is +bounded by the next record's address, not by a constant. Reports, per tick, how +many records changed and which unit types they are, so a rise in activity can be +tied to the hunt rather than to the clock. +""" +import os, sys, time, struct, collections, importlib.util +SD = __file__.rsplit('/', 1)[0] +spec = importlib.util.spec_from_file_location('gmem', SD + '/gmem.py') +gmem = importlib.util.module_from_spec(spec); spec.loader.exec_module(gmem) +w3 = importlib.util.spec_from_file_location('w3', SD + '/wave3_probe.py') +wave3 = importlib.util.module_from_spec(w3); w3.loader.exec_module(wave3) + +MAXREC = 0x400 + +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 10 + path = gmem.mem_path() + fd = os.open(path, os.O_RDONLY); size = os.fstat(fd).st_size + f = os.fdopen(os.dup(fd), 'rb') + offs = wave3.find_records(f, fd, size) + if not offs: print('NO ENTITY RECORDS'); return 2 + lens = [min(MAXREC, offs[i+1] - offs[i]) for i in range(len(offs)-1)] + [0x100] + ids = [wave3.resolve_id(f, size, o)[0] or '?' for o in offs] + print('records: %d, labelled %d' % (len(offs), sum(1 for i in ids if i != '?'))) + print('composition:', collections.Counter(ids).most_common(6)) + prev = [wave3.rd(f, o, l) for o, l in zip(offs, lens)] + log = open('/tmp/wave4-activity.tsv', 'w'); log.write('t\tactive\tunits\n') + t0 = time.time(); series = [] + while time.time() - t0 < secs: + time.sleep(every) + el = round(time.time() - t0) + act = [] + for k, (o, l) in enumerate(zip(offs, lens)): + cur = wave3.rd(f, o, l) + if cur != prev[k]: act.append(k) + prev[k] = cur + types = collections.Counter(ids[k] for k in act) + series.append((el, len(act))) + line = '%d\t%d\t%s\n' % (el, len(act), dict(types)) + log.write(line); log.flush() + print(' t=%4ds active=%3d %s' % (el, len(act), + [(u.replace('UN_',''), c) for u, c in types.most_common(5)]), flush=True) + log.close() + print('\nactive-record series:', series) + a = [n for _, n in series] + if a: print('active: min=%d max=%d first=%d last=%d' % (min(a), max(a), a[0], a[-1])) + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/re-capture/wave4_session.sh b/tools/re-capture/wave4_session.sh new file mode 100755 index 00000000..af1445a8 --- /dev/null +++ b/tools/re-capture/wave4_session.sh @@ -0,0 +1,17 @@ +#!/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:-200}"; EVERY="${2:-10}" +CFG=/tmp/nav-wave4.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=1 SYLPH_KILL_TURRETS=1 nohup python3 "$SD/pilot.py" "$CFG" "$SECS" \ + /tmp/wave4-pilot.log 2>&1 & + PILOT=$!; echo "--- HUNTING pilot (SYLPH_HUNT=1)" +else PILOT=""; echo "--- BIND FAILED, no pilot"; fi +python3 "$SD/wave4_probe.py" "$SECS" "$EVERY"; rc=$? +[ -n "$PILOT" ] && kill "$PILOT" 2>/dev/null +echo "--- pilot tail ---"; tail -12 /tmp/wave4-pilot.log 2>/dev/null +echo "WAVE4 DONE rc=$rc"