3142b70aa5
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.
100 lines
4.4 KiB
Nim
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."
|