199 lines
8.9 KiB
Nim
199 lines
8.9 KiB
Nim
## 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}</text>"): 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()
|