## 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 ## tile the chosen (col,row) <- j152: pick DIVERSITY ## j152: every row of the sweep table also reports the DIVERSITY cost (distinct ## tiles, entropy, top-tile share). j51 measured the randomness in this draw as ## LOAD-BEARING, so a geometry weight that improves the geometry numbers while ## collapsing the distribution is a regression, not a win. ## 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, tables] 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 col, row: int ## the chosen tile (diversity) 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.. 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, col: cc, row: cr) 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 f2(x: float): string = &"{x:.2f}" 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 detail = "--detail" in args let fixtures: seq[string] = block: if detail: var v: seq[string] for a in args: if not a.startsWith("--"): v.add a v 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"] # ── j152: the sweep, with the DIVERSITY cost on every row ──────────────────── ## (label, TR_TFIL_GEO_MODE, TR_TFIL_GEO_TAU, TR_TFIL_ARRIVE_TICKS) type Arm = tuple[label, mode, tau, arrive: string] proc diversity(picks: seq[Pick]): tuple[distinctN, topShare, entBits, normEnt: float] = ## Shannon entropy (bits) of the CHOICE distribution over tiles. `normEnt` is ## H / log2(distinct): 1.0 = the arm spreads its picks over exactly as many ## tiles as the baseline, 0.0 = every pick is the same tile. var counts: Table[(int, int), int] for p in picks: counts[(p.col, p.row)] = counts.getOrDefault((p.col, p.row)) + 1 result.distinctN = counts.len.float if picks.len == 0: return var h = 0.0 var top = 0 for _, n in counts.pairs: let q = n.float / picks.len.float h -= q * log2(q) top = max(top, n) result.topShare = top.float / picks.len.float result.entBits = h result.normEnt = if result.distinctN > 1.0: h / log2(result.distinctN) else: 0.0 proc pctS(x, n: int): string = if n == 0: return " n/a" pc(x.float / n.float) proc isPerp(p: Pick): bool = p.turn > PerpDeg proc isPromoted(p: Pick): bool = p.promoted proc isFar(p: Pick): bool = p.tta > 15.0 proc isReached(p: Pick): bool = p.reached proc isHotAtTta(p: Pick): bool = p.hotAtTta proc row(label: string, picks: seq[Pick]): string = let n = picks.len let emp = picks.filterIt(isPromoted(it)) let nes = picks.filterIt(not isPromoted(it)) let perp = picks.filterIt(isPerp(it)).len let perpE = emp.filterIt(isPerp(it)).len let perpN = nes.filterIt(isPerp(it)).len let far = picks.filterIt(isFar(it)).len let reach = picks.filterIt(isReached(it)).len let hot = picks.filterIt(isHotAtTta(it)).len let d = diversity(picks) &"{label:<22} {pctS(perp, n):>7} {pctS(perpE, emp.len):>7} {pctS(perpN, nes.len):>7}" & &" {pctS(far, n):>7} {f1(mean(picks.mapIt(it.tta))):>6}" & &" {pctS(reach, n):>7} {pctS(hot, n):>7} {pctS(emp.len, n):>7}" & &" {d.distinctN.int:>6} {f2(d.entBits):>6} {f2(d.normEnt):>6} {pc(d.topShare):>7}" & &" {f1(mean(picks.mapIt(it.cand.float))):>5}" proc header(): string = result = "arm".align(22, ' ') for (h, w) in [("perp", 7), ("perpE", 7), ("perpN", 7), ("far", 7), ("mtta", 6), ("REACH", 7), ("hotArr", 7), ("empty", 7), ("tiles", 6), ("Hbits", 6), ("H/", 6), ("top1", 7), ("cand", 5)]: result &= " " & h.align(w, ' ') # `perpE`/`perpN` = the perpendicular rate in the FORCED (empty safe set) and the # non-empty populations; `REACH` = the headline (tile actually stood on at tta); # `tiles`/`Hbits`/`H/`/`top1` = the diversity cost; `cand` = mean draw-set size. let arms: seq[Arm] = @[ ("BASELINE (off)", "off", "0", "0"), ("turn-soft tau90", "turn-soft", "90", "0"), ("turn-soft tau45", "turn-soft", "45", "0"), ("turn-soft tau20", "turn-soft", "20", "0"), ("turn-topk", "turn-topk", "45", "0"), ("turn-rej tau60", "turn-rej", "60", "0"), ("dist-soft tau90", "dist-soft", "90", "0"), ("dist-soft tau30", "dist-soft", "30", "0"), ("both-soft tau90", "both-soft", "90", "0"), ("both-soft tau45", "both-soft", "45", "0"), ("both-soft tau20", "both-soft", "20", "0"), ("both-topk", "both-topk", "45", "0"), ("both-rej tau60", "both-rej", "60", "0"), # j151 interaction: a soft distance preference vs the HARD arrival bound. ("arrive15 (j151)", "off", "0", "15"), ("arrive15+both t45", "both-soft", "45", "15")] echo "\n", header() for a in arms: putEnv("TR_TFIL_GEO_MODE", a.mode) putEnv("TR_TFIL_GEO_TAU", a.tau) putEnv("TR_TFIL_ARRIVE_TICKS", a.arrive) var picks: seq[Pick] for f in fixtures: if not fileExists(f): continue for seed in [7, 8, 9]: picks.add replay(f, seed) echo row(a.label, picks) # ── per-fixture detail (--detail only), for the BASELINE arm ──────────────── if detail: putEnv("TR_TFIL_GEO_MODE", arms[0].mode) putEnv("TR_TFIL_GEO_TAU", arms[0].tau) putEnv("TR_TFIL_ARRIVE_TICKS", arms[0].arrive) 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) report("ALL FIXTURES x 3 SEEDS", "", total)