j140 rebuild a real BitBrain gun: ADE+SBC at rack id 17, default off, and measure its scaling
The BITBRAIN name was sitting on a gun with no network in it. This is the gun
that actually runs the algorithm: an ADE layer (thresholded random projections
with ONLINE threshold adaptation) feeding the SBC head from
common_libs/bitbrain/, with the counted+decay mode available.
common_libs/guns/bitbrain_net.nim the gun
rack name BITBRAIN, rack id 17 (rack 17 -> 18 guns), both new guns default OFF
admitted by TR_RACK_BITBRAIN=both AND TR_BITBRAIN_NET=1 (the switch that also
disowns LEADGAIN's legacy TR_BITBRAIN_* aliases)
OUTPUT: a fine-grained aim CORRECTION on top of Pattern - the probability-
weighted mean of the nClasses class centres under inferProb - not a direct aim
point from the argmax. That is the shape docs/bitbrain_gate.md measured, and
Pattern is already a strong predictor, so the net's job is the signed residual.
Below TR_BITBRAIN_MINOBS the shift is exactly 0 and Pattern is returned
unchanged.
INPUT: a CONFIGURED set of FEATURE BLOCKS (TR_BITBRAIN_FEATURES=name:W), each
block's width == its resolution, laid out as a thermometer code over 0/255 slots
(so an ADE synapse 'matches' when its polarity agrees with the slot and a random
ADE fires iff its w synapses all match, rate 2^-w). Default is 52 slots over 9
blocks. NO long temporal window, per docs/state_window_gate.md: the only history
is a 12-tick ring feeding three rate/turn quantities.
Every knob env-configurable: _INPUT (width), _NCLASSES, _NADES, _WIDTHS
(clause widths), _FEATURES, _SPAN, _MODE, _DECAY_EVERY, _DECAY_SHIFT,
_MINOBS, _ADAPT_EVERY, _TARGET, _NETSEED, _NETLOG, _NET_RESET_ON_TARGET.
MEASURED SCALING (measure_bitbrain_scaling.nim, 3 recorded runs, 37412 ticks,
-d:release, one predict per power bin per tick, timed region = predicts only):
RAM 1.59 MB default (98.6% SBC tensors); linear in nClasses, QUADRATIC in
nAde, FLAT in input width; counted/bitset = 7.30x on RAM, ~1x on time.
ms/tick 2.70 default = 21% of the 13.16 ms budget; 64 classes busts it (149%),
nAde 512 uses 74%, nAde 64 uses 2%.
CAPACITY vs ACCURACY: over a 100x RAM range the offline mean |err| moves
17.254 -> 17.115 deg around Pattern's 16.964, and the sign flips along the
nClasses axis, so it is noise, not a trend. The corrector is consistently
slightly WORSE than Pattern. The ceiling is the STATE, not the classifier.
VETO-CAPABLE OFFLINE CHECK ONLY (docs/offline_harness_trust.md), never
presented as a live win.
ENGAGEMENT is proven, not assumed: test_bitbrain_net.nim (42 checks) shows 0
bytes before first use, different inputs -> different class outputs, a learn
raises SBC occupancy, bitset learn idempotent while counted learn is monotone,
threshold adaptation runs, and the global RNG is untouched.
Parity: shipped rack still onlyPattern, shipped movement still strafe. Guards:
test_env_report 25, test_rack_membership 48, test_tm_pattern_registration 20,
test_lead_gain_registration 13, test_lead_gain_legacy 24, test_bitbrain 56,
test_gun_harness 39, test_tfil_commit_env 30, test_bitbrain_net 42.
Clean archive build: [SuccessX].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
## ModularBot — plugin gun architecture tracer bullet.
|
||||
## Guns: HeadOnGun (0), LinearGun (1), TsetlinGun (2), CircularGun (3), GFGun (4), PatternMatcherGun (5), WallBounceGun (6), AccelGun (7), StopShotGun (8), DisplacementGun (9), AveragedLeadGun (10), DecayGFGun (11), KNNGun (12), TmSelectorGun (13), TmPatternGun (14), TmHorizonGun (15), LeadGainGun (16) via GunHarness.
|
||||
## Guns: HeadOnGun (0), LinearGun (1), TsetlinGun (2), CircularGun (3), GFGun (4), PatternMatcherGun (5), WallBounceGun (6), AccelGun (7), StopShotGun (8), DisplacementGun (9), AveragedLeadGun (10), DecayGFGun (11), KNNGun (12), TmSelectorGun (13), TmPatternGun (14), TmHorizonGun (15), LeadGainGun (16), BitbrainNetGun (17) via GunHarness.
|
||||
## Radar: RadarLockModule (1v1) / AdaptiveMeleeRadarModule (2+ enemies), auto-switched per tick.
|
||||
## Movement: OscillatorModule (perpendicular strafing).
|
||||
|
||||
@@ -31,6 +31,7 @@ import guns/tm_selector
|
||||
import guns/tm_pattern
|
||||
import guns/tm_horizon
|
||||
import guns/lead_gain
|
||||
import guns/bitbrain_net
|
||||
import movements/phantom_meteor
|
||||
import movements/rammer
|
||||
import movements/ram_decision
|
||||
@@ -150,7 +151,7 @@ let ResultLog* =
|
||||
block:
|
||||
let v = getEnv("TR_RESULT_LOG", "1").strip().toLowerAscii()
|
||||
not (v == "0" or v == "false" or v == "no" or v == "off")
|
||||
const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor", "Pattern", "WallBounce", "Accel", "StopShot", "Displace", "AvgLead", "DecayGF", "KNN", "TMSelect", "TMPattern", "TMHorizon", "LeadGain"]
|
||||
const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor", "Pattern", "WallBounce", "Accel", "StopShot", "Displace", "AvgLead", "DecayGF", "KNN", "TMSelect", "TMPattern", "TMHorizon", "LeadGain", "BitBrainNet"]
|
||||
|
||||
## Rack id of the new TM pattern gun. It defaults to `TR_RACK_TMPATTERN=off`;
|
||||
## unlike the other guns, its virtual-bullet spawn is gated on rack admission
|
||||
@@ -174,6 +175,16 @@ const TmHorizonId = 15
|
||||
## `TR_BITBRAIN_NET` is off (backward compatibility; see `gun_harness/selector`).
|
||||
const LeadGainId = 16
|
||||
|
||||
## Rack id of the ADE+SBC gun — the gun that actually runs the algorithm
|
||||
## (`guns/bitbrain_net.nim`). It takes the `BITBRAIN` name and the
|
||||
## `TR_BITBRAIN_*` knob prefix that the renamed corrector gave up. TWO switches
|
||||
## admit it, and both default off: `TR_RACK_BITBRAIN=both` in the rack table and
|
||||
## `TR_BITBRAIN_NET=1` in the gun itself (the switch that also disowns the
|
||||
## legacy `TR_BITBRAIN_*` aliases — see `gun_harness/selector`). Its
|
||||
## virtual-bullet spawn is gated on rack admission, so the shipped default never
|
||||
## spawns it and the shared tracker ring head is byte-for-byte unchanged.
|
||||
const BitbrainNetId = 17
|
||||
|
||||
## ── virtual-bullet debug overlay (TR_VBULLET_DEBUG*) ───────────────────────
|
||||
## OFF by default: the shipped bot draws nothing. When on, the tracker's
|
||||
## virtual bullets are overlaid so the training signal the selector learns
|
||||
@@ -246,6 +257,7 @@ type
|
||||
tmPattern: TmPatternGun
|
||||
tmHorizon: TmHorizonGun
|
||||
leadGain: LeadGainGun
|
||||
bbn: BitbrainNetGun
|
||||
mover: TFILModule
|
||||
ringMover: TFILRingModule
|
||||
strafeMover: StrafeModule
|
||||
@@ -943,6 +955,7 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
|
||||
# LEADGAIN: per-round wipe always clears observations/labels; `perRound` mode
|
||||
# also wipes its SBCs (the gate test's winning regime).
|
||||
bot.leadGain.resetRoundState()
|
||||
bot.bbn.resetRoundState()
|
||||
if e.roundNumber <= 1:
|
||||
bot.leadGain.resetLearning("round1")
|
||||
bot.isRamming = false
|
||||
@@ -1025,6 +1038,7 @@ method onGameStarted*(bot: ModularBot, e: GameStartedEventForBot) =
|
||||
# boundary. The round-1 fallback in `onRoundStarted` covers a missed callback.
|
||||
bot.tmHorizon.resetLearning("game_start")
|
||||
bot.leadGain.resetLearning("game_start")
|
||||
bot.bbn.resetLearning("game_start")
|
||||
# minNumberOfParticipants == maxNumberOfParticipants for fixed battles; self is -1
|
||||
bot.initialEnemyCount = e.gameSetup.minNumberOfParticipants - 1
|
||||
# New battle: reset the cumulative outcome counters and remember how many
|
||||
@@ -1133,6 +1147,7 @@ method run*(bot: ModularBot) =
|
||||
# TR_TMHORIZON_RESET_ON_TARGET, default on; no-op in 1v1).
|
||||
discard bot.tmHorizon.targetChanged(candidateId)
|
||||
discard bot.leadGain.targetChanged(candidateId)
|
||||
discard bot.bbn.targetChanged(candidateId)
|
||||
if bot.currentTargetId >= 0:
|
||||
bot.lastKnownTargetId = bot.currentTargetId
|
||||
|
||||
@@ -1277,6 +1292,7 @@ method run*(bot: ModularBot) =
|
||||
var tmpPreds: array[len(PowerBins), GunPrediction]
|
||||
var tmhPreds: array[len(PowerBins), GunPrediction]
|
||||
var lgPreds: array[len(PowerBins), GunPrediction]
|
||||
var bbnPreds: 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
|
||||
@@ -1288,12 +1304,13 @@ method run*(bot: ModularBot) =
|
||||
# owning gun's onResult, so attribution survives a mid-round rack change.
|
||||
# TMPATTERN and TMHORIZON keep their own admission gate even when the knob
|
||||
# is 0, so TR_VBULLET_ADMIT_ONLY=0 reproduces the exact pre-change rack.
|
||||
var admit: array[17, bool]
|
||||
for gi in 0..<17:
|
||||
var admit: array[18, bool]
|
||||
for gi in 0..<18:
|
||||
admit[gi] = ModuleVBullets and
|
||||
vBulletAdmitted(gi, bot.rackMode, ActiveRackMembership,
|
||||
VBulletAdmitOnly or gi == TmPatternId or
|
||||
gi == TmHorizonId or gi == LeadGainId)
|
||||
gi == TmHorizonId or gi == LeadGainId or
|
||||
gi == BitbrainNetId)
|
||||
for i in 0..<len(PowerBins):
|
||||
if admit[0]: headsUp[i] = bot.headOn.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||
if admit[1]: linPreds[i] = bot.linear.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||
@@ -1319,6 +1336,9 @@ method run*(bot: ModularBot) =
|
||||
# LEADGAIN: Pattern base, lead scaled by a per-band learned gain.
|
||||
if admit[LeadGainId]:
|
||||
lgPreds[i] = bot.leadGain.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||
# BITBRAIN: Pattern base + a fine-grained ADE/SBC angular correction.
|
||||
if admit[BitbrainNetId]:
|
||||
bbnPreds[i] = bot.bbn.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||
|
||||
if admit[0]: bot.tracker.spawnBullets(0, headsUp, bot.lastState, tid)
|
||||
if admit[1] and not gunDisabled(1): bot.tracker.spawnBullets(1, linPreds, bot.lastState, tid)
|
||||
@@ -1342,6 +1362,8 @@ method run*(bot: ModularBot) =
|
||||
bot.tracker.spawnBullets(TmHorizonId, tmhPreds, bot.lastState, tid)
|
||||
if admit[LeadGainId] and not gunDisabled(LeadGainId):
|
||||
bot.tracker.spawnBullets(LeadGainId, lgPreds, bot.lastState, tid)
|
||||
if admit[BitbrainNetId] and not gunDisabled(BitbrainNetId):
|
||||
bot.tracker.spawnBullets(BitbrainNetId, bbnPreds, bot.lastState, tid)
|
||||
|
||||
# Build slim enemy table for tickBullets
|
||||
var enemyPositions: Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]]
|
||||
@@ -1378,6 +1400,7 @@ method run*(bot: ModularBot) =
|
||||
of TmPatternId: bot.tmPattern.onResult(fe)
|
||||
of TmHorizonId: bot.tmHorizon.onResult(fe)
|
||||
of LeadGainId: bot.leadGain.onResult(fe)
|
||||
of BitbrainNetId: bot.bbn.onResult(fe)
|
||||
else: discard
|
||||
if fe.hit: inc bot.virtualHits else: inc bot.virtualMiss
|
||||
if DebugDrawOn and VBulletDebugOn:
|
||||
@@ -1433,6 +1456,7 @@ method run*(bot: ModularBot) =
|
||||
of TmPatternId: bot.tmPattern.predict(bot.lastState, bulletSpeed(power))
|
||||
of TmHorizonId: bot.tmHorizon.predict(bot.lastState, bulletSpeed(power))
|
||||
of LeadGainId: bot.leadGain.predict(bot.lastState, bulletSpeed(power))
|
||||
of BitbrainNetId: bot.bbn.predict(bot.lastState, bulletSpeed(power))
|
||||
else: bot.headOn.predict(bot.lastState, bulletSpeed(power))
|
||||
let aimTarget = aimAngle(getX(), getY(), pred.x, pred.y)
|
||||
|
||||
@@ -1500,7 +1524,7 @@ proc seedSelectorRng() =
|
||||
|
||||
when isMainModule:
|
||||
var bot = ModularBot(
|
||||
tracker: vb.initTracker(17), # 0: HeadOn, 1: Linear, 2: Tsetlin, 3: Circular, 4: GuessFactor, 5: Pattern, 6: WallBounce, 7: Accel, 8: StopShot, 9: Displace, 10: AvgLead, 11: DecayGF, 12: KNN, 13: TMSelect, 14: TMPattern, 15: TMHorizon, 16: LeadGain
|
||||
tracker: vb.initTracker(18), # 0: HeadOn, 1: Linear, 2: Tsetlin, 3: Circular, 4: GuessFactor, 5: Pattern, 6: WallBounce, 7: Accel, 8: StopShot, 9: Displace, 10: AvgLead, 11: DecayGF, 12: KNN, 13: TMSelect, 14: TMPattern, 15: TMHorizon, 16: LeadGain, 17: BitBrainNet
|
||||
headOn: HeadOnGun(),
|
||||
linear: LinearGun(),
|
||||
circular: CircularGun(),
|
||||
@@ -1518,6 +1542,7 @@ when isMainModule:
|
||||
tmPattern: initTmRadialGun(),
|
||||
tmHorizon: initTmHorizonGun(),
|
||||
leadGain: initLeadGainGun(),
|
||||
bbn: initBitbrainNetGun(),
|
||||
radar: RadarLockModule(),
|
||||
meleeRadar: initAdaptiveMeleeRadar(),
|
||||
mover: TFILModule(debugGraphics: DebugDrawOn),
|
||||
@@ -1557,5 +1582,6 @@ when isMainModule:
|
||||
tmHorizon: bot.tmHorizon,
|
||||
patternMatcher: bot.patternMatcher,
|
||||
leadGain: bot.leadGain,
|
||||
bitbrainNet: bot.bbn,
|
||||
))
|
||||
start(bot, botJsonPath)
|
||||
|
||||
@@ -32,6 +32,7 @@ import movements/wave_surfer
|
||||
import movements/learned_surfer
|
||||
import guns/tm_horizon
|
||||
import guns/lead_gain
|
||||
import guns/bitbrain_net
|
||||
import guns/pattern_matcher
|
||||
import vbullet_draw
|
||||
import geo_overlay
|
||||
@@ -65,6 +66,7 @@ type
|
||||
disabledGuns*: HashSet[int]
|
||||
tmHorizon*: TmHorizonGun
|
||||
leadGain*: LeadGainGun
|
||||
bitbrainNet*: BitbrainNetGun
|
||||
patternMatcher*: PatternMatcherGun
|
||||
|
||||
# ── helpers ──────────────────────────────────────────────────────────────────
|
||||
@@ -139,6 +141,9 @@ proc sourceOfLgPresence(name: string): string =
|
||||
else:
|
||||
sourceOfPresence(name)
|
||||
|
||||
proc sbcModeName(m: SbcMode): string {.inline.} =
|
||||
(if m == smCounted: "counted" else: "bitset")
|
||||
|
||||
proc disabledGunsString(guns: HashSet[int]): string =
|
||||
if guns.len == 0: return "none"
|
||||
var ids: seq[int]
|
||||
@@ -428,6 +433,29 @@ proc printEffectiveValues(ctx: EnvReportContext) =
|
||||
if dep.len > 0: emit("legacy TR_BITBRAIN_* aliases honoured", "yes", "legacy")
|
||||
else: emit("legacy TR_BITBRAIN_* aliases honoured", "no", "default")
|
||||
|
||||
# ── the ADE+SBC gun (rack id 17) ─────────────────────────────────────────
|
||||
# Two switches admit it, both default off: `TR_RACK_BITBRAIN` (rack table) and
|
||||
# `TR_BITBRAIN_NET` (the gun's own master switch, which is also the switch
|
||||
# that disowns the legacy `TR_BITBRAIN_*` aliases above).
|
||||
let bbn = ctx.bitbrainNet
|
||||
emit("TR_BITBRAIN_NET", onOff(bbn.enabled), sourceOfPresence(BBN_NET_ENV))
|
||||
emit("TR_BITBRAIN_INPUT", $bbn.inputWidth, sourceOf(BBN_INPUT_ENV))
|
||||
emit("TR_BITBRAIN_FEATURES", bbn.features, sourceOf(BBN_FEATURES_ENV))
|
||||
emit("TR_BITBRAIN_NCLASSES", $bbn.nClasses, sourceOf(BBN_CLASSES_ENV))
|
||||
emit("TR_BITBRAIN_NADES", $bbn.nAde, sourceOf(BBN_NADES_ENV))
|
||||
emit("TR_BITBRAIN_WIDTHS", bbn.widths.join(","), sourceOf(BBN_WIDTHS_ENV))
|
||||
emit("TR_BITBRAIN_SPAN", $bbn.maxDeg, sourceOf(BBN_SPAN_ENV))
|
||||
emit("TR_BITBRAIN_MODE", sbcModeName(bbn.mode), sourceOf(BBN_MODE_ENV))
|
||||
emit("TR_BITBRAIN_DECAY_EVERY", $bbn.decayEvery, sourceOf(BBN_DECAY_EVERY_ENV))
|
||||
emit("TR_BITBRAIN_DECAY_SHIFT", $bbn.decayShift, sourceOf(BBN_DECAY_SHIFT_ENV))
|
||||
emit("TR_BITBRAIN_MINOBS", $bbn.minObs, sourceOf(BBN_MINOBS_ENV))
|
||||
emit("TR_BITBRAIN_ADAPT_EVERY", $bbn.adaptEvery, sourceOf(BBN_ADAPT_ENV))
|
||||
emit("TR_BITBRAIN_TARGET", $bbn.targetRate, sourceOf(BBN_TARGET_ENV))
|
||||
emit("TR_BITBRAIN_NETSEED", $bbn.seed, sourceOf(BBN_SEED_ENV))
|
||||
emit("TR_BITBRAIN_NETLOG", onOff(bbn.logEnabled),
|
||||
sourceOfPresence(BBN_LOG_ENV))
|
||||
emit("bitbrain_net.ram", $bbn.networkBytes, "default") ## 0 until the lazy build
|
||||
|
||||
# ── Pattern match-shape + radial knobs ────────────────────────────────────
|
||||
# These are resolved lazily inside `predict` (which has not run at boot), so
|
||||
# unless something already forced them we report the raw env value and say so.
|
||||
@@ -600,14 +628,16 @@ proc knownEnvNames*(): seq[string] =
|
||||
"TR_BITBRAIN_RANGE", "TR_BITBRAIN_WARMUP", "TR_BITBRAIN_ADAPT",
|
||||
"TR_BITBRAIN_CALIB", "TR_BITBRAIN_SEED",
|
||||
# common_libs/bitbrain (counted-SBC library) reads these inline; job j102
|
||||
"TR_BITBRAIN_MODE", "TR_BITBRAIN_DECAY_EVERY", "TR_BITBRAIN_DECAY_SHIFT",
|
||||
]
|
||||
# the ADE+SBC gun (rack id 17) owns the rest of the TR_BITBRAIN_* namespace
|
||||
for n in BitbrainNetEnvNames: result.add n
|
||||
# rack names are constructed from the prefix + gun table, not spelled out
|
||||
for g in RackGunNames:
|
||||
result.add RackEnvPrefix & g
|
||||
# the LEGACY rack name of the renamed corrector (still honoured while
|
||||
# LG_NET_SWITCH_ENV is off — see gun_harness/selector's RackLegacyAlias)
|
||||
for (key, _) in RackLegacyAlias: result.add key
|
||||
# NOTE: the legacy rack alias `TR_RACK_BITBRAIN` needs no extra entry — it is
|
||||
# the CURRENT name of rack id 17, so the RackGunNames loop above already
|
||||
# registers it. See gun_harness/selector's RackLegacyAlias for how it is
|
||||
# routed to LEADGAIN while LG_NET_SWITCH_ENV is off.
|
||||
# inline reads with no exported constant (the guard test scans for these)
|
||||
for n in [
|
||||
"GUN_SELECTOR_WINDOW", "GUN_SELECTOR_MINOBS", "GUN_SELECTOR_TIE",
|
||||
|
||||
Reference in New Issue
Block a user