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:
2026-09-26 11:51:33 +02:00
parent 8dd9b3b3b5
commit 4db46f02dd
2 changed files with 258 additions and 10 deletions
+101 -10
View File
@@ -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)