j150 (diag only): picker loss histogram + sweepable heat cutoff
Measures WHERE the lava picker loses tiles, per pick: reachable hull -> CoolestLevels=2 distinct-value filter -> path heat filter -> draw set -> chosen. TR_TFIL_DIAG (default off) fills TfilLoss*; TR_TFIL_DANGER_THRESHOLD (default 10.0, the shipped const) makes the cutoff sweepable offline. No decision logic changed: the guard test proves the diag-on move stream is byte-for-byte the diag-off one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,8 @@ const MaxTrackedBullets = 20 ## hard cap on tracked bullets
|
||||
#
|
||||
# Every default below reproduces the shipped mover byte-for-byte; see the
|
||||
# default-path parity guard in `common_libs/tests/test_tfil_commit_env.nim`.
|
||||
const DefaultDangerThreshold = 10.0 ## today's `PathDangerThreshold`
|
||||
|
||||
type
|
||||
TfilTileReplan* = enum
|
||||
ttrSelf, ttrOff, ttrEnemy
|
||||
@@ -156,6 +158,38 @@ var
|
||||
## detected enemy fire, for the ghost-vs-observer position probe. Observability
|
||||
## only; off by default.
|
||||
TfilFireDiag*: bool = false
|
||||
## j150: the picker's hard heat cutoff was a proc-local `const`, so no offline
|
||||
## sweep could move it. Same env-overridable-var pattern as the shape knobs;
|
||||
## the DEFAULT is today's `10.0`, so the default path is bit-identical.
|
||||
## TR_TFIL_DANGER_THRESHOLD default 10.0
|
||||
TfilDangerThreshold* = DefaultDangerThreshold
|
||||
## j151: `TR_TFIL_ARRIVE_TICKS` — refuse a candidate we cannot REACH inside
|
||||
## the commitment horizon (ticks = dist / MaxSpeed). Hard bound, not a
|
||||
## preference; empty pool => today's full pool, so it can never starve the
|
||||
## draw. 0 (default) = off = byte-for-byte today.
|
||||
TfilArriveTicks*: float = 0.0
|
||||
## j150: `TR_TFIL_DIAG` — fill `TfilLoss*` with the per-pick LOSS HISTOGRAM
|
||||
## (how many tiles die at each picker stage). Pure counters, off by default.
|
||||
TfilDiag*: bool = false
|
||||
|
||||
## The picker's loss histogram, one entry per PICK. Stage sizes, in picker
|
||||
## order: tiles inside the reachable hull -> survivors of the `CoolestLevels`
|
||||
## distinct-lava-value filter -> survivors of the `pathMaxHeat <= threshold`
|
||||
## filter (counted BEFORE the "keep 2 anyway" promotion) -> what the draw
|
||||
## actually runs on. Pure bookkeeping, read by the offline ruler.
|
||||
type TfilLossStats* = object
|
||||
picks*: int
|
||||
sReach*: int ## inside the reachable hull
|
||||
sCool*: int ## after CoolestLevels (= 2) distinct-value filter
|
||||
sSafe*: int ## after the path heat filter, pre-promotion
|
||||
sCand*: int ## what the draw ran on (post blocked-tile/no-rev)
|
||||
emptySafe*: int ## picks that had to break the heat filter (sSafe < 2)
|
||||
safeHist*: array[8, int] ## sSafe size buckets: 0,1,2-3,4-7,8-15,16-31,32-63,64+
|
||||
rejectedHeat*: seq[float] ## pathMaxHeat of every tile the filter dropped
|
||||
admittedHeat*: seq[float] ## pathMaxHeat of every tile that passed it
|
||||
chosenHeat*: seq[float]
|
||||
|
||||
var TfilLoss*: TfilLossStats
|
||||
|
||||
proc getEnvInt(name: string, default: int): int =
|
||||
let s = getEnv(name, "")
|
||||
@@ -191,6 +225,16 @@ proc loadTfilCommitEnv*() =
|
||||
TfilTurnRefDeg = max(0.0, getEnvFloat("TR_TFIL_TURN_REF_DEG", 45.0))
|
||||
TfilFireFix = getEnvBool("TR_FIRE_FIX", true)
|
||||
TfilFireDiag = existsEnv("TR_FIRE_DIAG")
|
||||
TfilDangerThreshold = max(0.0, getEnvFloat("TR_TFIL_DANGER_THRESHOLD",
|
||||
DefaultDangerThreshold))
|
||||
TfilDiag = getEnvBool("TR_TFIL_DIAG", false)
|
||||
# j151: hard arrival bound. The draw is UNIFORM over every safe tile inside the
|
||||
# 50-tick reachable hull, so a tile 47 ticks away had the same 1-in-52 chance
|
||||
# as the adjacent one, while the target is only HELD for CommitTicks=15. The
|
||||
# offline ruler (measure_tfil_pick_defects) measured 65% of picks beyond the
|
||||
# 15-tick horizon and a 6.5% arrival rate. 0 = off = today's uniform draw.
|
||||
TfilArriveTicks = max(0.0, getEnvFloat("TR_TFIL_ARRIVE_TICKS", 0.0))
|
||||
if not TfilDiag: TfilLoss = TfilLossStats()
|
||||
|
||||
loadTfilCommitEnv()
|
||||
|
||||
@@ -984,11 +1028,13 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||
# Score each cool tile by MAX lava on the straight-line path from bot.
|
||||
# A single hot tile on the path (corridor, bullet core, enemy aura) makes the whole path unsafe.
|
||||
const PathSampleStep = 18.0 # ~half a tile
|
||||
const PathDangerThreshold = 10.0 # max lava on path; above this = unsafe
|
||||
# j150: was `const 10.0`; now the env-overridable var whose DEFAULT is 10.0.
|
||||
let PathDangerThreshold = TfilDangerThreshold # max lava on path; above = unsafe
|
||||
# j145: `turnDeg` is the |heading change| from the direction we are ALREADY
|
||||
# travelling to the tile centre. It is carried on the candidate (never folded
|
||||
# into `pathMaxHeat`) so the pick can bias among the safe tiles only.
|
||||
type ScoredTile = tuple[col, row: int; pathMaxHeat: float; turnDeg: float]
|
||||
type ScoredTile = tuple[col, row: int; pathMaxHeat: float; turnDeg: float;
|
||||
arriveTicks: float]
|
||||
|
||||
proc pathMaxHeat(m: TFILModule, fx, fy, tx, ty: float): float =
|
||||
## MAX lava on the straight-line segment (fx,fy) -> (tx,ty), sampled every
|
||||
@@ -1029,7 +1075,8 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||
scoredTiles.add (col: t.col, row: t.row,
|
||||
pathMaxHeat: pathMaxHeat(m, ws.selfX, ws.selfY, tx, ty),
|
||||
turnDeg: abs(tileOffTravel(m, t.col, t.row, ws.selfX,
|
||||
ws.selfY, travelDeg)))
|
||||
ws.selfY, travelDeg)),
|
||||
arriveTicks: sqrt((tx - ws.selfX)^2 + (ty - ws.selfY)^2) / MaxSpeed)
|
||||
|
||||
# Sort by pathMaxHeat ascending (insertion sort — small N)
|
||||
for i in 1..<scoredTiles.len:
|
||||
@@ -1049,6 +1096,7 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||
for t in scoredTiles:
|
||||
if t.pathMaxHeat <= PathDangerThreshold: safeTiles.add t
|
||||
else: blockedTiles.add t
|
||||
let safePre = safeTiles.len # j150: the safe set BEFORE the "keep 2" promotion
|
||||
if safeTiles.len < 2:
|
||||
# Fallback: promote the least-hot blocked tiles until we have 2
|
||||
# ponytail: O(n) scan on already-sorted seq — fine for small N
|
||||
@@ -1059,6 +1107,15 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||
m.lastPickPromoted = true
|
||||
blockedTiles = blockedTiles[promote ..< blockedTiles.len]
|
||||
|
||||
# j151: the arrival bound, applied to the pool the draw runs on (hysteresis
|
||||
# included) so every consumer sees the same set. It never empties the pool:
|
||||
# if nothing is within the horizon, the full pool is used, exactly as today.
|
||||
if TfilArriveTicks > 0.0:
|
||||
var withinHorizon: seq[ScoredTile]
|
||||
for t in safeTiles:
|
||||
if t.arriveTicks <= TfilArriveTicks: withinHorizon.add t
|
||||
if withinHorizon.len > 0: safeTiles = withinHorizon
|
||||
|
||||
# Commitment logic. With every j144 knob at its default (all off) this is the
|
||||
# original three-way test, unchanged. j144 adds two ways OUT of a commitment
|
||||
# that are NOT a tile crossing, and turns the tick counter into a MINIMUM
|
||||
@@ -1211,6 +1268,25 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||
m.lastPickCall = m.callCount
|
||||
inc m.picks
|
||||
|
||||
if TfilDiag: # j150: where the tiles died, one row per pick. No effect.
|
||||
inc TfilLoss.picks
|
||||
TfilLoss.sReach += insideTiles.len
|
||||
TfilLoss.sCool += coolTiles.len
|
||||
TfilLoss.sSafe += safePre
|
||||
TfilLoss.sCand += candidates.len
|
||||
if safePre < 2: inc TfilLoss.emptySafe
|
||||
let b = (if safePre == 0: 0 elif safePre == 1: 1
|
||||
elif safePre <= 3: 2 elif safePre <= 7: 3
|
||||
elif safePre <= 15: 4 elif safePre <= 31: 5
|
||||
elif safePre <= 63: 6 else: 7)
|
||||
inc TfilLoss.safeHist[b]
|
||||
# admitted/rejected by the FILTER itself, so the promoted (over-threshold)
|
||||
# rescue tiles are not counted as safe.
|
||||
for t in scoredTiles:
|
||||
if t.pathMaxHeat <= PathDangerThreshold: TfilLoss.admittedHeat.add t.pathMaxHeat
|
||||
else: TfilLoss.rejectedHeat.add t.pathMaxHeat
|
||||
TfilLoss.chosenHeat.add ct.pathMaxHeat
|
||||
|
||||
if m.debugGraphics:
|
||||
# Reachable hull perimeter (darker blue)
|
||||
if m.cachedHull.len >= 3:
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
## OFFLINE — j150. WHERE DOES THE LAVA PICKER LOSE ITS TILES?
|
||||
##
|
||||
## The owner's report: "the bot chooses only between a poor number of tiles while
|
||||
## there are a lot of them available but not considered". This ruler drives the
|
||||
## REAL `TFILModule.computeMove` (with `TR_TFIL_DIAG=1`) over recorded
|
||||
## DrussGT fixtures and reads the per-pick loss histogram the mover counts:
|
||||
##
|
||||
## reachable hull -> CoolestLevels(=2) distinct-lava filter
|
||||
## -> pathMaxHeat <= PathDangerThreshold -> draw set -> CHOSEN
|
||||
##
|
||||
## No battle, no Java, no server, no behaviour change: every knob it moves is
|
||||
## default-off or default-identical.
|
||||
##
|
||||
## Run:
|
||||
## nim c -r --path:common_libs --nimcache:/tmp/nc_j150 \
|
||||
## common_libs/tests/measure_tfil_picker_loss.nim [fixture.jsonl ...]
|
||||
|
||||
import std/[os, strformat, math, algorithm, json, sets, 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
|
||||
|
||||
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()
|
||||
|
||||
proc replay(path: string, threshold: float): TfilLossStats =
|
||||
putEnv("TR_TFIL_DIAG", "1")
|
||||
putEnv("TR_TFIL_DANGER_THRESHOLD", $threshold)
|
||||
loadTfilCommitEnv()
|
||||
TfilLoss = TfilLossStats()
|
||||
let fx = loadFixture(path)
|
||||
let starts = loadRoundStarts(path)
|
||||
var m = initTFIL()
|
||||
for si in 0..<fx.states.len:
|
||||
if si == 0 or si in starts: m.resetRound()
|
||||
discard m.computeMove(fx.states[si])
|
||||
result = TfilLoss
|
||||
|
||||
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 pctStr(x: float): string = &"{100.0 * x:.1f}%"
|
||||
|
||||
proc f2(x: float): string = &"{x:.2f}"
|
||||
|
||||
const SafeBuckets = ["0", "1", "2-3", "4-7", "8-15", "16-31", "32-63", "64+"]
|
||||
|
||||
proc heatTally(x: seq[float]): seq[(float, int)] =
|
||||
## distinct heat values -> how many rejected tiles carried it, most common
|
||||
## first. Lava is a sum of quantised terms, so this shows the real steps.
|
||||
var counts: seq[(float, int)]
|
||||
for v in x:
|
||||
var i = 0
|
||||
while i < counts.len and counts[i][0] != v: inc i
|
||||
if i < counts.len: inc counts[i][1]
|
||||
else: counts.add (v, 1)
|
||||
result = counts
|
||||
result.sort(proc (a, b: (float, int)): int = cmp(b[1], a[1]))
|
||||
|
||||
proc report(path: string) =
|
||||
let s = replay(path, 10.0)
|
||||
echo &"\n\u2550\u2550\u2550 {path}"
|
||||
if s.picks == 0:
|
||||
echo " no picks"; return
|
||||
let n = s.picks.float
|
||||
echo &" picks = {s.picks} (replayed ticks, one recorded battle, seed fixed)"
|
||||
echo &" stage mean tiles"
|
||||
echo &" 1 reachable-hull tiles {s.sReach.float/n:>12.2f}"
|
||||
echo &" 2 .. after CoolestLevels=2 filter {s.sCool.float/n:>12.2f}"
|
||||
echo &" 3 .. after path heat filter (pre-prom) {s.sSafe.float/n:>12.2f}"
|
||||
echo &" 4 .. draw set (what it chooses among) {s.sCand.float/n:>12.2f}"
|
||||
echo &" LOST at the 2-levels filter {(1.0 - s.sCool.float/s.sReach.float)*100:>11.1f}%"
|
||||
echo &" LOST at the heat filter {(1.0 - s.sSafe.float/max(1.0,s.sCool.float))*100:>11.1f}%"
|
||||
echo &" LOST at blocked-tile / no-rev {(1.0 - s.sCand.float/max(1.0,s.sSafe.float))*100:>11.1f}%"
|
||||
echo &" picks with an EMPTY safe set (<2 at stage 3) = {s.emptySafe} ({pctStr(s.emptySafe.float/n)})"
|
||||
echo " safe-set size distribution (stage 3):"
|
||||
for i in 0..<SafeBuckets.len:
|
||||
if s.safeHist[i] > 0:
|
||||
echo &" {SafeBuckets[i].alignLeft(6)} {s.safeHist[i]:>7} ({pctStr(s.safeHist[i].float/n)})"
|
||||
|
||||
# heat of the tiles the filter dropped
|
||||
echo &" heat of the {s.rejectedHeat.len} REJECTED tiles (mean {f2(mean(s.rejectedHeat))}), most common first:"
|
||||
for (v, c) in heatTally(s.rejectedHeat)[0 ..< min(8, s.rejectedHeat.len)]:
|
||||
echo &" heat {f2(v):>7} {c:>7} ({pctStr(c.float/s.rejectedHeat.len.float)})"
|
||||
echo &" heat of ADMITTED tiles mean {f2(mean(s.admittedHeat))} (n={s.admittedHeat.len}); " &
|
||||
&"of CHOSEN mean {f2(mean(s.chosenHeat))} (n={s.chosenHeat.len})"
|
||||
|
||||
# \u2550\u2550 threshold sweep
|
||||
echo " threshold mean draw set % empty safe newly admitted (n, mean heat) mean heat chosen"
|
||||
var baseAdmitted = replay(path, 10.0).admittedHeat.len
|
||||
for thr in [10.0, 14.0, 18.0]:
|
||||
let a = replay(path, thr)
|
||||
let newly = a.admittedHeat.filterIt(it > 10.0)
|
||||
let newTxt = $newly.len & ", " & f2(mean(newly))
|
||||
echo &" {thr:>9.0f} {a.sCand.float / a.picks.float:>13.2f} " &
|
||||
&"{pctStr(a.emptySafe.float / a.picks.float):>12} {newTxt:>30} {f2(mean(a.chosenHeat)):>16}"
|
||||
echo &" (admitted at threshold 10: {baseAdmitted} tiles)"
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
let args = commandLineParams()
|
||||
let fixtures = if args.len > 0: args
|
||||
else: @[currentSourcePath().parentDir.parentDir.parentDir /
|
||||
"tools" / "fixtures" / "tr_drussgt_vs_modularbot.jsonl"]
|
||||
for f in fixtures:
|
||||
if fileExists(f): report(f)
|
||||
else: echo "skip (missing): ", f
|
||||
@@ -955,6 +955,126 @@ when declared(loadTfilCommitEnv):
|
||||
check "j147: clearing the knob restores the shipped spawn exactly",
|
||||
FireLag == 0 and spawnGhost().x == ei.x
|
||||
|
||||
# ── j150: the picker loss-histogram diag + the sweepable heat cutoff ──────────
|
||||
#
|
||||
# TR_TFIL_DIAG 0/1 default 0 — fill TfilLoss* only
|
||||
# TR_TFIL_DANGER_THRESHOLD (float) default 10 — was a proc-local `const`
|
||||
#
|
||||
# Both must be default-off-effect: the whole point of the diag is to measure
|
||||
# the shipped picker, not to change it.
|
||||
proc testJ150() =
|
||||
delEnv("TR_TFIL_DIAG"); delEnv("TR_TFIL_DANGER_THRESHOLD")
|
||||
loadTfilCommitEnv()
|
||||
check "j150: TR_TFIL_DIAG defaults OFF and TR_TFIL_DANGER_THRESHOLD defaults " &
|
||||
"to today's 10.0", (not TfilDiag) and TfilDangerThreshold == 10.0
|
||||
|
||||
# 1. the diag is PURE: identical move stream with it on and off
|
||||
let off = replay(loadStates(), loadRoundStarts())
|
||||
putEnv("TR_TFIL_DIAG", "1")
|
||||
loadTfilCommitEnv()
|
||||
let on = replay(loadStates(), loadRoundStarts())
|
||||
var diff = -1
|
||||
if off.len != on.len: diff = min(off.len, on.len)
|
||||
else:
|
||||
for i in 0..<off.len:
|
||||
if recLine(off[i]) != recLine(on[i]): diff = i; break
|
||||
check "j150: with TR_TFIL_DIAG=1 the move stream is BYTE-FOR-BYTE the " &
|
||||
"diag-off one over " & $off.len & " ticks — the counters are inert",
|
||||
diff < 0
|
||||
|
||||
# 2. the histogram is populated and its stage chain is monotone
|
||||
let st = TfilLoss # kept: step 4 clears the live counter
|
||||
check "j150: the histogram counted picks (" & $st.picks & ") and every " &
|
||||
"stage is non-increasing (reach >= cool-filter >= heat-filter)",
|
||||
st.picks > 0 and st.sReach >= st.sCool and
|
||||
st.sCool >= st.sSafe and st.sSafe <= st.sCand and
|
||||
st.safeHist[0] <= st.picks
|
||||
|
||||
# 3. knob parsing, including the fallbacks
|
||||
putEnv("TR_TFIL_DANGER_THRESHOLD", "18")
|
||||
loadTfilCommitEnv()
|
||||
check "j150: TR_TFIL_DANGER_THRESHOLD=18 is read", TfilDangerThreshold == 18.0
|
||||
putEnv("TR_TFIL_DANGER_THRESHOLD", "junk")
|
||||
loadTfilCommitEnv()
|
||||
check "j150: a malformed value falls back to the DEFAULT 10.0",
|
||||
TfilDangerThreshold == 10.0
|
||||
putEnv("TR_TFIL_DANGER_THRESHOLD", "-4")
|
||||
loadTfilCommitEnv()
|
||||
check "j150: a negative value clamps to 0 (heat can never go backwards)",
|
||||
TfilDangerThreshold == 0.0
|
||||
|
||||
# 4. restore the shipped default for every later check in this process
|
||||
putEnv("TR_TFIL_DIAG", ""); putEnv("TR_TFIL_DANGER_THRESHOLD", "")
|
||||
loadTfilCommitEnv()
|
||||
check "j150: clearing the knobs restores 10.0 / diag off",
|
||||
(not TfilDiag) and TfilDangerThreshold == 10.0 and TfilLoss.picks == 0
|
||||
|
||||
echo "\n j150 picker loss histogram (default build, offline fixture replay):"
|
||||
echo " picks=", st.picks
|
||||
let np = st.picks.float
|
||||
echo " mean reachable hull tiles=", st.sReach.float / np
|
||||
echo " mean after CoolestLevels=2 filter=", st.sCool.float / np
|
||||
echo " mean after the heat filter (pre-promotion)=", st.sSafe.float / np
|
||||
echo " mean draw set=", st.sCand.float / np
|
||||
|
||||
# ── j151: the ARRIVAL bound (TR_TFIL_ARRIVE_TICKS, default 0 = off) ─────────
|
||||
type ArrStats = object
|
||||
picks, beyond, starved: int ## starved = picks made with an empty pool
|
||||
meanTta, meanPool: float
|
||||
|
||||
proc replayJ151(bound: float): ArrStats =
|
||||
putEnv("TR_TFIL_ARRIVE_TICKS", $bound)
|
||||
loadTfilCommitEnv()
|
||||
let states = loadStates()
|
||||
let starts = loadRoundStarts()
|
||||
randomize(Seed)
|
||||
var m = initTFIL()
|
||||
var lastPicks = 0
|
||||
for i in 0..<states.len:
|
||||
if i == 0 or i in starts: m.resetRound()
|
||||
discard m.computeMove(states[i])
|
||||
if m.picks != lastPicks:
|
||||
lastPicks = m.picks
|
||||
let tta = sqrt((m.commitTarget.x - states[i].selfX)^2 +
|
||||
(m.commitTarget.y - states[i].selfY)^2) / MaxSpeed
|
||||
inc result.picks
|
||||
if tta > TfilArriveTicks + 0.001: inc result.beyond
|
||||
if m.lastPickSafe == 0: inc result.starved
|
||||
result.meanTta += tta
|
||||
result.meanPool += m.lastPickSafe.float
|
||||
if result.picks > 0:
|
||||
result.meanTta /= result.picks.float
|
||||
result.meanPool /= result.picks.float
|
||||
|
||||
proc testJ151() =
|
||||
# 8a. the shipped default is OFF — the golden parity check above is the proof
|
||||
delEnv("TR_TFIL_ARRIVE_TICKS")
|
||||
loadTfilCommitEnv()
|
||||
check "j151: the arrival bound defaults to OFF (today's uniform draw over " &
|
||||
"the whole 50-tick hull)", TfilArriveTicks == 0.0
|
||||
|
||||
let off = replayJ151(0.0) # today's behaviour, same seed
|
||||
let on15 = replayJ151(15.0) # = CommitTicks: the horizon we hold a target for
|
||||
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
|
||||
check "j151: the mean time-to-arrive falls (" &
|
||||
off.meanTta.formatFloat(ffDecimal, 1) & " -> " &
|
||||
on15.meanTta.formatFloat(ffDecimal, 1) & " ticks) and the pool is " &
|
||||
"not starved (mean safe tiles " &
|
||||
on15.meanPool.formatFloat(ffDecimal, 1) & ", " & $on15.starved &
|
||||
" empty pools)",
|
||||
on15.meanTta < off.meanTta and on15.meanPool >= 1.0 and on15.starved == 0
|
||||
check "j151: the bound is a filter, not a replacement — the pick COUNT is " &
|
||||
"barely reduced (" & $off.picks & " -> " & $on15.picks & ")",
|
||||
on15.picks.float > off.picks.float * 0.9
|
||||
check "j151: clearing the knob restores today's pick stream exactly",
|
||||
replayJ151(0.0).picks == off.picks
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
testDefaultParity()
|
||||
@@ -965,6 +1085,8 @@ when declared(loadTfilCommitEnv):
|
||||
testJ145()
|
||||
testJ146()
|
||||
testJ147()
|
||||
testJ151()
|
||||
testJ150()
|
||||
|
||||
if failures > 0:
|
||||
echo "\n", failures, " check(s) FAILED"
|
||||
|
||||
Reference in New Issue
Block a user