diff --git a/ModularBot_garage/src/ModularBot.nim b/ModularBot_garage/src/ModularBot.nim index 1bb66f2..ec09410 100644 --- a/ModularBot_garage/src/ModularBot.nim +++ b/ModularBot_garage/src/ModularBot.nim @@ -90,6 +90,18 @@ let DisabledGuns = s proc gunDisabled(id: int): bool {.inline.} = id in DisabledGuns +## ── virtual-bullet spawn gate (TR_VBULLET_ADMIT_ONLY) ────────────────────── +## Rack membership filters SELECTION; this knob extends it to SPAWNING. With it +## set (default 1) a gun the current rack does not admit is skipped ENTIRELY in +## the live aim block — no `predict()`, no `spawnBullets()` — so 13 unselected +## guns stop computing a fitness table nobody reads (~40-46% of the per-tick +## budget in the shipped onlyPattern rack). Set to 0 to restore the pre-change +## behaviour (every non-TMPATTERN gun spawns) on the SAME frozen binary. +## `onResult` feedback is NEVER gated (see the gated aim block in `run`). +let VBulletAdmitOnly* = + block: + let v = getEnv("TR_VBULLET_ADMIT_ONLY", "1").strip().toLowerAscii() + not (v == "0" or v == "false" or v == "no" or v == "off") ## ── Movement rack (runtime switch) ───────────────────────────────────────── ## `TR_MOVEMENT` selects the movement engine, mirroring the gun rack's ## env-driven construction. Both engines are always constructed, so the switch @@ -995,45 +1007,58 @@ method run*(bot: ModularBot) = var knnPreds: array[len(PowerBins), GunPrediction] var tmselPreds: array[len(PowerBins), GunPrediction] var tmpPreds: array[len(PowerBins), GunPrediction] + # ── TR_VBULLET_ADMIT_ONLY gate ────────────────────────────────────── + # Rack membership used to filter only SELECTION, so every unselected gun + # still ran predict()+spawnBullets() each tick to feed a fitness table the + # selector would never read (~40-46% of the per-tick budget). `admit` + # skips BOTH calls for a gun the current rack does not admit. It is + # recomputed each tick because the rack mode can change mid-round (a melee + # thinning to 1v1). Bullets already in flight are NOT cancelled: they are + # resolved by `tickBullets` and the feedback `case` below still calls the + # owning gun's onResult, so attribution survives a mid-round rack change. + # TMPATTERN keeps its own admission gate even when the knob is 0, so + # TR_VBULLET_ADMIT_ONLY=0 reproduces the exact pre-change rack. + var admit: array[15, bool] + for gi in 0..<15: + admit[gi] = vBulletAdmitted(gi, bot.rackMode, ActiveRackMembership, + VBulletAdmitOnly or gi == TmPatternId) for i in 0..= Warmup and si < Warmup + Measure + let t0 = getMonoTime() + for gi in 0..= Warmup and si < Warmup + Measure + for gi in 0.. 0: acc.pred[gi] /= counts[gi].float + if onCalls[gi] > 0: acc.onRes[gi] /= onCalls[gi].float + result = acc + +proc main() = + randomize(1) + let fx = loadFixtures() + let states = buildStates(fx, MaxTicks) + echo fmt"fixtures={fx.len} ticks={states[0].len} warmup={Warmup} measured={Measure}" + echo "shipped rack: onlyPattern (DefaultRackMembership); predicate vb.vBulletAdmitted" + + # Two reps, take the min (least noise). + var offPs, offRes, onPs, onRes: int64 + var offSpawns, onSpawns, ticksRun: int + for rep in 0..<2: + let off = runArm(states, gateOn = false) + let on = runArm(states, gateOn = true) + if rep == 0: + offPs = off.psNs; offRes = off.resNs; offSpawns = off.spawns + onPs = on.psNs; onRes = on.resNs; onSpawns = on.spawns + ticksRun = off.ticks + else: + offPs = min(offPs, off.psNs); offRes = min(offRes, off.resNs) + onPs = min(onPs, on.psNs); onRes = min(onRes, on.resNs) + offSpawns = min(offSpawns, off.spawns); onSpawns = min(onSpawns, on.spawns) + + let n = max(1, ticksRun).float + let offPSms = offPs.float / 1.0e6 / n # ms/tick + let offRSms = offRes.float / 1.0e6 / n + let onPSms = onPs.float / 1.0e6 / n + let onRSms = onRes.float / 1.0e6 / n + let offTot = offPSms + offRSms + let onTot = onPSms + onRSms + + echo "" + echo "=================== PER-TICK COST: gate ON vs OFF (13-gun live rack) ===================" + echo "arm predict+spawn ms/t resolve+onResult ms/t total ms/t ticks/s spawns/t" + echo fmt"OFF {offPSms:>20.2f} {offRSms:>22.2f} {offTot:>12.2f} {1000.0/offTot:>7.0f} {offSpawns.float/n:>10.1f}" + echo fmt"ON {onPSms:>20.2f} {onRSms:>22.2f} {onTot:>12.2f} {1000.0/onTot:>7.0f} {onSpawns.float/n:>10.1f}" + echo fmt"reclaimed = {offTot - onTot:.2f} ms/tick ({100.0*(offTot-onTot)/offTot:.0f}% of the gate-OFF total)" + echo fmt"implied ticks/s: OFF {1000.0/offTot:.0f} -> ON {1000.0/onTot:.0f} (x{offTot/onTot:.2f})" + echo "(note: tickBullets also scans the fixed 8192-slot ring each tick, present in BOTH arms)" + + let pg = perGunCosts(states) + echo "" + echo "=================== PER-GUN COST (full rack, names what disappears) ===================" + echo "gun predict+spawn ms/t onResult ms/t gateON" + var sumPred, sumOn: float + for gi in 0..24.3f}{pg.onRes[gi]/1.0e6:>18.3f}{($admitted):>10}" + echo fmt"TOTAL {sumPred/1.0e6:>24.3f}{sumOn/1.0e6:>18.3f}" + echo "" + +when isMainModule: + main() diff --git a/common_libs/tests/test_vbullet_admit_gate.nim b/common_libs/tests/test_vbullet_admit_gate.nim new file mode 100644 index 0000000..e21015c --- /dev/null +++ b/common_libs/tests/test_vbullet_admit_gate.nim @@ -0,0 +1,99 @@ +## Pure unit guard for the `TR_VBULLET_ADMIT_ONLY` spawn-gate predicate. +## +## No Java, no battle, no virtual bullets: `vBulletAdmitted` is a pure function of +## (gunId, rack mode, membership table, admitOnly) over the SHIPPED +## `DefaultRackMembership`, so the live gate can be pinned without running the +## bot. Covers: +## * the shipped default rack admits Pattern (5) and nothing else; +## * `admitOnly=false` (TR_VBULLET_ADMIT_ONLY=0) restores "spawn for every gun"; +## * mode-restricted membership (`rmOnly1v1` / `rmOnlyMelee`) flips with mode; +## * an empty membership table filters nothing (pre-change callers); +## * out-of-range ids are admitted, negative ids are not; +## * the TMPATTERN special case: forcing `admitOnly=true` keeps gun 14 gated +## even when the knob is off — the exact composition the live loop uses. +## +## Run with plain: +## nim c -r common_libs/tests/test_vbullet_admit_gate.nim + +import gun_harness/virtual_bullets +import gun_harness/selector + +var failures = 0 +proc check(name: string, ok: bool) = + if ok: echo "PASS: ", name + else: echo "FAIL: ", name; inc failures + +proc testShippedDefaultRack() = + ## The shipped onlyPattern table: Pattern (5) admitted in both modes, every + ## other registered gun (0..14) off. + check "default: Pattern admitted in 1v1", + vBulletAdmitted(5, rm1v1, DefaultRackMembership, true) + check "default: Pattern admitted in melee", + vBulletAdmitted(5, rmMelee, DefaultRackMembership, true) + var allOthersOff = true + for gi in 0.. 0: + echo "\n", failures, " check(s) FAILED" + quit(1) +echo "\nAll vbullet-admit-gate checks passed."