j152 (default-off): geometry shapes the tile DRAW (TR_TFIL_GEO_MODE/TAU) + the offline sweep with the diversity cost

The owner: choose the tile pool not only from the heat point but from the
geometric position too. Heat stays the hard filter; the draw over the
survivors is re-weighted by turn and/or distance. Three weighting forms
(soft softmax / top-K third / rejection band), one env name carrying both
axes. Default = off = today's uniform draw, byte-for-byte (golden parity).

WHAT DIFFERS FROM j9 (TR_TFIL_TURN_BIAS, a live null): that was a tiebreak
weight among the non-empty safe set only. This runs on the WHOLE pool the
draw already runs on, including the 2 promoted least-hot tiles the ~65%
forced picks choose from.

OFFLINE (8 fixtures x 3 seeds, no java): headline 'tile actually reached at
tta' 4.5% -> 16.9% at TR_TFIL_GEO_MODE=both-soft TR_TFIL_GEO_TAU=45, with
no diversity collapse (distinct tiles 220 -> 219, normalised entropy
0.87 -> 0.86, top-tile share 7.0% -> 8.6%). perpE — the perpendicular rate
in the FORCED population — does not move in ANY arm: that pool is 2 tiles
ranked by heat alone and the only lever is a coin flip.

Also registers j150's TR_TFIL_DIAG / TR_TFIL_DANGER_THRESHOLD in
env_report + knownEnvNames + .env.example (test_env_report was red) and
documents DANGER_THRESHOLD as quantisation-limited: heat comes in 5s, so
the effective steps are 10/15/20 and 10-14 admits zero extra tiles.

No battle, no server, no A/B run.
This commit is contained in:
2026-09-27 10:22:24 +02:00
parent 94ffc63160
commit 38fbc6ecd1
5 changed files with 793 additions and 28 deletions
+133 -24
View File
@@ -14,6 +14,11 @@
## 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:
@@ -21,7 +26,7 @@
## 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/[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.
@@ -36,6 +41,7 @@ type Pick = object
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]()
@@ -100,7 +106,8 @@ proc replay(path: string, seed: int): seq[Pick] =
destHeat: m.lavaAt(cc, cr), dist: d, tta: d / MaxSpeed,
promoted: m.lastPickPromoted,
safePre: safeSetSize(m, TfilDangerThreshold),
cand: m.lastPickSafe, hotAtTta: false, reached: false)
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]]
@@ -122,6 +129,7 @@ proc mean(x: seq[float]): 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)}"
@@ -158,25 +166,126 @@ proc report(label, path: string, picks: seq[Pick]) =
# ── 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)
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)