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:
2026-09-26 23:08:57 +02:00
parent de5d02ba3f
commit d21f7ce5f5
11 changed files with 512 additions and 2 deletions
+23 -1
View File
@@ -152,6 +152,10 @@ var
## j134: the shared fire-detection correction (`TR_FIRE_FIX`, default on).
## Off = the shipped `prev - energy` detector byte-for-byte.
TfilFireFix*: bool = true
## j134/j147: the env-gated live trace (`TR_FIRE_DIAG`) — one `SPAWN` line per
## detected enemy fire, for the ghost-vs-observer position probe. Observability
## only; off by default.
TfilFireDiag*: bool = false
proc getEnvInt(name: string, default: int): int =
let s = getEnv(name, "")
@@ -186,6 +190,7 @@ proc loadTfilCommitEnv*() =
TfilTurnBias = max(0.0, getEnvFloat("TR_TFIL_TURN_BIAS", 0.0))
TfilTurnRefDeg = max(0.0, getEnvFloat("TR_TFIL_TURN_REF_DEG", 45.0))
TfilFireFix = getEnvBool("TR_FIRE_FIX", true)
TfilFireDiag = existsEnv("TR_FIRE_DIAG")
loadTfilCommitEnv()
@@ -426,9 +431,16 @@ proc spawnTrackedWave(m: var TFILModule, ws: WorldState, ei: EnemyInfo,
let heading = arctan2(predY - ei.y, predX - ei.x)
if m.bullets.len >= MaxTrackedBullets:
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`,
# default 0 = untouched). See `movement_harness/fire_tracker.nim`.
var gx = ei.x
var gy = ei.y
if FireLag > 0:
gx += (speed * cos(heading)) * FireLag.float
gy += (speed * sin(heading)) * FireLag.float
m.bullets.add TrackedBullet(
originX: ei.x, originY: ei.y,
x: ei.x, y: ei.y,
x: gx, y: gy,
velX: speed * cos(heading),
velY: speed * sin(heading),
power: power,
@@ -451,6 +463,16 @@ proc detectFires(m: var TFILModule, ws: WorldState) =
for ei in ws.enemies:
for p in m.fire.detect(ei.id, ei.energy, 0.09, 3.01, TfilFireFix):
m.spawnTrackedWave(ws, ei, p)
if TfilFireDiag and m.bullets.len > 0:
# Ghost-vs-observer probe: the tick we DETECTED the fire, our own
# position (the timeline anchor) and the ghost's DRAWN position.
let b = m.bullets[^1]
let sp = 20.0 - 3.0 * p
echo "[firediag] SPAWN tick=", ws.tick,
" sx=", ws.selfX, " sy=", ws.selfY,
" gx=", b.x, " gy=", b.y,
" p=", p,
" eta=", sqrt((ws.selfX - b.x)^2 + (ws.selfY - b.y)^2) / sp
m.fire.endScan()
proc advanceBullets(m: var TFILModule, selfX, selfY: float) =