diff --git a/docs/movement_campaign.md b/docs/movement_campaign.md index 9537cf9..c42e4f0 100644 --- a/docs/movement_campaign.md +++ b/docs/movement_campaign.md @@ -2540,3 +2540,110 @@ the fix. **Status: the shipped movement default is UNCHANGED (`TR_MOVEMENT=strafe`); the detector fix is ON by default behind `TR_STRAFE_FIRE_FIX` (revert with `TR_STRAFE_FIRE_FIX=0`).** + +## Fire fix propagated to all movers + +Job **j134** (2026-09-26; commits `6ad5d99`, `8827338`). Scope: ModularBot / +modules / tuning / the test harness only. + +### The shared helper — one implementation, not five + +The j133 detector was fixed **only in `strafe.nim`**; the other four movers kept +their own copy of the same broken `0.09 <= drop <= 3.01` classifier. Four copies +is exactly how the bug survived, so the fix now lives **once** in +`common_libs/movement_harness/fire_tracker.nim` (`FireTracker`). Each mover owns +its own wave geometry and spawn code but calls `m.fire.detect(id, energy, lo, +hi, fix)`; the mover supplies its **shipped window** (`0.09..3.01` for tfil / +tfil_ring / strafe / learned, `0.1..3.0` for surf) so the fix-off path is the +old code exactly, and its own switch so the tracker holds **no** enable flag. + +One global switch, `TR_FIRE_FIX` (default **ON**), gates every mover; STRAFE also +still honours `TR_STRAFE_FIRE_FIX` (j133 back-compat) and is on only when **both** +are on. Both names are registered in `env_report.nim` + `knownEnvNames()` +(`TR_FIRE_DIAG`, the live trace, is registered too). + +`ModularBot.nim` forwards both events to **every** mover (`onHitByBullet` -> +`e.bullet.power`; `onBulletHit` -> `e.damage`); previously only STRAFE received +them. + +### TASK C — per-mover catch rate (offline, 70-battle corpus, 67 065 true enemy fires) + +Every mover now calls the same `FireTracker`; only the window differs, so the +fixed rate must be (and is) identical. The SHIPPED rate differs by one shot for +`surf` because its window is `0.1..3.0` vs `0.09..3.01`. Full report: +`common_libs/tests/fixtures/strafe_fire_catch_report.txt` (regenerated by +`common_libs/tests/measure_strafe_fire_catch.py`, extended with the per-mover +table). + +| mover | window | shipped | fixed | blind before | blind after | +|---|---|---:|---:|---:|---:| +| tfil | 0.09–3.01 | 0.98888 | **1.00000** | 746 | **0** | +| tfil_ring | 0.09–3.01 | 0.98888 | **1.00000** | 746 | **0** | +| strafe | 0.09–3.01 | 0.98888 | **1.00000** | 746 | **0** | +| learned | 0.09–3.01 | 0.98888 | **1.00000** | 746 | **0** | +| surf | 0.10–3.00 | 0.98886 | **1.00000** | 747 | **0** | + +**Every mover is at 100%.** No mover was left unfixed. The shipped path is still +byte-identical with the switch off: `test_tfil_commit_env.nim` replays the +15 000+-tick TFIL trajectory against the pre-change golden with `TfilFireFix = +false` and still matches every call/speed/turnRate/target/commitTicks. + +### TASK B — live tick alignment (this is where the fix was wrong, and fixed) + +One real 7-round battle, strafe, vs `/tmp/tr_bots/WaveSurferGF`, with the +env-gated `TR_FIRE_DIAG=1` trace (kept; default off). **MEASURED:** the server +emits the hit event on turn N but applies the energy change to turn **N+1**'s +reading, and the bot's event handler runs with `bot.tick = getTurn - 1`. So the +correction must land on the reading **two `bot.tick`s after** the event, not the +next one. Paired post-fix lines (verbatim): + +``` +[firediag] EV dmg tick=62 getTurn=63 damage=4.0 +[firediag] READ tick=64 raw=4.0 bonus=0.0 dealt=4.0 <- correction on the reading that carries the +4.0 drop +[firediag] EV hit tick=109 getTurn=110 power=1.2437 +[firediag] READ tick=111 raw=-3.731198... bonus=3.731198... dealt=0.0 +``` + +Before this job the correction was applied on the **immediately next** reading: +on the same battle that put `bonus=5.803` on a reading with `raw=0.0` (a +spurious wave) while the real `raw=-5.803` gain one tick later was left +uncorrected — i.e. j133's fix was **correct in the offline model but mis-timed +live**. Fixed by a one-slot double buffer in `FireTracker` (`incoming` -> rotated +`pending` at `endScan`), which makes the live path agree with the corpus model. + +Aggregate over the whole trace: enemy-hit corrections on the reading of +`event_tick+2` **44 aligned, 8 events ended a round with no later reading, 3 +misaligned (two simultaneous hits in one turn, matched as one sum)**; our-damage +corrections **52 aligned, 6 round-boundary, 0 misaligned**. + +### Guard tests + clean-archive compile + +`git archive HEAD | tar -x` into a clean dir, then: +`ModularBot` compiles; `test_strafe_fire_fix` **14/14**, `test_tfil_commit_env` +**30/30**, `test_tfil_ring_weights` **24/24**, `test_wavesurfer_velocity` +**7/7**, `test_learned_surfer` **24 checks / 0 failures** — **99 checks, 0 +failures**. + +### Direct answers + +1. **Are all movers now at 100% catch?** **Yes.** tfil, tfil_ring, strafe, + learned, and surf all go 0.98888 (or 0.98886 for surf) -> **1.00000** on the + 67 065-fire corpus; each was blind to 746/747 shots, now 0. +2. **Is the live tick alignment confirmed?** **Yes — and it was NOT same-turn.** + The server applies the energy change one turn after the event, so the + correction is applied on the reading two `bot.tick`s after the event; 44/47 + in-window hit corrections and 52/52 in-window damage corrections land on the + exact reading that carries the change (the rest are round boundaries or + simultaneous events). The j133 guess that "the reading is same-turn and the + corpus +1 is a capture artifact" was **wrong**; the fix now encodes the + measured lag. + +### MEASURED vs INFERRED + +**MEASURED:** the per-mover catch table on 67 065 fires; the live paired +event/reading lines and the `event_tick+2` alignment counts from one real +7-round battle; the two-buffer fix; the clean-archive compile; 99/99 guard +checks. **INFERRED:** that the correction size (`3*power`, `damage`) is +unchanged — it is read straight from the server events, not re-derived. +**NOT MEASURED:** the live movement/damage effect of the fix (the change is +~1.11% of fires, far below any panel's MDE; no panel was run, per scope).