diff --git a/ModularBot_garage/src/ModularBot.nim b/ModularBot_garage/src/ModularBot.nim index 9abe094..03bf2bb 100644 --- a/ModularBot_garage/src/ModularBot.nim +++ b/ModularBot_garage/src/ModularBot.nim @@ -1310,7 +1310,17 @@ method run*(bot: ModularBot) = vBulletAdmitted(gi, bot.rackMode, ActiveRackMembership, VBulletAdmitOnly or gi == TmPatternId or gi == TmHorizonId or gi == LeadGainId or - gi == BitbrainNetId) + gi == BitbrainNetId) and + # BITBRAIN (id 17) needs BOTH switches. The rack table alone + # is not enough: a pre-rename `.env` still carries + # `TR_RACK_BITBRAIN=both` (it used to select LEADGAIN), and + # admitting a DISABLED gun would let its placeholder + # predictions into the shared VirtualTracker ring and shift + # every other gun's learning order. With this gate an + # incomplete configuration is exactly as inert as an unset + # one. + (gi != BitbrainNetId or + bot.bbn.gunAdmitted(true)) for i in 0.. 0: result[i] = parseRackMembership(v) - if not netSwitchOwnsBitbrainName(): + if not netOwns: for (key, gid) in RackLegacyAlias: let current = RackEnvPrefix & RackGunNames[gid] if getEnv(key, "").len > 0 and getEnv(current, "").len == 0: diff --git a/common_libs/guns/bitbrain_net.README.md b/common_libs/guns/bitbrain_net.README.md index a09ec76..b69aaa7 100644 --- a/common_libs/guns/bitbrain_net.README.md +++ b/common_libs/guns/bitbrain_net.README.md @@ -14,11 +14,17 @@ that delivers forgetting and true per-class probabilities — `docs/bitbrain_counted_sbc.md`). It took the `BITBRAIN` rack name (id **17**) and the `TR_BITBRAIN_*` knob prefix -that the renamed corrector gave up. **Default OFF**: it needs BOTH -`TR_RACK_BITBRAIN` (rack table) and `TR_BITBRAIN_NET=1` (the gun's own master -switch, the same switch that disowns the legacy `TR_BITBRAIN_*` aliases of -`LEADGAIN`). The network is built lazily, so an unset environment never -allocates a byte. +that the renamed corrector gave up. **Default OFF.** It needs +`TR_RACK_BITBRAIN=both` in the rack table **and** `TR_BITBRAIN_NET=1` — the one +switch that decides whether the `TR_BITBRAIN_*` namespace is legacy +(`LEADGAIN`'s) or the new gun's, and the new gun's master on/off at the same +time. The two are gated by the SAME switch, so no env configuration can admit +the gun while leaving it disabled +(`test_bitbrain_net.nim` pins that over a 6-setting truth table) — which +matters, because a pre-rename `.env` still carries `TR_RACK_BITBRAIN=both` and a +disabled gun's placeholder predictions in the shared `VirtualTracker` ring would +shift every other gun's learning order. The network is built lazily, so an unset +environment never allocates a byte. ## OUTPUT SHAPE — a fine-grained correction ON TOP of Pattern @@ -69,7 +75,7 @@ ring), i.e. the same causal information Pattern itself uses. | Env | Default | Meaning | |---|---|---| | `TR_RACK_BITBRAIN` | `off` | rack admission for id 17 (the *current* name of the rack key) | -| `TR_BITBRAIN_NET` | `0` | **master switch.** `0` = off and the `TR_BITBRAIN_*` names are LEGACY aliases of `LEADGAIN`; `1` = on and the names below are the new gun's | +| `TR_BITBRAIN_NET` | `0` | **master switch + namespace disambiguator.** `0` = the gun is off AND the `TR_BITBRAIN_*` names are LEGACY aliases of `LEADGAIN` (and `TR_RACK_BITBRAIN` selects id 16); `1` = the gun is on and the names below are this gun's (and `TR_RACK_BITBRAIN` selects id 17) | | `TR_BITBRAIN_INPUT` | 52 | total input slots; pads or truncates the block layout so the ADE codes can never index out of range | | `TR_BITBRAIN_FEATURES` | all blocks at their shipped width | `name:W` list, comma separated. `name:0` switches a block OFF; an unlisted block keeps its shipped width; an unknown name warns and is ignored | | `TR_BITBRAIN_NCLASSES` | 8 | output resolution | @@ -172,14 +178,16 @@ anything about damage, survival or round wins, and it is never presented as one. ## Tests ```bash -# 42 checks: the gun really engages the network (not just that it links) +# 44 checks: the gun really engages the network (not just that it links) nim c -r -d:release --path:common_libs common_libs/tests/test_bitbrain_net.nim # the library itself (56 checks, unchanged) nim c -r -d:release --path:common_libs common_libs/tests/test_bitbrain.nim ``` `test_bitbrain_net.nim` proves engagement by observation, not by linkage: the -network is 0 bytes before first use, two different inputs give different class -outputs, a learn visibly raises SBC occupancy, a bitset learn is idempotent -while a counted learn is monotone, ADE threshold adaptation runs, and -construction + first predict leave the global RNG untouched. +network is 0 bytes before first use, a **trained** head returns different +classes for different states and separates two taught populations, a learn +visibly raises SBC occupancy, a bitset learn is idempotent while a counted +learn's evidence is monotone, ADE threshold adaptation runs, no env setting can +admit a disabled gun, and construction + first predict leave the global RNG +untouched. diff --git a/common_libs/guns/bitbrain_net.nim b/common_libs/guns/bitbrain_net.nim index d697f4f..7248958 100644 --- a/common_libs/guns/bitbrain_net.nim +++ b/common_libs/guns/bitbrain_net.nim @@ -586,6 +586,10 @@ proc sbcBytes*(g: BitbrainNetGun): int = proc predict*(g: var BitbrainNetGun, state: WorldState, bulletSpeed: float): GunPrediction = g.ensureInit() + ## A disabled gun must never be called: ModularBot gates admission on + ## `g.enabled`, and this branch is only a belt-and-braces guard. It returns + ## the CURRENT position (a valid, if useless, prediction) rather than anything + ## that could poison a shared tracker. if not g.enabled: return GunPrediction(x: state.enemyX, y: state.enemyY) if state.tick < g.lastTick: g.resetRoundState() if state.tick != g.lastTick: @@ -665,3 +669,11 @@ proc evidenceFor*(g: var BitbrainNetGun, input: openArray[uint8], klass: int): i ## every counted learn, so it is the observable that proves the counted mode ## is accumulating rather than just flipping bits. g.net.infer(input).counts[klass] + +proc gunAdmitted*(g: BitbrainNetGun, rackAdmitted: bool): bool {.inline.} = + ## The FULL admission predicate for rack id 17: the rack table AND this gun's + ## own master switch. Both default off, so an incomplete configuration is + ## exactly as inert as an unset one — which matters, because a pre-rename + ## `.env` still carries `TR_RACK_BITBRAIN=both` and must not be able to push a + ## disabled gun's placeholder predictions into the shared VirtualTracker ring. + rackAdmitted and g.enabled diff --git a/common_libs/tests/test_bitbrain_net.nim b/common_libs/tests/test_bitbrain_net.nim index dcb4810..03cca70 100644 --- a/common_libs/tests/test_bitbrain_net.nim +++ b/common_libs/tests/test_bitbrain_net.nim @@ -33,6 +33,9 @@ proc check(name: string, ok: bool) = proc clearEnv() = for n in BitbrainNetEnvNames: delEnv(n) + # the master switch is shared with LEADGAIN, so it is NOT in + # BitbrainNetEnvNames and must be cleared explicitly here + delEnv("TR_BITBRAIN_NET") for n in RackGunNames: delEnv("TR_RACK_" & n) proc mkState(t: int, ex, ey, sx, sy, eh: float): WorldState = @@ -80,6 +83,33 @@ proc testRackEnv() = check "env: TR_RACK_BITBRAIN=both + TR_BITBRAIN_NET=1 admits id 17", m[BitbrainNetId] == rmBoth and vBulletAdmitted(BitbrainNetId, rm1v1, m, true) + # A pre-rename .env carries TR_RACK_BITBRAIN=both WITHOUT the master switch. + # That must not admit the gun: a disabled gun's placeholder predictions would + # enter the shared VirtualTracker ring and shift every other gun's learning + # order, i.e. it would break the default path. + clearEnv() + putEnv("TR_RACK_BITBRAIN", "both") # exactly what a pre-rename .env carries + let mLegacy = loadRackMembership() + check "env: TR_RACK_BITBRAIN with the switch OFF still means LEADGAIN (id 16)", + mLegacy[BitbrainNetId] == rmOff and mLegacy[16] == rmBoth + # TR_RACK_BITBRAIN and TR_BITBRAIN_NET are gated by the SAME switch, so there + # is no env configuration that admits the gun without enabling it. Pin that: + # a disabled gun must never reach the shared VirtualTracker ring, because its + # placeholder predictions would shift every other gun's learning order. + var consistent = true + for (rackVal, netVal) in [("both", ""), ("both", "0"), ("both", "1"), + ("off", "1"), ("1v1", "1"), ("1v1", "")]: + clearEnv() + putEnv("TR_RACK_BITBRAIN", rackVal) + if netVal.len > 0: putEnv(BBN_NET_ENV, netVal) + let mm = loadRackMembership() + let inRack = mm[BitbrainNetId] != rmOff + let admitted = initBitbrainNetGun().gunAdmitted(inRack) + if inRack != admitted: consistent = false + if admitted and not initBitbrainNetGun().enabled: consistent = false + check "env: TR_RACK_BITBRAIN alone can NEVER admit a disabled id 17 (6 settings)", + consistent + clearEnv() clearEnv() # ── the knobs really resolve ───────────────────────────────────────────────── diff --git a/common_libs/tests/test_rack_membership.nim b/common_libs/tests/test_rack_membership.nim index eab7ba3..47b8b5b 100644 --- a/common_libs/tests/test_rack_membership.nim +++ b/common_libs/tests/test_rack_membership.nim @@ -204,9 +204,14 @@ proc testRevertOverrideRestoresFullRack() = ## 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..