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:
@@ -137,10 +137,10 @@ proc detect*(t: var FireTracker, id: int, energy: float,
|
|||||||
import std/[math, os, strutils]
|
import std/[math, os, strutils]
|
||||||
|
|
||||||
## ── j147: the DETECTION LAG back-date (`TR_FIRE_LAG`, default 0) ─────────────
|
## ── j147: the DETECTION LAG back-date (`TR_FIRE_LAG`, default 0) ─────────────
|
||||||
## MEASURED LIVE (`common_libs/tests/measure_fire_ghost_lag.py`, 1171/1171 ghost
|
## MEASURED LIVE (`common_libs/tests/measure_fire_ghost_lag.py`, 4 sessions,
|
||||||
## spawns over two movers x 4 rounds, `TR_FIRE_DIAG=1`): the server dispatches a
|
## 1777 matched ghost spawns over both movers, `TR_FIRE_DIAG=1`): the server
|
||||||
## turn's fire AFTER our `go()` for that same turn, so the energy drop of a
|
## dispatches a turn's fire AFTER our `go()` for that same turn, so the energy
|
||||||
## turn-T shot first reaches our scan at turn T+1 (our bot tick T). A bullet
|
## drop of a turn-T shot first reaches our scan at turn T+1 (our bot tick T). A bullet
|
||||||
## takes its FIRST step during the turn it is fired, so by then the true bullet
|
## takes its FIRST step during the turn it is fired, so by then the true bullet
|
||||||
## is already `speed` px (11..20 px, one whole bullet step) downrange and the
|
## is already `speed` px (11..20 px, one whole bullet step) downrange and the
|
||||||
## arrival deadline is a full tick shorter than the ghost's. Both movers place
|
## arrival deadline is a full tick shorter than the ghost's. Both movers place
|
||||||
|
|||||||
@@ -579,16 +579,17 @@ proc spawnTrackedWave(m: var StrafeModule, ws: WorldState, ei: EnemyInfo,
|
|||||||
m.bullets.del(0)
|
m.bullets.del(0)
|
||||||
# j147: back-date the shot by the measured detection lag (`TR_FIRE_LAG`,
|
# j147: back-date the shot by the measured detection lag (`TR_FIRE_LAG`,
|
||||||
# default 0 = untouched). See `movement_harness/fire_tracker.nim`.
|
# default 0 = untouched). See `movement_harness/fire_tracker.nim`.
|
||||||
|
let vx = speed * cos(heading)
|
||||||
|
let vy = speed * sin(heading)
|
||||||
var gx = ei.x
|
var gx = ei.x
|
||||||
var gy = ei.y
|
var gy = ei.y
|
||||||
if FireLag > 0:
|
if FireLag > 0:
|
||||||
gx += (speed * cos(heading)) * FireLag.float
|
gx += vx * FireLag.float
|
||||||
gy += (speed * sin(heading)) * FireLag.float
|
gy += vy * FireLag.float
|
||||||
m.bullets.add TrackedBullet(
|
m.bullets.add TrackedBullet(
|
||||||
originX: ei.x, originY: ei.y,
|
originX: ei.x, originY: ei.y,
|
||||||
x: gx, y: gy,
|
x: gx, y: gy,
|
||||||
velX: speed * cos(heading),
|
velX: vx, velY: vy,
|
||||||
velY: speed * sin(heading),
|
|
||||||
power: power, alive: true, age: 0)
|
power: power, alive: true, age: 0)
|
||||||
|
|
||||||
proc noteEnemyBulletHit*(m: var StrafeModule, power: float) =
|
proc noteEnemyBulletHit*(m: var StrafeModule, power: float) =
|
||||||
|
|||||||
@@ -433,16 +433,18 @@ proc spawnTrackedWave(m: var TFILModule, ws: WorldState, ei: EnemyInfo,
|
|||||||
m.bullets.del(0) # ponytail: drop oldest; fine for 20-bullet cap
|
m.bullets.del(0) # ponytail: drop oldest; fine for 20-bullet cap
|
||||||
# j147: back-date the shot by the measured detection lag (`TR_FIRE_LAG`,
|
# j147: back-date the shot by the measured detection lag (`TR_FIRE_LAG`,
|
||||||
# default 0 = untouched). See `movement_harness/fire_tracker.nim`.
|
# default 0 = untouched). See `movement_harness/fire_tracker.nim`.
|
||||||
|
let vx = speed * cos(heading)
|
||||||
|
let vy = speed * sin(heading)
|
||||||
var gx = ei.x
|
var gx = ei.x
|
||||||
var gy = ei.y
|
var gy = ei.y
|
||||||
if FireLag > 0:
|
if FireLag > 0:
|
||||||
gx += (speed * cos(heading)) * FireLag.float
|
gx += vx * FireLag.float
|
||||||
gy += (speed * sin(heading)) * FireLag.float
|
gy += vy * FireLag.float
|
||||||
m.bullets.add TrackedBullet(
|
m.bullets.add TrackedBullet(
|
||||||
originX: ei.x, originY: ei.y,
|
originX: ei.x, originY: ei.y,
|
||||||
x: gx, y: gy,
|
x: gx, y: gy,
|
||||||
velX: speed * cos(heading),
|
velX: vx,
|
||||||
velY: speed * sin(heading),
|
velY: vy,
|
||||||
power: power,
|
power: power,
|
||||||
alive: true,
|
alive: true,
|
||||||
age: 0)
|
age: 0)
|
||||||
|
|||||||
@@ -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`.
|
retract the geometric measurement, and does NOT change `TR_MOVEMENT=strafe`.
|
||||||
|
|
||||||
*(results appended below after the battles)*
|
*(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.
|
||||||
|
|||||||
Reference in New Issue
Block a user