j177: aim capture - log what the model BELIEVED, not what it did

j176 could not attribute the 11.9 deg aim error at 450+ px: the corpus had
no gun id and no bot-side belief, so staleness was an inverse (unidentifiable)
problem and a good gun was indistinguishable from a bad one. Both are cheap to
log and impossible to recover later.

New default-off knob TR_CAPTURE_AIM (presence-only). It appends TWO record
kinds to the EXISTING TR_RECORD_WORLDSTATE file:

  aim_scan - one per onScannedBot, written BEFORE the tracker update, so it is
    the pre-update belief by construction: tick, raw scanned values
    (ex,ey,eh,es,ee), our own state (sx,sy,sh,ss), the gun in force, the
    PREVIOUS belief (bx,by,bh,bs,blst), the scan parity age = tick - blst,
    and the radar-lock context (rlock, rdir, lbear, boff).
  aim_fire - one per real shot: gun, power, the aim angle handed to setFire,
    the turret angle and the signed turret error, gunHeat, the predicted
    intercept (ax,ay) and implied TOF, and the exact WorldState the predictor
    consumed (ex,ey,eh,es,ee,sx,sy) with the tick it came from (lst).

Row builders live in a new pure module src/aim_capture.nim - no bot API, no
env reads - so the offline guard test and the live bot go through the SAME
builders and a field the test proves present is a field the bot writes.
Per-tick world-state rows also gain a `gun` id. offline_range.nim skips
aim_* lines (they carry no `ex`), so the annotations are inert to the replay.
No aim model changed.

Knob registered in env_report.nim (context field, effective-value emit) and
knownEnvNames(); documented in .env.example. Defaults OFF, diagnostic only,
never live-tested.

Verification (no battle, no Java, no server, no GUI):
  - default parity: per-gun shots/hits over 20026 ticks of
    tr_drussgt_vs_modularbot.jsonl byte-for-byte identical to the golden
    generated from the PRE-CHANGE tree (git archive 55e92bc); the golden was
    regenerated from that pre-change tree and re-diffed, so it is not
    self-referential. Boot [env] block of the pre- and post-change binaries is
    identical except pid/cmdline/build line and the new knob's own line.
  - test_aim_capture: ALL PASS (every aim_scan/aim_fire key present, plus the
    annotation-inertness replay).
  - guards: test_tfil_commit_env 159/0, test_env_report 25/0,
    test_tfil_ring_weights 24/0, test_vbullet_draw 30/0.
  - .env.example round-trip (env_report via the j172 harness): 210 effective
    values + 70 [x]/[modules] lines, 0 diffs, 0 dropped keys, 0 warnings.
  - clean `git archive HEAD` + nim c -d:release: [SuccessX].
This commit is contained in:
2026-09-27 18:44:24 +02:00
parent 55e92bc5e0
commit 208f4a9092
7 changed files with 353 additions and 1 deletions
+19
View File
@@ -153,6 +153,7 @@
# TR_SURF_LOG one line per wave-surfing decision
# TR_FIRE_DIAG per-reading fire-detection tick/raw/correction
# TR_RECORD_WORLDSTATE dump every observed world state to JSONL
# TR_CAPTURE_AIM aim_scan/aim_fire records (gun id + bot belief)
# TR_RADAR_SCANLOG log every radar scan tick
# TR_RADAR_FORCE_SPIN force the old full-360 spin radar
# TR_TRACKER_PROBE dump the enemy-tracker internals
@@ -737,6 +738,24 @@ GUN_STATS_PATH=/tmp/gun_stats.jsonl # where the per-round gun stats are writte
GUN_SHOTLOG_PATH=/tmp/shot_log.jsonl # where the per-shot log is written
# ── measurement helpers (leave off unless you are measuring) ─────────────────
# WHAT: aim capture. Adds TWO record kinds to the TR_RECORD_WORLDSTATE file:
# aim_scan — one per radar scan: the raw reading, our own state, the gun
# that fired, the PREVIOUS tracker belief, and the scan parity
# (age = tick - previous lastSeenTick);
# aim_fire — one per real shot: gun, power, the aim angle, the turret error,
# gun heat, the predicted intercept/TOF, and the exact WorldState the
# predictor consumed (with the tick it came from). It also adds the `gun`
# id to the per-tick world-state rows.
# VALUES: presence-only, like the other keys in this block. Unset = off.
# STATUS: default-off, diagnostic only, never live-tested. j176 could not
# attribute the 11.9 deg aim error because the corpus had no gun id and no
# bot-side belief; this knob makes both a lookup instead of an inverse
# problem. No aim model changed with it.
# GOTCHA: it only writes when TR_RECORD_WORLDSTATE is on as well, and it makes
# the capture file bigger, not different: the extra lines are annotations and
# the offline replay skips them.
# TRY: TR_RECORD_WORLDSTATE=1 TR_CAPTURE_AIM=1 ./out/ModularBot
#TR_CAPTURE_AIM=1 # PRESENCE-only: aim_scan / aim_fire records (needs TR_RECORD_WORLDSTATE)
#TR_RECORD_WORLDSTATE=1 # PRESENCE-only: dump every observed world state
#TR_RADAR_SCANLOG=1 # PRESENCE-only: log every radar scan tick
#TR_TRACKER_PROBE=1 # PRESENCE-only: dump the enemy-tracker's internal state
+45
View File
@@ -44,6 +44,7 @@ import movement_harness/bullet_shadows
import targeting/enemy_tracker
import targeting/target_selector
import env_report
import aim_capture
import vbullet_draw
import geo_overlay
@@ -69,6 +70,13 @@ const ShotLog = true
## can enable recording for just the battle it spawns by exporting the env var.
let RecordWorldState* = existsEnv("TR_RECORD_WORLDSTATE")
const WorldStateRecordPath = "/tmp/worldstate_record.jsonl"
## j177 aim capture: with TR_CAPTURE_AIM set, append two extra record kinds to
## the SAME world-state file — `aim_scan` (one per radar scan: the raw reading,
## our own state, the GUN, the previous belief and the scan parity) and
## `aim_fire` (one per firing decision: the gun, the power, the aim angle, the
## turret error, and the exact WorldState the predictor consumed). Off by
## default, so a normal run writes byte-for-byte what it wrote before.
let CaptureAim* = existsEnv("TR_CAPTURE_AIM")
## Radar measurement switches (all RUNTIME, read once at process start):
## TR_RADAR_FORCE_SPIN=1 force the melee radar to the old stateless full
## spin (always 45 deg/tick). This reproduces the
@@ -477,6 +485,9 @@ proc recordWorldState(bot: ModularBot, ws: WorldState) =
"eid": tid,
}
if lst >= 0: row["lst"] = %lst
# j177: the gun in force when this state was built. Absent before j177,
# which made a per-gun decomposition of the aim error impossible.
if CaptureAim: row["gun"] = %bot.currentGun
try:
let f = open(WorldStateRecordPath, fmAppend)
f.writeLine($row)
@@ -616,6 +627,23 @@ proc recordRadarStats(bot: ModularBot) =
inc bot.arcWidthHist[min(11, int(width / 30.0))]
method onScannedBot*(bot: ModularBot, e: ScannedBotEvent) =
# j177 aim capture: the belief we are about to REPLACE, and the fire site's
# state, recorded BEFORE the update. Written first so the record is the
# pre-update belief by construction, not by argument.
if CaptureAim:
var rec = AimScan(tick: bot.tick, eid: e.scannedBotId,
ex: e.x, ey: e.y, eh: e.direction, es: e.speed, ee: e.energy,
sx: getX(), sy: getY(), sh: getDirection(), ss: getSpeed(),
gun: bot.currentGun,
rlock: bot.radarMode == 0, rdir: getRadarDirection(),
bx: 0.0, by: 0.0, blst: -1)
if bot.enemyTracker.enemies.contains(e.scannedBotId):
let prev = bot.enemyTracker.enemies[e.scannedBotId]
rec.bx = prev.x; rec.by = prev.y; rec.bh = prev.heading
rec.bs = prev.speed; rec.blst = prev.lastSeenTick
rec.lbear = bearing(rec.bx, rec.by, rec.sx, rec.sy)
rec.boff = (bearing(rec.ex, rec.ey, rec.sx, rec.sy) - rec.rdir) mod 360.0
appendLine(WorldStateRecordPath, scanRow(rec))
bot.enemyTracker.update(e.scannedBotId, e.x, e.y, e.direction, e.speed, e.energy, bot.tick)
bot.hasContact = true
if RadarScanLog and bot.radarMeleeActive:
@@ -1467,6 +1495,22 @@ method run*(bot: ModularBot) =
# Enqueue the selected gun so onBulletFired can stamp the server's bulletId.
# getEnergy() > power mirrors the server's "bot.energy <= firepower" reject.
if not floorBlocks and setFire(power) and getEnergy() > power:
# j177 aim capture: one `aim_fire` line per REAL shot, with the gun
# that fired and the exact WorldState the predictor consumed. This
# is the only place where `lst` is knowable, so it is the only place
# the scan parity of a firing decision can be recorded.
if CaptureAim:
let bspd = bulletSpeed(power)
appendLine(WorldStateRecordPath, fireRow(AimFire(
tick: bot.tick, eid: tid, gun: selectedGun, power: power,
aim: aimTarget, turret: gunDir, terr: normDelta, heat: gunHeat,
ax: pred.x, ay: pred.y, tof: (if bspd > 0: distPx / bspd else: 0.0),
ex: bot.lastState.enemyX, ey: bot.lastState.enemyY,
eh: bot.lastState.enemyHeading, es: bot.lastState.enemySpeed,
ee: bot.lastState.enemyEnergy,
sx: bot.lastState.selfX, sy: bot.lastState.selfY,
lst: (if tid >= 0 and bot.enemyTracker.enemies.contains(tid):
bot.enemyTracker.enemies[tid].lastSeenTick else: -1))))
bot.pendingFires.add(PendingShot(
gunId: selectedGun,
angleErr: abs(normDelta),
@@ -1558,6 +1602,7 @@ when isMainModule:
vBulletDebugGun: getEnv(VBulletDebugGunEnv, ""),
vBulletDebugMax: VBulletDebugMax,
recordWorldState: RecordWorldState,
captureAim: CaptureAim,
geoDebug: GeoDebugOn,
debugDraw: DebugDrawOn,
radarForceSpin: RadarForceSpin,
+129
View File
@@ -0,0 +1,129 @@
## j177 aim capture — the two records that make the aim error ATTRIBUTABLE.
##
## j176 could not answer "why is the aim 11.9 deg off at 450+ px" because the
## corpus has neither the bot's own belief (staleness was an INVERSE problem,
## unidentifiable) nor the gun id (a good gun's contribution was
## indistinguishable from a bad one's). Both are cheap to log and impossible
## to recover later. This module builds the two JSON records; ModularBot.nim
## calls it and the lines go into the EXISTING world-state capture
## (`TR_RECORD_WORLDSTATE` file), so the offline tooling sees one stream.
##
## It is deliberately PURE (no bot API, no env reads): the bot passes plain
## floats, the offline guard test passes the recorded fixture, and both go
## through the SAME row builders — so a field that the test proves present is
## a field the live bot writes.
##
## Key convention: `e*` = the enemy, `s*` = us, `b*` = the enemy's PREVIOUS
## belief in the tracker (before this scan's update), `lst` = the tick that
## state came from. `age = tick - blst` is the scan PARITY, recorded rather
## than inferred.
import std/[json, os, math]
const
ScanRecordKey* = "aim_scan" ## wrapper key, sibling of "meta"/"end"
FireRecordKey* = "aim_fire"
type
AimScan* = object
## One `onScannedBot` event, as the bot saw it BEFORE the update.
tick*: int
eid*: int
ex*, ey*: float ## raw scanned values
eh*, es*: float ## scanned heading (= direction) and speed
ee*: float
sx*, sy*: float ## our state at the scan (the FIRE SITE)
sh*, ss*: float
gun*: int ## bot.currentGun — the gun that fired, not the rack slot
bx*, by*: float ## previous belief, BEFORE this scan's update
bh*, bs*: float
blst*: int ## previous lastSeenTick; -1 = never scanned before
rlock*: bool ## radar lock engaged (false = the melee radar)
rdir*: float ## 36 deg scan window centre (radar heading)
lbear*: float ## bearing the lock is chasing (believed target)
boff*: float ## scanned bearing - rdir: where in the window it landed
AimFire* = object
## One firing decision, as the model computed it.
tick*: int
eid*: int
gun*: int ## bot.currentGun = the gun that fired
power*: float
aim*: float ## the raw angle handed to setFire/turret
turret*: float ## getGunDirection() at the command
terr*: float ## signed turret error (aim - turret)
heat*: float ## getGunHeat() BEFORE firing
ax*, ay*: float ## the intercept the gun predicted
tof*: float ## implied time of flight, ticks
ex*, ey*: float ## the WorldState the predictor CONSUMED
eh*, es*: float
ee*: float
sx*, sy*: float
lst*: int ## which tick that enemy state came from (scan parity)
proc bearing*(x, y, fx, fy: float): float =
arctan2(y - fy, x - fx).radToDeg
proc scanRow*(s: AimScan): JsonNode =
## The `aim_scan` line. Every field is unconditional: a capture that
## silently omits a field is worse than no capture.
result = newJObject()
result[ScanRecordKey] = newJObject()
let b = result[ScanRecordKey]
b["tick"] = %s.tick
b["eid"] = %s.eid
b["ex"] = %s.ex
b["ey"] = %s.ey
b["eh"] = %s.eh
b["es"] = %s.es
b["ee"] = %s.ee
b["sx"] = %s.sx
b["sy"] = %s.sy
b["sh"] = %s.sh
b["ss"] = %s.ss
b["gun"] = %s.gun
b["bx"] = %s.bx
b["by"] = %s.by
b["bh"] = %s.bh
b["bs"] = %s.bs
b["blst"] = %s.blst
b["age"] = %(if s.blst >= 0: s.tick - s.blst else: -1)
b["rlock"] = %s.rlock
b["rdir"] = %s.rdir
b["lbear"] = %s.lbear
b["boff"] = %s.boff
proc fireRow*(f: AimFire): JsonNode =
## The `aim_fire` line.
result = newJObject()
result[FireRecordKey] = newJObject()
let b = result[FireRecordKey]
b["tick"] = %f.tick
b["eid"] = %f.eid
b["gun"] = %f.gun
b["power"] = %f.power
b["aim"] = %f.aim
b["turret"] = %f.turret
b["terr"] = %f.terr
b["heat"] = %f.heat
b["ax"] = %f.ax
b["ay"] = %f.ay
b["tof"] = %f.tof
b["ex"] = %f.ex
b["ey"] = %f.ey
b["eh"] = %f.eh
b["es"] = %f.es
b["ee"] = %f.ee
b["sx"] = %f.sx
b["sy"] = %f.sy
b["lst"] = %f.lst
proc appendLine*(path: string, row: JsonNode) =
## Append one JSONL line, fully guarded: a full disk or a bad path must
## never take the bot down (same contract as the existing recorders).
try:
let f = open(path, fmAppend)
f.writeLine($row)
f.close()
except CatchableError:
discard
+4 -1
View File
@@ -52,6 +52,7 @@ type
vBulletDebugGun*: string
vBulletDebugMax*: int
recordWorldState*: bool
captureAim*: bool ## j177: aim_scan / aim_fire records, default off
geoDebug*: bool
debugDraw*: bool
radarForceSpin*: bool
@@ -245,6 +246,8 @@ proc printEffectiveValues(ctx: EnvReportContext) =
emit("TR_RESULT_LOG", onOff(ctx.resultLog), sourceOf("TR_RESULT_LOG"))
emit("TR_RECORD_WORLDSTATE", onOff(ctx.recordWorldState),
sourceOfPresence("TR_RECORD_WORLDSTATE"))
emit("TR_CAPTURE_AIM", onOff(ctx.captureAim),
sourceOfPresence("TR_CAPTURE_AIM"))
emit("TR_RADAR_FORCE_SPIN", onOff(ctx.radarForceSpin),
sourceOfPresence("TR_RADAR_FORCE_SPIN"))
emit("TR_RADAR_SCANLOG", onOff(ctx.radarScanLog),
@@ -657,7 +660,7 @@ proc knownEnvNames*(): seq[string] =
"GUN_SELECTOR_SHRINK", "GUN_SELECTOR_DWELL", "GUN_SELECTOR_MARGIN",
"GUN_SELECTOR_POINT_TIE", "GUN_SELECTOR_SEED",
"GUN_RACK_DISABLE", "GUN_STATS_PATH", "GUN_SHOTLOG_PATH",
"TR_MOVEMENT", "TR_MOVEMENT_LOG", "TR_RECORD_WORLDSTATE",
"TR_MOVEMENT", "TR_MOVEMENT_LOG", "TR_RECORD_WORLDSTATE", "TR_CAPTURE_AIM",
"TR_RADAR_FORCE_SPIN", "TR_RADAR_SCANLOG", "TR_RADAR_SCAN_LOG_PATH",
"TR_TRACKER_PROBE", "TR_TRACKER_PROBE_PATH", "TR_VBULLET_ADMIT_ONLY",
VBulletDebugEnv, VBulletDebugGunEnv, VBulletDebugMaxEnv,
@@ -23,6 +23,8 @@
## A trailing live end marker is also optional:
## {"end":{"enemy_died":<bool>,"ticks":<int>}}
## It lets the replay reproduce the live resolver's final-tick behaviour.
## `aim_scan` / `aim_fire` annotation lines (written only when
## TR_CAPTURE_AIM is set) carry no `ex` and are skipped.
##
## The replay never calls the gun selector, so it is RNG-free for every
## deterministic gun. Tsetlin is stochastic and is expected to differ.
@@ -196,6 +198,10 @@ proc loadFixture*(path: string): Fixture =
if node["end"].hasKey("enemy_died"):
result.enemyDied = node["end"]["enemy_died"].getBool()
continue
# j177: the recorder can also write `aim_scan` / `aim_fire` lines into the
# same file when TR_CAPTURE_AIM is set. They are annotations on ticks, not
# ticks, so they carry no `ex` and are skipped here.
if not node.hasKey("ex"): continue
result.states.add stateFromJson(node, arenaW, arenaH, enemyId)
result.lastSeen.add (if node.hasKey("lst"): node["lst"].getInt() else: -1)
result.enemyId = enemyId
+18
View File
@@ -0,0 +1,18 @@
# j177 gun-path default-parity golden.
# Generated from the PRE-CHANGE tree (`git archive 55e92bc`) with
# TR_CAPTURE_AIM unset, over the whole tr_drussgt_vs_modularbot.jsonl.
# Format: <gun> shots=<n> hits=<n>, one line per rack gun
HeadOn shots=400 hits=59
Linear shots=400 hits=47
Tsetlin shots=400 hits=74
Circular shots=400 hits=51
GuessFactor shots=400 hits=44
Pattern shots=400 hits=46
WallBounce shots=400 hits=55
Accel shots=400 hits=59
StopShot shots=400 hits=84
Displace shots=400 hits=46
AvgLead shots=400 hits=51
DecayGF shots=400 hits=47
KNN shots=400 hits=29
TMSelect shots=0 hits=0
+132
View File
@@ -0,0 +1,132 @@
## j177 guard: the aim capture actually contains every field it promises, and
## the default gun path is byte-for-byte unchanged.
##
## NO battle, NO Java, NO server, NO GUI.
## nim c -r --path:common_libs --path:ModularBot_garage/src \
## --nimcache:/tmp/nc_j177 common_libs/tests/test_aim_capture.nim
##
## Part 1 (the guard): with the capture ON, build one `aim_scan` and one
## `aim_fire` row from a real recorded tick of
## tools/fixtures/tr_drussgt_vs_modularbot.jsonl, through the SAME
## `aim_capture` row builders ModularBot.nim calls, and assert every required
## key is present and carries the value that was passed in. A capture that
## silently omits a field is worse than none.
##
## Part 2 (default parity): replay the whole recorded fixture through the real
## VirtualTracker + the guns with the capture OFF and compare the per-gun
## fitness report against `fixtures/aim_capture_gunpath.golden`. That golden
## was generated from the PRE-CHANGE tree (`git archive 55e92bc`) with the
## knob unset — regenerating it from this code would defeat the check.
## Part 2b: the same replay over a fixture that ALSO carries aim_scan/aim_fire
## lines must give the identical report, i.e. the annotations are inert.
import std/[json, os, strutils, sequtils, strformat]
import gun_harness/offline_range
import range_guns
import ../../ModularBot_garage/src/aim_capture
const
repoRoot = currentSourcePath().parentDir.parentDir.parentDir
fixtureRel = "tr_drussgt_vs_modularbot.jsonl"
fixture = repoRoot / "tools" / "fixtures" / fixtureRel
goldenPath = currentSourcePath().parentDir / "fixtures" / "aim_capture_gunpath.golden"
annotated = "/tmp/j177_annotated_fixture.jsonl"
ScanKeys = ["tick", "eid", "ex", "ey", "eh", "es", "ee", "sx", "sy", "sh", "ss",
"gun", "bx", "by", "bh", "bs", "blst", "age", "rlock", "rdir",
"lbear", "boff"]
FireKeys = ["tick", "eid", "gun", "power", "aim", "turret", "terr", "heat",
"ax", "ay", "tof", "ex", "ey", "eh", "es", "ee", "sx", "sy", "lst"]
var failures = 0
proc check(name: string, ok: bool) =
if ok: echo "PASS: ", name
else:
echo "FAIL: ", name
inc failures
proc replayReport(path: string): string =
## Per-gun shots/hits over the whole fixture, through the REAL tracker and
## the REAL guns, with the capture OFF.
let res = replayFixture(loadFixture(path), buildAllGunDrivers(seed = 1))
for r in res:
result.add r.name & " shots=" & $r.shots & " hits=" & $r.hits & "\n"
# ── Part 1: the records carry every field ────────────────────────────────────
proc fieldCheck() =
let states = loadFixture(fixture).states
check("fixture loaded", states.len > 1000)
let ws = states[900]
let scan = scanRow(AimScan(
tick: 900, eid: 7,
ex: ws.enemyX, ey: ws.enemyY, eh: ws.enemyHeading, es: ws.enemySpeed,
ee: ws.enemyEnergy, sx: ws.selfX, sy: ws.selfY, sh: ws.selfHeading,
ss: ws.selfSpeed, gun: 5,
bx: ws.enemyX - 8.0, by: ws.enemyY - 8.0, bh: ws.enemyHeading,
bs: ws.enemySpeed, blst: 896,
rlock: true, rdir: 42.5, lbear: bearing(ws.enemyX - 8, ws.enemyY - 8, ws.selfX, ws.selfY),
boff: -12.25))
let fire = fireRow(AimFire(
tick: 900, eid: 7, gun: 5, power: 1.6, aim: 88.25, turret: 74.0,
terr: 14.25, heat: 0.31, ax: ws.enemyX + 40.0, ay: ws.enemyY - 15.0,
tof: 12.5, ex: ws.enemyX, ey: ws.enemyY, eh: ws.enemyHeading,
es: ws.enemySpeed, ee: ws.enemyEnergy, sx: ws.selfX, sy: ws.selfY, lst: 897))
for k in ScanKeys:
check("aim_scan has " & k, scan[ScanRecordKey].hasKey(k))
for k in FireKeys:
check("aim_fire has " & k, fire[FireRecordKey].hasKey(k))
check("aim_scan carries the gun id", scan[ScanRecordKey]["gun"].getInt() == 5)
check("aim_fire carries the gun id", fire[FireRecordKey]["gun"].getInt() == 5)
check("aim_scan records the scan parity age",
scan[ScanRecordKey]["age"].getInt() == 4)
check("aim_fire records the source tick (parity)",
fire[FireRecordKey]["lst"].getInt() == 897)
check("aim_fire carries the aim angle", fire[FireRecordKey]["aim"].getFloat() == 88.25)
check("aim_fire carries the turret error", fire[FireRecordKey]["terr"].getFloat() == 14.25)
check("aim_fire carries the WorldState the model consumed",
fire[FireRecordKey]["ex"].getFloat() == ws.enemyX and
fire[FireRecordKey]["ey"].getFloat() == ws.enemyY)
echo "\n--- aim_scan record ---"
echo $scan
echo "--- aim_fire record ---"
echo $fire
echo ""
# ── Part 2: default gun-path parity ──────────────────────────────────────────
proc parityCheck() =
let clean = replayReport(fixture)
let ticks = loadFixture(fixture).states.len
if defined(aimCapGenGolden):
var g = "# j177 gun-path default-parity golden.\n"
g.add "# Generated from the PRE-CHANGE tree (`git archive 55e92bc`) with\n"
g.add "# TR_CAPTURE_AIM unset, over the whole " & fixtureRel & ".\n"
g.add "# Format: <gun> shots=<n> hits=<n>, one line per rack gun\n"
g.add clean
createDir(goldenPath.parentDir)
writeFile(goldenPath, g)
echo "wrote ", goldenPath, " (", ticks, " ticks)"
return
check("golden exists", fileExists(goldenPath))
if not fileExists(goldenPath): return
let g = lines(goldenPath).toSeq().filterIt(not it.startsWith("#")).join("\n").strip()
check("gun path byte-for-byte identical over " & $ticks & " ticks", g == clean.strip())
# 2b: the annotation lines must be inert for the replay.
let extra = @[
$scanRow(AimScan(tick: 0, eid: 1, ex: 1.0, ey: 2.0, eh: 3.0, es: 4.0, ee: 5.0,
sx: 6.0, sy: 7.0, sh: 8.0, ss: 9.0, gun: 3,
bx: 0.0, by: 0.0, blst: -1, rlock: true, rdir: 1.0, lbear: 2.0)),
$fireRow(AimFire(tick: 1, eid: 1, gun: 3, power: 1.5, aim: 1.0, turret: 2.0,
terr: 3.0, heat: 0.0, ax: 1.0, ay: 1.0, tof: 1.0,
ex: 1.0, ey: 1.0, eh: 1.0, es: 1.0, ee: 1.0,
sx: 1.0, sy: 1.0, lst: 0))]
writeFile(annotated, (lines(fixture).toSeq() & extra).join("\n"))
check("aim records do not perturb the replay", replayReport(annotated) == clean)
removeFile(annotated)
fieldCheck()
parityCheck()
echo (if failures == 0: "\nALL PASS" else: "\n" & $failures & " FAILURE(S)")
quit(if failures == 0: 0 else: 1)