j134 ledger: propagated fire fix to all 5 movers via one shared fire_tracker (TR_FIRE_FIX), per-mover catch table 98.888->100, live tick alignment measured (correction belongs on event_tick+2)

This commit is contained in:
2026-09-26 12:59:35 +02:00
parent 882733819b
commit 3237b65f40
+107
View File
@@ -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).