TFIL: time-indexed bullet heat (TR_TFIL_HEAT_TIME, DEFAULT OFF)
Make danger a function of time-to-arrival instead of flat distance. Bullet
core/aura/corridor heat becomes magnitude(power) * decay(dt), dt = along/speed:
* decay(dt) = exp(-dt/tau) is a function of TIME; a fixed tau projects a
pixel reach of speed*tau, so fast/weak bullets get a longer slope and slow
ones a shorter one — derived from speed = 20 - 3*power, not hand-tuned.
tau = TR_TFIL_HEAT_TAU.
* magnitude(power) scales the near-end heat with power from DAMAGE
(calcBulletDamage = 4p, linear in p; SCORE_PER_BULLET_DAMAGE = 1.0). Hit
probability is FLAT across power (docs/env_reference.md), so risk does not
justify power scaling — the cost of the hit does. Floored at 1.0 so a weak
bullet's near end is never less dangerous than the flat model.
Gain = TR_TFIL_HEAT_POWER_GAIN.
Every source is already f(dt), so the time-indexed planner (evaluate a cell at
the tick the bot would ARRIVE, i.e. heatDecay(dt - arrivalDelay)) is a one-line
change. It is intentionally NOT implemented here.
Default path is byte-identical: with TR_TFIL_HEAT_TIME unset both factors are
exactly 1.0 (IEEE x*1.0 is exact), and the committed golden replay in
common_libs/tests/test_tfil_commit_env.nim (20,026 ticks) still passes
byte-for-byte against the pre-change mover. The debug corridor outline is also
drawn only to the model's reach when enabled, so the GUI shows the shortening.
Offline field measurement (common_libs/tests/measure_tfil_heat_time.nim,
46,054 fixture ticks, tau=9/gain=1): corridor reach drops from 443px
wall-to-wall to 143px mean (32% retained); fraction of tiles > 10 goes
0.61 -> 0.57; largest contiguous safe region 118 -> 140 tiles; mean
distance-to-nearest-safe-tile 49 -> 42px. Saturation stays high because wall
radiance + pillar alone are 44% of tiles over threshold and are untouched.
Registers the three knobs in env_report (report + known-name set).
This commit is contained in:
@@ -94,6 +94,12 @@ proc getEnvBool(name: string, default: bool): bool =
|
||||
if s.len == 0: return default
|
||||
s in ["1", "true", "on", "yes"]
|
||||
|
||||
proc getEnvFloat(name: string, default: float): float =
|
||||
let s = getEnv(name, "")
|
||||
if s.len == 0: return default
|
||||
try: result = parseFloat(s.strip())
|
||||
except ValueError: result = default
|
||||
|
||||
proc loadTfilCommitEnv*() =
|
||||
## Read the commit knobs. Called once at module init; the guard test calls it
|
||||
## again after `putEnv` so the non-default arms can be exercised in one process.
|
||||
@@ -107,6 +113,66 @@ proc loadTfilCommitEnv*() =
|
||||
|
||||
loadTfilCommitEnv()
|
||||
|
||||
# ── Time-indexed bullet heat (TR_TFIL_HEAT_TIME=1, default OFF = shipped) ─────
|
||||
#
|
||||
# WHY: the flat model gives every bullet-overlapping tile the same heat and
|
||||
# paints the bullet's corridor all the way to the arena wall, regardless of how
|
||||
# far away or how weak the bullet still is. `CorridorHeat` (20) is twice
|
||||
# `PathDangerThreshold` (10), so ONE weak far bullet saturates a 108px-wide
|
||||
# swath from its nose to the wall, and a path the bullet will not reach until
|
||||
# long after the bot has left it is already marked unsafe.
|
||||
#
|
||||
# WHAT: heat becomes a function of `dt`, the time (ticks) until the bullet
|
||||
# REACHES that cell:
|
||||
#
|
||||
# dt = along / speed # along = distance from the bullet
|
||||
# heat = magnitude(power) * decay(dt)
|
||||
#
|
||||
# * `decay(dt) = exp(-dt / tau)` is a function of TIME, not pixels. A fixed time
|
||||
# constant `tau` therefore projects a PIXEL reach of `speed * tau`: a fast
|
||||
# bullet's slope is longer, a slow one's shorter — DERIVED from the physics
|
||||
# (`speed = 20 - 3*power`), not hand-tuned per power. `tau` = TR_TFIL_HEAT_TAU.
|
||||
# * `magnitude(power)` scales the near-end heat with power from DAMAGE, not from
|
||||
# hit chance: server damage is `calcBulletDamage = 4p`, linear in p, and
|
||||
# `SCORE_PER_BULLET_DAMAGE = 1.0`, so a stronger bullet costs more when it
|
||||
# hits. Hit probability is FLAT across power (docs/env_reference.md), so risk
|
||||
# does NOT justify power scaling — the COST of the hit does. Floored at 1.0 so
|
||||
# a weak bullet's near end is never LESS dangerous than the flat model.
|
||||
# Gain = TR_TFIL_HEAT_POWER_GAIN.
|
||||
#
|
||||
# TIME-INDEXED PLANNER (NOT implemented, by design): because every source is
|
||||
# already expressed as `f(dt)`, evaluating a cell at the tick the bot would
|
||||
# ARRIVE there is the one-line change `heatDecay(dt - arrivalDelay)` — heat a
|
||||
# later bullet's path by that bullet's lead on the bot's own arrival time, so
|
||||
# the bot can use the path and leave before the bullet arrives. This is the real
|
||||
# fix for the user's second point; the shipped move is unchanged until then.
|
||||
var
|
||||
TfilHeatTime*: bool = false
|
||||
TfilHeatTau*: float = 9.0 ## decay time constant, ticks
|
||||
TfilHeatPowerGain*: float = 1.0 ## extra near-end heat at max power (damage proxy)
|
||||
|
||||
proc loadTfilHeatEnv*() =
|
||||
## Read the heat-model knobs. Called once at module init; also callable after
|
||||
## `putEnv` so one process can A/B both models (the offline ruler does this).
|
||||
TfilHeatTime = getEnvBool("TR_TFIL_HEAT_TIME", false)
|
||||
TfilHeatTau = max(0.05, getEnvFloat("TR_TFIL_HEAT_TAU", 9.0))
|
||||
TfilHeatPowerGain = max(0.0, getEnvFloat("TR_TFIL_HEAT_POWER_GAIN", 1.0))
|
||||
|
||||
loadTfilHeatEnv()
|
||||
|
||||
proc heatDecay*(dt: float): float =
|
||||
## Fraction of a bullet's heat still present `dt` ticks before it arrives.
|
||||
## Exactly 1.0 when the time model is off, so the default field is
|
||||
## bit-identical to the flat model (multiplying any heat by 1.0 is exact).
|
||||
if not TfilHeatTime or dt <= 0.0: return 1.0
|
||||
exp(-dt / TfilHeatTau)
|
||||
|
||||
proc bulletMagScale*(power: float): float =
|
||||
## Near-end heat multiplier from the bullet's DAMAGE (4p, linear in power),
|
||||
## floored at 1.0. Exactly 1.0 when the time model is off.
|
||||
if not TfilHeatTime: return 1.0
|
||||
1.0 + TfilHeatPowerGain * (power / 3.0)
|
||||
|
||||
proc bulletRadii(power: float): tuple[core, aura: float] =
|
||||
let t = (power - 0.1) / 2.9
|
||||
let core = BulletCoreRadiusMin + t * (BulletCoreRadiusMax - BulletCoreRadiusMin)
|
||||
@@ -444,6 +510,12 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||
let bx = b.x
|
||||
let by = b.y
|
||||
let (coreR, auraR) = bulletRadii(b.power)
|
||||
# Time model: project the tile's nearest point onto the bullet heading. When
|
||||
# the model is off these factors are exactly 1.0, so heat is unchanged.
|
||||
let bSpeed = sqrt(b.velX * b.velX + b.velY * b.velY)
|
||||
let bUx = if bSpeed > 0.0: b.velX / bSpeed else: 0.0
|
||||
let bUy = if bSpeed > 0.0: b.velY / bSpeed else: 0.0
|
||||
let bMag = bulletMagScale(b.power)
|
||||
let colMin = max(0, int(floor((bx - auraR - m.marginX) / GridSize)))
|
||||
let colMax = min(m.cols-1, int(floor((bx + auraR - m.marginX) / GridSize)))
|
||||
let rowMin = max(0, int(floor((by - auraR - m.marginY) / GridSize)))
|
||||
@@ -458,15 +530,23 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||
let dy = nearY - by
|
||||
let d2 = dx*dx + dy*dy
|
||||
if d2 <= coreR * coreR:
|
||||
m.lava[row * m.cols + col] += BulletCore
|
||||
let along = dx * bUx + dy * bUy
|
||||
m.lava[row * m.cols + col] += BulletCore * bMag *
|
||||
heatDecay(along / bSpeed)
|
||||
elif d2 <= auraR * auraR:
|
||||
m.lava[row * m.cols + col] += BulletAura
|
||||
let along = dx * bUx + dy * bUy
|
||||
m.lava[row * m.cols + col] += BulletAura * bMag *
|
||||
heatDecay(along / bSpeed)
|
||||
|
||||
# Corridor heat — rotated rectangle from bullet position to arena wall, auraR wide
|
||||
for b in m.bullets:
|
||||
let cg = corridorGeom(b, m.arenaWidth, m.arenaHeight)
|
||||
if cg.tMin == 0.0: continue # zero-speed bullet, skip
|
||||
let (_, auraR) = bulletRadii(b.power)
|
||||
# Time model: the corridor gradient is `dt = along / speed` (see the heat
|
||||
# block above). Off -> exactly 1.0, so the corridor is the flat shipped one.
|
||||
let bSpeed = sqrt(b.velX * b.velX + b.velY * b.velY)
|
||||
let bMag = bulletMagScale(b.power)
|
||||
let wx = cg.bx + cg.dx * cg.tMin
|
||||
let wy = cg.by + cg.dy * cg.tMin
|
||||
# Bounding box of the 4 corners
|
||||
@@ -492,7 +572,8 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||
let along = relX * cg.dx + relY * cg.dy
|
||||
let perp = relX * cg.px + relY * cg.py
|
||||
if along >= 0.0 and along <= cg.tMin and perp >= -auraR and perp <= auraR:
|
||||
m.lava[row * m.cols + col] += CorridorHeat
|
||||
m.lava[row * m.cols + col] += CorridorHeat * bMag *
|
||||
heatDecay(along / bSpeed)
|
||||
|
||||
# Enemy heat auras — core (18px) and aura ring (54px), same pattern as bullets
|
||||
for ei in ws.enemies:
|
||||
@@ -582,8 +663,18 @@ proc computeMove*(m: var TFILModule, ws: WorldState): MoveCommand =
|
||||
let (_, auraR) = bulletRadii(b.power)
|
||||
let cg = corridorGeom(b, m.arenaWidth, m.arenaHeight)
|
||||
if cg.tMin == 0.0: continue
|
||||
let wx = cg.bx + cg.dx * cg.tMin
|
||||
let wy = cg.by + cg.dy * cg.tMin
|
||||
# When the time model is on, draw only as far as the corridor still blocks
|
||||
# (heat > PathDangerThreshold = 10); otherwise the outline would claim a
|
||||
# wall-to-wall threat the field no longer has. Off -> reach = cg.tMin.
|
||||
var reach = cg.tMin
|
||||
if TfilHeatTime:
|
||||
let speed = sqrt(b.velX * b.velX + b.velY * b.velY)
|
||||
let near = CorridorHeat * bulletMagScale(b.power)
|
||||
reach = if speed > 0.0 and near > 10.0:
|
||||
min(reach, -TfilHeatTau * ln(10.0 / near) * speed)
|
||||
else: 0.0
|
||||
let wx = cg.bx + cg.dx * reach
|
||||
let wy = cg.by + cg.dy * reach
|
||||
let corners: seq[(float, float)] = @[
|
||||
(cg.bx + cg.px * auraR, cg.by + cg.py * auraR),
|
||||
(cg.bx - cg.px * auraR, cg.by - cg.py * auraR),
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
## 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..<n:
|
||||
if m.lava[i] > 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..<n:
|
||||
if safe[i]:
|
||||
dist[i] = 0
|
||||
q.addLast(i)
|
||||
else:
|
||||
dist[i] = -1
|
||||
while q.len > 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..<n:
|
||||
if dist[i] >= 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..<n:
|
||||
if safe[i] and not seen[i]:
|
||||
var size = 0
|
||||
var q2 = initDeque[int]()
|
||||
q2.addLast(i); seen[i] = true
|
||||
while q2.len > 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..<fx.states.len:
|
||||
let ws = fx.states[si]
|
||||
if si == 0 or si in starts: m.resetRound()
|
||||
discard m.computeMove(ws)
|
||||
inc result.ticks
|
||||
|
||||
let fm = fieldMetrics(m)
|
||||
result.sumFracOver += fm.fracOver
|
||||
result.sumLargestSafe += fm.largestSafe.float
|
||||
result.maxLargestSafe = max(result.maxLargestSafe, fm.largestSafe)
|
||||
result.sumDistAll += fm.meanDistAll
|
||||
result.sumDistUnsafe += fm.meanDistUnsafe
|
||||
if not fm.hasSafe: inc result.noSafeTicks
|
||||
|
||||
if timeOn:
|
||||
# Corridor reach: heat along the corridor is
|
||||
# CorridorHeat * mag * exp(-dt/tau); it stops blocking at dt* where it
|
||||
# falls to SafeThreshold, i.e. dt* = -tau*ln(thr/(CorridorHeat*mag)).
|
||||
for b in m.bullets:
|
||||
let cg = corridorGeom(b, m.arenaWidth, m.arenaHeight)
|
||||
if cg.tMin == 0.0: continue
|
||||
let speed = sqrt(b.velX * b.velX + b.velY * b.velY)
|
||||
let mag = 1.0 + gain * (b.power / 3.0)
|
||||
let near = CorridorHeat * mag
|
||||
var reachPx = 0.0
|
||||
var reachDt = 0.0
|
||||
if near > 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()
|
||||
Reference in New Issue
Block a user