## Fixture replay for the TFIL-RING mover (`movements/the_floor_is_lava_ring.nim`). ## ## It `include`s the mover (not `import`s it) so the replay can read the ## private `commitTarget` / `commitTicks` — the same reason ## `test_tfil_commit_env.nim` includes `the_floor_is_lava.nim`. Living in its ## OWN module keeps those privates in this module's scope, so a file that ## includes BOTH movers still compiles (there is no name clash between them: ## this one only sees ring's). ## ## `--path:common_libs` relative to the repo root. ## ## GOLDEN GENERATION (j165 default parity). Compile this file with the golden ## flag against the PRE-CHANGE ring, e.g. from a `git show HEAD:...` tree: ## ## TFIL_RING_GOLDEN_OUT= \ ## nim c -r --path:. -d:tfilRingGenGolden common_libs/tests/tfil_ring_replay.nim ## ## Nothing here references a j165 symbol, so the SAME file generates the golden ## on the pre-change mover and checks it on the post-change one. Regenerating ## the golden from the new code would defeat the check — only do that after a ## DELIBERATE change to the ring defaults. import std/[os, json, random, math] import std/strutils except fromHex import gun_harness/gun_interface include movements/the_floor_is_lava_ring const Seed = 20250923 ## same seed as the tfil replay: comparable arms ArenaW = 800.0 ArenaH = 600.0 fixtureRel = "tr_drussgt_vs_modularbot.jsonl" const RingGoldenPath* = currentSourcePath().parentDir / "fixtures" / "tfil_ring_commit_default.golden" type RingTickRec* = object spd, trn: float ## the emitted MoveCommand tx, ty: float ## where we are steering to ct: int ## ticks left on the commitment picked*: bool ## this tick made a NEW pick (not in the golden) # Thin accessors for the mover's PRIVATE per-round state. The guard needs them # to measure what the commitment did; the mover's own API stays unchanged. proc ringPicks*(m: TFILRingModule): int = m.picks proc ringTarget*(m: TFILRingModule): tuple[x, y: float] = m.commitTarget proc ringRecLine*(r: RingTickRec): string = $r.spd & " " & $r.trn & " " & $r.tx & " " & $r.ty & " " & $r.ct proc loadRingStates*(): seq[WorldState] = let path = currentSourcePath().parentDir.parentDir.parentDir / "tools" / "fixtures" / fixtureRel for rawLine in lines(path): let line = rawLine.strip() if line.len == 0: continue let n = parseJson(line) if n.hasKey("meta") or n.hasKey("end"): continue let ex = n["ex"].getFloat() let ey = n["ey"].getFloat() result.add WorldState( enemyX: ex, enemyY: ey, enemyHeading: n["eh"].getFloat(), enemySpeed: n["es"].getFloat(), enemyEnergy: n["ee"].getFloat(), selfX: n["sx"].getFloat(), selfY: n["sy"].getFloat(), selfHeading: n["sh"].getFloat(), selfSpeed: n["ss"].getFloat(), selfEnergy: n["se"].getFloat(), arenaWidth: ArenaW, arenaHeight: ArenaH, tick: n["tick"].getInt(), enemies: @[EnemyInfo(id: 1, x: ex, y: ey, heading: n["eh"].getFloat(), speed: n["es"].getFloat(), energy: n["ee"].getFloat())]) proc loadRingStarts*(): seq[int] = let side = currentSourcePath().parentDir.parentDir.parentDir / "tools" / "fixtures" / "drussgt_meta" / (fixtureRel & ".rounds.json") if not fileExists(side): return for r in parseFile(side)["rounds"]: result.add r["startTick"].getInt() proc replayRing*(): seq[RingTickRec] = ## Drive the REAL ring `computeMove` over the recorded WorldState stream with a ## fixed seed, touching no env knob. With every j165 knob unset this is the ## pre-change code path exactly. randomize(Seed) var m = initTFILRing() let states = loadRingStates() let starts = loadRingStarts() for i in 0..