Files
SirRoboGarage/common_libs/tests/test_rack_membership.nim
T
SirStone 59c5af0499 j140 fix: a pre-rename .env must not admit the disabled BitBrain net gun
TR_RACK_BITBRAIN=both is what the owner's live .env carries, and with the new
ADE+SBC gun registered at id 17 under the SAME rack name that value was also
landing on id 17 - so a gun that was not enabled (TR_BITBRAIN_NET unset) was
admitted into the rack and its placeholder predictions were pushed into the
shared VirtualTracker ring, which shifts every other gun's learning order.

While the namespace is LEGACY, loadRackMembership now skips id 17's
TR_RACK_BITBRAIN entirely, so that value addresses ONLY the gun it always
addressed (LEADGAIN, id 16). ModularBot additionally gates admission on
BitbrainNetGun.gunAdmitted(), and test_bitbrain_net pins the truth table: over
6 (rack, switch) settings there is NO configuration that admits the gun while
leaving it disabled.

Guards: test_env_report 25, test_rack_membership 49 (was 48; the revert
one-liner now sets TR_BITBRAIN_NET=1 and one truth-table check was added),
test_tm_pattern_registration 20, test_bitbrain 56, test_gun_harness 39,
test_tfil_commit_env 30, test_lead_gain_registration 13,
test_lead_gain_legacy 24, test_bitbrain_net 44.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-26 16:54:43 +02:00

316 lines
14 KiB
Nim

## Pure unit guard for the melee/1v1 rack-membership mechanism.
##
## No Java, no battle, no virtual bullets: the fitness windows are seeded
## directly so the selection is deterministic and fast. Covers:
## * mode filtering selects the right gun subset,
## * an EMPTY filtered set falls back to the full rack,
## * `TR_RACK_<GUN>` env overrides parse correctly (both / 1v1 / melee / off),
## * the shipped default is `onlyPattern` (Pattern admitted, all else `off`),
## * the revert override restores the old full rack,
## * an all-`both` membership reproduces the pre-change selection exactly.
##
## Run with plain:
## nim c -r common_libs/tests/test_rack_membership.nim
import std/[random, tables, os, strutils]
import gun_harness/gun_interface
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 recordHit(fw: var FitnessWindow, hit: bool) =
fw.hits[fw.head] = hit
fw.head = (fw.head + 1) mod WindowSize
inc fw.count
proc seedWindow(t: var VirtualTracker, targetId, gunId, binIdx, hits, misses: int) =
if targetId notin t.fitness:
t.fitness[targetId] = newSeq[GunFitness](t.numGuns)
var fw = addr t.fitness[targetId][gunId].bins[binIdx]
for _ in 0..<hits: recordHit(fw[], true)
for _ in 0..<misses: recordHit(fw[], false)
# ── mode derivation (server truth) ────────────────────────────────────────────
proc testModeFromEnemyCount() =
check "rackMode: 1 enemy alive -> 1v1",
rackMode(1) == rm1v1
check "rackMode: 2+ enemies alive -> melee",
rackMode(2) == rmMelee and rackMode(5) == rmMelee
check "rackMode: 0 enemies (should not occur) stays melee, matching the radar",
rackMode(0) == rmMelee
# ── pure filter ────────────────────────────────────────────────────────────────
proc testAdmittedGunsFiltering() =
# 0: both, 1: only1v1, 2: onlyMelee, 3: off
let mem = [rmBoth, rmOnly1v1, rmOnlyMelee, rmOff]
check "admittedGuns(1v1) keeps both + only1v1, drops onlyMelee/off",
admittedGuns(4, rm1v1, mem) == @[0, 1]
check "admittedGuns(melee) keeps both + onlyMelee, drops only1v1/off",
admittedGuns(4, rmMelee, mem) == @[0, 2]
proc testEmptyFilterFallsBackToFullRack() =
let allOff = [rmOff, rmOff, rmOff, rmOff]
check "admittedGuns: all-off rack degrades to the FULL rack (1v1)",
admittedGuns(4, rm1v1, allOff) == @[0, 1, 2, 3]
check "admittedGuns: all-off rack degrades to the FULL rack (melee)",
admittedGuns(4, rmMelee, allOff) == @[0, 1, 2, 3]
# A rack where only melee guns exist still falls back to all guns in 1v1.
let onlyMelee = [rmOnlyMelee, rmOnlyMelee]
check "admittedGuns: a rack with NO gun for the mode degrades to the full rack",
admittedGuns(2, rm1v1, onlyMelee) == @[0, 1]
proc testEmptyMembershipMeansNoFiltering() =
check "admittedGuns: an empty membership table filters nothing",
admittedGuns(3, rm1v1, []) == @[0, 1, 2]
check "rackAdmitted: an empty membership table admits every id",
rackAdmitted(0, rm1v1, []) and rackAdmitted(99, rmMelee, [])
proc testRackAdmitted() =
let mem = [rmBoth, rmOnly1v1, rmOff]
check "rackAdmitted: both is admitted in every mode",
rackAdmitted(0, rm1v1, mem) and rackAdmitted(0, rmMelee, mem)
check "rackAdmitted: only1v1 is admitted only in 1v1",
rackAdmitted(1, rm1v1, mem) and not rackAdmitted(1, rmMelee, mem)
check "rackAdmitted: off is never admitted",
not rackAdmitted(2, rm1v1, mem) and not rackAdmitted(2, rmMelee, mem)
check "rackAdmitted: negative id is never admitted",
not rackAdmitted(-1, rm1v1, mem)
# ── selection-level filtering ─────────────────────────────────────────────────
proc testSelectionFollowsMode() =
# gun0 50%, gun1 100% (1v1 only), gun2 100% (melee only).
var t = initTracker(3)
seedWindow(t, 7, 0, 0, 25, 25)
seedWindow(t, 7, 1, 0, 50, 0)
seedWindow(t, 7, 2, 0, 50, 0)
let mem = @[rmBoth, rmOnly1v1, rmOnlyMelee]
randomize(7)
var one: array[3, int]
for _ in 0..<100: inc one[t.bestGun(7, rackMode = rm1v1, membership = mem)]
var melee: array[3, int]
for _ in 0..<100: inc melee[t.bestGun(7, rackMode = rmMelee, membership = mem)]
check "selection: 1v1 rack only ever returns admitted guns {0,1}",
one[2] == 0 and one[0] + one[1] == 100
check "selection: 1v1 rack picks the only1v1 gun when it dominates",
one[1] == 100
check "selection: melee rack only ever returns admitted guns {0,2}",
melee[1] == 0 and melee[0] + melee[2] == 100
check "selection: melee rack picks the onlyMelee gun when it dominates",
melee[2] == 100
proc testSelectionEmptyFilterFallsBack() =
var t = initTracker(3)
seedWindow(t, 7, 0, 0, 10, 40) # 20%
seedWindow(t, 7, 1, 0, 50, 0) # 100%
seedWindow(t, 7, 2, 0, 10, 40) # 20%
let allOff = @[rmOff, rmOff, rmOff]
randomize(11)
var seen: array[3, int]
for _ in 0..<100: inc seen[t.bestGun(7, rackMode = rm1v1, membership = allOff)]
check "selection: an all-off rack still selects (falls back to full rack)",
seen[0] + seen[1] + seen[2] == 100
check "selection: the fallback still picks the best gun (gun 1)",
seen[1] == 100
# ── env parsing / defaults ────────────────────────────────────────────────────
proc clearRackEnv() =
for name in RackGunNames:
delEnv("TR_RACK_" & name)
proc testParseMembership() =
check "parse: 'both' -> rmBoth", parseRackMembership("both") == rmBoth
check "parse: '1v1' -> rmOnly1v1", parseRackMembership("1v1") == rmOnly1v1
check "parse: 'melee' -> rmOnlyMelee", parseRackMembership("melee") == rmOnlyMelee
check "parse: 'off' -> rmOff", parseRackMembership("off") == rmOff
check "parse: case/whitespace-insensitive",
parseRackMembership(" 1V1 ") == rmOnly1v1 and
parseRackMembership("MeLeE") == rmOnlyMelee
check "parse: empty -> shipped default rmBoth",
parseRackMembership("") == rmBoth
check "parse: unknown -> rmBoth (never fatal)",
parseRackMembership("banana") == rmBoth
proc testDefaultsOnlyPattern() =
clearRackEnv()
# SHIPPED DEFAULT: `onlyPattern`. Pattern (id 5) is `both`; every other gun is
# `off`. The selector mechanism is untouched, but the rack has one member.
const PatternId = 5
var exactlyOne = true
var admittedCount = 0
for i in 0..<RackGunNames.len:
if DefaultRackMembership[i] != rmOff:
inc admittedCount
if i != PatternId or DefaultRackMembership[i] != rmBoth: exactlyOne = false
check "defaults: the shipped table admits exactly one gun", admittedCount == 1
check "defaults: that one gun is PATTERN (id 5), in both racks",
exactlyOne and DefaultRackMembership[PatternId] == rmBoth
check "defaults: every non-Pattern gun is `off`",
DefaultRackMembership[0] == rmOff and DefaultRackMembership[1] == rmOff and
DefaultRackMembership[4] == rmOff and DefaultRackMembership[6] == rmOff and
DefaultRackMembership[12] == rmOff and DefaultRackMembership[13] == rmOff and
DefaultRackMembership[14] == rmOff
check "defaults: the default rack admits ONLY Pattern in 1v1",
admittedGuns(18, rm1v1, DefaultRackMembership) == @[PatternId]
check "defaults: the default rack admits ONLY Pattern in melee",
admittedGuns(18, rmMelee, DefaultRackMembership) == @[PatternId]
let loaded = loadRackMembership()
check "defaults: with a clean environment loadRackMembership() == shipped table",
loaded == DefaultRackMembership
check "defaults: RackGunNames covers the shipped 18-gun rack",
RackGunNames.len == 18 and DefaultRackMembership.len == 18
proc testFloorRespectsAdmission() =
## The FLOOR path (`bestRate <= 0` or below the floor rate) historically fell
## back to gun 0 (HeadOn). `chooseFromFit` returns `admitted[0]`, so with the
## onlyPattern default (HeadOn `off`) the floor must return Pattern, an
## ADMITTED gun — never the removed HeadOn.
var t = initTracker(15)
# HeadOn strong but `off`; every admitted gun cold, so bestRate == 0 -> floor.
seedWindow(t, 7, 0, 0, 50, 0)
var diag: SelectorDiag
let g = t.bestGun(7, addr diag, rackMode = rm1v1,
membership = DefaultRackMembership)
check "floor: fires when the only admitted gun has no data", diag.floorFired
check "floor: returns the ADMITTED gun (Pattern, id 5), NOT HeadOn", g == 5
# Same field, same floor, but with HeadOn admissible: it returns gun 0, which
# is exactly WHY HeadOn had to be removed for the floor to be safe.
var oldAll: array[15, RackMembership]
for i in 0..<15: oldAll[i] = rmBoth
var t2 = initTracker(15)
seedWindow(t2, 7, 0, 0, 0, 50) # HeadOn 0%, eligible (>= MinObs)
var d2: SelectorDiag
let gOld = t2.bestGun(7, addr d2, rackMode = rm1v1, membership = oldAll)
check "floor: with HeadOn admitted the old floor returns gun 0",
d2.floorFired and gOld == 0
# The LIVE path (`selectGun`) takes the same floor branch.
var t3 = initTracker(15)
seedWindow(t3, 7, 0, 0, 50, 0)
var d3: SelectorDiag
let gLive = t3.selectGun(7, tick = 0, diag = addr d3, rackMode = rm1v1,
membership = DefaultRackMembership)
check "floor: the live selectGun floor also returns Pattern",
d3.floorFired and gLive == 5
proc testRevertOverrideRestoresFullRack() =
## The documented one-liner: every TR_RACK_<GUN> back to `both` (TMPATTERN
## stays `off`). This must reproduce the old full rack without a rebuild.
clearRackEnv()
putEnv("TR_RACK_PATTERN", "both")
# The ADE+SBC gun (id 17) additionally needs TR_BITBRAIN_NET=1; without the
# switch TR_RACK_BITBRAIN is the LEGACY name for LEADGAIN (id 16), so the
# documented one-liner has to flip the namespace over as well.
putEnv("TR_BITBRAIN_NET", "1")
for name in RackGunNames:
if name != "TMPATTERN": putEnv("TR_RACK_" & name, "both")
let m = loadRackMembership()
delEnv("TR_BITBRAIN_NET")
var full = true
for i in 0..<RackGunNames.len:
let want = if i == 14: rmOff else: rmBoth
if m[i] != want: full = false
check "revert: the documented one-liner restores the all-`both` full rack", full
check "revert: 1v1 rack admits every gun again (TMPATTERN excluded)",
admittedGuns(18, rm1v1, m) == @[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17]
check "revert: the legacy TR_RACK_BITBRAIN alone still selects LEADGAIN only",
loadRackMembership()[17] == rmOff
clearRackEnv()
proc testEnvOverrides() =
clearRackEnv()
putEnv("TR_RACK_TSETLIN", "1v1")
putEnv("TR_RACK_DISPLACE", "melee")
putEnv("TR_RACK_HEADON", "off")
let m = loadRackMembership()
check "env: TR_RACK_TSETLIN=1v1 moves only Tsetlin (id 2)",
m[2] == rmOnly1v1
check "env: TR_RACK_DISPLACE=melee moves only Displace (id 9)",
m[9] == rmOnlyMelee
check "env: TR_RACK_HEADON=off removes only HeadOn (id 0)",
m[0] == rmOff
check "env: every untouched gun keeps its shipped default (off)",
m[1] == rmOff and m[3] == rmOff and m[13] == rmOff and m[14] == rmOff and
m[5] == rmBoth
putEnv("TR_RACK_KNN", "banana")
check "env: unknown values are ignored, not fatal",
loadRackMembership()[12] == rmBoth
clearRackEnv()
# ── parity: default path reproduces pre-change selection ─────────────────────
proc testDefaultReproducesPreChange() =
## The pre-change selector is `bestGun(t, id)` (empty membership). With the
## all-`both` default table and EITHER mode, the selection sequence must be
## identical, RNG draw for RNG draw.
var t = initTracker(5)
seedWindow(t, 7, 0, 0, 60, 40)
seedWindow(t, 7, 1, 0, 40, 60)
seedWindow(t, 7, 2, 0, 80, 20)
seedWindow(t, 7, 3, 0, 55, 45)
seedWindow(t, 7, 4, 0, 20, 80)
let allBoth = @[rmBoth, rmBoth, rmBoth, rmBoth, rmBoth]
randomize(1234)
var pre: seq[int]
for _ in 0..<300: pre.add t.bestGun(7)
randomize(1234)
var as1v1: seq[int]
for _ in 0..<300: as1v1.add t.bestGun(7, rackMode = rm1v1, membership = allBoth)
randomize(1234)
var asMelee: seq[int]
for _ in 0..<300: asMelee.add t.bestGun(7, rackMode = rmMelee, membership = allBoth)
check "parity: all-`both` membership reproduces pre-change selection (1v1)",
pre == as1v1
check "parity: all-`both` membership reproduces pre-change selection (melee)",
pre == asMelee
proc testDefaultReproducesPreChangeSelectGun() =
## Same parity check through the live hysteresis path (`selectGun`).
var t = initTracker(4)
seedWindow(t, 7, 0, 0, 60, 40)
seedWindow(t, 7, 1, 0, 70, 30)
seedWindow(t, 7, 2, 0, 40, 60)
seedWindow(t, 7, 3, 0, 50, 50)
let allBoth = @[rmBoth, rmBoth, rmBoth, rmBoth]
randomize(99)
var a = initTracker(4)
a = t
var pre: seq[int]
for tick in 0..<300: pre.add a.selectGun(7, tick = tick)
randomize(99)
var b = initTracker(4)
b = t
var post: seq[int]
for tick in 0..<300:
post.add b.selectGun(7, tick = tick, rackMode = rmMelee, membership = allBoth)
check "parity: selectGun with all-`both` membership is byte-identical",
pre == post
# ── driver ────────────────────────────────────────────────────────────────────
testModeFromEnemyCount()
testAdmittedGunsFiltering()
testEmptyFilterFallsBackToFullRack()
testEmptyMembershipMeansNoFiltering()
testRackAdmitted()
testSelectionFollowsMode()
testSelectionEmptyFilterFallsBack()
testParseMembership()
testDefaultsOnlyPattern()
testFloorRespectsAdmission()
testRevertOverrideRestoresFullRack()
testEnvOverrides()
testDefaultReproducesPreChange()
testDefaultReproducesPreChangeSelectGun()
if failures > 0:
echo "\n", failures, " check(s) FAILED"
quit(1)
echo "\nAll rack-membership checks passed."