j147: the 1-tick fire-detection lag is OURS — measure it, then back-date it (TR_FIRE_LAG)
MEASURED LIVE (common_libs/tests/measure_fire_ghost_lag.py, 4 sessions, 1777 matched ghost spawns, both movers): the server dispatches a turn's fire AFTER our go() for that same turn, so a turn-T shot's energy drop first reaches our scan at turn T+1 — and a bullet takes its FIRST step during the turn it is fired, so the true bullet is already one whole bullet step (11-20 px) downrange. Both movers place the ghost at the SCANNED enemy position (where the bullet was born), so the whole ghost trajectory is the true one shifted one turn later and the arrival deadline is a full tick late. MEASURED: detection lag +1 tick on 100% of 1777 matched spawns; ghost-vs- observer displacement 19.06 px mean / 22.00 p90 (tfil) and 16.08 / 21.81 (strafe); arrival-deadline error 0.99 / 0.77 ticks. NOT a rendering artefact: the draw/advance order is correct (advanceBullets -> detectFires -> build). THE FIX: TR_FIRE_LAG (int, default 0 = today byte-for-byte) in the shared fire_tracker, applied by both movers at spawn: x = origin + dir*speed*lag. The deadline needs no separate change — both movers derive it from the ghost's own position, so a correct position gives a correct deadline. WITH IT: displacement 19.06 -> 5.37 px mean (the residue is the enemy's own <=8 px scan staleness) and the deadline error 0.99 -> 0.06 ticks. Guards: test_tfil_commit_env 77 -> 87 checks (default golden parity, exact n-step back-date, deadline shortens by exactly lag, junk/negative degrade to 0, reaped exactly one tick earlier); test_env_report + test_env_dotenv green. TR_FIRE_LAG registered in env_report + knownEnvNames + .env.example + docs/env_reference.md. Live A/B pre-registered in docs/movement_campaign.md (Batch 8) with its MDE stated up front; arms tools/ab/arms_fire_lag.txt. TR_FIRE_DIAG gains a per-round ROUND line (the tick->getTurn anchor) and a per-spawn SPAWN line (the ghost's drawn position).
This commit is contained in:
@@ -268,6 +268,7 @@ name, with no new knob:
|
||||
| `TR_RACK_<GUN>` = `both`/`1v1`/`melee` | that gun may be selected | `TR_RACK_<GUN>=off`: the gun is removed from the rack |
|
||||
| `TR_POWER_POLICY` | energy-aware power caps (default) | uncapped: the gun's own preferred power |
|
||||
| `TR_FIRE_FIX` | the corrected enemy-fire detector (default) | the shipped `prev - energy` detector |
|
||||
| `TR_FIRE_LAG` = `<int>` | back-date every detected enemy fire by N ticks at spawn (0 = shipped; **1 = the measured live detection lag**, j147) | n/a — it is a value knob |
|
||||
| `TR_RADAR_FORCE_SPIN` | force the old stateless full-spin melee radar (**off by default**) | the adaptive arc-narrowing radar (default) |
|
||||
| `TR_TFIL_HEAT_TIME` | time-indexed bullet heat (**off by default**) | flat, time-independent heat (default) |
|
||||
| `TR_VBULLET_DEBUG` | draw the virtual-bullet overlay (**off by default**) | nothing drawn |
|
||||
|
||||
@@ -3454,3 +3454,120 @@ middle 30.4% · corr10 32.1% · bullets 65.0% · nofield 3.9%.
|
||||
**The shipped default is untouched.** `TR_MOVEMENT=strafe` remains the default;
|
||||
`TR_TFIL_BULLET_CORE` / `TR_TFIL_BULLET_AURA` default to today's `10.0` / `5.0`,
|
||||
so `TR_MOVEMENT=tfil` still means today's tfil, byte-for-byte (guard check 1).
|
||||
|
||||
# Batch 8 — the fire-detection lag (j147)
|
||||
|
||||
*Pre-registered BEFORE any battle of this batch was launched. No battle of this
|
||||
batch existed when this section was written; the frozen binary for it is the
|
||||
commit that adds `TR_FIRE_LAG` and the `TR_FIRE_DIAG` ghost-spawn trace.*
|
||||
|
||||
## The owner's report, and what was measured
|
||||
|
||||
*"i don't know if is the drawing only the arrives 1 tick later in the gui, but
|
||||
the bullet auras looks like are all 1 tick-ish behind the real bullet!"*
|
||||
|
||||
The first job was to answer **drawing or decision**, not to fix anything. Three
|
||||
measurements, in order, each one able to stop the next:
|
||||
|
||||
### 1. The corpus says the ENERGY DROP is on the fire's own row (lag 0)
|
||||
|
||||
`/tmp/tfil_ab2/out` (70 battles, `runN.jsonl` + `runN.events.jsonl`): for every
|
||||
true fire event, the row at which the shooter's energy drop becomes visible is
|
||||
`fireTick - 1` for **702/702** self fires in round 1 and 100% over the corpus —
|
||||
i.e. in the recorded frame the drop and the shot are the SAME instant (a bullet
|
||||
takes its first step during the turn it is fired, MEASURED: 1293/1293 `hitwall`
|
||||
events have their first out-of-bounds bullet position at step
|
||||
`hitwallTick - fireTick + 1`, which is only consistent with a first step inside
|
||||
the firing turn). So the corpus alone cannot see a lag: it has no view of WHEN
|
||||
our scan runs relative to the dispatch.
|
||||
|
||||
### 2. The corpus is NOT the bot's view, so the lag had to be measured LIVE
|
||||
|
||||
`common_libs/tests/measure_fire_ghost_lag.py`. The bot logs one
|
||||
`[firediag] SPAWN tick=… sx=… sy=… gx=… gy=… p=… eta=…` line per detected fire
|
||||
(the ghost's DRAWN position and our own position, the timeline anchor). The
|
||||
capture supplies the true fire events (origin, direction, power) and the rounds.
|
||||
The timeline is anchored without guessing: `[firediag] EV hit tick=… getTurn=…`
|
||||
lines vs. the sidecar's own event turns match exactly, and give
|
||||
`getTurn = bot.tick + 1` (j134, re-verified) — so a ghost logged at bot tick `t`
|
||||
was placed during server turn `t + 1`.
|
||||
|
||||
| arm | matched spawns | detection lag | ghost-vs-observer px (mean / median / p90) | arrival-deadline error (ticks, mean / median) |
|
||||
|---|---:|---|---:|---:|
|
||||
| tfil, lag 0 | 413 | **+1 tick, 100%** | **19.06 / 19.16 / 22.00** | **0.987 / 0.991** |
|
||||
| tfil, `TR_FIRE_LAG=1` | 446 | +1 tick, 100% | **5.37 / 5.65 / 8.96** | **0.063 / 0.051** |
|
||||
| strafe, lag 0 | 497 | +1 tick (77.9%; the rest are duplicate/split waves of a fire already counted) | **16.08 / 18.23 / 21.81** | 0.771 / 0.944 |
|
||||
| strafe, `TR_FIRE_LAG=1` | 466 | +1 tick, 100% | **6.01 / 5.91 / 9.80** | **0.065 / 0.051** |
|
||||
|
||||
**The answer to the owner: it is NOT only the drawing — the decision is late.**
|
||||
The aura is displaced by exactly **one whole bullet step (11..20 px, 19.1 px
|
||||
mean for tfil)**, in the direction of travel, and the arrival deadline the
|
||||
mover reads is **a full tick late (0.99 ticks)**. The mechanism is measured, not
|
||||
guessed: the server dispatches a turn's fire **after** our `go()` for that turn,
|
||||
so the energy drop of a turn-`T` shot first reaches our scan at turn `T+1`; and
|
||||
because a bullet takes its first step during the turn it is fired, the true
|
||||
bullet is already one step downrange when we see it. Both movers place the ghost
|
||||
at the SCANNED enemy position — where the bullet was *born* — and then advance
|
||||
it once per tick, so the entire ghost trajectory is the true one shifted one
|
||||
turn later, for the bullet's whole life.
|
||||
|
||||
**It is OURS.** The draw/advance order was checked and is correct (both movers
|
||||
`advanceBullets()` -> `detectFires()` -> build the field, i.e. a ghost spawned
|
||||
this tick is drawn at its age-0 position and every older ghost has been advanced
|
||||
exactly once: build-then-advance, which is the correct direction; an
|
||||
advance-then-build order would have shown the aura one tick AHEAD). With
|
||||
`TR_FIRE_LAG=1` the ghosts land on the observer's bullet to within the enemy's
|
||||
own scan staleness (5.4 px mean, max 8 px = the enemy's top speed), which is the
|
||||
floor this design can reach: the origin is the enemy's *scanned* position, not
|
||||
its fire-time position.
|
||||
|
||||
### The treatment
|
||||
|
||||
`TR_FIRE_LAG` (int, **default 0 = today's behaviour byte-for-byte**, `x` is only
|
||||
touched when `lag > 0`), read once in the shared
|
||||
`common_libs/movement_harness/fire_tracker.nim` and applied by BOTH movers at
|
||||
spawn: `x = origin + dir * speed * lag`, `y = …`. The arrival deadline needs no
|
||||
separate change — every mover derives it from the ghost's own position
|
||||
(`heatDecay(along / speed)`, the `dot < 0` reap), so a correct position gives a
|
||||
correct deadline. Guard: `test_tfil_commit_env.nim` 77 -> **87 checks**, all pass
|
||||
(default parity on the golden replay, exact n-step back-date, deadline shortens
|
||||
by exactly `lag`, junk/negative degrade to 0, the ghost is reaped exactly one
|
||||
tick earlier).
|
||||
|
||||
## Arms (frozen, `tools/ab/arms_fire_lag.txt`)
|
||||
|
||||
| # | arm | mover | `TR_FIRE_LAG` | what it isolates |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `tfil_off` | tfil | 0 (default) | **the reference** — today's tfil |
|
||||
| 2 | `tfil_lag1` | tfil | 1 | the back-date, on tfil |
|
||||
| 3 | `strafe_off` | strafe | 0 (default) | **the reference** — today's strafe |
|
||||
| 4 | `strafe_lag1` | strafe | 1 | the back-date, on strafe |
|
||||
|
||||
Panel: the FROZEN 15-opponent `tools/ab/panel_movement.txt`. Harness:
|
||||
`tools/ab/tournament_run.sh` + `tournament_analyze.py`.
|
||||
|
||||
## Pre-registered prediction, MDE and decision rule
|
||||
|
||||
* **MDE, stated up front.** The verdict layer is the paired per-opponent
|
||||
difference over 15 opponents, exactly as batches 4-7. Batch 7 (5 runs/arm)
|
||||
measured **MDE = 12.8 damage/run and 0.28 wins/run**; this batch runs **3
|
||||
runs/arm**, so by `sqrt(5/3)` the MDE degrades to roughly **16 damage/run and
|
||||
0.36 wins/run** — and the incoming-hit-rate MDE to roughly **1.9 points**.
|
||||
**Any true effect smaller than that is invisible here by construction, and a
|
||||
null will be recorded as "not distinguishable", never as "no effect".**
|
||||
* **Prediction.** `tfil_lag1` > `tfil_off` and `strafe_lag1` > `strafe_off` on
|
||||
damage/run and round wins, because the field the mover decides on is displaced
|
||||
by a whole bullet step today and stops being after the fix. The **mechanism is
|
||||
the incoming hit rate** (the dodge should survive strictly more), and the
|
||||
offline gate already measured the mechanism geometrically (19.1 -> 5.4 px,
|
||||
0.99 -> 0.06 ticks), so a mechanism-positive / outcome-null result is the
|
||||
EXPECTED shape given the MDE, and is recorded as such — the same verdict
|
||||
pattern as j144, j145 and j146.
|
||||
* **Verdict rule (unchanged, not re-interpreted afterwards).** The cross-opponent
|
||||
sign test p < 0.05 on one primary metric (damage/run or round wins) with the
|
||||
other not down, SD/SE/95% CI/MDE reported.
|
||||
* **Nothing separates -> nothing changes.** `TR_FIRE_LAG` stays default 0 and
|
||||
the shipped movers are untouched. A mechanism-positive outcome-null does NOT
|
||||
retract the geometric measurement, and does NOT change `TR_MOVEMENT=strafe`.
|
||||
|
||||
*(results appended below after the battles)*
|
||||
|
||||
Reference in New Issue
Block a user