re: add a hunting pilot; withdraw the "10 of 116 records are dynamic" result

The user asked for an actively hunting pilot, since a player who kills nothing
cannot trigger an event-gated wave and both previous runs used the survival
pilot.

pilot.py gains SYLPH_HUNT=1. The substantive change is which contacts ENGAGE may
shoot: it previously skipped every "hard" target -- "turrets and hulls are not
the objective" -- and stood off 2500 units from turrets, on the assumption that
an e007 Turret is an AA mount on a capital ship. It is a craft, one of the main
enemy types of the first six missions, and at 100 HP the cheapest kill on the
field. Under SYLPH_HUNT it is a target and the keep-out drops to 600. The run
confirms the pilot engages: steady ENGAGE, fire=1, committed to an
e010_ADAN_Attacker_S at ~2.2 km, hull and escorted asset untouched over 160 s.

Withdrawn: "only 10 of 116 records ever changed a byte in 170 s". This run
measured 41-56 records changing in every 10 s tick. The old figure does not
reproduce.

I cannot say why, because I changed two variables at once -- the record bound
(fixed 0x200 to bounded-by-next-record) and the pilot (survival to hunting).
Either explains it. That is a design error, and the honest outcome is a
retraction without a replacement explanation rather than a story that fits.
The conclusion it had supported is unaffected: the roster identity now rests on
the exact 10-of-10 unit-composition match measured independently.

Still open, and explicitly not concluded: the pilot's own entity scan shows ADAN
drifting 147 -> 129 -> 142, and the late rise has the shape of an arrival, but
the sample-to-sample swing is +/-10, the same size as the effect. AGENT.md warns
that polling faster than the guest updates manufactures a curve out of noise, so
no wave conclusion is drawn. The run probably did not kill anything either
(fc=0, asset at 100%), so it does not test the event-gated model. A stable
per-record liveness field and a working kill counter are both needed first;
REMAINING OB at 0xbdb59668 still does not read as a counter.
This commit is contained in:
Sylpheed RE agent
2026-08-24 12:45:43 +00:00
parent 2030324419
commit 08352d09ac
5 changed files with 167 additions and 6 deletions

View File

@@ -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 4156 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

View File

@@ -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 4156 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.

View File

@@ -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:

54
tools/re-capture/wave4_probe.py Executable file
View File

@@ -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())

View File

@@ -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" \
</dev/null >/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"