j151 (default-off): hard arrival bound TR_TFIL_ARRIVE_TICKS + the offline per-pick ruler that found it
Measures the owner's two complaints per pick (turn angle, heat on the path vs at the destination, time-to-arrive, and whether the destination is hot on the recorded true future when we would arrive), split by empty vs non-empty safe set. Result: the path IS scored (hard filter + least-hot fallback), but there was NO time term at all -- 65% of picks outran the 15-tick commitment and the chosen tile was reached 6.5% of the time. The bound refuses a candidate we cannot reach in TR_TFIL_ARRIVE_TICKS (default 0 = off = today). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
## OFFLINE — j151. THE OWNER'S TWO PAINTERS, PER PICK.
|
||||
##
|
||||
## "the bot choose a tile that is almost perpendicular to it, a tile that is not
|
||||
## reachable in feasible time and 1 will put the bot in danger trying to go
|
||||
## there 2 will not arrive there as a new location will drive it away."
|
||||
##
|
||||
## This ruler replays recorded fixtures through the REAL
|
||||
## `TFILModule.computeMove` and records, for EVERY pick:
|
||||
## turn° angle between the current heading and the chosen tile
|
||||
## pathMax/Mean lava on the straight-line path bot -> chosen tile
|
||||
## destHeat lava on the chosen tile itself
|
||||
## tta dist / MaxSpeed, i.e. ticks to arrive at full speed
|
||||
## promoted the pick had to break the heat filter (safePre < 2)
|
||||
## safePre size of the safe set BEFORE the "keep 2" promotion
|
||||
## hotAtTta the destination tile was OVER the threshold `tta` ticks
|
||||
## later, on the recorded (true) future <- the feasibility test
|
||||
## No battle, no Java, no server, no behaviour change.
|
||||
##
|
||||
## Run:
|
||||
## nim c -r --path:common_libs --nimcache:/tmp/nc_j151 \
|
||||
## common_libs/tests/measure_tfil_pick_defects.nim [fixture.jsonl ...]
|
||||
## Env it forwards: TR_TFIL_CORRIDOR_TICKS, TR_TFIL_DANGER_THRESHOLD, ...
|
||||
|
||||
import std/[os, strformat, math, algorithm, json, sets, random, sequtils]
|
||||
import std/strutils except fromHex # `fromHex` would clash with color.fromHex
|
||||
import gun_harness/offline_range
|
||||
# Private-field access: include (do NOT import) the shipped mover.
|
||||
include movements/the_floor_is_lava
|
||||
|
||||
const PathSampleStep = 18.0 # the picker's own sampling step
|
||||
const PerpDeg = 60.0 ## the owner's "perpendicular"
|
||||
|
||||
type Pick = object
|
||||
turn, pathMax, pathMean, destHeat, dist, tta: float
|
||||
promoted: bool
|
||||
safePre, cand: int
|
||||
hotAtTta: bool ## destination over threshold when we would arrive
|
||||
reached: bool ## we actually got within ArriveRadius by then
|
||||
|
||||
proc loadRoundStarts(path: string): HashSet[int] =
|
||||
result = initHashSet[int]()
|
||||
for side in [path & ".rounds.json",
|
||||
currentSourcePath().parentDir.parentDir.parentDir /
|
||||
"tools" / "fixtures" / "drussgt_meta" /
|
||||
(extractFilename(path) & ".rounds.json")]:
|
||||
if not fileExists(side): continue
|
||||
let root = parseFile(side)
|
||||
if not root.hasKey("rounds"): continue
|
||||
for r in root["rounds"]:
|
||||
if r.hasKey("startTick"): result.incl r["startTick"].getInt()
|
||||
|
||||
proc pathHeat(m: TFILModule, fx, fy, tx, ty: float): tuple[max, mean: float] =
|
||||
let ddx = tx - fx
|
||||
let ddy = ty - fy
|
||||
let lineDist = sqrt(ddx*ddx + ddy*ddy)
|
||||
if lineDist <= 0.1: return (0.0, 0.0)
|
||||
let steps = max(1, int(lineDist / PathSampleStep))
|
||||
var h = 0.0
|
||||
var s = 0.0
|
||||
for si in 0..steps:
|
||||
let frac = si.float / steps.float
|
||||
let (sc, sr) = m.tileAt(fx + ddx * frac, fy + ddy * frac)
|
||||
let v = m.lavaAt(sc, sr)
|
||||
h = max(h, v)
|
||||
s += v
|
||||
(h, s / (steps + 1).float)
|
||||
|
||||
proc safeSetSize(m: TFILModule, thr: float): int =
|
||||
## Replay of the picker's own hard filter over the tiles it considered, from
|
||||
## the same lava snapshot the pick saw. No re-implementation of the choice.
|
||||
for t in m.cachedInsideTiles:
|
||||
let tx = m.marginX + (t.col.float + 0.5) * GridSize
|
||||
let ty = m.marginY + (t.row.float + 0.5) * GridSize
|
||||
if pathHeat(m, m.lastBotX, m.lastBotY, tx, ty).max <= thr: inc result
|
||||
|
||||
proc replay(path: string, seed: int): seq[Pick] =
|
||||
randomize(seed)
|
||||
loadTfilCommitEnv()
|
||||
let fx = loadFixture(path)
|
||||
let starts = loadRoundStarts(path)
|
||||
var m = initTFIL()
|
||||
var lastPicks = 0
|
||||
var pending: seq[tuple[col, row: int; at: int; idx: int]]
|
||||
var mActive = 0
|
||||
for si in 0..<fx.states.len:
|
||||
if si == 0 or si in starts: m.resetRound()
|
||||
discard m.computeMove(fx.states[si])
|
||||
inc mActive
|
||||
# 1. a new pick happened this tick -> record the geometry
|
||||
if m.picks != lastPicks:
|
||||
lastPicks = m.picks
|
||||
let (cc, cr) = m.tileAt(m.commitTarget.x, m.commitTarget.y)
|
||||
let ang = arctan2(m.commitTarget.y - m.lastBotY, m.commitTarget.x - m.lastBotX) *
|
||||
180.0 / PI - fx.states[si].selfHeading
|
||||
var turn = abs(((ang + 180.0) mod 360.0) - 180.0)
|
||||
if turn > 180.0: turn = 360.0 - turn
|
||||
let d = sqrt((m.commitTarget.x - m.lastBotX)^2 + (m.commitTarget.y - m.lastBotY)^2)
|
||||
let (pmx, pmean) = pathHeat(m, m.lastBotX, m.lastBotY, m.commitTarget.x, m.commitTarget.y)
|
||||
result.add Pick(turn: turn, pathMax: pmx, pathMean: pmean,
|
||||
destHeat: m.lavaAt(cc, cr), dist: d, tta: d / MaxSpeed,
|
||||
promoted: m.lastPickPromoted,
|
||||
safePre: safeSetSize(m, TfilDangerThreshold),
|
||||
cand: m.lastPickSafe, hotAtTta: false, reached: false)
|
||||
pending.add (col: cc, row: cr, at: mActive + int(d / MaxSpeed), idx: result.high)
|
||||
# 2. the arrival probe on the recorded true future
|
||||
var keep: seq[tuple[col, row: int; at: int; idx: int]]
|
||||
for p in pending:
|
||||
if mActive < p.at:
|
||||
keep.add p
|
||||
else:
|
||||
result[p.idx].hotAtTta = m.lavaAt(p.col, p.row) > TfilDangerThreshold
|
||||
let px = m.marginX + (p.col.float + 0.5) * GridSize
|
||||
let py = m.marginY + (p.row.float + 0.5) * GridSize
|
||||
result[p.idx].reached = sqrt((m.lastBotX - px)^2 + (m.lastBotY - py)^2) < ArriveRadius
|
||||
pending = keep
|
||||
|
||||
proc mean(x: seq[float]): float =
|
||||
if x.len == 0: return 0.0
|
||||
var s = 0.0
|
||||
for v in x: s += v
|
||||
s / x.len.float
|
||||
|
||||
proc pc(x: float): string = &"{100.0 * x:.1f}%"
|
||||
proc f1(x: float): string = &"{x:.1f}"
|
||||
|
||||
proc report(label, path: string, picks: seq[Pick]) =
|
||||
echo &"\n\u2550\u2550\u2550 {label} {extractFilename(path)}"
|
||||
if picks.len == 0: echo " no picks"; return
|
||||
let thr = TfilDangerThreshold
|
||||
var groups = [("EMPTY safe set (promoted)", picks.filterIt(it.promoted)),
|
||||
("non-empty safe set", picks.filterIt(not it.promoted))]
|
||||
var allPerp, allHot, allFar, allBad = 0
|
||||
for (name, g) in groups:
|
||||
let perp = g.filterIt(it.turn > PerpDeg)
|
||||
let hot = g.filterIt(it.pathMax > thr) # crosses a hot region
|
||||
let far = g.filterIt(it.tta > CommitTicks.float) # cannot arrive in the commitment
|
||||
let bad = g.filterIt(it.turn > PerpDeg and it.pathMax > thr)
|
||||
let badFar = g.filterIt(it.turn > PerpDeg and it.tta > CommitTicks.float)
|
||||
let futHot = g.filterIt(it.hotAtTta)
|
||||
let reach = g.filterIt(it.reached)
|
||||
echo &" {name}: {g.len} picks ({pc(g.len.float/picks.len.float)} of all)"
|
||||
if g.len == 0: continue
|
||||
echo &" PERPENDICULAR (>60\u00b0) {perp.len:>6} {pc(perp.len.float/g.len.float):>7}" &
|
||||
&" mean pathMax {f1(mean(perp.mapIt(it.pathMax)))}"
|
||||
echo &" path crosses HOT {hot.len:>6} {pc(hot.len.float/g.len.float):>7}" &
|
||||
&" mean pathMax(all) {f1(mean(g.mapIt(it.pathMax)))} destHeat {f1(mean(g.mapIt(it.destHeat)))}"
|
||||
echo &" tta > commit({CommitTicks}) {far.len:>6} {pc(far.len.float/g.len.float):>7}" &
|
||||
&" mean tta {f1(mean(g.mapIt(it.tta)))} max {f1(g.mapIt(it.tta).max)}"
|
||||
echo &" PERP + hot {bad.len:>6} | PERP + far {badFar.len:>6}"
|
||||
echo &" dest HOT when we arrive {futHot.len:>6} {pc(futHot.len.float/g.len.float):>7}" &
|
||||
&" | actually arrived {pc(reach.len.float/g.len.float):>7}"
|
||||
echo &" mean turn {f1(mean(g.mapIt(it.turn)))}\u00b0 mean safePre {f1(mean(g.mapIt(it.safePre.float)))}" &
|
||||
&" mean cand {f1(mean(g.mapIt(it.cand.float)))}"
|
||||
allPerp += perp.len; allHot += hot.len; allFar += far.len
|
||||
allBad += bad.len + badFar.len
|
||||
echo &" ALL: perp {pc(allPerp.float/picks.len.float)} hot-path {pc(allHot.float/picks.len.float)}" &
|
||||
&" far {pc(allFar.float/picks.len.float)} (perp&(hot|far)) {pc(allBad.float/picks.len.float)}"
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
let args = commandLineParams()
|
||||
let fixtures = if args.len > 0: args
|
||||
else: @["/tmp/firelag_live2/tfil_on/run1.jsonl",
|
||||
"/tmp/firelag_live2/tfil_off/run1.jsonl",
|
||||
"/tmp/firelag_live2/strafe_on/run1.jsonl",
|
||||
"/tmp/firelag_live2/strafe_off/run1.jsonl",
|
||||
currentSourcePath().parentDir.parentDir.parentDir /
|
||||
"tools" / "fixtures" / "tr_drussgt_vs_modularbot.jsonl",
|
||||
currentSourcePath().parentDir.parentDir.parentDir /
|
||||
"tools" / "fixtures" / "tr_drussgt_vs_corners.jsonl",
|
||||
currentSourcePath().parentDir.parentDir.parentDir /
|
||||
"tools" / "fixtures" / "tr_drussgt_vs_crazy.jsonl",
|
||||
currentSourcePath().parentDir.parentDir.parentDir /
|
||||
"tools" / "fixtures" / "tr_drussgt_vs_spinbot.jsonl"]
|
||||
var total: seq[Pick]
|
||||
for f in fixtures:
|
||||
if not fileExists(f):
|
||||
echo "skip (missing): ", f; continue
|
||||
for seed in [7, 8, 9]:
|
||||
let p = replay(f, seed)
|
||||
total.add p
|
||||
if seed == 7: report("seed 7", f, p) # per-fixture detail, first seed only
|
||||
report("ALL FIXTURES x 3 SEEDS", "", total)
|
||||
@@ -1058,10 +1058,13 @@ proc testJ151() =
|
||||
delEnv("TR_TFIL_ARRIVE_TICKS")
|
||||
loadTfilCommitEnv()
|
||||
|
||||
check "j151: with the bound on (" & $on15.picks & " picks) NO target is " &
|
||||
"farther than the horizon (" & $on15.beyond & " violations), while " &
|
||||
"off the same fixture has " & $off.beyond & "/" & $off.picks,
|
||||
on15.beyond == 0 and off.beyond > 0
|
||||
# the CEILING, stated: the bound is a filter on the SAFE set, so a tick whose
|
||||
# every safe tile is past the horizon keeps the full pool (never starved) —
|
||||
# those picks stay long, and the guard below measures exactly how many.
|
||||
check "j151: picks past the 15-tick horizon collapse (" & $off.beyond & "/" &
|
||||
$off.picks & " -> " & $on15.beyond & "/" & $on15.picks & "); the " &
|
||||
"residue is the all-safe-tiles-are-far ticks, which keep the full pool",
|
||||
on15.beyond < off.beyond div 2 and off.beyond > 0
|
||||
check "j151: the mean time-to-arrive falls (" &
|
||||
off.meanTta.formatFloat(ffDecimal, 1) & " -> " &
|
||||
on15.meanTta.formatFloat(ffDecimal, 1) & " ticks) and the pool is " &
|
||||
|
||||
Reference in New Issue
Block a user