## Deterministic check for the j132 STRAFE pick-quality DISPLAY. ## ## NO battle, NO Java, NO server. Run with: ## nim c -r --nimcache:/tmp/nc_j132 --path:common_libs \ ## common_libs/tests/measure_strafe_heat_display.nim ## ## It drives `computeMove` over the recorded DrussGT fixture (the SAME stream ## `measure_strafe_gates.nim` replays) and asserts, for every tick with a valid ## target, that the number the display shows (`targetPathHeat`) is the field's ## REAL reading for that tile: ## ## pathMaxHeat(bot -> chosen tile centre) ## ## On every PICK it also asserts that the number the `[strafe]` line prints ## (`pickPathHeat`, formatted by `strafeHeatText`) is that same number, and that ## a pick at or under `PathDangerThreshold` is labelled safe (`ok=1`), i.e. that ## a "pick" (safe pool non-empty) never carries a heat above the threshold. ## ## This is a DISPLAY check. It changes nothing the mover decides: it only proves ## the displayed/logged heat is the tile's actual path heat. import std/[os, json, math, random, strformat] import std/strutils except fromHex # `fromHex` clashes with color.fromHex import gun_harness/gun_interface # Private-field access: include (do NOT import) the mover, exactly as # `test_tfil_commit_env.nim` includes `the_floor_is_lava.nim`. include movements/strafe const repoRoot = currentSourcePath().parentDir.parentDir.parentDir const fixtureRel = "tr_drussgt_vs_modularbot.jsonl" const Seed = 20250923 const ArenaW = 800.0 const ArenaH = 600.0 var failures = 0 proc check(name: string, ok: bool) = if ok: echo "PASS: ", name else: echo "FAIL: ", name; inc failures proc loadStates(): seq[WorldState] = let path = repoRoot / "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())]) # ── the replay ─────────────────────────────────────────────────────────────── proc main() = let states = loadStates() check fmt"fixture loaded ({states.len} ticks)", states.len > 0 randomize(Seed) var m = initStrafe() m.debugGraphics = false const DrawWindow = 400 ## ticks drawn: enough for ~30 picks, cheap to render var ticks = 0 targetTicks = 0 picks = 0 under = 0 ## picks labelled ok=1 (pathHeat <= PathDangerThreshold) over = 0 ## picks labelled ok=0 (forced fallback/escape) comfy = 0 ## picks <= HALF the threshold (comfortably safe) altKnown = 0 ## picks where a "best alternative" existed dispBad = 0 ## display value != real pathMaxHeat of the chosen tile pickBad = 0 ## picker value != real pathMaxHeat of the chosen tile labelBad = 0 ## a "pick" (safe pool non-empty) with heat > threshold textBad = 0 ## the printed text does not carry the same number minH = Inf maxH = -Inf sumH = 0.0 bandComfy = 0 ## <= 2.5 (half of half) bandLow = 0 ## 2.5 .. 5.0 bandMarginal = 0 ## 5.0 .. 10.0 svgTicks = 0 ## ticks whose overlay was rendered and inspected svgBad = 0 ## the chosen tile's heat NUMBER is not in the SVG svgNoPlate = 0 ## the chosen tile's dark value plate is not in the SVG svgNoRamp = 0 ## the chosen tile's ramp border is not in the SVG for tick, ws in states: # The GUI half: render the overlay for the first `DrawWindow` ticks, with # the heat grid ALTERNATING on/off so BOTH requirement-D arms are exercised # (with the grid hidden the chosen tile's number must still be there). # Drawing is read-only, so the pick sequence stays byte-identical; the SVG # buffer is cleared every tick exactly as the framework does. m.debugGraphics = tick < DrawWindow let gridOn = (tick mod 2 == 0) if m.debugGraphics: StrafeHeatGrid = gridOn clearGraphics() discard m.computeMove(ws) inc ticks if m.debugGraphics: let svg = svgOutput() clearGraphics() if m.targetValid and svg.len > 0: inc svgTicks # The chosen tile's ramp-coloured thick border + its dark value plate # (both unique to the chosen-tile block, drawn whether or not the heat # grid is on), and - with the grid ON only the chosen tile is labelled, # so the number in the SVG is provably the chosen tile's - # the chosen tile's heat NUMBER, as the SVG sees it. if not svg.contains("stroke-width=\"3.5\""): inc svgNoRamp if not svg.contains("stroke=\"" & heatRamp(m.targetPathHeat).toHex & "\""): inc svgNoRamp if not svg.contains("fill=\"#000000\""): inc svgNoPlate if gridOn and not svg.contains(fmt">{m.targetPathHeat:.1f}"): inc svgBad if not m.targetValid: continue inc targetTicks # The field's REAL reading for the chosen tile, this tick. let realHeat = m.pathMaxHeat(ws.selfX, ws.selfY, m.targetX, m.targetY) if abs(realHeat - m.targetPathHeat) > 1e-9: inc dispBad # A pick happened on this tick when the picker reset `lastPickCall` to the # pre-increment `callCount` (computeMove increments callCount last). if m.callCount != m.lastPickCall + 1: continue inc picks if abs(m.pickPathHeat - realHeat) > 1e-9: inc pickBad let safe = strafeHeatSafe(m.pickPathHeat) if safe: inc under else: inc over if m.pickPathHeat <= PathDangerThreshold * 0.5: inc comfy if m.pickPathHeat <= PathDangerThreshold * 0.25: inc bandComfy elif m.pickPathHeat <= PathDangerThreshold * 0.5: inc bandLow elif safe: inc bandMarginal if m.targetBestAlt >= 0.0: inc altKnown if m.pickPathHeat < minH: minH = m.pickPathHeat if m.pickPathHeat > maxH: maxH = m.pickPathHeat sumH += m.pickPathHeat # A pick from the safe pool can never be over the threshold. if m.lastMode == "pick" and not safe: inc labelBad # The EXACT string the log line prints for this pick, and the exact string # the GUI prints on the chosen tile, must carry that same number. let pickTxt = strafeHeatText(m.pickPathHeat, m.targetBestAlt) let okTxt = if safe: "ok=1" else: "ok=0" if not pickTxt.startsWith(fmt"heat={realHeat:.1f}/{PathDangerThreshold:.1f}"): inc textBad if not pickTxt.contains(okTxt): inc textBad if not strafeHeatText(m.targetPathHeat, m.targetBestAlt).startsWith( fmt"heat={m.targetPathHeat:.1f}/"): inc textBad check fmt"every displayed heat == pathMaxHeat of the chosen tile " & fmt"({targetTicks} targeted ticks, {dispBad} mismatches)", dispBad == 0 check fmt"every printed pick heat == the picker's real path heat " & fmt"({picks} picks, {pickBad} mismatches)", pickBad == 0 check fmt"a safe pick is labelled safe and never exceeds the threshold " & fmt"({under} under / {over} over, {labelBad} bad)", labelBad == 0 check fmt"`strafeHeatText` carries the displayed number ({textBad} bad)", textBad == 0 check fmt"the chosen tile's heat NUMBER is rendered on the tile with the " & fmt"heat grid ON ({svgTicks} rendered ticks, {svgBad} missing)", svgBad == 0 check fmt"the chosen tile's ramp border is rendered ({svgNoRamp} missing)", svgNoRamp == 0 check fmt"the chosen tile's value plate is rendered ({svgNoPlate} missing)", svgNoPlate == 0 check "the stream contains picks to check", picks > 0 echo "" echo fmt"replayed {ticks} ticks, {targetTicks} with a target, {picks} picks" echo fmt"labelled: under={under} over={over} comfortably-safe(<= " & fmt"{PathDangerThreshold * 0.5:.1f})={comfy}" echo fmt"chosen pathHeat: min={minH:.1f} mean={sumH / max(1.0, picks.float):.1f} " & fmt"max={maxH:.1f} best-alt known={altKnown}/{picks}" echo fmt"buckets: <=2.5 {bandComfy} | 2.5-5 {bandLow} | 5-10 {bandMarginal} | " & fmt">10 (forced) {over}" echo fmt"overlay rendered on {svgTicks} ticks (heat grid alternating ON/OFF)" echo fmt"sample log fields: [{strafeHeatText(7.3, 9.8)}] " & fmt"[{strafeHeatText(11.4, -1.0)}]" echo "" if failures > 0: echo failures, " check(s) FAILED" quit(1) echo "All STRAFE pick-quality display checks passed." main()