## OFFLINE — does the time-indexed bullet heat (TR_TFIL_HEAT_TIME=1) actually ## open free zones? READ-ONLY: no live battles, no change to shipped defaults. ## ## Reuses the machinery of `measure_tfil_heat_field.nim` (jobs 42/43): drive the ## REAL `TFILModule.computeMove` over the committed DrussGT fixtures (so bullet ## tracking, reachable hull and cached inside-tiles are identical to the live ## mover) and read its private `lava` via `include`. ## ## Because `lava` depends only on bullets/enemies/walls/pillars — NOT on the ## bot's picks or the RNG — the OFF and ON replays differ in nothing but the ## heat function. That makes the before/after a clean static-field comparison. ## ## Metrics, per the user's question: ## * fraction of grid tiles above PathDangerThreshold (10) ## * largest contiguous safe region (4-connected), in tiles ## * mean grid-step / px distance from a tile to the nearest safe tile ## plus the corridor reach: the `dt` (and px) at which a bullet's corridor heat ## falls back to <= threshold, vs the old wall-to-wall corridor. ## ## Run: ## nim c -r --path:common_libs common_libs/tests/measure_tfil_heat_time.nim \ ## [fixture.jsonl ...] import std/[os, strformat, math, deques, sets, json] import gun_harness/offline_range # Private-field access: include (do NOT import) the shipped mover. include movements/the_floor_is_lava const SafeThreshold = 10.0 type FieldMetrics = object fracOver: float ## tiles with lava > 10 / all tiles largestSafe: int ## largest 4-connected safe region, tiles meanDistAll: float ## mean nearest-safe-tile distance over ALL tiles meanDistUnsafe: float ## ... over the unsafe tiles only hasSafe: bool RunStats = object ticks: int sumFracOver: float sumLargestSafe: float maxLargestSafe: int sumDistAll: float sumDistUnsafe: float noSafeTicks: int # corridor reach (ON model only) corridors: int sumReachDt: float sumReachPx: float sumOldPx: float histLo: array[6, int] ## reach px buckets: <50,50-100,... ,>=250 proc loadRoundStarts(fixturePath: string): HashSet[int] = result = initHashSet[int]() let side = currentSourcePath().parentDir.parentDir.parentDir / "tools" / "fixtures" / "drussgt_meta" / (extractFilename(fixturePath) & ".rounds.json") if not fileExists(side): return let root = parseFile(side) if not root.hasKey("rounds"): return for r in root["rounds"]: if r.hasKey("startTick"): result.incl r["startTick"].getInt() # ── the three field metrics, on a real `m.lava` ────────────────────────────── proc fieldMetrics(m: TFILModule): FieldMetrics = let n = m.cols * m.rows var over = 0 var safe = newSeq[bool](n) for i in 0.. SafeThreshold: inc over else: safe[i] = true result.fracOver = over.float / n.float result.hasSafe = over < n const Neigh = [(-1, 0), (1, 0), (0, -1), (0, 1)] # multi-source BFS from every safe tile -> distance to nearest safe tile var dist = newSeq[int](n) var q = initDeque[int]() for i in 0.. 0: let cur = q.popFirst() let cc = cur mod m.cols let rr = cur div m.cols for (dc, dr) in Neigh: let nc = cc + dc let nr = rr + dr if nc < 0 or nc >= m.cols or nr < 0 or nr >= m.rows: continue let ni = nr * m.cols + nc if dist[ni] < 0: dist[ni] = dist[cur] + 1 q.addLast(ni) var sumAll = 0.0 var sumUnsafe = 0.0 var nUnsafe = 0 for i in 0..= 0: sumAll += dist[i].float if (not safe[i]) and dist[i] >= 0: sumUnsafe += dist[i].float inc nUnsafe result.meanDistAll = sumAll / n.float result.meanDistUnsafe = if nUnsafe > 0: sumUnsafe / nUnsafe.float else: 0.0 # largest 4-connected safe region var seen = newSeq[bool](n) var best = 0 for i in 0.. 0: let cur = q2.popFirst() inc size let cc = cur mod m.cols let rr = cur div m.cols for (dc, dr) in Neigh: let nc = cc + dc let nr = rr + dr if nc < 0 or nc >= m.cols or nr < 0 or nr >= m.rows: continue let ni = nr * m.cols + nc if safe[ni] and not seen[ni]: seen[ni] = true q2.addLast(ni) if size > best: best = size result.largestSafe = best # ── replay one fixture under one heat model ────────────────────────────────── proc analyse(path: string, timeOn: bool, tau, gain: float): RunStats = putEnv("TR_TFIL_HEAT_TIME", if timeOn: "1" else: "0") putEnv("TR_TFIL_HEAT_TAU", $tau) putEnv("TR_TFIL_HEAT_POWER_GAIN", $gain) loadTfilHeatEnv() let fx = loadFixture(path) let starts = loadRoundStarts(path) var m = initTFIL() for si in 0.. SafeThreshold: reachDt = -tau * ln(SafeThreshold / near) reachPx = reachDt * speed inc result.corridors result.sumReachDt += reachDt result.sumReachPx += reachPx result.sumOldPx += cg.tMin let bkt = if reachPx < 50.0: 0 elif reachPx < 100.0: 1 elif reachPx < 150.0: 2 elif reachPx < 200.0: 3 elif reachPx < 250.0: 4 else: 5 inc result.histLo[bkt] proc f2(x: float): string = &"{x:.2f}" proc reportOne(name: string, ticks: int, off, on: RunStats) = let n = max(1, ticks).float echo "" echo "═══════════════════════════════════════════════════════════════════════════" echo &"FILE {name} ticks={ticks}" echo " OFF (shipped flat) ON (time-indexed)" echo &" frac tiles > 10 {f2(off.sumFracOver/n):>19} {f2(on.sumFracOver/n):>19}" echo &" largest safe region (tiles){f2(off.sumLargestSafe/n):>19} {f2(on.sumLargestSafe/n):>19}" & &" (max {off.maxLargestSafe} -> {on.maxLargestSafe})" echo &" mean dist->safe (all tiles){f2(off.sumDistAll/n):>19} {f2(on.sumDistAll/n):>19}" & &" grid-steps (=x36 px: {f2(off.sumDistAll/n*36.0)} -> {f2(on.sumDistAll/n*36.0)})" echo &" mean dist->safe (unsafe) {f2(off.sumDistUnsafe/n):>19} {f2(on.sumDistUnsafe/n):>19}" echo &" ticks with NO safe tile {off.noSafeTicks:>19} {on.noSafeTicks:>19}" if on.corridors > 0: echo "" echo " corridor reach under the ON model (where it stops blocking):" echo &" mean reach dt = {f2(on.sumReachDt/on.corridors.float)} ticks" echo &" mean reach px = {f2(on.sumReachPx/on.corridors.float)} px" & &" vs mean old wall-to-wall corridor = {f2(on.sumOldPx/on.corridors.float)} px" echo &" retained fraction = {f2(on.sumReachPx/on.sumOldPx*100.0)}% of the old corridor length" const Names = ["<50", "50-100", "100-150", "150-200", "200-250", ">=250"] for i in 0..5: echo &" reach {Names[i]:>7} px : {on.histLo[i]} of {on.corridors} bullet-ticks" proc main() = var files: seq[string] for i in 1..paramCount(): files.add paramStr(i) if files.len == 0: let dir = currentSourcePath().parentDir.parentDir.parentDir / "tools" / "fixtures" files = @[ dir / "tr_drussgt_vs_modularbot.jsonl", dir / "tr_drussgt_vs_modularbot_shield.jsonl", dir / "tr_drussgt_vs_spinbot.jsonl", dir / "tr_drussgt_vs_corners.jsonl", ] const Tau = 9.0 const Gain = 1.0 # Sweep hooks: HT_TAU / HT_GAIN override the constants without a rebuild. var tau = Tau var gain = Gain if existsEnv("HT_TAU"): tau = parseFloat(getEnv("HT_TAU")) if existsEnv("HT_GAIN"): gain = parseFloat(getEnv("HT_GAIN")) echo &"# Time-indexed bullet heat — offline field comparison (tau={tau} ticks, gain={gain})" var ticks = 0 for f in files: if not fileExists(f): stderr.writeLine("missing fixture: " & f) continue let off = analyse(f, timeOn = false, tau = tau, gain = gain) let on = analyse(f, timeOn = true, tau = tau, gain = gain) reportOne(extractFilename(f), off.ticks, off, on) ticks += off.ticks echo "" echo &"# done: {ticks} fixture ticks across {files.len} files (OFF vs ON)" when isMainModule: main()