diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 03ea31c..3c5353b 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -58,3 +58,26 @@ class cannot hide behind `e106` passing. handedness, node-instance recursion) is not re-breaking a correct assembly — compare the viewer's placement against `assemble_ship`'s output directly before blaming the format layer. + +--- + +## Viewer: `include_external` is already on — that hypothesis is dead + +**Checked 2026-08-11.** The item above names "first that it passes +`include_external = true` (`iso_loader.rs:4012`)" as the cheap first step. It +does: `ShipBrowser::show_external` defaults to `true` +(`iso_loader.rs:643`), the checkbox reads it (`ui.rs:1593`) and it is threaded +through `RequestShipRender` → `build_ship_model` → `assemble_ship` unchanged +(`ui.rs:1689`, `iso_loader.rs:4012`). So a ship rendered by the viewer is the +full external assembly, not the bare hull. + +The viewer also does not have a transform stack of its own to blame: it bakes +`ScenePart::apply` straight into the vertices and rotates normals by the same +`p.m` (`iso_loader.rs:4030-4062`), so its placement is `assemble_ship`'s output +by construction. What remains unexcluded, in order of cheapness: the mirror +handling (`det < 0` reverses triangle winding only — a reflected part keeps its +reflected geometry), `Xbg7Model::models_named` resolving the wrong sub-model when +a resource name repeats, and the exhaust cones. **Next step is a visual**: the +diagnosis has run out of things it can settle by reading, so the viewer needs to +be run against a known-good class (`e106`) and its render compared with +`ship_render`'s. diff --git a/docs/re/captures/escort-decay-ab.csv b/docs/re/captures/escort-decay-ab.csv new file mode 100644 index 0000000..7ee39a3 --- /dev/null +++ b/docs/re/captures/escort-decay-ab.csv @@ -0,0 +1,161 @@ +run,turret_rule,t_s,acropolis_hull,e007_alive,e007_killed_cum +mission01,off,0,25000.0,0,0 +mission01,off,10,25000.0,5,0 +mission01,off,20,25000.0,6,0 +mission01,off,30,25000.0,6,0 +mission01,off,40,25000.0,6,0 +mission01,off,50,25000.0,8,0 +mission01,off,60,25000.0,11,0 +mission01,off,70,25000.0,11,3 +mission01,off,80,25000.0,8,3 +mission01,off,90,25000.0,8,3 +mission01,off,100,25000.0,10,3 +mission01,off,110,25000.0,14,3 +mission01,off,120,25000.0,16,3 +mission01,off,130,25000.0,17,3 +mission01,off,140,25000.0,20,3 +mission01,off,150,25000.0,23,3 +mission01,off,160,25000.0,26,4 +mission01,off,170,24509.5,33,4 +mission01,off,180,24509.5,37,4 +mission01,off,190,24419.5,38,5 +mission01,off,200,24212.5,41,5 +mission01,off,210,23447.5,40,5 +mission01,off,220,23447.5,40,5 +mission01,off,220,23447.5,40,5 +mission01,off,230,23447.5,41,5 +mission01,off,230,23447.5,41,5 +mission01,off,240,22912.0,43,5 +mission01,off,240,22867.0,43,5 +mission01,off,250,22667.0,42,6 +mission01,off,250,22647.0,42,6 +mission01,off,260,22334.5,44,6 +mission01,off,260,22177.9,44,6 +mission01,off,270,21101.2,46,6 +mission01,off,270,21101.2,45,6 +mission01,off,280,20292.6,46,6 +mission01,off,280,20279.1,46,6 +mission01,off,290,19598.5,58,6 +mission01,off,290,19598.5,58,6 +mission01,off,300,18725.2,58,6 +mission01,off,300,18658.5,47,6 +mission01,off,310,18231.9,47,6 +mission01,off,310,18231.9,47,6 +mission01,off,320,17425.2,47,6 +mission01,off,330,17046.9,49,6 +mission01,off,340,16044.8,49,7 +mission01,off,350,15461.5,50,7 +mission01,off,360,14956.3,51,7 +mission01,off,370,14425.5,51,8 +mission01,off,380,13910.5,51,8 +mission01,off,390,13418.9,49,11 +mission01,off,400,13218.9,45,12 +mission01,off,410,12517.9,44,12 +mission01,off,420,12161.3,44,13 +mission01,off,430,11601.3,44,13 +mission02,on,0,25000.0,0,0 +mission02,on,10,25000.0,5,0 +mission02,on,20,25000.0,6,0 +mission02,on,30,25000.0,5,0 +mission02,on,40,25000.0,5,1 +mission02,on,50,25000.0,7,1 +mission02,on,60,25000.0,10,1 +mission02,on,70,25000.0,7,4 +mission02,on,80,25000.0,7,4 +mission02,on,90,25000.0,7,4 +mission02,on,100,25000.0,7,4 +mission02,on,110,25000.0,9,4 +mission02,on,120,25000.0,14,4 +mission02,on,130,25000.0,16,4 +mission02,on,140,25000.0,19,4 +mission02,on,150,25000.0,19,4 +mission02,on,160,25000.0,23,5 +mission02,on,170,24730.0,28,5 +mission02,on,180,24730.0,32,5 +mission02,on,190,24730.0,36,5 +mission02,on,200,24640.0,43,6 +mission02,on,210,24190.0,46,6 +mission02,on,220,24055.0,47,6 +mission02,on,220,24055.0,47,6 +mission02,on,230,24055.0,49,6 +mission02,on,230,24055.0,49,6 +mission02,on,240,23474.5,52,6 +mission02,on,240,23474.5,52,6 +mission02,on,250,23474.5,53,6 +mission02,on,250,23474.5,53,6 +mission02,on,260,23474.5,54,6 +mission02,on,260,23474.5,54,6 +mission02,on,270,22697.0,54,6 +mission02,on,270,22697.0,54,6 +mission02,on,280,22091.2,56,6 +mission02,on,280,22091.2,55,7 +mission02,on,290,21577.9,55,7 +mission02,on,290,21555.4,55,7 +mission02,on,300,20799.1,56,7 +mission02,on,310,19781.1,55,7 +mission02,on,320,18925.8,56,8 +mission02,on,330,18592.5,56,8 +mission02,on,340,17432.5,57,8 +mission02,on,350,16932.6,59,8 +mission02,on,360,16171.6,58,9 +mission02,on,370,15017.6,58,11 +mission02,on,380,14217.2,59,11 +mission02,on,390,13322.2,59,12 +mission02,on,400,12808.9,58,13 +mission02,on,410,12090.6,61,16 +mission02,on,420,11440.6,66,19 +mission02,on,430,11127.3,55,19 +mission03,on,0,25000.0,0,0 +mission03,on,10,25000.0,3,0 +mission03,on,20,25000.0,8,0 +mission03,on,30,25000.0,5,0 +mission03,on,40,25000.0,6,0 +mission03,on,50,25000.0,8,0 +mission03,on,60,25000.0,9,0 +mission03,on,70,25000.0,16,0 +mission03,on,80,25000.0,10,0 +mission03,on,90,25000.0,11,0 +mission03,on,100,25000.0,10,2 +mission03,on,110,25000.0,15,2 +mission03,on,120,25000.0,12,2 +mission03,on,130,25000.0,10,4 +mission03,on,140,25000.0,11,6 +mission03,on,150,25000.0,14,6 +mission03,on,160,25000.0,17,7 +mission03,on,170,24685.0,24,7 +mission03,on,180,24640.0,23,7 +mission03,on,190,24640.0,27,7 +mission03,on,200,24460.0,26,8 +mission03,on,210,24100.0,28,8 +mission03,on,220,24100.0,29,9 +mission03,on,230,24100.0,28,11 +mission03,on,230,24100.0,28,11 +mission03,on,240,23810.0,35,11 +mission03,on,240,23810.0,36,11 +mission03,on,250,23630.0,36,12 +mission03,on,250,23630.0,36,12 +mission03,on,260,23630.0,38,13 +mission03,on,260,23630.0,38,13 +mission03,on,270,22998.0,39,13 +mission03,on,270,22908.0,39,13 +mission03,on,280,22197.5,40,13 +mission03,on,280,22188.5,40,13 +mission03,on,290,21602.4,38,13 +mission03,on,290,21602.4,38,13 +mission03,on,300,21042.4,39,13 +mission03,on,300,20975.8,39,13 +mission03,on,310,20529.1,41,15 +mission03,on,310,20529.1,41,15 +mission03,on,320,19812.5,38,17 +mission03,on,320,19767.5,38,17 +mission03,on,330,19434.2,39,18 +mission03,on,340,18785.9,37,19 +mission03,on,350,18339.2,37,19 +mission03,on,360,17528.1,38,19 +mission03,on,370,16901.5,37,19 +mission03,on,380,16260.7,35,19 +mission03,on,390,15708.2,37,19 +mission03,on,400,15346.2,37,19 +mission03,on,410,14412.1,36,20 +mission03,on,420,13951.9,37,22 +mission03,on,430,13211.9,35,23 diff --git a/docs/re/captures/mission03-turretrule-t430.png b/docs/re/captures/mission03-turretrule-t430.png new file mode 100644 index 0000000..1a2d0fe Binary files /dev/null and b/docs/re/captures/mission03-turretrule-t430.png differ diff --git a/docs/re/mission-outcome-stage02.md b/docs/re/mission-outcome-stage02.md index 67f7b5c..55d47d4 100644 --- a/docs/re/mission-outcome-stage02.md +++ b/docs/re/mission-outcome-stage02.md @@ -66,6 +66,8 @@ the mission scores **objectives** and **the escort**, and the fighter population moment the escort passes 33 % is not a good run — it is **unspent risk budget**. Concretely, for the next attempt, in priority order: +(**Step 1 below was run on 2026-08-11 and did not hold — see the follow-up A/B at +the end of this file before acting on it.**) 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 @@ -87,3 +89,93 @@ 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. + +--- + +# Follow-up A/B (2026-08-11): making turrets targets does **not** save the escort + +**Status: ✅ measured (3 runs), and it falsifies the causal claim above.** The +section above ends by naming step 1 — "turrets near the asset must become +targets" — as the fix worth ~50 % of the escort damage. That was an *inference +from an attribution*, never a measurement. It has now been run, and it does not +hold. + +## First, a correction to the run labelling + +`pilot.py` gained the `SYLPH_KILL_TURRETS` gate at 21:20:28 on 2026-08-10; +`/sylph-home/re/mission02` started at **21:22:25**, i.e. *after* it, with the +gate **on**. So `mission02` was never a second baseline — it is a treatment run +that the previous session produced but never reported. The arms are: + +| run | turret rule | evidence (`pilot.log`) | +|---|---|---| +| `mission01` | **off** (baseline) | `tgt=e007` on **0** of 3968 lines | +| `mission02` | **on** | `tgt=e007` on 1588 of 3844 lines | +| `mission03` | **on** (this session) | `tgt=e007` on 1218 of 3514 lines | + +`mission01` predates the edit, so the *only* difference between the arms is the +gate. Everything else — build, save slot, nav route, loadout — is identical. + +## The result, all three runs truncated to a common t = 428 s + +| run | rule | ACROPOLIS @428 s | % | decay rate | turrets seen | turrets killed | attack onset | +|---|---|---|---|---|---|---|---| +| `mission01` | off | 11735 | 46.9 % | 50.3 HP/s | 92 | 30 | 166 s | +| `mission02` | on | 11127 | 44.5 % | 52.7 HP/s | 98 | 34 | 167 s | +| `mission03` | on | 13257 | 53.0 % | 44.4 HP/s | 88 | 41 | 166 s | + +**The two runs of the *same* arm differ by 8.5 percentage points (2130 HP) — +more than either differs from the baseline.** The baseline sits *between* the two +treatment runs on escort hull and on decay rate. So the effect of the turret rule +is **not resolvable at n=1 per arm**, and the honest statement is: it was not +demonstrated. Extrapolating each run's own rate, the escort still reaches zero at +**t ≈ 590–670 s** in every arm — the mission is lost in all three. + +The rule does do the mechanical thing it was written to do (turret kills 30 → 34, +41; 1218–1588 frames spent with a turret as the committed target). It just does +not convert into escort hull. + +![Stage 02 at t≈430 s with the turret rule on — WARSHIPS 0000, REMAINING OB 008](captures/mission03-turretrule-t430.png) + +## What *is* reproducible across all three runs + +These are the numbers to build on, because they repeat to within a percent: + +- **The assault on the ACROPOLIS is scripted, not emergent.** Onset at + **166 / 167 / 166 s** — ±1 s across three runs with completely different pilot + behaviour in between. Nothing the pilot does moves it. +- **The damage split is a property of the scenario, not of our targeting.** + `e007` turrets vs `e010` bombers is **50.6/49.4, 50.0/50.0, 51.0/49.0** — and + the baseline, in which we never fired at a turret at all, splits the same way. + A co-presence attribution that is invariant to whether we attack one of the two + classes is measuring the wave script, not our contribution. +- **Turrets die anyway**: 30 of 92 are already dead by t=428 s in the baseline, + from friendly fire. The turret rule adds ~4–11 kills on top of that, which is + why its escort-hull effect is small enough to be buried in run-to-run noise. +- `WARSHIPS 0000` and `REMAINING OB` rising (004 → 008) in **every** run, + including this one. + +Per-run series: [`captures/escort-decay-ab.csv`](captures/escort-decay-ab.csv) +(10 s grid, hull + turret alive/killed counts for all three runs). + +## The methodological finding, which is the transferable one + +**A single 430 s flight cannot resolve an escort-hull effect smaller than ~9 +percentage points.** Every pilot conclusion drawn from one run — including step 1 +above, and including anything drawn from `mission03` alone — is inside the noise. +Each run costs ~10 minutes of wall clock and one blocking foreground call, so a +properly powered A/B is ~6 runs ≈ 1 hour. **Any future pilot tuning must budget +that, or not be believed.** + +## What this means for the reason we wanted a win + +Story progress was wanted only because unit definitions instantiate per stage, so +unit-field coverage is stuck at 21/110 ([unit struct](structures/unit-struct-runtime.md)). +This result says the pilot route to that coverage is expensive and unproven: the +escort dies on a script at ~10–11 minutes, our best lever moved it by less than +the noise, and the objective class (`WARSHIPS`) has never been touched in any run +ever. **Before spending another hour on pilot tuning, the cheaper lever to price +is the save file** — if the profile's save data can be read and understood, stage +unlock state is a much shorter path to the same coverage, and the save format is +itself something the reimplementation needs. Marked `NEEDS-HUMAN` as a direction +choice; no save-file modification has been attempted. diff --git a/tools/re-capture/mission_run.sh b/tools/re-capture/mission_run.sh index c5db31e..be9458b 100755 --- a/tools/re-capture/mission_run.sh +++ b/tools/re-capture/mission_run.sh @@ -47,7 +47,7 @@ python3 "$SD/mission_state.py" watch "$OUT/cfg.json" "$SECS" 2 "$OUT/mission.jso > "$OUT/mission.log" 2>&1 & WATCHER=$! -python3 "$SD/pilot.py" "$OUT/cfg.json" "$SECS" > "$OUT/pilot.log" 2>&1 +SYLPH_KILL_TURRETS="${SYLPH_KILL_TURRETS:-0}" 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 diff --git a/tools/re-capture/pilot.py b/tools/re-capture/pilot.py index 92aef8b..cba7aad 100644 --- a/tools/re-capture/pilot.py +++ b/tools/re-capture/pilot.py @@ -100,6 +100,18 @@ MISSILE_PERIOD = 2.0 # s between launches; 300 rounds is not unlimited # configuration here — override with $SYLPH_ASSET for another stage. ASSET_NAME = os.environ.get("SYLPH_ASSET", "Acropolis") +# Turrets are keep-out zones everywhere else in this file, and that rule is what +# stopped the pilot being shot down. But the 2026-08-10 outcome run measured the +# cost of it: of the damage that sinks the ACROPOLIS, roughly half comes from +# e007 turrets sitting 1.5-3 km off it (the other half from e010 bombers), and a +# turret is 100 HP -- one missile, or ~7 nose-gun hits. Several were already at +# 45-95 HP from stray fire and were never finished off, because nothing ever +# targets them. So the rule that keeps us alive is also what loses the escort. +# Gated rather than simply changed, so the measured baseline stays reproducible: +# SYLPH_KILL_TURRETS=1 lets DEFEND -- and only DEFEND -- treat a turret that is +# near the asset as a target. ENGAGE still avoids them. +KILL_TURRETS = os.environ.get("SYLPH_KILL_TURRETS") == "1" + class Pilot: KP, KD = 2.2, 0.45 @@ -230,7 +242,9 @@ class Pilot: """ out = [] for off, nm, p, v, r, hard in hos: - if hard: + # A turret near the asset IS a killable objective (100 HP) when + # SYLPH_KILL_TURRETS is set; a capital hull never is. + if hard and not (KILL_TURRETS and "Turret" in nm): continue rel = a_p - p d = float(np.linalg.norm(rel)) @@ -520,9 +534,12 @@ class Pilot: self.set_throttle(0) fire = True - # a turret inside its keep-out radius outranks the target + # A turret inside its keep-out radius outranks the target -- except the + # one we are deliberately attacking, or the keep-out would steer us off + # the very thing we chose to kill and neither goal would be served. + tgt_off = tgt[0] if tgt else None for off, nm, p, v, r, hard in hos: - if not hard: + if not hard or (KILL_TURRETS and off == tgt_off): continue d = float(np.linalg.norm(p - me_p)) if d < self.TURRET_KEEPOUT: