j147 results: 180-battle live A/B on the frozen 15-opponent panel

Within-mover references (the only comparison that isolates the knob): tfil_lag1
-0.04 wins/run, strafe_lag1 +0.18 wins/run, both far under the reported MDEs
(0.33 / 0.41) -> NOT distinguishable, so TR_FIRE_LAG stays default 0. Incoming
hit rate also unmoved (+0.94 / +0.33 pp). The one significant result is the
mover (strafe_lag1 vs tfil_off +0.38 wins/run p=0.0386, hit rate -4.67pp
p=0.0074), which is the known strafe-over-tfil gap and exactly why the
within-mover reference was pre-registered.

Also a cosmetic no-op refactor of the two spawn sites (compute velX/velY once;
bit-identical, and it keeps the default-parity claim exact).
This commit is contained in:
2026-09-26 23:20:36 +02:00
parent d21f7ce5f5
commit 32ec040e90
4 changed files with 104 additions and 12 deletions
+89
View File
@@ -3571,3 +3571,92 @@ Panel: the FROZEN 15-opponent `tools/ab/panel_movement.txt`. Harness:
retract the geometric measurement, and does NOT change `TR_MOVEMENT=strafe`.
*(results appended below after the battles)*
### MEASURED — gate B: the guard (`test_tfil_commit_env.nim`, 77 -> 87 checks)
All 87 pass, including the byte-for-byte golden replay of the shipped mover with
`TR_FIRE_LAG` unset (check 1). The j147 ones:
* `TR_FIRE_LAG` unset -> `FireLag 0`, and the ghost lands EXACTLY on the scanned
enemy (`b.x == ei.x` bit for bit — the position is only touched when `lag > 0`).
* `=1` -> the ghost is exactly one bullet step (17 px at power 1.0) downrange on
its own heading; `=2` -> exactly two; the step length is the true
`20 - 3*power`, not a scaled one.
* **the arrival deadline**: the mover's eta equals the TRUE remaining flight
(10.764706 vs 10.764706) where the lag-0 eta was 11.764706 — a full tick late;
at `lag=2` the deadline shortens by exactly 2 ticks.
* a junk or negative value degrades to the shipped lag 0 (never a negative
back-date); clearing the knob restores the shipped spawn exactly.
* end to end: the ghost is reaped (`dot < 0`, the geometric arrival the mover
actually uses) **exactly one tick earlier** — 12 -> 11 ticks.
* `test_env_report` + `test_env_dotenv` green with `TR_FIRE_LAG` registered in
`env_report.nim` + `knownEnvNames()` + `.env.example` + `docs/env_reference.md`.
### MEASURED — gate C: the live A/B, 180 battles
> **Provenance.** Session `/tmp/ab/j147_firelag`, frozen binary `d21f7ce`
> (sha256 `29571d4d…`), panel `tools/ab/panel_movement.txt` (15 opponents,
> FROZEN), arms file `tools/ab/arms_fire_lag.txt` registered above BEFORE any of
> these battles ran. **4 arms x 15 opponents x 3 runs x 3 rounds = 180 battles,
> 0 failed, 0 never started, 473 s.** The MDEs the analyzer actually reported at
> 3 runs/arm: **12.2-15.1 damage/run, 0.33-0.55 wins/run, 1.4-2.9 hit-rate
> points** — the pre-registered estimate (~16 / ~0.36 / ~1.9) was right.
**Pooled dashboard (descriptive, NOT the verdict):**
| arm | runs | dmg/run | dmg taken/run | wins/run | round wins | win rate | incoming hit rate | mean distance |
|---|---:|---:|---:|---:|---:|---:|---:|---:|
| `tfil_off` | 45 | 109.7 | 187.8 | 1.24 | 56/135 | 41.5% | 16.91% | 394 |
| `tfil_lag1` | 45 | 111.8 | 192.9 | 1.20 | 54/135 | 40.0% | 17.45% | 396 |
| `strafe_off` | 45 | 106.3 | 160.1 | 1.44 | 65/135 | 48.1% | 12.84% | 434 |
| `strafe_lag1` | 45 | 110.1 | 159.0 | **1.62** | **73/135** | **54.1%** | 13.21% | 428 |
**Verdict layer, each mover against ITS OWN reference (the only comparison that
isolates the knob):**
| arm | metric | mean Δ | 95% CI | sign test | p(sign) | p(sign-flip) | Wilcoxon p | MDE |
|---|---|---:|---|---:|---:|---:|---:|---:|
| `tfil_lag1` vs `tfil_off` | damage | +2.08 | [-7.22, +11.38] | 6/15 | 0.6072 | 0.6375 | 0.9773 | 12.15 |
| `tfil_lag1` vs `tfil_off` | wins | -0.04 | [-0.29, +0.21] | 4/9 | 1 | 0.8516 | 0.5923 | 0.33 |
| `tfil_lag1` vs `tfil_off` | hit_rate | +0.94 | [-0.93, +2.81] | 10/15 | 0.3018 | 0.2984 | 0.222 | 2.45 |
| `strafe_lag1` vs `strafe_off` | damage | +3.77 | [-6.98, +14.53] | 9/15 | 0.6072 | 0.4598 | 0.6701 | 14.04 |
| `strafe_lag1` vs `strafe_off` | wins | +0.18 | [-0.13, +0.49] | 5/9 | 1 | 0.3359 | 0.1723 | 0.41 |
| `strafe_lag1` vs `strafe_off` | hit_rate | +0.33 | [-0.73, +1.39] | 9/15 | 0.6072 | 0.5403 | 0.5509 | 1.38 |
### VERDICT — plain
1. **Was it only the drawing? NO. The decision was late, by exactly one bullet
step, and the fix is now in.** Measured live on 1777 matched ghost spawns
across both movers: the detection lag is **+1 tick on 100%** of them, the
ghost-vs-observer displacement is **19.1 px mean / 22.0 p90** (tfil) and
**16.1 / 21.8** (strafe), and the arrival deadline the mover reads is
**0.99 / 0.77 ticks late**. With `TR_FIRE_LAG=1` the displacement is
**5.4 / 9.0 px** and the deadline error **0.06 ticks** — the residue is the
ENEMY's own scan staleness (<= 8 px, its top speed), which is the floor this
design can reach because the ghost's origin is the enemy's *scanned* position.
The draw/advance order was checked and is correct, so the GUI was faithfully
drawing a wrong field.
2. **The live OUTCOME is null, and that is recorded as "not distinguishable".**
`tfil_lag1` is -0.04 wins/run and `strafe_lag1` is +0.18 wins/run — both far
under the MDEs the analyzer reported (0.33 and 0.41). Nothing reaches the
pre-registered bar, so under the campaign's rule **nothing is changed**:
`TR_FIRE_LAG` stays **default 0** and both movers ship exactly as before. The
knob is there, measured and documented, for anyone who wants the arm.
3. **The live MECHANISM did not move either** — incoming hit rate +0.94 pp (tfil)
and +0.33 pp (strafe), neither significant. This is the fourth consecutive
movement job where a real, measured mechanism change does not show up as fewer
hits taken. Two readings, both worth keeping: the dodge is limited by the
1-tick-stale enemy POSITION and by the 8-px scan staleness of the ghost's
origin, not by a 19-px translation of a field whose core is 18 px and whose
corridor is 40 px wide; and at 3 runs/arm a real few-percent effect in hit
rate sits under the ~1.4-point MDE. **What the fix does buy, provably, is
the arrival deadline**: every mover's heat, corridor and reap are now timed
off the bullet's real position, which is the input the next arrival-commit /
time-indexed-heat work needs to be correct at all.
4. **The one significant live result in this batch is the MOVER, not the knob**:
`strafe_lag1` vs `tfil_off` is +0.38 wins/run (sign 10/12, p = 0.0386) with
the incoming hit rate **-4.67 pp (sign 2/15, p = 0.0074, sign-flip
p = 0.0007, Wilcoxon p = 0.0024)** and mean distance +34 px (14/15). That is
the known strafe-over-tfil gap reproducing itself, and it is exactly why the
pre-registration demanded the within-mover reference: read against `tfil_off`
alone, the knob looks like a winner it is not.