Files
SirRoboGarage/common_libs/tests/test_vbullet_admit_gate.nim
SirStone 3142b70aa5 Gate virtual-bullet spawn on rack admission: +68% tick rate, selected gun unchanged
The default rack is now Pattern-only (31c7c01), but membership filters SELECTION,
not SPAWNING - so all 13 unselected guns still ran `predict` + `spawnBullets`
every tick to feed fitness tables nobody reads. Measured waste: Tsetlin alone
0.98 ms/tick, KNN 0.30, plus 10 more. This generalises the gate TMPATTERN already
had to every gun, behind `TR_VBULLET_ADMIT_ONLY` (default 1 = gate, 0 = old).

OFFLINE COST (release build, 400 ticks, min of 2 reps, 13-gun rack):
  OFF  2.14 ms/tick  (implied 467 ticks/s)
  ON   0.04 ms/tick  (implied 27667 ticks/s)
  -> reclaimed 2.10 ms/tick, ~98% of the virtual-bullet cost. Tsetlin's 0.98
     disappears, KNN's 0.30 disappears, only Pattern (0.017) survives.
Note the absolute scale is lower than an earlier unoptimised measurement (~6.1
ms/t) because this is a -d:release build; the ON-vs-OFF DELTA is the robust result.

LIVE TICK RATE (ONE frozen binary, 4 runs/arm x 4 rounds, all 8 concurrent, gate
varied by env only):
  ON   146.2 ticks/s  (142.3, 145.2, 147.9, 149.4)
  OFF   86.9 ticks/s  ( 95.0,  41.4,  99.1, 112.1)
  NO OVERLAP: ON min 142.3 > OFF max 112.1. Excluding a game-outcome outlier in
  the OFF arm, OFF max is still 112.1. Outside the noise.
**+68% tick rate**, which also makes every future A/B faster. Both arms still pay
the fixed 8192-slot ring scan in tickBullets.

SAFETY, verified not assumed: Pattern is admitted under the shipped rack, so its
own fitness keeps accumulating and the SELECTED gun is unchanged - Pattern 100% in
all 4 runs both arms, and Pattern was the ONLY gun with vShots>0 in the ON arm
while all 13 had vShots>0 in the OFF arm. So admission is the correct predicate.
Mid-round transitions are safe by construction: only predict/spawn are gated, while
tickBullets still resolves every active bullet and the feedback case still calls
the owning gun's onResult.

A TOOLING BUG THIS CAUGHT, and a correction to the task's assumption:
`acceptance_offline_vs_online.nim` IS affected (I had assumed it was not). It
compares the live per-gun vShots against an offline replay that always spawns all
guns, so under the default gate the online non-Pattern vShots are 0 while offline
is ~400 - a guaranteed mismatch. Fixed by pinning `TR_VBULLET_ADMIT_ONLY=0` inside
that test (same putEnv/defer pattern as TR_RECORD_WORLDSTATE), keeping 12/12. The
test is about offline/online METRIC parity, so it needs every gun spawning.
Unaffected (verified from source): audit_virtual_guns.nim,
measure_cornering_guns.nim, sweep_tm_pattern.nim - all offline, none read
GUN_STATS_PATH.

Guards: test_vbullet_admit_gate 12 (new, pure), test_gun_harness 39,
test_vbullet_metric 11, test_power_selection 3, test_adaptive_radar 41,
test_tfil_ring_weights 24, test_power_policy 26, test_ram_decision 28,
test_rack_membership 48, test_selector_tiebreak 19, test_tm_pattern_registration 20,
test_tm_pattern_learning 3, acceptance_offline_vs_online 12/12. ModularBot compiles.
2026-09-22 02:24:18 +02:00

100 lines
4.4 KiB
Nim

## 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..<len(DefaultRackMembership):
if gi == 5: continue
if vBulletAdmitted(gi, rm1v1, DefaultRackMembership, true): allOthersOff = false
if vBulletAdmitted(gi, rmMelee, DefaultRackMembership, true): allOthersOff = false
check "default: every other gun (incl. TMPATTERN 14) is NOT admitted",
allOthersOff
proc testKnobOffRestoresFullRack() =
## TR_VBULLET_ADMIT_ONLY=0: the gate is a no-op for the general guns, i.e.
## every id spawns regardless of rack membership.
var allAdmitted = true
for gi in 0..<len(DefaultRackMembership):
if not vBulletAdmitted(gi, rm1v1, DefaultRackMembership, false): allAdmitted = false
if not vBulletAdmitted(gi, rmMelee, DefaultRackMembership, false): allAdmitted = false
check "knob=0: vBulletAdmitted is true for EVERY gun in both modes", allAdmitted
proc testModeRestrictedMembership() =
# 0: both, 1: only1v1, 2: onlyMelee, 3: off
let mem = [rmBoth, rmOnly1v1, rmOnlyMelee, rmOff]
check "1v1: both + only1v1 admitted, onlyMelee/off not",
vBulletAdmitted(0, rm1v1, mem, true) and
vBulletAdmitted(1, rm1v1, mem, true) and
(not vBulletAdmitted(2, rm1v1, mem, true)) and
(not vBulletAdmitted(3, rm1v1, mem, true))
check "melee: both + onlyMelee admitted, only1v1/off not",
vBulletAdmitted(0, rmMelee, mem, true) and
vBulletAdmitted(2, rmMelee, mem, true) and
(not vBulletAdmitted(1, rmMelee, mem, true)) and
(not vBulletAdmitted(3, rmMelee, mem, true))
check "knob=0 overrides even rmOff / mode restrictions",
vBulletAdmitted(3, rm1v1, mem, false) and
vBulletAdmitted(1, rmMelee, mem, false)
proc testEmptyAndOutOfRange() =
let empty: seq[RackMembership] = @[]
check "empty membership filters nothing (pre-change callers)",
vBulletAdmitted(0, rm1v1, empty, true) and
vBulletAdmitted(13, rmMelee, empty, true)
let mem = [rmOff, rmOff]
check "id past the membership table is admitted",
vBulletAdmitted(7, rm1v1, mem, true)
check "negative id is not admitted when the gate is on",
(not vBulletAdmitted(-1, rm1v1, mem, true))
proc testTmPatternStaysGatedWhenKnobOff() =
## The live loop composes the TMPATTERN gate as
## vBulletAdmitted(14, mode, mem, VBulletAdmitOnly or id == TmPatternId)
## so gun 14 stays gated even when TR_VBULLET_ADMIT_ONLY=0.
let knobOff = false
for mode in [rm1v1, rmMelee]:
let composed = vBulletAdmitted(14, mode, DefaultRackMembership,
knobOff or 14 == 14)
let alwaysGated = vBulletAdmitted(14, mode, DefaultRackMembership, true)
check "knob=0 keeps TMPATTERN (14) gated exactly as when gated",
composed == alwaysGated and composed == false
testShippedDefaultRack()
testKnobOffRestoresFullRack()
testModeRestrictedMembership()
testEmptyAndOutOfRange()
testTmPatternStaysGatedWhenKnobOff()
if failures > 0:
echo "\n", failures, " check(s) FAILED"
quit(1)
echo "\nAll vbullet-admit-gate checks passed."