diff --git a/docs/re/captures/mission01-t485.png b/docs/re/captures/mission01-t485.png new file mode 100644 index 0000000..b2a234e Binary files /dev/null and b/docs/re/captures/mission01-t485.png differ diff --git a/docs/re/mission-outcome-stage02.md b/docs/re/mission-outcome-stage02.md new file mode 100644 index 0000000..67f7b5c --- /dev/null +++ b/docs/re/mission-outcome-stage02.md @@ -0,0 +1,89 @@ +# Why Stage 02 is never won — the escort sinks at ~11 minutes (2026-08-10) + +**Status: ✅ measured, one 500 s run.** The standing open item since 2026-07-29 was +"no mission completed". This is the first session whose deliverable was the +*ending* rather than a measurement, and it settles why: **the mission is lost +before it can be won, and the pilot's survival policy is what guarantees it.** + +Run: `tools/re-capture/mission_run.sh 500 mission01` — boot → Stage 02 in flight → +`pilot.py` (escort-weighted targeting, target commitment, guided missiles) for +500 s, with every entity's hull sampled at 2 Hz and a screenshot every 30 s. +Artifacts at `/sylph-home/re/mission01/` (9 MB `mission.jsonl`, not committed). + +## The escort's decay is linear, and it ends the mission + +| t (s) | ACROPOLIS hull | % | +|---|---|---| +| 0–160 | 25000 | 100 % | +| 180 | 24510 | 98.0 | +| 280 | 20279 | 81.1 | +| 380 | 13799 | 55.2 | +| 480 | 8541 | 34.2 | +| 485 (end) | 8182 | 32.7 | + +Untouched until **t ≈ 170 s**, then **≈53 HP/s** with no let-up — so the asset +reaches zero at **t ≈ 640 s**, and the whole-run average rate puts it at 722 s. +Either way the escort is dead at **10–12 minutes**, and "the ACROPOLIS is sunk" +is a defeat condition ([mission-escort-state](mission-escort-state.md)). + +This also retires a suspicion: the 240 s time-box of earlier runs was *not* +hiding a win, and the ~500 s ceiling of a single blocking tool call is **not** +the binding constraint. A longer session would simply watch the loss arrive. + +## What is actually killing it — and the conflict that follows + +Attributing damage by co-presence (which hostiles are within 3000 units of the +asset in the sample where its hull drops, damage split evenly among the classes +present — suggestive, not per-shot proof), only **two** classes are ever near it: + +| class | samples present | attributed damage | +|---|---|---| +| `UN_e007_ADAN_Turret` | 200 | 8483 | +| `UN_e010_ADAN_Attacker_S` | 194 | 8334 | + +Roughly half the damage comes from **turrets** — and `pilot.py` treats turrets as +**keep-out zones at 2500 units, never as targets**. That rule is not arbitrary: a +turret is what shot down every pilot before 2026-07-30, and it is why the craft +now survives. But it means **the policy that keeps the pilot alive also +guarantees the escort dies.** Survival and the objective are in direct conflict, +and the pilot currently resolves it entirely in favour of survival. + +The HUD at t≈485 s says the same thing from the game's side: + +- `YOU KILLED WARSHIPS` **0000** — not one warship in 500 s, across every run ever; +- `YOU KILLED WARPLANES` **0009** — fighters only; +- `REMAINING OB` **004 → 008** — objectives are being *added* by waves faster than + any are cleared, so the pilot is not touching the objective set at all; +- SHIELD and ARMOR bars full, hull **1500/1500**, 120 missiles spent. + +![Stage 02 at t≈485 s](captures/mission01-t485.png) + +## The conclusion that matters + +The pilot optimises the wrong thing. It maximises survival and fighter kills; +the mission scores **objectives** and **the escort**, and the fighter population +(134 → 92) is close to irrelevant to both. An untouched 1500/1500 hull at the +moment the escort passes 33 % is not a good run — it is **unspent risk budget**. + +Concretely, for the next attempt, in priority order: + +1. **Turrets near the asset must become targets**, not keep-out zones — accepting + hull damage is the only way to cut ~50 % of the incoming escort damage. The + keep-out rule should be scoped to turrets that are *not* threatening the + asset, rather than applied globally. +2. **Engage warships.** `WARSHIPS 0000` forever means the objective class has + never been attacked; `REMAINING OB` rising is the scoreboard saying so. +3. Re-check whether the escort damage rate actually falls once turrets die — + that is the experiment that tells us whether (1) is sufficient or whether the + bombers need dedicated intercept too. + +## Method note, learned the hard way + +A harness-tracked **background** task does *not* protect the display: the same +run launched in the background lost Xvfb 11 s in, at the turn boundary +(`skip_intro` exit 3, "DISPLAY LOST"). The comment in `launch_mission.sh` saying +the script may be run as a tracked background task is **wrong**; the +one-blocking-foreground-call rule still stands, which caps a single attempt at +the tool's 600 s timeout. And do not pipe a long run through `tail` — the first +attempt printed nothing because `timeout` killed the pipeline before it flushed; +the on-disk artifacts are what survived. diff --git a/tools/re-capture/launch_mission.sh b/tools/re-capture/launch_mission.sh index 77cdadc..5cf8449 100755 --- a/tools/re-capture/launch_mission.sh +++ b/tools/re-capture/launch_mission.sh @@ -19,7 +19,9 @@ alive(){ ps -o pid=,stat= -C xenia_canary 2>/dev/null | awk '$2 !~ /^Z/ {print $ # that actually bought was the opposite: a process nothing owns is a process # nothing keeps alive, and both were being reaped a couple of minutes in — the # long-standing "Xvfb and the emulator die on their own every few minutes" note. -# Run this whole script as ONE tracked background task and leave Xvfb, openbox +# MEASURED WRONG 2026-08-10: a harness-tracked BACKGROUND task does not protect +# them either — the display was lost 11 s in, at the turn boundary. Run this +# whole script as ONE BLOCKING FOREGROUND call and leave Xvfb, openbox # and xenia as its children: they then live exactly as long as the session does. # `nohup` still shields them from a stray HUP; the exit-status wrapper means a # death is reported with the server's own account instead of being inferred. diff --git a/tools/re-capture/mission_run.sh b/tools/re-capture/mission_run.sh new file mode 100755 index 0000000..c5db31e --- /dev/null +++ b/tools/re-capture/mission_run.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Attempt to COMPLETE a mission and record how it ends. +# +# Every previous flight session was time-boxed to 240 s to measure something +# (escort hull, lethality, ship placement) and none ever reached a mission +# outcome — "no mission completed" has been the standing open item. Unit +# definitions are instantiated per stage, so story progress is the only thing +# that grows unit coverage past 21/110, and that needs a WIN, not a survival. +# +# So this run is deliberately long and its only deliverable is the ENDING: +# screenshots throughout, every entity's hull sampled, and the pilot log kept +# whole (never tail-piped — a frozen log tail is what mission-end looks like +# from outside, and tailing throws away the transition). +# +# Runs as ONE tracked background task with Xvfb/openbox/xenia as plain nohup +# children — see docs/re/session-lifetime notes; do NOT setsid anything. +# +# Usage: mission_run.sh [flight_seconds] [tag] +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:-900}" +TAG="${2:-mission}" +SHOTS=/sylph-home/re/shots +OUT="/sylph-home/re/$TAG" +mkdir -p "$SHOTS" "$OUT" + +"$SD/launch_mission.sh" fly || { echo "BOOT FAILED"; exit 1; } +python3 "$SD/entities2.py" self 0x130 "$OUT/cfg.json" || { echo "BIND FAILED"; exit 1; } + +echo "=== initial entity table ===" +python3 "$SD/mission_state.py" scan "$OUT/cfg.json" + +# A screenshot every 30 s for the WHOLE run: the outcome card (MISSION COMPLETE +# / GAME OVER) is on screen only briefly, so sampling must not stop early. +( n=$(( SECS / 30 + 4 )) + for i in $(seq 1 "$n"); do + printf '%s SHOT %03d\n' "$(date +%s)" "$i" >> "$OUT/shots.log" + screenshot "$SHOTS/$TAG-$(printf %03d "$i").png" >/dev/null 2>&1 + sleep 30 + done ) & +SHOTTER=$! + +date +%s > "$OUT/t0" +python3 "$SD/mission_state.py" watch "$OUT/cfg.json" "$SECS" 2 "$OUT/mission.jsonl" \ + > "$OUT/mission.log" 2>&1 & +WATCHER=$! + +python3 "$SD/pilot.py" "$OUT/cfg.json" "$SECS" > "$OUT/pilot.log" 2>&1 +PILOT_RC=$? +wait $WATCHER 2>/dev/null +kill $SHOTTER 2>/dev/null +screenshot "$SHOTS/$TAG-end.png" >/dev/null 2>&1 +cp -f "$SHOTS/$TAG-end.png" "$OUT/end.png" 2>/dev/null + +echo "PILOT_RC=$PILOT_RC" +echo "--- last 5 pilot lines ---"; tail -5 "$OUT/pilot.log" +echo "--- shots: $(ls "$SHOTS/$TAG-"*.png 2>/dev/null | wc -l) ---" +echo "MISSION RUN DONE ($TAG, ${SECS}s)"