j132 strafe pick quality: chosen tile's heat in the [strafe] log line (heat=h/10.0 alt=best-other ok=0|1) and ON the tile (green/yellow/red ramp + value), legible with TR_STRAFE_HEAT_GRID=0; deterministic display check vs pathMaxHeat
This commit is contained in:
@@ -159,6 +159,21 @@
|
||||
## TR_STRAFE_WALL_SAFE 24.0 px a wing point never lands nearer
|
||||
## TR_STRAFE_ESCAPE 1 the all-hot guaranteed wall escape
|
||||
##
|
||||
## ── Pick quality on screen and in the log (j132) ────────────────────────────
|
||||
## The chosen tile's QUALITY used to be invisible: the overlay graded candidates
|
||||
## with a binary colour and the `[strafe]` line never printed the chosen tile's
|
||||
## heat, so a comfortably-safe pick could not be told from a marginal one.
|
||||
## Both now carry the VALUE of the UNCHANGED safety rule `pathHeat <= 10`:
|
||||
## * the `[strafe]` line gains `heat=<h>/10.0 alt=<best other candidate>
|
||||
## ok=<1|0>` (see `strafeHeatText`);
|
||||
## * the chosen tile gets a thick green->yellow->red border keyed to how close
|
||||
## `h` is to the threshold, with `h` printed ON the tile, independent of
|
||||
## `TR_STRAFE_HEAT_GRID` (so it is legible with the grid hidden);
|
||||
## * candidates are stroked with the same ramp and, when the grid is hidden,
|
||||
## labelled with their path heat, so the overlay reads on its own.
|
||||
## This is display/logging only: `PathDangerThreshold` and every pick are
|
||||
## byte-identical.
|
||||
##
|
||||
## The default `TR_MOVEMENT=tfil` path is untouched; this module is only ever
|
||||
## called when the bot explicitly selects `strafe`.
|
||||
|
||||
@@ -226,6 +241,36 @@ const PathDangerThreshold = 10.0 ## max lava on path; above this = unsafe
|
||||
const DangerReplanThreshold = 25.0 ## serious-threat replan (bullet core)
|
||||
const MaxTrackedBullets = 20 ## hard cap on tracked bullets
|
||||
|
||||
# ── pick-quality display (j132) ──────────────────────────────────────────────
|
||||
|
||||
proc strafeHeatSafe*(h: float): bool {.inline.} =
|
||||
## The picker's safety rule as a predicate, so the log and the overlay grade a
|
||||
## tile EXACTLY the way the picker judges it. Unchanged rule: `<= 10`.
|
||||
h <= PathDangerThreshold
|
||||
|
||||
proc heatRamp(h: float): Color =
|
||||
## GREEN at or below half the threshold (comfortably safe), YELLOW exactly AT
|
||||
## the threshold (marginal), RED above it (saturating at twice it). A cue
|
||||
## only: the picker's rule stays `strafeHeatSafe`, so this can never admit or
|
||||
## reject a tile.
|
||||
let t = if PathDangerThreshold > 0.0: h / PathDangerThreshold else: 0.0
|
||||
if t <= 0.5:
|
||||
fromHex("#00DD00")
|
||||
elif t <= 1.0:
|
||||
fromRgb(uint8(255.0 * (t - 0.5) * 2.0), 255'u8, 0'u8)
|
||||
else:
|
||||
fromRgb(255'u8, uint8(255.0 * clamp(2.0 - t, 0.0, 1.0)), 0'u8)
|
||||
|
||||
proc strafeHeatText*(heat, alt: float): string =
|
||||
## The display fields appended to the `[strafe]` line: the chosen tile's path
|
||||
## heat against the threshold, the coolest OTHER candidate (`-` = none left on
|
||||
## the table), and the safe/over label. `heat=7.3/10.0 alt=9.8 ok=1` reads as
|
||||
## "7.3 of the 10.0 allowed, the best tile passed up was 9.8 (marginal), and
|
||||
## 7.3 is safe". One line, greppable: `grep -o 'heat=[0-9.]*/[0-9.]*'`.
|
||||
let altTxt = if alt < 0.0: "-" else: fmt"{alt:.1f}"
|
||||
let okTxt = if strafeHeatSafe(heat): "ok=1" else: "ok=0"
|
||||
fmt"heat={heat:.1f}/{PathDangerThreshold:.1f} alt={altTxt} " & okTxt
|
||||
|
||||
# ── Env knobs ────────────────────────────────────────────────────────────────
|
||||
|
||||
proc getEnvFloat(name: string, default: float): float =
|
||||
@@ -363,6 +408,10 @@ type
|
||||
targetX*, targetY*: float ## chosen tile centre (world coords)
|
||||
targetValid*: bool
|
||||
targetLava: float ## heat at the chosen tile when picked
|
||||
# ── pick quality (j132): display/log only, never read by a decision ──
|
||||
targetPathHeat*: float ## path max heat to the chosen tile, THIS tick
|
||||
pickPathHeat*: float ## what the picker measured for the chosen tile
|
||||
targetBestAlt*: float ## coolest OTHER candidate's path heat (-1 none)
|
||||
dwell*: int ## ticks remaining on the current target
|
||||
dir*: float ## commanded sign: +1 forward, -1 backward
|
||||
bandOffset: float ## random in [-band, band], re-rolled per pick
|
||||
@@ -434,6 +483,9 @@ proc resetRound*(m: var StrafeModule) =
|
||||
m.prevEnergy = @[]
|
||||
m.targetValid = false
|
||||
m.targetLava = 0.0
|
||||
m.targetPathHeat = 0.0
|
||||
m.pickPathHeat = 0.0
|
||||
m.targetBestAlt = -1.0
|
||||
m.dwell = 0
|
||||
m.dir = 1.0
|
||||
m.bandOffset = 0.0
|
||||
@@ -915,6 +967,15 @@ proc pickTarget(m: var StrafeModule, ws: WorldState, lineForward: float,
|
||||
m.targetValid = true
|
||||
m.lastMode = mode
|
||||
|
||||
# ── pick quality (j132): the number the picker measured, plus the coolest
|
||||
# candidate it did NOT take (what it left on the table). Display/log only.
|
||||
m.pickPathHeat = pool[chosen].pathHeat
|
||||
m.targetBestAlt = -1.0
|
||||
for c in cands:
|
||||
if c.col == pool[chosen].col and c.row == pool[chosen].row: continue
|
||||
if m.targetBestAlt < 0.0 or c.pathHeat < m.targetBestAlt:
|
||||
m.targetBestAlt = c.pathHeat
|
||||
|
||||
# sign is frozen for the whole dwell: reversal timing == dwell timing.
|
||||
let hx = cos(ws.selfHeading * DegToRad)
|
||||
let hy = sin(ws.selfHeading * DegToRad)
|
||||
@@ -945,7 +1006,9 @@ proc pickTarget(m: var StrafeModule, ws: WorldState, lineForward: float,
|
||||
fmt"along={pool[chosen].along.int} dir={m.dir.int} " &
|
||||
fmt"dwell={m.dwell} line={lineForward.int} band={m.bandOffset:.0f} " &
|
||||
fmt"dist={rng.dist:.0f} target={StrafeRange:.0f} tilt={m.rangeTilt:.1f} " &
|
||||
fmt"wall={m.wallDist:.0f} kappa={kappa:.4f} mode={mode}"
|
||||
fmt"wall={m.wallDist:.0f} kappa={kappa:.4f} " &
|
||||
strafeHeatText(pool[chosen].pathHeat, m.targetBestAlt) & " " &
|
||||
fmt"mode={mode}"
|
||||
if escaped or safe.len == 0 or mode == "escape":
|
||||
echo fmt"[strafe] WARNING: no SAFE tile on the line " &
|
||||
fmt"(cands={cands.len}, clearance={pool[chosen].clearance:.0f}, " &
|
||||
@@ -1076,6 +1139,16 @@ proc computeMove*(m: var StrafeModule, ws: WorldState): MoveCommand =
|
||||
else:
|
||||
dec m.dwell
|
||||
|
||||
# ── pick quality (j132), display only: the chosen tile's LIVE path heat.
|
||||
# Recomputed every tick because the field moves with the bullets; the GUI
|
||||
# prints it ON the tile and the `[strafe]` line prints it at pick time. On a
|
||||
# pick tick it EQUALS `pickPathHeat`, because it is the same `pathMaxHeat`
|
||||
# call over the same field and the same position - asserted by
|
||||
# `common_libs/tests/measure_strafe_heat_display.nim`.
|
||||
m.targetPathHeat = if m.targetValid:
|
||||
m.pathMaxHeat(ws.selfX, ws.selfY, m.targetX, m.targetY)
|
||||
else: 0.0
|
||||
|
||||
# ── heading band: turn ONLY to stay on the line / arc tangent, never to the
|
||||
# target. In escape mode the band reference is the wall-away normal, so the
|
||||
# body slowly rotates inward until the sign can push us off the wall. ──
|
||||
@@ -1205,22 +1278,40 @@ proc computeMove*(m: var StrafeModule, ws: WorldState): MoveCommand =
|
||||
let (c, r) = m.tileAt(wx, wy)
|
||||
let x0 = m.marginX + c.float * GridSize
|
||||
let y0 = m.marginY + r.float * GridSize
|
||||
if m.lavaAt(c, r) <= PathDangerThreshold:
|
||||
setStrokeColor(fromHex("#00FF00"))
|
||||
setStrokeWidth(1.0)
|
||||
else:
|
||||
setStrokeColor(fromHex("#804000"))
|
||||
setStrokeWidth(1.0)
|
||||
# GRADE the candidate by the SAME number the picker uses: the max heat
|
||||
# on the straight path from the bot (the old binary looked at the
|
||||
# tile's own lava, which is not the safety rule).
|
||||
let ph = m.pathMaxHeat(ws.selfX, ws.selfY,
|
||||
x0 + GridSize * 0.5, y0 + GridSize * 0.5)
|
||||
setStrokeColor(heatRamp(ph))
|
||||
setStrokeWidth(if strafeHeatSafe(ph): 1.5 else: 1.0)
|
||||
drawRectangle(x0, y0, GridSize, GridSize)
|
||||
# With the full heat grid HIDDEN (`TR_STRAFE_HEAT_GRID=0`) the
|
||||
# candidates carry their own number, so the STRAFE overlay reads by
|
||||
# itself; with the grid ON the grid already labels every tile.
|
||||
if not StrafeHeatGrid:
|
||||
setFillColor(heatRamp(ph))
|
||||
setFont("Arial", 10.0)
|
||||
drawText(fmt"{ph:.1f}", x0 + 3.0, y0 + 14.0)
|
||||
|
||||
# Chosen target tile (magenta fill) + the sign-coloured movement ray.
|
||||
# ── chosen target tile (j132): its QUALITY, not just its position ────────
|
||||
# A thick ramp-coloured border (green = comfortably safe, yellow = at the
|
||||
# threshold, red = over) plus the tile's path heat printed ON the tile over
|
||||
# a dark plate, so it stays legible whatever the fill colour is. The magenta
|
||||
# dot still marks the tile as the chosen target. Drawn independently of
|
||||
# `StrafeHeatGrid`, so the pick reads clearly with `TR_STRAFE_HEAT_GRID=0`.
|
||||
if m.targetValid:
|
||||
let (cc, cr) = m.tileAt(m.targetX, m.targetY)
|
||||
let gx0 = m.marginX + cc.float * GridSize
|
||||
let gy0 = m.marginY + cr.float * GridSize
|
||||
setStrokeColor(fromHex("#FF00FF"))
|
||||
setStrokeWidth(2.5)
|
||||
setStrokeColor(heatRamp(m.targetPathHeat))
|
||||
setStrokeWidth(3.5)
|
||||
drawRectangle(gx0, gy0, GridSize, GridSize)
|
||||
setFillColor(fromHex("#000000"))
|
||||
fillRectangle(gx0 + 1.0, gy0 + 18.0, GridSize - 2.0, 17.0)
|
||||
setFillColor(heatRamp(m.targetPathHeat))
|
||||
setFont("Arial", 12.0)
|
||||
drawText(fmt"{m.targetPathHeat:.1f}", gx0 + 4.0, gy0 + 31.0)
|
||||
setFillColor(fromHex("#FF00FF"))
|
||||
fillCircle(m.targetX, m.targetY, 5.0)
|
||||
# movement ray: green forward, red backward (the sign flip is the point)
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
for ws in states:
|
||||
discard m.computeMove(ws)
|
||||
inc ticks
|
||||
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 "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"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()
|
||||
Reference in New Issue
Block a user