j140 rename the lead-gain corrector: BitBrain -> LEADGAIN (+ legacy TR_BITBRAIN_* aliases)
The gun at rack id 16 learned a multiplier for Pattern's lead, separately
per range band. It was called BITBRAIN and shipped a TR_BITBRAIN_* prefix,
which is why the name read as a neural network it no longer contains.
guns/bitbrain_gun.nim -> guns/lead_gain.nim (rack id 16 UNCHANGED)
RackGunNames[16] BITBRAIN -> LEADGAIN
TR_BITBRAIN_* knobs -> TR_LEADGAIN_*
[bb] log line -> [lg]
BACKWARD COMPATIBILITY is mandatory: the live .env carries
TR_RACK_BITBRAIN=both, TR_BITBRAIN_GAINS, TR_BITBRAIN_MEM=decay and
TR_BITBRAIN_LOG=1, and those must keep behaving identically. The new ADE+SBC
gun (next commit) claims the BITBRAIN name and the TR_BITBRAIN_* prefix, so
the namespace is disambiguated by ONE deterministic switch, TR_BITBRAIN_NET
(default 0):
TR_BITBRAIN_NET unset/0 -> LEGACY: the 14 frozen legacy suffixes are aliases
for TR_LEADGAIN_*, and TR_RACK_BITBRAIN still
selects rack id 16. One [depr] line on stderr
names the new spelling of each honoured knob.
TR_BITBRAIN_NET = 1 -> the TR_BITBRAIN_* names belong to the new gun.
The legacy suffix set and the new gun's knob set are DISJOINT, so no name is
ever claimed twice; the new name always wins over its alias.
Parity: shipped rack is still onlyPattern, shipped movement is still strafe.
Guards unchanged: test_env_report 25, test_rack_membership 48,
test_tm_pattern_registration 20, test_lead_gain_registration 13 (was
test_bitbrain_registration), test_bitbrain 56, test_gun_harness 39,
test_tfil_commit_env 30. New: test_lead_gain_legacy 24.
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), BitBrainGun (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) via GunHarness.
|
||||
## Radar: RadarLockModule (1v1) / AdaptiveMeleeRadarModule (2+ enemies), auto-switched per tick.
|
||||
## Movement: OscillatorModule (perpendicular strafing).
|
||||
|
||||
@@ -30,7 +30,7 @@ import guns/knn_gun
|
||||
import guns/tm_selector
|
||||
import guns/tm_pattern
|
||||
import guns/tm_horizon
|
||||
import guns/bitbrain_gun
|
||||
import guns/lead_gain
|
||||
import movements/phantom_meteor
|
||||
import movements/rammer
|
||||
import movements/ram_decision
|
||||
@@ -150,7 +150,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", "BitBrain"]
|
||||
const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor", "Pattern", "WallBounce", "Accel", "StopShot", "Displace", "AvgLead", "DecayGF", "KNN", "TMSelect", "TMPattern", "TMHorizon", "LeadGain"]
|
||||
|
||||
## 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
|
||||
@@ -165,11 +165,14 @@ const TmPatternId = 14
|
||||
## head — hence every other gun's learning order — is byte-for-byte unchanged.
|
||||
const TmHorizonId = 15
|
||||
|
||||
## Rack id of the BitBrain ADE+SBC fine-grained aim corrector. It defaults to
|
||||
## `TR_RACK_BITBRAIN=off` and its virtual-bullet spawn is gated on rack
|
||||
## admission, so the shipped default never spawns it and the shared tracker ring
|
||||
## head — hence every other gun's learning order — is byte-for-byte unchanged.
|
||||
const BitBrainId = 16
|
||||
## Rack id of the LEADGAIN per-range-band lead-gain corrector (it used to be
|
||||
## called BitBrain, before the ADE+SBC network was removed from it; the rack id
|
||||
## is unchanged). It defaults to `TR_RACK_LEADGAIN=off` and its virtual-bullet
|
||||
## spawn is gated on rack admission, so the shipped default never spawns it and
|
||||
## the shared tracker ring head — hence every other gun's learning order — is
|
||||
## byte-for-byte unchanged. `TR_RACK_BITBRAIN` still selects it while
|
||||
## `TR_BITBRAIN_NET` is off (backward compatibility; see `gun_harness/selector`).
|
||||
const LeadGainId = 16
|
||||
|
||||
## ── virtual-bullet debug overlay (TR_VBULLET_DEBUG*) ───────────────────────
|
||||
## OFF by default: the shipped bot draws nothing. When on, the tracker's
|
||||
@@ -242,7 +245,7 @@ type
|
||||
tmSelector: TmSelectorGun
|
||||
tmPattern: TmPatternGun
|
||||
tmHorizon: TmHorizonGun
|
||||
bitbrain: BitBrainGun
|
||||
leadGain: LeadGainGun
|
||||
mover: TFILModule
|
||||
ringMover: TFILRingModule
|
||||
strafeMover: StrafeModule
|
||||
@@ -937,11 +940,11 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
|
||||
bot.tmHorizon.resetRoundState()
|
||||
if e.roundNumber <= 1:
|
||||
bot.tmHorizon.resetLearning("round1")
|
||||
# BitBrain: per-round wipe always clears observations/labels; `perRound` mode
|
||||
# LEADGAIN: per-round wipe always clears observations/labels; `perRound` mode
|
||||
# also wipes its SBCs (the gate test's winning regime).
|
||||
bot.bitbrain.resetRoundState()
|
||||
bot.leadGain.resetRoundState()
|
||||
if e.roundNumber <= 1:
|
||||
bot.bitbrain.resetLearning("round1")
|
||||
bot.leadGain.resetLearning("round1")
|
||||
bot.isRamming = false
|
||||
bot.ramDurationTicks = 0
|
||||
bot.ramCooldownTicks = 0
|
||||
@@ -1021,7 +1024,7 @@ method onGameStarted*(bot: ModularBot, e: GameStartedEventForBot) =
|
||||
# state, so this is the only place the learning is dropped at a battle
|
||||
# boundary. The round-1 fallback in `onRoundStarted` covers a missed callback.
|
||||
bot.tmHorizon.resetLearning("game_start")
|
||||
bot.bitbrain.resetLearning("game_start")
|
||||
bot.leadGain.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
|
||||
@@ -1129,7 +1132,7 @@ method run*(bot: ModularBot) =
|
||||
# target changes to a different bot id (gated by
|
||||
# TR_TMHORIZON_RESET_ON_TARGET, default on; no-op in 1v1).
|
||||
discard bot.tmHorizon.targetChanged(candidateId)
|
||||
discard bot.bitbrain.targetChanged(candidateId)
|
||||
discard bot.leadGain.targetChanged(candidateId)
|
||||
if bot.currentTargetId >= 0:
|
||||
bot.lastKnownTargetId = bot.currentTargetId
|
||||
|
||||
@@ -1273,7 +1276,7 @@ method run*(bot: ModularBot) =
|
||||
var tmselPreds: array[len(PowerBins), GunPrediction]
|
||||
var tmpPreds: array[len(PowerBins), GunPrediction]
|
||||
var tmhPreds: array[len(PowerBins), GunPrediction]
|
||||
var bbPreds: array[len(PowerBins), GunPrediction]
|
||||
var lgPreds: 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
|
||||
@@ -1290,7 +1293,7 @@ method run*(bot: ModularBot) =
|
||||
admit[gi] = ModuleVBullets and
|
||||
vBulletAdmitted(gi, bot.rackMode, ActiveRackMembership,
|
||||
VBulletAdmitOnly or gi == TmPatternId or
|
||||
gi == TmHorizonId or gi == BitBrainId)
|
||||
gi == TmHorizonId or gi == LeadGainId)
|
||||
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]))
|
||||
@@ -1313,9 +1316,9 @@ method run*(bot: ModularBot) =
|
||||
# TMHORIZON likewise: Pattern base + horizon TM correction.
|
||||
if admit[TmHorizonId]:
|
||||
tmhPreds[i] = bot.tmHorizon.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||
# BITBRAIN: Pattern base + ADE/SBC fine-grained angular correction.
|
||||
if admit[BitBrainId]:
|
||||
bbPreds[i] = bot.bitbrain.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||
# LEADGAIN: Pattern base, lead scaled by a per-band learned gain.
|
||||
if admit[LeadGainId]:
|
||||
lgPreds[i] = bot.leadGain.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)
|
||||
@@ -1337,8 +1340,8 @@ method run*(bot: ModularBot) =
|
||||
bot.tracker.spawnBullets(TmPatternId, tmpPreds, bot.lastState, tid)
|
||||
if admit[TmHorizonId] and not gunDisabled(TmHorizonId):
|
||||
bot.tracker.spawnBullets(TmHorizonId, tmhPreds, bot.lastState, tid)
|
||||
if admit[BitBrainId] and not gunDisabled(BitBrainId):
|
||||
bot.tracker.spawnBullets(BitBrainId, bbPreds, bot.lastState, tid)
|
||||
if admit[LeadGainId] and not gunDisabled(LeadGainId):
|
||||
bot.tracker.spawnBullets(LeadGainId, lgPreds, bot.lastState, tid)
|
||||
|
||||
# Build slim enemy table for tickBullets
|
||||
var enemyPositions: Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]]
|
||||
@@ -1374,7 +1377,7 @@ method run*(bot: ModularBot) =
|
||||
of 13: bot.tmSelector.onResult(fe)
|
||||
of TmPatternId: bot.tmPattern.onResult(fe)
|
||||
of TmHorizonId: bot.tmHorizon.onResult(fe)
|
||||
of BitBrainId: bot.bitbrain.onResult(fe)
|
||||
of LeadGainId: bot.leadGain.onResult(fe)
|
||||
else: discard
|
||||
if fe.hit: inc bot.virtualHits else: inc bot.virtualMiss
|
||||
if DebugDrawOn and VBulletDebugOn:
|
||||
@@ -1429,7 +1432,7 @@ method run*(bot: ModularBot) =
|
||||
of 13: bot.tmSelector.predict(bot.lastState, bulletSpeed(power))
|
||||
of TmPatternId: bot.tmPattern.predict(bot.lastState, bulletSpeed(power))
|
||||
of TmHorizonId: bot.tmHorizon.predict(bot.lastState, bulletSpeed(power))
|
||||
of BitBrainId: bot.bitbrain.predict(bot.lastState, bulletSpeed(power))
|
||||
of LeadGainId: bot.leadGain.predict(bot.lastState, bulletSpeed(power))
|
||||
else: bot.headOn.predict(bot.lastState, bulletSpeed(power))
|
||||
let aimTarget = aimAngle(getX(), getY(), pred.x, pred.y)
|
||||
|
||||
@@ -1497,7 +1500,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: BitBrain
|
||||
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
|
||||
headOn: HeadOnGun(),
|
||||
linear: LinearGun(),
|
||||
circular: CircularGun(),
|
||||
@@ -1514,7 +1517,7 @@ when isMainModule:
|
||||
tmSelector: initTmSelectorGun(),
|
||||
tmPattern: initTmRadialGun(),
|
||||
tmHorizon: initTmHorizonGun(),
|
||||
bitbrain: initBitBrainGun(),
|
||||
leadGain: initLeadGainGun(),
|
||||
radar: RadarLockModule(),
|
||||
meleeRadar: initAdaptiveMeleeRadar(),
|
||||
mover: TFILModule(debugGraphics: DebugDrawOn),
|
||||
@@ -1553,6 +1556,6 @@ when isMainModule:
|
||||
disabledGuns: DisabledGuns,
|
||||
tmHorizon: bot.tmHorizon,
|
||||
patternMatcher: bot.patternMatcher,
|
||||
bitbrain: bot.bitbrain,
|
||||
leadGain: bot.leadGain,
|
||||
))
|
||||
start(bot, botJsonPath)
|
||||
|
||||
@@ -31,7 +31,7 @@ import movements/strafe
|
||||
import movements/wave_surfer
|
||||
import movements/learned_surfer
|
||||
import guns/tm_horizon
|
||||
import guns/bitbrain_gun
|
||||
import guns/lead_gain
|
||||
import guns/pattern_matcher
|
||||
import vbullet_draw
|
||||
import geo_overlay
|
||||
@@ -64,7 +64,7 @@ type
|
||||
shotLogPath*: string
|
||||
disabledGuns*: HashSet[int]
|
||||
tmHorizon*: TmHorizonGun
|
||||
bitbrain*: BitBrainGun
|
||||
leadGain*: LeadGainGun
|
||||
patternMatcher*: PatternMatcherGun
|
||||
|
||||
# ── helpers ──────────────────────────────────────────────────────────────────
|
||||
@@ -113,6 +113,32 @@ proc sourceOfPresence(name: string): string =
|
||||
elif existsEnv(name): "env"
|
||||
else: "default"
|
||||
|
||||
proc legacyNameFor(name: string): string =
|
||||
## The legacy `TR_BITBRAIN_<X>` alias for a `TR_LEADGAIN_<X>` knob, or "" when
|
||||
## the knob has no legacy name / the namespace now belongs to the new gun.
|
||||
if netSwitchOwnsBitbrainName(): return ""
|
||||
if not name.startsWith(NewPrefix): return ""
|
||||
let suffix = name[NewPrefix.len .. ^1]
|
||||
for s in LegacyKnobEnvNames:
|
||||
if s == suffix: return LegacyPrefix & suffix
|
||||
""
|
||||
|
||||
proc sourceOfLg(name: string): string =
|
||||
## `sourceOf`, plus credit for the legacy `TR_BITBRAIN_*` alias. Tagged
|
||||
## `legacy` so a migrated name is distinguishable from an unmigrated one.
|
||||
let legacy = legacyNameFor(name)
|
||||
if legacy.len > 0 and getEnv(name, "").len == 0 and getEnv(legacy, "").len > 0:
|
||||
if isFromEnvFile(legacy): "legacy/.env" else: "legacy"
|
||||
else:
|
||||
sourceOf(name)
|
||||
|
||||
proc sourceOfLgPresence(name: string): string =
|
||||
let legacy = legacyNameFor(name)
|
||||
if legacy.len > 0 and not existsEnv(name) and existsEnv(legacy):
|
||||
if isFromEnvFile(legacy): "legacy/.env" else: "legacy"
|
||||
else:
|
||||
sourceOfPresence(name)
|
||||
|
||||
proc disabledGunsString(guns: HashSet[int]): string =
|
||||
if guns.len == 0: return "none"
|
||||
var ids: seq[int]
|
||||
@@ -373,29 +399,34 @@ proc printEffectiveValues(ctx: EnvReportContext) =
|
||||
sourceOf("TR_TMHORIZON_RETRAIN_EVERY"))
|
||||
emit("TR_TMHORIZON_EPOCHS", $tmh.retrainEpochs, sourceOf("TR_TMHORIZON_EPOCHS"))
|
||||
|
||||
# ── the BitBrain ADE+SBC corrector ────────────────────────────────────────
|
||||
# All knobs are resolved at gun construction (`initBitBrainGun`), so the
|
||||
# context holds the real values. The network itself is only built on the first
|
||||
# `predict`, which is why `[rack]` membership is the actual enable switch.
|
||||
emit("TR_BITBRAIN_MEM", memModeName(ctx.bitbrain.memMode),
|
||||
sourceOf(BB_MEM_ENV))
|
||||
emit("TR_BITBRAIN_GAINS", bbGainsString(ctx.bitbrain.cands),
|
||||
sourceOf(BB_GAINS_ENV))
|
||||
emit("TR_BITBRAIN_N", $ctx.bitbrain.nClasses, sourceOf(BB_N_ENV))
|
||||
emit("TR_BITBRAIN_NADE", $ctx.bitbrain.nAde, sourceOf(BB_NADE_ENV))
|
||||
emit("TR_BITBRAIN_RANGE", $ctx.bitbrain.maxDeg, sourceOf(BB_RANGE_ENV))
|
||||
emit("TR_BITBRAIN_LOG", onOff(ctx.bitbrain.logEnabled),
|
||||
sourceOfPresence(BB_LOG_ENV))
|
||||
emit("TR_BITBRAIN_MIN_OBS", $ctx.bitbrain.minObs, sourceOf(BB_MIN_OBS_ENV))
|
||||
emit("TR_BITBRAIN_WARMUP", $ctx.bitbrain.warmupN, sourceOf(BB_WARMUP_ENV))
|
||||
emit("TR_BITBRAIN_ADAPT", $ctx.bitbrain.adaptEvery, sourceOf(BB_ADAPT_ENV))
|
||||
emit("TR_BITBRAIN_CALIB", $ctx.bitbrain.calibEvery, sourceOf(BB_CALIB_ENV))
|
||||
emit("TR_BITBRAIN_DECAY", $ctx.bitbrain.decayEvery, sourceOf(BB_DECAY_ENV))
|
||||
emit("TR_BITBRAIN_DECAY_FRAC", $ctx.bitbrain.decayFrac,
|
||||
sourceOf(BB_DECAY_FRAC_ENV))
|
||||
emit("TR_BITBRAIN_SEED", $ctx.bitbrain.seed, sourceOf(BB_SEED_ENV))
|
||||
emit("TR_BITBRAIN_RESET_ON_TARGET", onOff(ctx.bitbrain.resetOnTarget),
|
||||
sourceOf(BB_RESET_ON_TARGET_ENV))
|
||||
# ── the LEADGAIN per-range-band lead-gain corrector ───────────────────────
|
||||
# All knobs are resolved at gun construction (`initLeadGainGun`), so the
|
||||
# context holds the real values. `sourceOfLg`/`sourceOfLgPresence` also
|
||||
# credit the LEGACY `TR_BITBRAIN_*` alias, so a pre-rename `.env` shows up as
|
||||
# coming from the env (tagged `legacy`) instead of silently reading default.
|
||||
let lg = ctx.leadGain
|
||||
emit("TR_LEADGAIN_MEM", memModeName(lg.memMode), sourceOfLg(LG_MEM_ENV))
|
||||
emit("TR_LEADGAIN_GAINS", lgGainsString(lg.cands), sourceOfLg(LG_GAINS_ENV))
|
||||
emit("TR_LEADGAIN_N", $lg.nClasses, sourceOfLg(LG_N_ENV))
|
||||
emit("TR_LEADGAIN_NADE", $lg.nAde, sourceOfLg(LG_NADE_ENV))
|
||||
emit("TR_LEADGAIN_RANGE", $lg.maxDeg, sourceOfLg(LG_RANGE_ENV))
|
||||
emit("TR_LEADGAIN_LOG", onOff(lg.logEnabled), sourceOfLgPresence(LG_LOG_ENV))
|
||||
emit("TR_LEADGAIN_MIN_OBS", $lg.minObs, sourceOfLg(LG_MIN_OBS_ENV))
|
||||
emit("TR_LEADGAIN_WARMUP", $lg.warmupN, sourceOfLg(LG_WARMUP_ENV))
|
||||
emit("TR_LEADGAIN_ADAPT", $lg.adaptEvery, sourceOfLg(LG_ADAPT_ENV))
|
||||
emit("TR_LEADGAIN_CALIB", $lg.calibEvery, sourceOfLg(LG_CALIB_ENV))
|
||||
emit("TR_LEADGAIN_DECAY", $lg.decayEvery, sourceOfLg(LG_DECAY_ENV))
|
||||
emit("TR_LEADGAIN_DECAY_FRAC", $lg.decayFrac, sourceOfLg(LG_DECAY_FRAC_ENV))
|
||||
emit("TR_LEADGAIN_SEED", $lg.seed, sourceOfLg(LG_SEED_ENV))
|
||||
emit("TR_LEADGAIN_RESET_ON_TARGET", onOff(lg.resetOnTarget),
|
||||
sourceOfLg(LG_RESET_ON_TARGET_ENV))
|
||||
# The disambiguation switch + the deprecation line it gates, in the report so
|
||||
# an owner staring at a `[depr]` line can see exactly why it fired.
|
||||
emit("TR_BITBRAIN_NET", onOff(netSwitchOwnsBitbrainName()),
|
||||
sourceOfPresence("TR_BITBRAIN_NET"))
|
||||
let dep = lgDeprecationLine()
|
||||
if dep.len > 0: emit("legacy TR_BITBRAIN_* aliases honoured", "yes", "legacy")
|
||||
else: emit("legacy TR_BITBRAIN_* aliases honoured", "no", "default")
|
||||
|
||||
# ── Pattern match-shape + radial knobs ────────────────────────────────────
|
||||
# These are resolved lazily inside `predict` (which has not run at boot), so
|
||||
@@ -451,9 +482,14 @@ proc printModules(ctx: EnvReportContext) =
|
||||
# Guns. Rack membership IS the on/off switch: `off` = disabled, any other
|
||||
# mode = enabled in that rack.
|
||||
for i in 0..<len(RackGunNames):
|
||||
var src = sourceOf(RackEnvPrefix & RackGunNames[i])
|
||||
if src == "default" and not netSwitchOwnsBitbrainName():
|
||||
for (key, gid) in RackLegacyAlias:
|
||||
if gid == i and getEnv(key, "").len > 0:
|
||||
src = if isFromEnvFile(key): "legacy/.env" else: "legacy"
|
||||
emitModule("gun." & RackGunNames[i].toLowerAscii(),
|
||||
ActiveRackMembership[i] != rmOff,
|
||||
sourceOf(RackEnvPrefix & RackGunNames[i]),
|
||||
src,
|
||||
"rack: " & rackMembershipName(ActiveRackMembership[i]))
|
||||
|
||||
# Movement engines + the engine the tick actually runs.
|
||||
@@ -552,15 +588,26 @@ proc knownEnvNames*(): seq[string] =
|
||||
TMH_SHIFT_ENV, TMH_BIG_MULT_ENV, TMH_LOG_ENV, TMH_RESET_ON_TARGET_ENV,
|
||||
TMH_WINDOW_ENV, TMH_RESET_DROP_ENV, TMH_NSTATES_ENV, TMH_ACCURVE_ENV,
|
||||
TMH_RETRAIN_EVERY_ENV, TMH_EPOCHS_ENV,
|
||||
BB_MEM_ENV, BB_GAINS_ENV, BB_N_ENV, BB_NADE_ENV, BB_RANGE_ENV, BB_LOG_ENV,
|
||||
BB_MIN_OBS_ENV, BB_WARMUP_ENV, BB_ADAPT_ENV, BB_CALIB_ENV, BB_DECAY_ENV,
|
||||
BB_DECAY_FRAC_ENV, BB_SEED_ENV, BB_RESET_ON_TARGET_ENV,
|
||||
LG_MEM_ENV, LG_GAINS_ENV, LG_N_ENV, LG_NADE_ENV, LG_RANGE_ENV, LG_LOG_ENV,
|
||||
LG_MIN_OBS_ENV, LG_WARMUP_ENV, LG_ADAPT_ENV, LG_CALIB_ENV, LG_DECAY_ENV,
|
||||
LG_DECAY_FRAC_ENV, LG_SEED_ENV, LG_RESET_ON_TARGET_ENV, LG_NET_SWITCH_ENV,
|
||||
# LEGACY aliases of the corrector knobs above (honoured while
|
||||
# LG_NET_SWITCH_ENV is off). Registered so the tree-scan guard does not
|
||||
# flag the owner's pre-rename .env as unknown.
|
||||
"TR_BITBRAIN_GAINS", "TR_BITBRAIN_MEM", "TR_BITBRAIN_MIN_OBS",
|
||||
"TR_BITBRAIN_DECAY", "TR_BITBRAIN_DECAY_FRAC", "TR_BITBRAIN_LOG",
|
||||
"TR_BITBRAIN_RESET_ON_TARGET", "TR_BITBRAIN_N", "TR_BITBRAIN_NADE",
|
||||
"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",
|
||||
]
|
||||
# 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
|
||||
# inline reads with no exported constant (the guard test scans for these)
|
||||
for n in [
|
||||
"GUN_SELECTOR_WINDOW", "GUN_SELECTOR_MINOBS", "GUN_SELECTOR_TIE",
|
||||
|
||||
@@ -47,8 +47,14 @@ const
|
||||
RackGunNames*: array[17, string] = [
|
||||
"HEADON", "LINEAR", "TSETLIN", "CIRCULAR", "GUESSFACTOR", "PATTERN",
|
||||
"WALLBOUNCE", "ACCEL", "STOPSHOT", "DISPLACE", "AVGLEAD", "DECAYGF",
|
||||
"KNN", "TMSELECT", "TMPATTERN", "TMHORIZON", "BITBRAIN"]
|
||||
"KNN", "TMSELECT", "TMPATTERN", "TMHORIZON", "LEADGAIN"]
|
||||
RackEnvPrefix* = "TR_RACK_"
|
||||
## LEADGAIN (id 16) is the per-range-band lead-gain corrector
|
||||
## (`guns/lead_gain.nim`). It USED to be called BITBRAIN; the name now
|
||||
## describes its internals (it learns a multiplier for Pattern's lead per
|
||||
## range band). Its rack id is UNCHANGED so every test that asserts the id
|
||||
## literals still holds. The `BITBRAIN` name and the `TR_BITBRAIN_*` prefix
|
||||
## now belong to the ADE+SBC gun `guns/bitbrain_net.nim` (rack id 17).
|
||||
## SHIPPED DEFAULT: `onlyPattern`. Pattern (id 5) is admitted in both racks;
|
||||
## every other gun is `off`. The selection mechanism is untouched and remains
|
||||
## fully functional — only the rack's membership changed. Re-enable any gun
|
||||
@@ -74,11 +80,36 @@ const
|
||||
rmOff, # 13 TMSELECT — off
|
||||
rmOff, # 14 TMPATTERN — off (already shipped off; TM pattern gun)
|
||||
rmOff, # 15 TMHORIZON — off (horizon-based TM corrector; expected to lose)
|
||||
rmOff] # 16 BITBRAIN — off (fine-grained ADE+SBC corrector)
|
||||
rmOff] # 16 LEADGAIN — off (per-range-band lead-gain corrector)
|
||||
## NOTE: the table is registered in the SAME commit as the gun id (16) and the
|
||||
## live wiring, so `TR_RACK_BITBRAIN=both` is the ONLY thing that admits it and
|
||||
## live wiring, so `TR_RACK_LEADGAIN=both` is the ONLY thing that admits it and
|
||||
## an unset environment is byte-for-byte the shipped Pattern-only rack.
|
||||
|
||||
const
|
||||
## ── BACKWARD COMPATIBILITY: the legacy rack knob names ────────────────────
|
||||
## `TR_RACK_<gun name>` is derived from `RackGunNames`, so renaming a gun
|
||||
## silently retires its old switch. These entries keep an old switch alive:
|
||||
## each maps a legacy `TR_RACK_*` name onto the gun id it used to address.
|
||||
## `TR_RACK_BITBRAIN` is the ONE genuinely ambiguous legacy name — the rack is
|
||||
## keyed by gun name, and the new ADE+SBC gun is now the one called
|
||||
## `BITBRAIN`. It is disambiguated by the same switch the knobs use,
|
||||
## `TR_BITBRAIN_NET` (default 0):
|
||||
## * unset/0 -> LEGACY: `TR_RACK_BITBRAIN` selects LEADGAIN (id 16), the gun
|
||||
## it always selected, and the new gun stays off (the shipped default);
|
||||
## * 1 -> `TR_RACK_BITBRAIN` selects the new BITBRAIN gun (id 17).
|
||||
RackLegacyAlias*: array[1, (string, int)] = [("TR_RACK_BITBRAIN", 16)]
|
||||
RackLegacyAliasGunName* = "LEADGAIN" ## what the legacy name selects today
|
||||
|
||||
proc netSwitchOwnsBitbrainName*(): bool =
|
||||
## `TR_BITBRAIN_NET` (default 0) is THE disambiguation switch for the whole
|
||||
## `TR_BITBRAIN_*` namespace. Unset/0 => the namespace is LEGACY and belongs
|
||||
## to the renamed lead-gain corrector; 1 => it belongs to the ADE+SBC gun.
|
||||
## Defined here (and identically in `guns/lead_gain.nim`) because
|
||||
## `gun_harness/selector` must not depend on a concrete gun module.
|
||||
case getEnv("TR_BITBRAIN_NET", "").strip().toLowerAscii()
|
||||
of "1", "true", "yes", "on": true
|
||||
else: false
|
||||
|
||||
proc parseRackMembership*(value: string): RackMembership =
|
||||
## Parse a `TR_RACK_<GUN>` value. Empty / unknown values fall back to the
|
||||
## shipped `both` and warn on stderr, so a typo cannot silently move a gun and
|
||||
@@ -96,12 +127,28 @@ proc parseRackMembership*(value: string): RackMembership =
|
||||
proc loadRackMembership*(): array[len(RackGunNames), RackMembership] =
|
||||
## Default table plus every `TR_RACK_<GUN>` override. A proc (not inlined into
|
||||
## the `let`) so the unit test can exercise env parsing in-process.
|
||||
## Legacy alias names (`RackLegacyAlias`) are applied only when the
|
||||
## corresponding CURRENT name is unset, so a migrated config always wins.
|
||||
result = DefaultRackMembership
|
||||
var legacyTouched: seq[string]
|
||||
for i in 0..<len(RackGunNames):
|
||||
let key = RackEnvPrefix & RackGunNames[i]
|
||||
let v = getEnv(key, "")
|
||||
if v.len > 0:
|
||||
result[i] = parseRackMembership(v)
|
||||
if not netSwitchOwnsBitbrainName():
|
||||
for (key, gid) in RackLegacyAlias:
|
||||
let current = RackEnvPrefix & RackGunNames[gid]
|
||||
if getEnv(key, "").len > 0 and getEnv(current, "").len == 0:
|
||||
result[gid] = parseRackMembership(getEnv(key, ""))
|
||||
legacyTouched.add key
|
||||
if legacyTouched.len > 0:
|
||||
let gid = RackLegacyAlias[0][1]
|
||||
stderr.writeLine("[depr] legacy rack knob " & legacyTouched.join(",") &
|
||||
" now names the ADE+SBC gun (BITBRAIN, rack id 17); it still selects " &
|
||||
RackGunNames[gid] & " (rack id " & $gid &
|
||||
") until TR_BITBRAIN_NET=1. Set TR_RACK_" & RackGunNames[gid] &
|
||||
" to make it explicit.")
|
||||
|
||||
let ActiveRackMembership* = loadRackMembership()
|
||||
## Process-wide rack table, frozen at startup.
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
# bitbrain_gun — quick recap (inputs / outputs)
|
||||
|
||||
Recap card. Everything below is read off `common_libs/guns/bitbrain_gun.nim`.
|
||||
|
||||
## What it is today
|
||||
|
||||
- A **lead-gain corrector on top of Pattern's prediction**. It scales Pattern's lead over the line of sight by a learned gain.
|
||||
- The **ADE/SBC neural network is REMOVED from the gun**. The name still says "BitBrain", but there is no net. The generic `common_libs/bitbrain/` library still exists and is tested separately (`test_bitbrain.nim`).
|
||||
|
||||
## INPUTS
|
||||
|
||||
| Input | Source in code |
|
||||
|---|---|
|
||||
| World state (self pos, enemy pos, tick) | `WorldState state` |
|
||||
| **Base aim** — Pattern's prediction | `g.tmh.pattern.predict(state, bulletSpeed)`: `TmHorizonGun.pattern`, a `PatternMatcherGun` (`guns/pattern_matcher`) |
|
||||
| Range band (5 bands: 0/100/200/300/450) | `bbBandOf(dist)`, `BB_BAND_LO/HI` |
|
||||
| **Gate** — gain applies only from band 3 up | `BB_GAIN_BAND_MIN = 3` → range **≥ ~300 px** |
|
||||
| **Label / feedback** — deferred observation lookup | at fire time store lead + tolerance; `h = round(dist/speed)` ticks later `tmhObservedAt(g.tmh, state.tick, selfX, selfY)` returns the enemy's OBSERVED bearing |
|
||||
| Aim tolerance (target's angular half-width) | `bbTolDeg(dist) = atan(18/range)` in degrees |
|
||||
| Config knobs | env, resolved once in `initBitBrainGun` (see table) |
|
||||
|
||||
## OUTPUTS
|
||||
|
||||
| Output | Formula / meaning |
|
||||
|---|---|
|
||||
| Aim point | `aim = LOS + gain * (patternAim - LOS)` — applied as angular `shift = (gain - 1.0) * lead` deg via `tmhApplyShift`; when `gain == 1.0` the base prediction is returned unchanged |
|
||||
| `gain` | argmax **hit rate** per band over the candidate list; `BB_CAND` default has **5 candidates** `{0.0, 0.25, 0.5, 0.75, 1.0}` (0 = HeadOn, 1 = Pattern); one candidate = fixed gain, no learning |
|
||||
| `[bb]` log line (only if `TR_BITBRAIN_LOG=1`; emitted only when `(gain, band)` changes) | see below |
|
||||
| Does **NOT** output | a predicted angle / bearing. It never aims on its own — it only rescales Pattern's lead. |
|
||||
|
||||
`[bb]` fields, one at a time:
|
||||
|
||||
| field | meaning |
|
||||
|---|---|
|
||||
| `t` | current tick |
|
||||
| `band` | lower edge of the range band in use (e.g. `300+`) |
|
||||
| `gain` | the gain being applied to Pattern's lead |
|
||||
| `shift` | angular shift actually applied = `(gain-1)*lead`, degrees |
|
||||
| `rate` | hit rate of the chosen gain in this band |
|
||||
| `n` | resolved samples in this band |
|
||||
| `ncand` | number of candidate gains |
|
||||
| `trained` | total resolved samples this battle |
|
||||
| `pend` | deferred labels still waiting |
|
||||
| `dropped` | labels that could not be resolved (stale / out-of-order) |
|
||||
| `mode` | memory mode: perRound / retained / decay |
|
||||
|
||||
## KNOB TABLE (`TR_BITBRAIN_*`)
|
||||
|
||||
**LIVE** — the resolved field is read by the learner/predict path:
|
||||
|
||||
| Env | Meaning |
|
||||
|---|---|
|
||||
| `TR_BITBRAIN_GAINS` | comma-separated candidate gains (replaces `BB_CAND`) |
|
||||
| `TR_BITBRAIN_MEM` | perRound / retained / decay memory |
|
||||
| `TR_BITBRAIN_MIN_OBS` | samples before a band is trusted (in `bbGain`) |
|
||||
| `TR_BITBRAIN_DECAY` | decay interval in resolved samples (`resolvePending`) |
|
||||
| `TR_BITBRAIN_DECAY_FRAC` | per-decay shrink of the hit counts (`bbApplyDecay`) |
|
||||
| `TR_BITBRAIN_LOG` | `1` = emit the `[bb]` line |
|
||||
| `TR_BITBRAIN_RESET_ON_TARGET` | wipe learning when the enemy id changes (`targetChanged`) |
|
||||
|
||||
**INERT** — kept only so old configs and the boot report don't warn; never touch the gain learner:
|
||||
|
||||
| Env | Stored as (only read by boot report / guard test) |
|
||||
|---|---|
|
||||
| `TR_BITBRAIN_N` | `nClasses` |
|
||||
| `TR_BITBRAIN_NADE` | `nAde` |
|
||||
| `TR_BITBRAIN_RANGE` | `maxDeg` |
|
||||
| `TR_BITBRAIN_WARMUP` | `warmupN` |
|
||||
| `TR_BITBRAIN_ADAPT` | `adaptEvery` |
|
||||
| `TR_BITBRAIN_CALIB` | `calibEvery` |
|
||||
| `TR_BITBRAIN_SEED` | `seed` |
|
||||
|
||||
*(Also `TR_RACK_BITBRAIN` — the rack admission switch, see below.)*
|
||||
|
||||
## HOW TO TURN IT ON
|
||||
|
||||
Default is **off**; the shipped rack never calls it. Minimal `.env`:
|
||||
|
||||
```
|
||||
TR_RACK_BITBRAIN=both
|
||||
TR_RACK_PATTERN=off
|
||||
TR_BITBRAIN_GAINS=1.0 # 1.0 = identity = aims EXACTLY like Pattern
|
||||
TR_BITBRAIN_MEM=decay
|
||||
TR_BITBRAIN_LOG=1
|
||||
```
|
||||
|
||||
⚠️ **`TR_BITBRAIN_GAINS=1.0` is the identity** — with one candidate at 1.0 the gun aims **exactly like Pattern**.
|
||||
**Do not read the candidate list as a recommendation.** Measured live: fixed gains **above** 1.0 are
|
||||
*decisively harmful* (1.5 → −93.8 dmg/run, p=0.0006; 1.25 → −34.2 dmg/run), a learner restricted to **≤ 1.0**
|
||||
is a **wash** (−2.5 dmg/run, p=0.87), and zero lead (= HeadOn) is **catastrophic** (14 vs 279 dmg/run).
|
||||
So the default set `{0, 0.25, 0.5, 0.75, 1.0}` is **not** a recommendation either — it merely *allows* the gun
|
||||
to shrink the lead toward HeadOn. Multi-candidate lists are for running the experiment, not for playing.
|
||||
|
||||
## MEASURED VERDICT
|
||||
|
||||
- **Neutral vs Pattern** across many opponents: damage/run 214.5 vs 210.9, round wins 49.4% vs 49.8% over 32 opponents (`docs/gauntlet_bitbrain_vs_pattern.md`).
|
||||
- **Specifically worse on DrussGT** alone: −18.1 damage/run (`docs/gauntlet_bitbrain_vs_pattern.md`, `docs/bitbrain_gun_verdict.md`).
|
||||
- The **lead-amplitude (gain) axis is CLOSED** — nothing beats Pattern in either direction (`docs/bitbrain_campaign.md` §Phase 2 / §2.6).
|
||||
|
||||
## PROVENANCE
|
||||
|
||||
- **Derived from code (this file):** what it is today, all inputs, the output formula, the `[bb]` fields, the LIVE/INERT split, and the "how to turn it on" env lines.
|
||||
- **Taken from the named evidence docs:** the numbers in MEASURED VERDICT above — see `docs/gauntlet_bitbrain_vs_pattern.md`, `docs/bitbrain_gun_verdict.md`, `docs/bitbrain_campaign.md`.
|
||||
@@ -0,0 +1,138 @@
|
||||
# lead_gain — quick recap (inputs / outputs)
|
||||
|
||||
Recap card. Everything below is read off `common_libs/guns/lead_gain.nim`.
|
||||
|
||||
## The name
|
||||
|
||||
This gun is **LEADGAIN** (rack id 16, `TR_LEADGAIN_*`). It used to be called
|
||||
`BITBRAIN` and to live in `guns/bitbrain_gun.nim`, but the ADE+SBC network was
|
||||
removed when it was rebuilt into what it actually is: **it learns a multiplier
|
||||
for Pattern's lead, separately per range band.** The rack id is unchanged. The
|
||||
real ADE+SBC gun is `guns/bitbrain_net.nim` (rack id 17).
|
||||
|
||||
### Backward compatibility (read this before editing your `.env`)
|
||||
|
||||
The `TR_BITBRAIN_*` names your `.env` already contains still work, and still
|
||||
select **this** gun. The disambiguation is one switch, `TR_BITBRAIN_NET`
|
||||
(default `0`):
|
||||
|
||||
| `TR_BITBRAIN_NET` | who owns `TR_BITBRAIN_*` |
|
||||
|---|---|
|
||||
| unset / `0` | LEGACY — these are aliases for `TR_LEADGAIN_*`; the new ADE+SBC gun is off |
|
||||
| `1` | the new ADE+SBC gun (rack id 17) |
|
||||
|
||||
So the owner's existing `TR_RACK_LEADGAIN=both TR_BITBRAIN_GAINS=… TR_BITBRAIN_MEM=decay
|
||||
TR_BITBRAIN_LOG=1` keeps behaving exactly as before, and one `[depr]` line on
|
||||
stderr names the new `TR_LEADGAIN_*` spelling of each knob it honoured. The two
|
||||
name sets are disjoint by construction, so no name is ever claimed twice.
|
||||
Migrated names are `TR_LEADGAIN_GAINS`, `_MEM`, `_MIN_OBS`, `_DECAY`,
|
||||
`_DECAY_FRAC`, `_LOG`, `_RESET_ON_TARGET`, `_N`, `_NADE`, `_RANGE`, `_WARMUP`,
|
||||
`_ADAPT`, `_CALIB`, `_SEED` and `TR_RACK_LEADGAIN`.
|
||||
|
||||
## What it is today
|
||||
|
||||
- A **lead-gain corrector on top of Pattern's prediction**. It scales Pattern's lead over the line of sight by a learned gain.
|
||||
- The **ADE/SBC neural network is REMOVED from this gun** (it lives in `guns/bitbrain_net.nim` now). The generic `common_libs/bitbrain/` library is intact and tested separately (`test_bitbrain.nim`, 56 checks).
|
||||
|
||||
## INPUTS
|
||||
|
||||
| Input | Source in code |
|
||||
|---|---|
|
||||
| World state (self pos, enemy pos, tick) | `WorldState state` |
|
||||
| **Base aim** — Pattern's prediction | `g.tmh.pattern.predict(state, bulletSpeed)`: `TmHorizonGun.pattern`, a `PatternMatcherGun` (`guns/pattern_matcher`) |
|
||||
| Range band (5 bands: 0/100/200/300/450) | `lgBandOf(dist)`, `LG_BAND_LO/HI` |
|
||||
| **Gate** — gain applies only from band 3 up | `LG_GAIN_BAND_MIN = 3` → range **≥ ~300 px** |
|
||||
| **Label / feedback** — deferred observation lookup | at fire time store lead + tolerance; `h = round(dist/speed)` ticks later `tmhObservedAt(g.tmh, state.tick, selfX, selfY)` returns the enemy's OBSERVED bearing |
|
||||
| Aim tolerance (target's angular half-width) | `lgTolDeg(dist) = atan(18/range)` in degrees |
|
||||
| Config knobs | env, resolved once in `initLeadGainGun` (see table) |
|
||||
|
||||
## OUTPUTS
|
||||
|
||||
| Output | Formula / meaning |
|
||||
|---|---|
|
||||
| Aim point | `aim = LOS + gain * (patternAim - LOS)` — applied as angular `shift = (gain - 1.0) * lead` deg via `tmhApplyShift`; when `gain == 1.0` the base prediction is returned unchanged |
|
||||
| `gain` | argmax **hit rate** per band over the candidate list; `LG_CAND` default has **5 candidates** `{0.0, 0.25, 0.5, 0.75, 1.0}` (0 = HeadOn, 1 = Pattern); one candidate = fixed gain, no learning |
|
||||
| `[lg]` log line (only if `TR_LEADGAIN_LOG=1`; emitted only when `(gain, band)` changes) | see below |
|
||||
| Does **NOT** output | a predicted angle / bearing. It never aims on its own — it only rescales Pattern's lead. |
|
||||
|
||||
`[lg]` fields, one at a time:
|
||||
|
||||
| field | meaning |
|
||||
|---|---|
|
||||
| `t` | current tick |
|
||||
| `band` | lower edge of the range band in use (e.g. `300+`) |
|
||||
| `gain` | the gain being applied to Pattern's lead |
|
||||
| `shift` | angular shift actually applied = `(gain-1)*lead`, degrees |
|
||||
| `rate` | hit rate of the chosen gain in this band |
|
||||
| `n` | resolved samples in this band |
|
||||
| `ncand` | number of candidate gains |
|
||||
| `trained` | total resolved samples this battle |
|
||||
| `pend` | deferred labels still waiting |
|
||||
| `dropped` | labels that could not be resolved (stale / out-of-order) |
|
||||
| `mode` | memory mode: perRound / retained / decay |
|
||||
|
||||
## KNOB TABLE (`TR_LEADGAIN_*`)
|
||||
|
||||
Every old `TR_BITBRAIN_<X>` in the **LIVE** and **INERT** tables below is still
|
||||
honoured as an alias, and one `[depr]` line on stderr names the `TR_LEADGAIN_*`
|
||||
spelling (see "The name" above). The ADE+SBC gun uses a **different** set of
|
||||
`TR_BITBRAIN_*` names (`TR_BITBRAIN_INPUT`, `_NCLASSES`, `_NADES`, …) plus
|
||||
`TR_BITBRAIN_MODE` / `_DECAY_EVERY` / `_DECAY_SHIFT` from the library; the two
|
||||
sets are disjoint, so nothing is claimed twice.
|
||||
|
||||
**LIVE** — the resolved field is read by the learner/predict path:
|
||||
|
||||
| Env (new / legacy alias) | Meaning |
|
||||
|---|---|
|
||||
| `TR_LEADGAIN_GAINS` / `TR_BITBRAIN_GAINS` | comma-separated candidate gains (replaces `LG_CAND`) |
|
||||
| `TR_LEADGAIN_MEM` / `TR_BITBRAIN_MEM` | perRound / retained / decay memory |
|
||||
| `TR_LEADGAIN_MIN_OBS` / `TR_BITBRAIN_MIN_OBS` | samples before a band is trusted (in `lgGainFor`) |
|
||||
| `TR_LEADGAIN_DECAY` / `TR_BITBRAIN_DECAY` | decay interval in resolved samples (`resolvePending`) |
|
||||
| `TR_LEADGAIN_DECAY_FRAC` / `TR_BITBRAIN_DECAY_FRAC` | per-decay shrink of the hit counts (`lgApplyDecay`) |
|
||||
| `TR_LEADGAIN_LOG` / `TR_BITBRAIN_LOG` | `1` = emit the `[lg]` line |
|
||||
| `TR_LEADGAIN_RESET_ON_TARGET` / `TR_BITBRAIN_RESET_ON_TARGET` | wipe learning when the enemy id changes (`targetChanged`) |
|
||||
| `TR_RACK_LEADGAIN` / `TR_RACK_BITBRAIN` | the rack admission switch |
|
||||
| `TR_BITBRAIN_NET` | `0` (default) keeps `TR_BITBRAIN_*` legacy; `1` hands the namespace to the new ADE+SBC gun |
|
||||
|
||||
**INERT** — kept only so old configs and the boot report don't warn; never touch the gain learner:
|
||||
|
||||
| Env | Stored as (only read by boot report / guard test) |
|
||||
|---|---|
|
||||
| `TR_LEADGAIN_N` / `TR_BITBRAIN_N` | `nClasses` |
|
||||
| `TR_LEADGAIN_NADE` / `TR_BITBRAIN_NADE` | `nAde` |
|
||||
| `TR_LEADGAIN_RANGE` / `TR_BITBRAIN_RANGE` | `maxDeg` |
|
||||
| `TR_LEADGAIN_WARMUP` / `TR_BITBRAIN_WARMUP` | `warmupN` |
|
||||
| `TR_LEADGAIN_ADAPT` / `TR_BITBRAIN_ADAPT` | `adaptEvery` |
|
||||
| `TR_LEADGAIN_CALIB` / `TR_BITBRAIN_CALIB` | `calibEvery` |
|
||||
| `TR_LEADGAIN_SEED` / `TR_BITBRAIN_SEED` | `seed` |
|
||||
|
||||
## HOW TO TURN IT ON
|
||||
|
||||
Default is **off**; the shipped rack never calls it. Minimal `.env`:
|
||||
|
||||
```
|
||||
TR_RACK_LEADGAIN=both
|
||||
TR_RACK_PATTERN=off
|
||||
TR_LEADGAIN_GAINS=1.0 # 1.0 = identity = aims EXACTLY like Pattern
|
||||
TR_LEADGAIN_MEM=decay
|
||||
TR_LEADGAIN_LOG=1
|
||||
```
|
||||
|
||||
⚠️ **`TR_LEADGAIN_GAINS=1.0` is the identity** — with one candidate at 1.0 the gun aims **exactly like Pattern**.
|
||||
**Do not read the candidate list as a recommendation.** Measured live: fixed gains **above** 1.0 are
|
||||
*decisively harmful* (1.5 → −93.8 dmg/run, p=0.0006; 1.25 → −34.2 dmg/run), a learner restricted to **≤ 1.0**
|
||||
is a **wash** (−2.5 dmg/run, p=0.87), and zero lead (= HeadOn) is **catastrophic** (14 vs 279 dmg/run).
|
||||
So the default set `{0, 0.25, 0.5, 0.75, 1.0}` is **not** a recommendation either — it merely *allows* the gun
|
||||
to shrink the lead toward HeadOn. Multi-candidate lists are for running the experiment, not for playing.
|
||||
|
||||
|
||||
## MEASURED VERDICT (measured under the old `BITBRAIN` name)
|
||||
|
||||
- **Neutral vs Pattern** across many opponents: damage/run 214.5 vs 210.9, round wins 49.4% vs 49.8% over 32 opponents (`docs/gauntlet_bitbrain_vs_pattern.md`).
|
||||
- **Specifically worse on DrussGT** alone: −18.1 damage/run (`docs/gauntlet_bitbrain_vs_pattern.md`, `docs/bitbrain_gun_verdict.md`).
|
||||
- The **lead-amplitude (gain) axis is CLOSED** — nothing beats Pattern in either direction (`docs/bitbrain_campaign.md` §Phase 2 / §2.6).
|
||||
|
||||
## PROVENANCE
|
||||
|
||||
- **Derived from code (this file):** what it is today, all inputs, the output formula, the `[lg]` fields, the LIVE/INERT split, and the "how to turn it on" env lines.
|
||||
- **Taken from the named evidence docs:** the numbers in MEASURED VERDICT above — see `docs/gauntlet_bitbrain_vs_pattern.md`, `docs/bitbrain_gun_verdict.md`, `docs/bitbrain_campaign.md`.
|
||||
@@ -1,8 +1,16 @@
|
||||
## bitbrain_gun.nim — BitBrain (id 16), REBUILT as a LEAD-GAIN CORRECTOR.
|
||||
## lead_gain.nim — LEADGAIN (rack id 16): a per-range-band LEAD-GAIN corrector.
|
||||
##
|
||||
## ── WHY THIS FILE WAS REWRITTEN (Phase 0/1 evidence) ──────────────────────────
|
||||
## ── THE NAME ──────────────────────────────────────────────────────────────────
|
||||
## This gun used to be called `BITBRAIN` and to live in `guns/bitbrain_gun.nim`,
|
||||
## but its ADE+SBC network was removed when it was rebuilt into what it actually
|
||||
## is: **it learns a multiplier for Pattern's lead, separately per range band.**
|
||||
## `LEADGAIN` says that; `BITBRAIN` (a neural network) did not. The rack id 16
|
||||
## is UNCHANGED (many tests assert the id literals) and the real ADE+SBC gun is
|
||||
## the separate `guns/bitbrain_net.nim` at rack id 17.
|
||||
##
|
||||
## ── WHY THE FILE WAS REBUILT (Phase 0/1 evidence) ────────────────────────────
|
||||
## The previous design was an ADDITIVE angular shift: an ADE+SBC network
|
||||
## classified the +h-tick angular error over ±`TR_BITBRAIN_RANGE` degrees and
|
||||
## classified the +h-tick angular error over ±`TR_LEADGAIN_RANGE` degrees and
|
||||
## added the argmax class centre to Pattern's bearing. Phase 0 measured it as
|
||||
## statistically identical to Pattern (`docs/bitbrain_gun_verdict.md`,
|
||||
## commit d93ce44) and as carrying no measurable aim information
|
||||
@@ -22,7 +30,7 @@
|
||||
## reached through the TmHorizonGun observation ring.
|
||||
## * OUTPUT — `aim = LOS + gain * (patternAim - LOS)`, i.e. Pattern's lead over
|
||||
## the line of sight is multiplied by a learned `gain` (one of
|
||||
## `BB_CAND`, so it may be BELOW 1.0 — the point).
|
||||
## `LG_CAND`, so it may be BELOW 1.0 — the point).
|
||||
## * LABEL — the same deferred-label path the old corrector used: at fire
|
||||
## time we remember the base lead and the aim tolerance; `h =
|
||||
## round(dist/speed)` ticks later `tmhObservedAt` returns the
|
||||
@@ -40,39 +48,62 @@
|
||||
## hit-probability proxy directly instead of mean squared error.
|
||||
## * STATE — the range band (the ruler's 5 bands). Range is known causally at
|
||||
## fire time, so a per-band gain table is shippable with no learning
|
||||
## at all; BitBrain learns that table online. The correction is
|
||||
## at all; this gun learns that table online. The correction is
|
||||
## additionally gated to bands with range >= 300 px
|
||||
## (`BB_GAIN_BAND_MIN`), where Phase 1 measured Pattern's lead to be
|
||||
## (`LG_GAIN_BAND_MIN`), where Phase 1 measured Pattern's lead to be
|
||||
## uninformative. That gate is causal (range is known).
|
||||
##
|
||||
## The gain statistics are battle-scale: a round boundary wipes the observation
|
||||
## ring and deferred labels but NOT the gain counts (a new round is not a new
|
||||
## enemy). `resetLearning` wipes them on a new battle / target change; with
|
||||
## `TR_BITBRAIN_MEM=decay` every `TR_BITBRAIN_DECAY` resolved samples decays the
|
||||
## counts by `TR_BITBRAIN_DECAY_FRAC` toward the gain-1.0 column.
|
||||
## `TR_LEADGAIN_MEM=decay` every `TR_LEADGAIN_DECAY` resolved samples decays the
|
||||
## counts by `TR_LEADGAIN_DECAY_FRAC` toward the gain-1.0 column.
|
||||
##
|
||||
## ── WHAT IS STILL HERE ONLY FOR THE BOOT REPORT / GUARD TESTS ─────────────────
|
||||
## The ADE+SBC network is GONE from the gun. The 53-bit TMH input, the class
|
||||
## geometry (`bbCenterDeg`/`bbClassOf`), `TR_BITBRAIN_N`/`NADE`/`WARMUP`/`ADAPT`/
|
||||
## `CALIB`/`SEED` and `TR_BITBRAIN_RANGE` are retained as resolved configuration
|
||||
## so the boot report (`env_report.nim`) and the registration guard tests keep
|
||||
## working unchanged; they no longer affect the gain learner. The generic
|
||||
## The ADE+SBC network is GONE from the gun. The class geometry
|
||||
## (`lgCenterDeg`/`lgClassOf`) and `TR_LEADGAIN_N`/`NADE`/`WARMUP`/`ADAPT`/
|
||||
## `CALIB`/`SEED`/`RANGE` are retained as resolved configuration so the boot
|
||||
## report (`env_report.nim`) and the registration guard tests keep working
|
||||
## unchanged; they no longer affect the gain learner. The generic
|
||||
## `common_libs/bitbrain/` library is untouched and still tested by
|
||||
## `test_bitbrain.nim`.
|
||||
## `test_bitbrain.nim`, and the new ADE+SBC gun that actually uses it is
|
||||
## `guns/bitbrain_net.nim` (rack id 17).
|
||||
##
|
||||
## ── TR_BITBRAIN_GAINS (the candidate set as an env knob) ─────────────────────
|
||||
## `TR_BITBRAIN_GAINS` is a comma-separated candidate list, e.g.
|
||||
## `TR_BITBRAIN_GAINS=1.0,1.25,1.5,2.0`. It replaces the fixed shipped candidate
|
||||
## ── BACKWARD COMPATIBILITY: the `TR_BITBRAIN_*` legacy aliases ───────────────
|
||||
## The owner's live `.env` predates the rename and contains `TR_RACK_BITBRAIN`,
|
||||
## `TR_BITBRAIN_GAINS`, `TR_BITBRAIN_MEM`, `TR_BITBRAIN_LOG`, … Those names are
|
||||
## the OLD corrector's knobs and MUST keep working unchanged. The
|
||||
## `TR_BITBRAIN_*` prefix, however, now belongs to the NEW ADE+SBC gun
|
||||
## (`guns/bitbrain_net.nim`). The two uses are separated by ONE deterministic
|
||||
## switch, `TR_BITBRAIN_NET` (the new gun's master switch, default 0 = off):
|
||||
##
|
||||
## * `TR_BITBRAIN_NET` UNSET / 0 → LEGACY MODE. Every `TR_BITBRAIN_<X>` name
|
||||
## listed in `LegacyKnobEnvNames` is a legacy alias for this gun's
|
||||
## `TR_LEADGAIN_<X>`, and the new ADE+SBC gun is OFF. This is the owner's
|
||||
## current configuration, so its behaviour is unchanged.
|
||||
## * `TR_BITBRAIN_NET=1` → NEW-NETWORK MODE. `TR_BITBRAIN_<X>` names
|
||||
## the NEW gun's knobs (see `bitbrain_net.nim`) and this gun reads ONLY
|
||||
## `TR_LEADGAIN_<X>`.
|
||||
##
|
||||
## The legacy sets are DISJOINT (see `legacyKnobEnvNames` / the new gun's
|
||||
## `netKnobEnvNames`), so no name is ever claimed by both. `TR_RACK_BITBRAIN` is
|
||||
## the one genuinely ambiguous name (the rack is keyed by gun name and the new
|
||||
## gun is now the one called `BITBRAIN`); it is resolved by the SAME switch —
|
||||
## see `selector.nim`'s `RackLegacyAliases`.
|
||||
##
|
||||
## ── TR_LEADGAIN_GAINS (the candidate set as an env knob) ─────────────────────
|
||||
## `TR_LEADGAIN_GAINS` is a comma-separated candidate list, e.g.
|
||||
## `TR_LEADGAIN_GAINS=1.0,1.25,1.5,2.0`. It replaces the fixed shipped candidate
|
||||
## set `{0, 0.25, 0.5, 0.75, 1.0}` for this gun instance, so every live arm is
|
||||
## pure-env (no recompile). Two degenerate cases are deliberate:
|
||||
## * unset / unparsable -> the shipped `BB_CAND` set, byte-identical behaviour;
|
||||
## * unset / unparsable -> the shipped `LG_CAND` set, byte-identical behaviour;
|
||||
## * exactly ONE value -> a FIXED gain, applied from the first shot with NO
|
||||
## learning at all (the learner is bypassed), still gated to the long bands.
|
||||
## The applied `gain` (and the resulting angular `shift`) is printed on the
|
||||
## existing change-gated `[bb]` line, so a run's liveness AND the correction it
|
||||
## existing change-gated `[lg]` line, so a run's liveness AND the correction it
|
||||
## actually applied are both auditable from the bot's stdout.
|
||||
##
|
||||
## DEFAULT OFF / PARITY: this gun is admitted ONLY when `TR_RACK_BITBRAIN` says so
|
||||
## DEFAULT OFF / PARITY: this gun is admitted ONLY when `TR_RACK_LEADGAIN` says so
|
||||
## (default `off`). The shipped rack never calls `predict`, so `ensureInit` never
|
||||
## runs and the shipped bot is byte-for-byte unchanged.
|
||||
|
||||
@@ -83,55 +114,60 @@ import guns/pattern_matcher
|
||||
|
||||
const
|
||||
## ── env knobs (all resolved once at gun construction) ─────────────────────
|
||||
BB_MEM_ENV* = "TR_BITBRAIN_MEM" ## perRound|retained|decay
|
||||
BB_GAINS_ENV* = "TR_BITBRAIN_GAINS" ## comma-separated candidate gains
|
||||
BB_N_ENV* = "TR_BITBRAIN_N" ## (legacy geometry; inert)
|
||||
BB_NADE_ENV* = "TR_BITBRAIN_NADE" ## (legacy ADE count; inert)
|
||||
BB_RANGE_ENV* = "TR_BITBRAIN_RANGE" ## (legacy class half-range; inert)
|
||||
BB_LOG_ENV* = "TR_BITBRAIN_LOG" ## 1 = per-change [bb] log
|
||||
BB_MIN_OBS_ENV* = "TR_BITBRAIN_MIN_OBS" ## samples before a band is trusted
|
||||
BB_WARMUP_ENV* = "TR_BITBRAIN_WARMUP" ## (legacy; inert)
|
||||
BB_ADAPT_ENV* = "TR_BITBRAIN_ADAPT" ## (legacy; inert)
|
||||
BB_CALIB_ENV* = "TR_BITBRAIN_CALIB" ## (legacy; inert)
|
||||
BB_DECAY_ENV* = "TR_BITBRAIN_DECAY" ## decay interval (samples)
|
||||
BB_DECAY_FRAC_ENV* = "TR_BITBRAIN_DECAY_FRAC" ## per-decay count shrink
|
||||
BB_SEED_ENV* = "TR_BITBRAIN_SEED" ## (legacy; inert)
|
||||
BB_RESET_ON_TARGET_ENV* = "TR_BITBRAIN_RESET_ON_TARGET"
|
||||
LG_MEM_ENV* = "TR_LEADGAIN_MEM" ## perRound|retained|decay
|
||||
LG_GAINS_ENV* = "TR_LEADGAIN_GAINS" ## comma-separated candidate gains
|
||||
LG_N_ENV* = "TR_LEADGAIN_N" ## (legacy geometry; inert)
|
||||
LG_NADE_ENV* = "TR_LEADGAIN_NADE" ## (legacy ADE count; inert)
|
||||
LG_RANGE_ENV* = "TR_LEADGAIN_RANGE" ## (legacy class half-range; inert)
|
||||
LG_LOG_ENV* = "TR_LEADGAIN_LOG" ## 1 = per-change [lg] log
|
||||
## ── the one switch that disambiguates the legacy `TR_BITBRAIN_*` names ────
|
||||
## Read by BOTH guns (see `bitbrain_net.nim`). Unset/0 => the `TR_BITBRAIN_*`
|
||||
## names are LEGACY aliases for this gun; 1 => they belong to the new ADE+SBC
|
||||
## gun. It is also the new gun's master on/off switch.
|
||||
LG_NET_SWITCH_ENV* = "TR_BITBRAIN_NET"
|
||||
LG_MIN_OBS_ENV* = "TR_LEADGAIN_MIN_OBS" ## samples before a band is trusted
|
||||
LG_WARMUP_ENV* = "TR_LEADGAIN_WARMUP" ## (legacy; inert)
|
||||
LG_ADAPT_ENV* = "TR_LEADGAIN_ADAPT" ## (legacy; inert)
|
||||
LG_CALIB_ENV* = "TR_LEADGAIN_CALIB" ## (legacy; inert)
|
||||
LG_DECAY_ENV* = "TR_LEADGAIN_DECAY" ## decay interval (samples)
|
||||
LG_DECAY_FRAC_ENV* = "TR_LEADGAIN_DECAY_FRAC" ## per-decay count shrink
|
||||
LG_SEED_ENV* = "TR_LEADGAIN_SEED" ## (legacy; inert)
|
||||
LG_RESET_ON_TARGET_ENV* = "TR_LEADGAIN_RESET_ON_TARGET"
|
||||
## ── fixed geometry ────────────────────────────────────────────────────────
|
||||
BB_PENDING_CAP* = 512 ## deferred-label queue (>= 4 buckets x 50 ticks)
|
||||
LG_PENDING_CAP* = 512 ## deferred-label queue (>= 4 buckets x 50 ticks)
|
||||
## ── the gain learner ──────────────────────────────────────────────────────
|
||||
BB_NBANDS* = 5 ## the ruler's range bands
|
||||
BB_NHB* = 4 ## horizon buckets (for the per-tick label dedupe)
|
||||
BB_BAND_LO* = [0.0, 100.0, 200.0, 300.0, 450.0]
|
||||
BB_BAND_HI* = [100.0, 200.0, 300.0, 450.0, 1.0e18]
|
||||
LG_NBANDS* = 5 ## the ruler's range bands
|
||||
LG_NHB* = 4 ## horizon buckets (for the per-tick label dedupe)
|
||||
LG_BAND_LO* = [0.0, 100.0, 200.0, 300.0, 450.0]
|
||||
LG_BAND_HI* = [100.0, 200.0, 300.0, 450.0, 1.0e18]
|
||||
## The DEFAULT candidate lead gains the band selector picks from. 0.0 == HeadOn
|
||||
## (aim at the current position) and 1.0 == Pattern (use the full lead).
|
||||
## `TR_BITBRAIN_GAINS` replaces this set per gun; unset -> this exact set.
|
||||
BB_CAND* = [0.0, 0.25, 0.50, 0.75, 1.0]
|
||||
BB_NCAND* = 5
|
||||
BB_BB_RADIUS* = 18.0 ## hit-detection radius in px (ruler tolerance)
|
||||
## Apply the correction only from this band up (range >= BB_BAND_LO[3] = 300).
|
||||
## `TR_LEADGAIN_GAINS` replaces this set per gun; unset -> this exact set.
|
||||
LG_CAND* = [0.0, 0.25, 0.50, 0.75, 1.0]
|
||||
LG_NCAND* = 5
|
||||
LG_BOT_RADIUS* = 18.0 ## hit-detection radius in px (ruler tolerance)
|
||||
## Apply the correction only from this band up (range >= LG_BAND_LO[3] = 300).
|
||||
## [MEASURED] below 300 Pattern's lead is informative and shrinking it loses
|
||||
## hits; see the header note.
|
||||
BB_GAIN_BAND_MIN* = 3
|
||||
LG_GAIN_BAND_MIN* = 3
|
||||
## ── shipped defaults ──────────────────────────────────────────────────────
|
||||
BB_N_DEF = 32
|
||||
BB_NADE_DEF = 256
|
||||
BB_RANGE_DEF = 40.0
|
||||
BB_MIN_OBS_DEF = 8
|
||||
BB_WARMUP_DEF = 400
|
||||
BB_ADAPT_DEF = 32
|
||||
BB_CALIB_DEF = 512
|
||||
BB_DECAY_DEF = 250
|
||||
BB_DECAY_FRAC_DEF = 0.02
|
||||
BB_SEED_DEF = 20240921
|
||||
BB_RESET_ON_TARGET_DEF = true
|
||||
LG_N_DEF = 32
|
||||
LG_NADE_DEF = 256
|
||||
LG_RANGE_DEF = 40.0
|
||||
LG_MIN_OBS_DEF = 8
|
||||
LG_WARMUP_DEF = 400
|
||||
LG_ADAPT_DEF = 32
|
||||
LG_CALIB_DEF = 512
|
||||
LG_DECAY_DEF = 250
|
||||
LG_DECAY_FRAC_DEF = 0.02
|
||||
LG_SEED_DEF = 20240921
|
||||
LG_RESET_ON_TARGET_DEF = true
|
||||
|
||||
type
|
||||
BitMemMode* = enum
|
||||
bmPerRound, bmRetained, bmDecay
|
||||
LeadMemMode* = enum
|
||||
lgPerRound, lgRetained, lgDecay
|
||||
|
||||
BbPending = object
|
||||
LgPending = object
|
||||
## One deferred training sample. `lead` is Pattern's lead over LOS at fire
|
||||
## time (radians) and `tol` the target's angular half-width then; the label
|
||||
## is resolved `horizon` ticks later.
|
||||
@@ -143,14 +179,14 @@ type
|
||||
lead: float
|
||||
tolDeg: float
|
||||
|
||||
BitBrainGun* = object
|
||||
LeadGainGun* = object
|
||||
tmh: TmHorizonGun
|
||||
initialized: bool
|
||||
# ── resolved config (kept in the boot report) ─────────────────────────────
|
||||
nClasses*: int
|
||||
maxDeg*: float
|
||||
nAde*: int
|
||||
memMode*: BitMemMode
|
||||
memMode*: LeadMemMode
|
||||
logEnabled*: bool
|
||||
minObs*: int
|
||||
warmupN*: int
|
||||
@@ -171,11 +207,11 @@ type
|
||||
sinceDecay: int
|
||||
decays*: int
|
||||
# ── readout / accounting ──────────────────────────────────────────────────
|
||||
lastGain*: array[BB_NBANDS, float]
|
||||
lastGain*: array[LG_NBANDS, float]
|
||||
corrections*: int
|
||||
lastLogKey: string
|
||||
# ── deferred labels ───────────────────────────────────────────────────────
|
||||
pending: array[BB_PENDING_CAP, BbPending]
|
||||
pending: array[LG_PENDING_CAP, LgPending]
|
||||
pendingCount*: int
|
||||
pendingDropped*: int
|
||||
# ── per-tick caches ───────────────────────────────────────────────────────
|
||||
@@ -186,27 +222,27 @@ type
|
||||
|
||||
# ── small pure helpers ───────────────────────────────────────────────────────
|
||||
|
||||
proc wrapRadBB(r: float): float {.inline.} =
|
||||
proc wrapRadLg(r: float): float {.inline.} =
|
||||
result = r
|
||||
while result > PI: result -= 2.0 * PI
|
||||
while result < -PI: result += 2.0 * PI
|
||||
|
||||
proc memModeName*(m: BitMemMode): string =
|
||||
proc memModeName*(m: LeadMemMode): string =
|
||||
case m
|
||||
of bmPerRound: "perRound"
|
||||
of bmRetained: "retained"
|
||||
of bmDecay: "decay"
|
||||
of lgPerRound: "perRound"
|
||||
of lgRetained: "retained"
|
||||
of lgDecay: "decay"
|
||||
|
||||
proc bbGainsString*(cands: seq[float]): string =
|
||||
proc lgGainsString*(cands: seq[float]): string =
|
||||
## The resolved candidate set as the env's comma-separated form (boot report).
|
||||
for i, c in cands:
|
||||
if i > 0: result.add ","
|
||||
result.add $c
|
||||
|
||||
proc parseGains*(value: string): seq[float] =
|
||||
## Parse `TR_BITBRAIN_GAINS`. Empty / unparsable / out-of-range / duplicate
|
||||
proc parseLgGains*(value: string): seq[float] =
|
||||
## Parse `TR_LEADGAIN_GAINS`. Empty / unparsable / out-of-range / duplicate
|
||||
## input cannot silently select a different regime: it falls back to the
|
||||
## shipped `BB_CAND` set, exactly like the other env knobs fall back to their
|
||||
## shipped `LG_CAND` set, exactly like the other env knobs fall back to their
|
||||
## defaults. Values are clamped to [0, 8] (0 == HeadOn, 1 == Pattern) and
|
||||
## de-duplicated, then sorted so the argmax tie rule (keep the smaller
|
||||
## candidate) is unchanged.
|
||||
@@ -223,42 +259,107 @@ proc parseGains*(value: string): seq[float] =
|
||||
if abs(u - v) < 1e-9: dup = true
|
||||
if not dup: seen.add v
|
||||
if seen.len == 0:
|
||||
for c in BB_CAND: seen.add c
|
||||
for c in LG_CAND: seen.add c
|
||||
return seen
|
||||
seen.sort()
|
||||
seen
|
||||
|
||||
proc parseMemMode*(value: string): BitMemMode =
|
||||
proc parseLgMemMode*(value: string): LeadMemMode =
|
||||
## Empty / unknown values fall back to the shipped `perRound`, so a typo
|
||||
## cannot silently select another regime.
|
||||
case value.strip().toLowerAscii()
|
||||
of "retained", "retain", "accum", "accumulate": bmRetained
|
||||
of "decay", "forget", "age": bmDecay
|
||||
else: bmPerRound
|
||||
of "retained", "retain", "accum", "accumulate": lgRetained
|
||||
of "decay", "forget", "age": lgDecay
|
||||
else: lgPerRound
|
||||
|
||||
proc envFloatBB(name: string, default: float): float =
|
||||
let v = getEnv(name, "")
|
||||
proc lgEnv(name: string): string
|
||||
## Forward declaration: the legacy-alias lookup is defined below, after the
|
||||
## frozen `LegacyKnobEnvNames` table it depends on.
|
||||
|
||||
proc envFloatLg(name: string, default: float): float =
|
||||
let v = lgEnv(name)
|
||||
if v.len == 0: return default
|
||||
try: parseFloat(v.strip()) except ValueError: default
|
||||
|
||||
proc envIntBB(name: string, default: int): int =
|
||||
let v = getEnv(name, "")
|
||||
# ── legacy `TR_BITBRAIN_*` aliases (backward compatibility) ───────────────────
|
||||
|
||||
const
|
||||
LegacyPrefix* = "TR_BITBRAIN_"
|
||||
NewPrefix* = "TR_LEADGAIN_"
|
||||
## The COMPLETE, FROZEN set of the old corrector's knob suffixes. A
|
||||
## `TR_BITBRAIN_<X>` in this set is a legacy alias for `TR_LEADGAIN_<X>`; any
|
||||
## other `TR_BITBRAIN_*` name belongs to the new ADE+SBC gun
|
||||
## (`bitbrain_net.nim`). The two sets are DISJOINT by construction, so the
|
||||
## mapping is total and deterministic — no name is claimed twice.
|
||||
LegacyKnobEnvNames* = [
|
||||
"GAINS", "MEM", "MIN_OBS", "DECAY", "DECAY_FRAC", "LOG", "RESET_ON_TARGET",
|
||||
"N", "NADE", "RANGE", "WARMUP", "ADAPT", "CALIB", "SEED"]
|
||||
## Knobs that actually change behaviour (the rest are inert configuration kept
|
||||
## for the boot report). A deprecation line is only worth printing for these
|
||||
## plus the inert ones, because a stale inert name is still a stale name.
|
||||
LegacyRackEnvName* = "TR_RACK_BITBRAIN"
|
||||
|
||||
proc netSwitchOn*(): bool =
|
||||
## `TR_BITBRAIN_NET` unset/0 => the `TR_BITBRAIN_*` names are LEGACY aliases
|
||||
## for this gun. 1 => they belong to the new ADE+SBC gun. The same predicate
|
||||
## is defined in `gun_harness/selector` (`netSwitchOwnsBitbrainName`), which
|
||||
## cannot import a concrete gun module.
|
||||
case getEnv(LG_NET_SWITCH_ENV, "").strip().toLowerAscii()
|
||||
of "1", "true", "yes", "on": true
|
||||
else: false
|
||||
|
||||
var deprecationShown = false
|
||||
|
||||
proc lgDeprecationLine*(): string =
|
||||
## The single clear deprecation line the owner sees. Names every legacy
|
||||
## `TR_BITBRAIN_*` knob that is actually set in the environment and the new
|
||||
## name that now owns it. Empty when there is nothing to migrate.
|
||||
if netSwitchOn(): return ""
|
||||
var parts: seq[string]
|
||||
for suffix in LegacyKnobEnvNames:
|
||||
let old = LegacyPrefix & suffix
|
||||
if getEnv(old, "").len > 0:
|
||||
parts.add old & " -> " & NewPrefix & suffix
|
||||
if getEnv(LegacyRackEnvName, "").len > 0:
|
||||
parts.add LegacyRackEnvName & " -> TR_RACK_LEADGAIN"
|
||||
if parts.len == 0: return ""
|
||||
result = "[depr] " & LegacyPrefix & "* is the OLD lead-gain corrector's namespace; " &
|
||||
"it was renamed to " & NewPrefix & "* (gun LEADGAIN, rack id 16). " &
|
||||
"Still honoured: " & parts.join("; ") &
|
||||
". The new ADE+SBC gun owns the " & LegacyPrefix &
|
||||
"* names once " & LG_NET_SWITCH_ENV & "=1."
|
||||
|
||||
proc lgEnv(name: string): string =
|
||||
## Read a `TR_LEADGAIN_<X>` knob, falling back to the legacy
|
||||
## `TR_BITBRAIN_<X>` alias while `TR_BITBRAIN_NET` is off. The NEW name always
|
||||
## wins when both are set, so a migrated config is authoritative.
|
||||
var v = getEnv(name, "")
|
||||
if v.len > 0: return v
|
||||
if netSwitchOn(): return ""
|
||||
let suffix = if name.startsWith(NewPrefix): name[NewPrefix.len .. ^1] else: ""
|
||||
if suffix.len == 0: return ""
|
||||
for s in LegacyKnobEnvNames:
|
||||
if s == suffix: return getEnv(LegacyPrefix & suffix, "")
|
||||
""
|
||||
|
||||
proc envIntLg(name: string, default: int): int =
|
||||
let v = lgEnv(name)
|
||||
if v.len == 0: return default
|
||||
try: parseInt(v.strip()) except ValueError: default
|
||||
|
||||
proc envBoolBB(name: string, default: bool): bool =
|
||||
case getEnv(name, "").strip().toLowerAscii()
|
||||
proc envBoolLg(name: string, default: bool): bool =
|
||||
case lgEnv(name).strip().toLowerAscii()
|
||||
of "1", "true", "yes", "on": true
|
||||
of "0", "false", "no", "off": false
|
||||
else: default
|
||||
|
||||
proc bbCenterDeg*(k, nClasses: int, maxDeg: float): float =
|
||||
proc lgCenterDeg*(k, nClasses: int, maxDeg: float): float =
|
||||
## Centre (degrees) of correction class `k` over ±maxDeg. Retained for the
|
||||
## registration guard test and the boot report; inert for the gain learner.
|
||||
let w = 2.0 * maxDeg / float(nClasses)
|
||||
-maxDeg + (float(k) + 0.5) * w
|
||||
|
||||
proc bbClassOf*(errRad: float, nClasses: int, maxDeg: float): int =
|
||||
proc lgClassOf*(errRad: float, nClasses: int, maxDeg: float): int =
|
||||
## Bin a signed angular error (radians) into one of `nClasses` bins over
|
||||
## [−maxDeg, +maxDeg]. Retained for the registration guard test; inert.
|
||||
let x = radToDeg(errRad)
|
||||
@@ -267,45 +368,50 @@ proc bbClassOf*(errRad: float, nClasses: int, maxDeg: float): int =
|
||||
if k >= nClasses: k = nClasses - 1
|
||||
k
|
||||
|
||||
proc bbBandOf*(range: float): int {.inline.} =
|
||||
proc lgBandOf*(range: float): int {.inline.} =
|
||||
## Range band (the ruler's bands), known causally at fire time.
|
||||
for b in 0 ..< BB_NBANDS:
|
||||
if range >= BB_BAND_LO[b] and range < BB_BAND_HI[b]: return b
|
||||
BB_NBANDS - 1
|
||||
for b in 0 ..< LG_NBANDS:
|
||||
if range >= LG_BAND_LO[b] and range < LG_BAND_HI[b]: return b
|
||||
LG_NBANDS - 1
|
||||
|
||||
proc bbTolDeg*(range: float): float {.inline.} =
|
||||
proc lgTolDeg*(range: float): float {.inline.} =
|
||||
## The target's angular half-width at `range` — atan(18/range) — i.e. the exact
|
||||
## tolerance the offline ruler uses for its hit-probability proxy.
|
||||
radToDeg(arctan2(BB_BB_RADIUS, max(range, 1e-9)))
|
||||
radToDeg(arctan2(LG_BOT_RADIUS, max(range, 1e-9)))
|
||||
|
||||
# ── construction / lazy init ─────────────────────────────────────────────────
|
||||
|
||||
proc initBitBrainGun*(): BitBrainGun =
|
||||
result.nClasses = clamp(envIntBB(BB_N_ENV, BB_N_DEF), 2, 512)
|
||||
result.nAde = clamp(envIntBB(BB_NADE_ENV, BB_NADE_DEF), 8, 4096)
|
||||
result.maxDeg = clamp(envFloatBB(BB_RANGE_ENV, BB_RANGE_DEF), 1.0, 180.0)
|
||||
result.memMode = parseMemMode(getEnv(BB_MEM_ENV, ""))
|
||||
result.logEnabled = envBoolBB(BB_LOG_ENV, false)
|
||||
result.minObs = max(1, envIntBB(BB_MIN_OBS_ENV, BB_MIN_OBS_DEF))
|
||||
result.warmupN = max(0, envIntBB(BB_WARMUP_ENV, BB_WARMUP_DEF))
|
||||
result.adaptEvery = max(1, envIntBB(BB_ADAPT_ENV, BB_ADAPT_DEF))
|
||||
result.calibEvery = max(1, envIntBB(BB_CALIB_ENV, BB_CALIB_DEF))
|
||||
result.decayEvery = max(1, envIntBB(BB_DECAY_ENV, BB_DECAY_DEF))
|
||||
result.decayFrac = clamp(envFloatBB(BB_DECAY_FRAC_ENV, BB_DECAY_FRAC_DEF), 0.0, 1.0)
|
||||
result.seed = int64(envIntBB(BB_SEED_ENV, BB_SEED_DEF))
|
||||
result.resetOnTarget = envBoolBB(BB_RESET_ON_TARGET_ENV, BB_RESET_ON_TARGET_DEF)
|
||||
result.cands = parseGains(getEnv(BB_GAINS_ENV, ""))
|
||||
result.bandN = newSeq[float64](BB_NBANDS)
|
||||
result.bandHits = newSeq[seq[float64]](BB_NBANDS)
|
||||
for b in 0 ..< BB_NBANDS:
|
||||
proc initLeadGainGun*(): LeadGainGun =
|
||||
result.nClasses = clamp(envIntLg(LG_N_ENV, LG_N_DEF), 2, 512)
|
||||
result.nAde = clamp(envIntLg(LG_NADE_ENV, LG_NADE_DEF), 8, 4096)
|
||||
result.maxDeg = clamp(envFloatLg(LG_RANGE_ENV, LG_RANGE_DEF), 1.0, 180.0)
|
||||
result.memMode = parseLgMemMode(lgEnv(LG_MEM_ENV))
|
||||
result.logEnabled = envBoolLg(LG_LOG_ENV, false)
|
||||
result.minObs = max(1, envIntLg(LG_MIN_OBS_ENV, LG_MIN_OBS_DEF))
|
||||
result.warmupN = max(0, envIntLg(LG_WARMUP_ENV, LG_WARMUP_DEF))
|
||||
result.adaptEvery = max(1, envIntLg(LG_ADAPT_ENV, LG_ADAPT_DEF))
|
||||
result.calibEvery = max(1, envIntLg(LG_CALIB_ENV, LG_CALIB_DEF))
|
||||
result.decayEvery = max(1, envIntLg(LG_DECAY_ENV, LG_DECAY_DEF))
|
||||
result.decayFrac = clamp(envFloatLg(LG_DECAY_FRAC_ENV, LG_DECAY_FRAC_DEF), 0.0, 1.0)
|
||||
result.seed = int64(envIntLg(LG_SEED_ENV, LG_SEED_DEF))
|
||||
result.resetOnTarget = envBoolLg(LG_RESET_ON_TARGET_ENV, LG_RESET_ON_TARGET_DEF)
|
||||
result.cands = parseLgGains(lgEnv(LG_GAINS_ENV))
|
||||
result.bandN = newSeq[float64](LG_NBANDS)
|
||||
result.bandHits = newSeq[seq[float64]](LG_NBANDS)
|
||||
for b in 0 ..< LG_NBANDS:
|
||||
result.bandHits[b] = newSeq[float64](result.cands.len)
|
||||
result.lastTick = -1
|
||||
result.lastEnqTick = -1
|
||||
result.lastEnqBucket = -1
|
||||
result.observedTargetId = -1
|
||||
for b in 0 ..< BB_NBANDS: result.lastGain[b] = 1.0
|
||||
for b in 0 ..< LG_NBANDS: result.lastGain[b] = 1.0
|
||||
# ONE deprecation line per process, naming the new `TR_LEADGAIN_*` names.
|
||||
let dep = lgDeprecationLine()
|
||||
if dep.len > 0 and not deprecationShown:
|
||||
deprecationShown = true
|
||||
stderr.writeLine(dep)
|
||||
|
||||
proc ensureInit*(g: var BitBrainGun) =
|
||||
proc ensureInit*(g: var LeadGainGun) =
|
||||
## Build the observation ring on first use. No network, no global-RNG use, so
|
||||
## the shipped default path is untouched and construction stays cheap.
|
||||
if g.initialized: return
|
||||
@@ -314,7 +420,7 @@ proc ensureInit*(g: var BitBrainGun) =
|
||||
|
||||
# ── the gain learner ─────────────────────────────────────────────────────────
|
||||
|
||||
proc bbAccumulate(g: var BitBrainGun, leadDeg, reqDeg, tolDeg: float, band: int) =
|
||||
proc lgAccumulate(g: var LeadGainGun, leadDeg, reqDeg, tolDeg: float, band: int) =
|
||||
## Score every candidate gain on this resolved sample: a candidate "hits" when
|
||||
## it would have put the aim within the target's angular half-width.
|
||||
for ci in 0 ..< g.cands.len:
|
||||
@@ -323,23 +429,23 @@ proc bbAccumulate(g: var BitBrainGun, leadDeg, reqDeg, tolDeg: float, band: int)
|
||||
g.bandN[band] += 1.0
|
||||
inc g.trained
|
||||
|
||||
proc bbApplyDecay(g: var BitBrainGun) =
|
||||
## Forgetting for `TR_BITBRAIN_MEM=decay`: shrink the hit counts and, more
|
||||
proc lgApplyDecay(g: var LeadGainGun) =
|
||||
## Forgetting for `TR_LEADGAIN_MEM=decay`: shrink the hit counts and, more
|
||||
## strongly, pull them toward the gain-1.0 column so stale evidence ages out.
|
||||
let f = 1.0 - g.decayFrac
|
||||
if f >= 1.0: return
|
||||
for b in 0 ..< BB_NBANDS:
|
||||
for b in 0 ..< LG_NBANDS:
|
||||
for ci in 0 ..< g.cands.len:
|
||||
g.bandHits[b][ci] *= f
|
||||
g.bandN[b] *= f
|
||||
inc g.decays
|
||||
|
||||
proc bbGain(g: BitBrainGun, band: int): float =
|
||||
proc lgGainFor(g: LeadGainGun, band: int): float =
|
||||
## The band's gain is the candidate with the highest observed hit rate.
|
||||
## Ties keep the SMALLER candidate (the scan is ascending), which is the
|
||||
## conservative choice for the long-range regime this corrector targets.
|
||||
## Returns 1.0 (Pattern) below the range gate or when the band is cold.
|
||||
if band < BB_GAIN_BAND_MIN: return 1.0
|
||||
if band < LG_GAIN_BAND_MIN: return 1.0
|
||||
if g.cands.len == 0: return 1.0
|
||||
# A single candidate is a FIXED gain: apply it from the first shot, never
|
||||
# consult the counts. This is the no-learning arm of the live sweep.
|
||||
@@ -357,7 +463,7 @@ proc bbGain(g: BitBrainGun, band: int): float =
|
||||
|
||||
# ── deferred-label resolution (prequential learning) ─────────────────────────
|
||||
|
||||
proc resolvePending(g: var BitBrainGun, state: WorldState) =
|
||||
proc resolvePending(g: var LeadGainGun, state: WorldState) =
|
||||
var w = 0
|
||||
for i in 0 ..< g.pendingCount:
|
||||
let p = g.pending[i]
|
||||
@@ -368,12 +474,12 @@ proc resolvePending(g: var BitBrainGun, state: WorldState) =
|
||||
elif due == state.tick:
|
||||
let obs = tmhObservedAt(g.tmh, state.tick, p.selfX, p.selfY)
|
||||
if obs.ok and (state.tick - obs.lastSeenTick) <= TMH_STALE_MAX:
|
||||
let err = wrapRadBB(obs.bearing - p.baseBearing)
|
||||
let reqLead = wrapRadBB(err + p.lead)
|
||||
g.bbAccumulate(radToDeg(p.lead), radToDeg(reqLead), p.tolDeg, p.band)
|
||||
let err = wrapRadLg(obs.bearing - p.baseBearing)
|
||||
let reqLead = wrapRadLg(err + p.lead)
|
||||
g.lgAccumulate(radToDeg(p.lead), radToDeg(reqLead), p.tolDeg, p.band)
|
||||
inc g.sinceDecay
|
||||
if g.memMode == bmDecay and g.sinceDecay >= g.decayEvery:
|
||||
g.bbApplyDecay()
|
||||
if g.memMode == lgDecay and g.sinceDecay >= g.decayEvery:
|
||||
g.lgApplyDecay()
|
||||
g.sinceDecay = 0
|
||||
else:
|
||||
inc g.pendingDropped
|
||||
@@ -383,8 +489,8 @@ proc resolvePending(g: var BitBrainGun, state: WorldState) =
|
||||
|
||||
# ── logging ──────────────────────────────────────────────────────────────────
|
||||
|
||||
proc bbLog(g: var BitBrainGun, state: WorldState, band: int, gain, leadDeg: float) =
|
||||
## ONE change-gated `[bb]` line (behind TR_BITBRAIN_LOG=1) so a user tailing
|
||||
proc lgLog(g: var LeadGainGun, state: WorldState, band: int, gain, leadDeg: float) =
|
||||
## ONE change-gated `[lg]` line (behind TR_LEADGAIN_LOG=1) so a user tailing
|
||||
## the GUI log sees the gain the corrector is applying. The APPLIED gain and
|
||||
## the resulting angular `shift` are both on the line: the boot report proves
|
||||
## the knob reached the process, this proves the gun actually used it.
|
||||
@@ -396,14 +502,14 @@ proc bbLog(g: var BitBrainGun, state: WorldState, band: int, gain, leadDeg: floa
|
||||
var rate = 0.0
|
||||
for ci in 0 ..< g.cands.len:
|
||||
if abs(g.cands[ci] - gain) < 1e-9: rate = g.bandHits[band][ci] / max(1.0, g.bandN[band])
|
||||
echo fmt"[bb] t={state.tick} band={BB_BAND_LO[band]:.0f}+ gain={gain:.2f} " &
|
||||
echo fmt"[lg] t={state.tick} band={LG_BAND_LO[band]:.0f}+ gain={gain:.2f} " &
|
||||
fmt"shift={shiftDeg:+.2f}deg rate={rate:.3f} n={g.bandN[band]:.0f} " &
|
||||
fmt"ncand={g.cands.len} trained={g.trained} " &
|
||||
fmt"pend={g.pendingCount} dropped={g.pendingDropped} mode={memModeName(g.memMode)}"
|
||||
|
||||
# ── reset hooks (mirroring TmHorizonGun) ─────────────────────────────────────
|
||||
|
||||
proc resetRound(g: var BitBrainGun) =
|
||||
proc resetRound(g: var LeadGainGun) =
|
||||
## PER-ROUND reset: observation ring, deferred labels and per-tick caches (the
|
||||
## bots teleport between rounds). The gain counts are deliberately KEPT — they
|
||||
## are battle-scale and a new round is not a new enemy.
|
||||
@@ -414,14 +520,14 @@ proc resetRound(g: var BitBrainGun) =
|
||||
g.lastEnqBucket = -1
|
||||
g.lastLogKey = ""
|
||||
|
||||
proc resetRoundState*(g: var BitBrainGun) =
|
||||
proc resetRoundState*(g: var LeadGainGun) =
|
||||
if not g.initialized: return
|
||||
g.resetRound()
|
||||
|
||||
proc resetLearning*(g: var BitBrainGun, reason = "") =
|
||||
proc resetLearning*(g: var LeadGainGun, reason = "") =
|
||||
## PER-BATTLE / PER-ENEMY wipe: gain counts, counters and the round state.
|
||||
if not g.initialized: return
|
||||
for b in 0 ..< BB_NBANDS:
|
||||
for b in 0 ..< LG_NBANDS:
|
||||
for ci in 0 ..< g.cands.len: g.bandHits[b][ci] = 0.0
|
||||
g.bandN[b] = 0.0
|
||||
g.lastGain[b] = 1.0
|
||||
@@ -432,9 +538,9 @@ proc resetLearning*(g: var BitBrainGun, reason = "") =
|
||||
g.observedTargetId = -1
|
||||
g.resetRound()
|
||||
if reason.len > 0 and g.logEnabled:
|
||||
echo fmt"[bb-reset] reason={reason}"
|
||||
echo fmt"[lg-reset] reason={reason}"
|
||||
|
||||
proc targetChanged*(g: var BitBrainGun, enemyId: int): bool =
|
||||
proc targetChanged*(g: var LeadGainGun, enemyId: int): bool =
|
||||
## Per-ENEMY reset: wipe when the target changes to a different bot id. First
|
||||
## acquisition never wipes, so the round-start pick does not cold-start us.
|
||||
if not g.resetOnTarget: return false
|
||||
@@ -448,13 +554,13 @@ proc targetChanged*(g: var BitBrainGun, enemyId: int): bool =
|
||||
|
||||
# ── Gun interface ────────────────────────────────────────────────────────────
|
||||
|
||||
proc isWarmedUp*(g: BitBrainGun): bool {.inline.} = true
|
||||
proc isWarmedUp*(g: LeadGainGun): bool {.inline.} = true
|
||||
|
||||
proc networkBytes*(g: BitBrainGun): int =
|
||||
proc networkBytes*(g: LeadGainGun): int =
|
||||
## No neural network is held any more; kept for the boot report / guard test.
|
||||
0
|
||||
|
||||
proc predict*(g: var BitBrainGun, state: WorldState,
|
||||
proc predict*(g: var LeadGainGun, state: WorldState,
|
||||
bulletSpeed: float): GunPrediction =
|
||||
g.ensureInit()
|
||||
|
||||
@@ -467,27 +573,27 @@ proc predict*(g: var BitBrainGun, state: WorldState,
|
||||
g.resolvePending(state)
|
||||
g.lastTick = state.tick
|
||||
|
||||
# The base prediction is Pattern; BitBrain only scales its lead over LOS.
|
||||
# The base prediction is Pattern; LEADGAIN only scales its lead over LOS.
|
||||
let base = g.tmh.pattern.predict(state, bulletSpeed)
|
||||
if bulletSpeed <= 0.0: return base
|
||||
|
||||
let dist = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY)
|
||||
let h = tmhHorizonFor(dist, bulletSpeed)
|
||||
let hb = tmhHorizonBucket(h)
|
||||
let band = bbBandOf(dist)
|
||||
let band = lgBandOf(dist)
|
||||
|
||||
let los = arctan2(state.enemyY - state.selfY, state.enemyX - state.selfX)
|
||||
let baseBearing = arctan2(base.y - state.selfY, base.x - state.selfX)
|
||||
let lead = wrapRadBB(baseBearing - los)
|
||||
let lead = wrapRadLg(baseBearing - los)
|
||||
|
||||
# Enqueue one deferred sample per (tick, horizon bucket): `predict` runs once
|
||||
# per power bin, so all four horizons contribute evidence.
|
||||
if g.lastEnqTick != state.tick or g.lastEnqBucket != hb:
|
||||
if g.pendingCount < BB_PENDING_CAP:
|
||||
g.pending[g.pendingCount] = BbPending(
|
||||
if g.pendingCount < LG_PENDING_CAP:
|
||||
g.pending[g.pendingCount] = LgPending(
|
||||
fireTick: state.tick, horizon: h, band: band,
|
||||
selfX: state.selfX, selfY: state.selfY,
|
||||
baseBearing: baseBearing, lead: lead, tolDeg: bbTolDeg(dist))
|
||||
baseBearing: baseBearing, lead: lead, tolDeg: lgTolDeg(dist))
|
||||
inc g.pendingCount
|
||||
else:
|
||||
inc g.pendingDropped
|
||||
@@ -496,15 +602,15 @@ proc predict*(g: var BitBrainGun, state: WorldState,
|
||||
|
||||
# Readout: a fractional gain may be BELOW 1.0. When cold / gated out the
|
||||
# learner returns 1.0 and the base prediction is returned unchanged.
|
||||
let gain = g.bbGain(band)
|
||||
let gain = g.lgGainFor(band)
|
||||
g.lastGain[band] = gain
|
||||
if abs(gain - 1.0) < 1e-9: return base
|
||||
inc g.corrections
|
||||
g.bbLog(state, band, gain, radToDeg(lead))
|
||||
g.lgLog(state, band, gain, radToDeg(lead))
|
||||
tmhApplyShift(state.selfX, state.selfY, base.x, base.y,
|
||||
radToDeg((gain - 1.0) * lead))
|
||||
|
||||
proc onResult*(g: var BitBrainGun, e: FeedbackEvent) =
|
||||
proc onResult*(g: var LeadGainGun, e: FeedbackEvent) =
|
||||
## Labels come from our own observation ring, not from virtual-bullet
|
||||
## feedback, so there is nothing to do here. The hook exists for the rack.
|
||||
discard
|
||||
@@ -3,7 +3,7 @@
|
||||
## Reads the recorded live-vs-real-DrussGT corpus, drives each arm over the
|
||||
## recorded enemy trajectory, and scores every tick×power-bin prediction against
|
||||
## the aim-independent interception point (see prediction_quality.nim). Owns the
|
||||
## RANGE-BAND table that is "the bar" for the BitBrain campaign.
|
||||
## RANGE-BAND table that is "the bar" for the lead-gain campaign.
|
||||
##
|
||||
## NO CLOSED-LOOP CLAIM IS MADE HERE. Every arm below is an open-loop prediction
|
||||
## scored on a FIXED trajectory. Wins, damage and survival are decided live.
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import std/[os, strformat, strutils, times, math]
|
||||
import gun_harness/[gun_interface, virtual_bullets, prediction_quality]
|
||||
import guns/[head_on, pattern_matcher, tm_horizon, bitbrain_gun]
|
||||
import guns/[head_on, pattern_matcher, tm_horizon, lead_gain]
|
||||
|
||||
# arm indices (fixed order = fixed output)
|
||||
const
|
||||
@@ -29,7 +29,7 @@ const
|
||||
A_G30* = 6
|
||||
A_NAIVE* = 7
|
||||
A_TMH* = 8
|
||||
A_BB* = 9
|
||||
A_LG* = 9
|
||||
# ── Phase 1: the MISSING gain sweep. Gains >= 1 were measured worse at every
|
||||
# band in Phase 0; the unexplored region is gain < 1. gain 0.0 is HeadOn
|
||||
# (A_HEADON) and gain 1.0 is Pattern (A_PATTERN), so only 0.25/0.50/0.75 are
|
||||
@@ -40,12 +40,12 @@ const
|
||||
A_G075* = 12
|
||||
# Phase 1 fixed causal per-band gain rule: the hitProxy-argmax curve measured by
|
||||
# the sub-unity sweep ([1,1,1,0,0] == Pattern below 300 px, HeadOn above). This
|
||||
# is the rule BitBrain must match; it needs no learning (range is known at fire
|
||||
# is the rule the corrector must match; it needs no learning (range is known at fire
|
||||
# time). The table was selected in-sample from this corpus.
|
||||
A_BAND* = 13
|
||||
BandGainTable* = [1.0, 1.0, 1.0, 0.0, 0.0]
|
||||
ArmNames* = ["Oracle", "OracleQuant", "HeadOn", "Pattern", "PatternGain1.5",
|
||||
"PatternGain2.0", "PatternGain3.0", "NaiveLinear", "TMHorizon", "BitBrain",
|
||||
"PatternGain2.0", "PatternGain3.0", "NaiveLinear", "TMHorizon", "LeadGain",
|
||||
"PatternGain0.25", "PatternGain0.50", "PatternGain0.75", "PatternBandGain"]
|
||||
|
||||
const
|
||||
@@ -95,7 +95,7 @@ type Ctx = object
|
||||
pattern: PatternMatcherGun
|
||||
naive: NaiveLinearGun
|
||||
tmh: TmHorizonGun
|
||||
bb: BitBrainGun
|
||||
lg: LeadGainGun
|
||||
headon: HeadOnGun
|
||||
st: WorldState
|
||||
enemy: seq[EnemyInfo]
|
||||
@@ -169,10 +169,10 @@ proc runRound(ctx: var Ctx, arms: var seq[ArmAcc], r: int) =
|
||||
let tp = predict(ctx.tmh, ctx.st, speed)
|
||||
let tl = wrap180(bearingDeg(ox, oy, tp.x, tp.y) - los)
|
||||
arms[A_TMH].record(rng, wrap180(tl - targetLead), tl, targetLead)
|
||||
# BitBrain (base Pattern + ADE/SBC corrector)
|
||||
let bp = predict(ctx.bb, ctx.st, speed)
|
||||
# LEADGAIN (Pattern base + per-band learned lead gain)
|
||||
let bp = predict(ctx.lg, ctx.st, speed)
|
||||
let bl = wrap180(bearingDeg(ox, oy, bp.x, bp.y) - los)
|
||||
arms[A_BB].record(rng, wrap180(bl - targetLead), bl, targetLead)
|
||||
arms[A_LG].record(rng, wrap180(bl - targetLead), bl, targetLead)
|
||||
|
||||
proc runOne(runPath: string, arms: var seq[ArmAcc], shotsCont, shotsQuant: var ShotStat,
|
||||
doShots: bool, timing: bool, cont: bool): int =
|
||||
@@ -185,7 +185,7 @@ proc runOne(runPath: string, arms: var seq[ArmAcc], shotsCont, shotsQuant: var S
|
||||
pattern: PatternMatcherGun(),
|
||||
naive: NaiveLinearGun(lastTick: -1),
|
||||
tmh: initTmHorizonGun(),
|
||||
bb: initBitBrainGun(),
|
||||
lg: initLeadGainGun(),
|
||||
headon: HeadOnGun(),
|
||||
st: WorldState(arenaWidth: c.arenaW, arenaHeight: c.arenaH),
|
||||
enemy: newSeq[EnemyInfo](1))
|
||||
@@ -295,10 +295,10 @@ proc main() =
|
||||
let mHead = overallMean(arms, A_HEADON)
|
||||
let mPat = overallMean(arms, A_PATTERN)
|
||||
let mTmh = overallMean(arms, A_TMH)
|
||||
let mBb = overallMean(arms, A_BB)
|
||||
let mLg = overallMean(arms, A_LG)
|
||||
let mLin = overallMean(arms, A_NAIVE)
|
||||
let ordOk = mHead > mPat and mHead > mTmh and mHead > mBb
|
||||
echo fmt"3. HeadOn (static LOS) mean|err| = {mHead:.3f} deg vs Pattern {mPat:.3f} / TMHorizon {mTmh:.3f} / BitBrain {mBb:.3f}"
|
||||
let ordOk = mHead > mPat and mHead > mTmh and mHead > mLg
|
||||
echo fmt"3. HeadOn (static LOS) mean|err| = {mHead:.3f} deg vs Pattern {mPat:.3f} / TMHorizon {mTmh:.3f} / LeadGain {mLg:.3f}"
|
||||
let ordMsg = if ordOk: "OK (static gun worst among real guns)" else: "UNEXPECTED: a predictive gun is worse than static LOS"
|
||||
echo fmt" -> {ordMsg}"
|
||||
echo fmt" NaiveLinear mean|err| = {mLin:.3f} deg (over-leads; see the lead-gain sweep for why a larger"
|
||||
@@ -317,7 +317,7 @@ proc main() =
|
||||
echo "=".repeat(120)
|
||||
echo "HEADROOM -- the direct answer: how far each arm is from the oracle ceiling, per band"
|
||||
echo "=" .repeat(120)
|
||||
let hdr = "band Pattern n Pattern|err| Pattern hpx Oracle hpx headroom pp naive hpx TMHoriz hpx BitBrain hpx"
|
||||
let hdr = "band Pattern n Pattern|err| Pattern hpx Oracle hpx headroom pp naive hpx TMHoriz hpx LeadGain hpx"
|
||||
echo hdr
|
||||
echo "-".repeat(hdr.len)
|
||||
for b in 0 ..< NBands:
|
||||
@@ -325,7 +325,7 @@ proc main() =
|
||||
let orc = arms[A_ORACLE].bands[b]
|
||||
let hp = pat.hitProxy
|
||||
let ohp = orc.hitProxy
|
||||
echo fmt"{BandLabels[b]:<9} {pat.n:>8} {fmt3(meanAbs(pat)):>12} {fmt4(hp):>12} {fmt4(ohp):>12} {ohp - hp:>13.4f} {fmt4(arms[A_NAIVE].bands[b].hitProxy):>11} {fmt4(arms[A_TMH].bands[b].hitProxy):>12} {fmt4(arms[A_BB].bands[b].hitProxy):>13}"
|
||||
echo fmt"{BandLabels[b]:<9} {pat.n:>8} {fmt3(meanAbs(pat)):>12} {fmt4(hp):>12} {fmt4(ohp):>12} {ohp - hp:>13.4f} {fmt4(arms[A_NAIVE].bands[b].hitProxy):>11} {fmt4(arms[A_TMH].bands[b].hitProxy):>12} {fmt4(arms[A_LG].bands[b].hitProxy):>13}"
|
||||
echo ""
|
||||
echo "hitProxy = fraction of tick-bins aimed within atan(18/range) of the true interception point."
|
||||
echo "headroom pp = oracle hitProxy - Pattern hitProxy = the absolute hit-probability points available"
|
||||
@@ -403,17 +403,17 @@ proc main() =
|
||||
echo fmt" {BandLabels[b]:<9} best gain {GainValues[bestHi]:.2f} hitProxy {bestHp:.4f} vs Pattern {patHp:.4f} => {bestHp-patHp:+.4f} pp"
|
||||
echo curve & "]"
|
||||
echo ""
|
||||
echo "DIRECT COMPARISON — Pattern vs the FIXED causal per-band rule [1,1,1,0.00,0.00] vs BitBrain (learned online):"
|
||||
let hdrd = "band Pattern hpx fixed-band hpx BitBrain hpx fixed-Pat pp BB-Pat pp"
|
||||
echo "DIRECT COMPARISON — Pattern vs the FIXED causal per-band rule [1,1,1,0.00,0.00] vs LeadGain (learned online):"
|
||||
let hdrd = "band Pattern hpx fixed-band hpx LeadGain hpx fixed-Pat pp BB-Pat pp"
|
||||
echo hdrd
|
||||
echo "-".repeat(hdrd.len)
|
||||
for b in 0 ..< NBands:
|
||||
let patHp = arms[A_PATTERN].bands[b].hitProxy
|
||||
let fixHp = arms[A_BAND].bands[b].hitProxy
|
||||
let bbHp = arms[A_BB].bands[b].hitProxy
|
||||
let bbHp = arms[A_LG].bands[b].hitProxy
|
||||
echo fmt"{BandLabels[b]:<9} {patHp:>11.4f} {fixHp:>16.4f} {bbHp:>14.4f} {fixHp-patHp:>+14.4f} {bbHp-patHp:>+10.4f}"
|
||||
echo "fixed-band hpx = the [1,1,1,0,0] table applied causally; it was selected in-sample."
|
||||
echo "BitBrain is learned online from labels inside each run (cold start at gain 1.0)."
|
||||
echo "LeadGain is learned online from labels inside each run (cold start at gain 1.0)."
|
||||
echo ""
|
||||
echo "LEAD CORRELATION PER GAIN (Pearson of applied lead with required lead). Pearson is invariant"
|
||||
echo "under positive scaling, so every g>0 column must be IDENTICAL to Pattern; g=0 has no lead and"
|
||||
@@ -445,7 +445,7 @@ proc main() =
|
||||
let sp = arms[A_PATTERN].bands[b]
|
||||
let sn = arms[A_NAIVE].bands[b]
|
||||
let st = arms[A_TMH].bands[b]
|
||||
let sb = arms[A_BB].bands[b]
|
||||
let sb = arms[A_LG].bands[b]
|
||||
echo fmt"{BandLabels[b]:<9} {fmt3(meanAbs(arms[A_HEADON].bands[b])):>9} {fmt3(meanAbsReq(arms[A_HEADON].bands[b])):>9} {fmt3(meanAbsReq(sp)):>9} {fmt3(captureSlope(sp)):>10} {fmt3(leadCorr(sp)):>10} " &
|
||||
fmt"{fmt3(captureSlope(sn)):>10} {fmt3(leadCorr(sn)):>10} {fmt3(captureSlope(st)):>10} " &
|
||||
fmt"{fmt3(leadCorr(st)):>10} {fmt3(captureSlope(sb)):>10} {fmt3(leadCorr(sb)):>10}"
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
## Default-parity + registration guard for the BitBrain gun (id 16).
|
||||
##
|
||||
## No Java, no battle, no network build. Covers:
|
||||
## * the rack table carries BITBRAIN at id 16 and it defaults to `off`;
|
||||
## * the shipped rack still admits exactly Pattern;
|
||||
## * TR_RACK_BITBRAIN=both is what admits it, and the spawn gate honours it;
|
||||
## * `initBitBrainGun()` is LAZY (no network) and does NOT touch the global
|
||||
## RNG, so the default path cannot perturb the selector's random draws;
|
||||
## * the mode parser defaults to `perRound`.
|
||||
##
|
||||
## Run: nim c -r common_libs/tests/test_bitbrain_registration.nim
|
||||
|
||||
import std/[random, os, math]
|
||||
import gun_harness/virtual_bullets
|
||||
import gun_harness/selector
|
||||
import guns/bitbrain_gun
|
||||
|
||||
const BitBrainId = 16
|
||||
const PatternId = 5
|
||||
|
||||
var failures = 0
|
||||
proc check(name: string, ok: bool) =
|
||||
if ok: echo "PASS: ", name
|
||||
else: echo "FAIL: ", name; inc failures
|
||||
|
||||
proc clearRackEnv() =
|
||||
for name in RackGunNames: delEnv("TR_RACK_" & name)
|
||||
|
||||
proc testTable() =
|
||||
check "rack: BITBRAIN is registered at id 16", RackGunNames[BitBrainId] == "BITBRAIN"
|
||||
check "rack: BITBRAIN defaults to `off`", DefaultRackMembership[BitBrainId] == rmOff
|
||||
var onlyPattern = true
|
||||
for i in 0..<RackGunNames.len:
|
||||
let want = if i == PatternId: rmBoth else: rmOff
|
||||
if DefaultRackMembership[i] != want: onlyPattern = false
|
||||
check "rack: the shipped default is still the onlyPattern rack", onlyPattern
|
||||
check "rack: the default rack admits only Pattern (1v1)",
|
||||
admittedGuns(17, rm1v1, DefaultRackMembership) == @[PatternId]
|
||||
check "gate: BitBrain is NOT spawned under the default rack",
|
||||
not vBulletAdmitted(BitBrainId, rm1v1, DefaultRackMembership, true)
|
||||
|
||||
proc testEnvOverride() =
|
||||
clearRackEnv()
|
||||
putEnv("TR_RACK_BITBRAIN", "both")
|
||||
let m = loadRackMembership()
|
||||
check "env: TR_RACK_BITBRAIN=both admits BitBrain",
|
||||
m[BitBrainId] == rmBoth and
|
||||
vBulletAdmitted(BitBrainId, rm1v1, m, true)
|
||||
check "env: admitting BitBrain leaves Pattern as the only other member",
|
||||
admittedGuns(17, rm1v1, m) == @[PatternId, BitBrainId]
|
||||
clearRackEnv()
|
||||
|
||||
proc testLazyAndRngClean() =
|
||||
delEnv("TR_BITBRAIN_MEM")
|
||||
var g = initBitBrainGun()
|
||||
check "lazy: constructing the gun does NOT build the network (0 bytes)",
|
||||
g.networkBytes == 0
|
||||
check "default: unset TR_BITBRAIN_MEM is perRound",
|
||||
g.memMode == bmPerRound
|
||||
check "parse: retained/decay/unknown",
|
||||
parseMemMode("retained") == bmRetained and
|
||||
parseMemMode("decay") == bmDecay and
|
||||
parseMemMode("banana") == bmPerRound
|
||||
# Global RNG parity: constructing the gun must not consume global randomness.
|
||||
randomize(1234)
|
||||
let a = rand(1_000_000)
|
||||
randomize(1234)
|
||||
var g2 = initBitBrainGun()
|
||||
discard g2
|
||||
let b = rand(1_000_000)
|
||||
check "parity: initBitBrainGun() does not perturb the global RNG", a == b
|
||||
|
||||
proc testGeometry() =
|
||||
check "geometry: class 0 centre is the low edge + half a bin",
|
||||
abs(bbCenterDeg(0, 32, 40.0) - (-40.0 + 0.5 * 80.0 / 32.0)) < 1e-9
|
||||
check "geometry: bbClassOf round-trips the centre",
|
||||
bbClassOf(degToRad(bbCenterDeg(17, 32, 40.0)), 32, 40.0) == 17
|
||||
|
||||
testTable()
|
||||
testEnvOverride()
|
||||
testLazyAndRngClean()
|
||||
testGeometry()
|
||||
|
||||
if failures > 0:
|
||||
echo "\n", failures, " check(s) FAILED"
|
||||
quit(1)
|
||||
echo "\nAll BitBrain registration checks passed."
|
||||
@@ -0,0 +1,175 @@
|
||||
## Backward-compatibility guard for the LEADGAIN rename (gun id 16).
|
||||
##
|
||||
## The owner's live `.env` predates the rename and carries `TR_RACK_BITBRAIN`,
|
||||
## `TR_BITBRAIN_GAINS`, `TR_BITBRAIN_MEM`, `TR_BITBRAIN_LOG`. Those names used to
|
||||
## address the corrector that is now called LEADGAIN, and they MUST keep working,
|
||||
## because the `TR_BITBRAIN_*` prefix now belongs to the new ADE+SBC gun
|
||||
## (`guns/bitbrain_net.nim`, rack id 17). This test pins the disambiguation:
|
||||
##
|
||||
## * `TR_BITBRAIN_NET` unset/0 -> LEGACY MODE: every `TR_BITBRAIN_<X>` in the
|
||||
## frozen `LegacyKnobEnvNames` set is an alias for `TR_LEADGAIN_<X>`, and
|
||||
## `TR_RACK_BITBRAIN` still selects rack id 16.
|
||||
## * `TR_BITBRAIN_NET=1` -> NEW-NETWORK MODE: the `TR_BITBRAIN_*` names
|
||||
## belong to the new gun; this gun reads only `TR_LEADGAIN_<X>`.
|
||||
##
|
||||
## The two name sets are DISJOINT, so the mapping is total and deterministic.
|
||||
## No Java, no battle, no network build.
|
||||
##
|
||||
## Run: nim c -r common_libs/tests/test_lead_gain_legacy.nim
|
||||
|
||||
import std/[os, strutils]
|
||||
import gun_harness/virtual_bullets
|
||||
import gun_harness/selector
|
||||
import guns/lead_gain
|
||||
|
||||
const LeadGainId = 16
|
||||
|
||||
var failures = 0
|
||||
proc check(name: string, ok: bool) =
|
||||
if ok: echo "PASS: ", name
|
||||
else: echo "FAIL: ", name; inc failures
|
||||
|
||||
proc clearEnv() =
|
||||
for suffix in LegacyKnobEnvNames:
|
||||
delEnv(LegacyPrefix & suffix)
|
||||
delEnv(NewPrefix & suffix)
|
||||
delEnv(LegacyRackEnvName)
|
||||
delEnv("TR_RACK_" & RackGunNames[LeadGainId])
|
||||
delEnv(LG_NET_SWITCH_ENV)
|
||||
|
||||
# ── the alias table itself ───────────────────────────────────────────────────
|
||||
|
||||
proc testAliasTableIsDisjoint() =
|
||||
## Every legacy name must be a `TR_BITBRAIN_<X>` whose `X` is in the frozen
|
||||
## set, and the new name must be the `TR_LEADGAIN_<X>` of the same suffix.
|
||||
var ok = LegacyKnobEnvNames.len == 14
|
||||
var suffixes: seq[string]
|
||||
for s in LegacyKnobEnvNames:
|
||||
if s.len == 0 or s in suffixes: ok = false
|
||||
suffixes.add s
|
||||
check "table: 14 unique legacy suffixes", ok
|
||||
check "table: the legacy prefix is TR_BITBRAIN_", LegacyPrefix == "TR_BITBRAIN_"
|
||||
check "table: the new prefix is TR_LEADGAIN_", NewPrefix == "TR_LEADGAIN_"
|
||||
# The corrector's own live knobs must all be in the alias set, otherwise the
|
||||
# owner's `.env` silently stops working.
|
||||
for knob in [LG_MEM_ENV, LG_GAINS_ENV, LG_LOG_ENV, LG_MIN_OBS_ENV, LG_DECAY_ENV,
|
||||
LG_DECAY_FRAC_ENV, LG_RESET_ON_TARGET_ENV, LG_N_ENV, LG_NADE_ENV,
|
||||
LG_RANGE_ENV, LG_WARMUP_ENV, LG_ADAPT_ENV, LG_CALIB_ENV, LG_SEED_ENV]:
|
||||
let suffix = knob[NewPrefix.len .. ^1]
|
||||
if suffix notin LegacyKnobEnvNames: ok = false
|
||||
check "table: every corrector knob has a legacy alias", ok
|
||||
|
||||
# ── the switch ───────────────────────────────────────────────────────────────
|
||||
|
||||
proc testSwitch() =
|
||||
clearEnv()
|
||||
check "switch: unset => legacy mode (new ADE+SBC namespace disowned)",
|
||||
not netSwitchOn()
|
||||
putEnv(LG_NET_SWITCH_ENV, "0")
|
||||
check "switch: 0 => legacy mode", not netSwitchOn()
|
||||
putEnv(LG_NET_SWITCH_ENV, "1")
|
||||
check "switch: 1 => new-network mode", netSwitchOn()
|
||||
putEnv(LG_NET_SWITCH_ENV, "yes")
|
||||
check "switch: yes => new-network mode", netSwitchOn()
|
||||
putEnv(LG_NET_SWITCH_ENV, "banana")
|
||||
check "switch: an unknown value is NOT new-network mode (fails safe)",
|
||||
not netSwitchOn()
|
||||
clearEnv()
|
||||
|
||||
# ── knob aliasing ────────────────────────────────────────────────────────────
|
||||
|
||||
proc testKnobAlias() =
|
||||
clearEnv()
|
||||
# the owner's exact .env
|
||||
putEnv("TR_BITBRAIN_GAINS", "1.0,0.5,0.25")
|
||||
putEnv("TR_BITBRAIN_MEM", "decay")
|
||||
putEnv("TR_BITBRAIN_LOG", "1")
|
||||
var g = initLeadGainGun()
|
||||
check "alias: TR_BITBRAIN_GAINS feeds the candidate set",
|
||||
g.cands == @[0.25, 0.5, 1.0]
|
||||
check "alias: TR_BITBRAIN_MEM=decay is honoured", g.memMode == lgDecay
|
||||
check "alias: TR_BITBRAIN_LOG=1 turns the log on", g.logEnabled
|
||||
# the new name always wins over the legacy alias
|
||||
putEnv("TR_LEADGAIN_GAINS", "0.5,0.75")
|
||||
var g2 = initLeadGainGun()
|
||||
check "alias: the NEW name wins when both are set", g2.cands == @[0.5, 0.75]
|
||||
# the switch disowns the whole legacy namespace
|
||||
putEnv(LG_NET_SWITCH_ENV, "1")
|
||||
delEnv("TR_LEADGAIN_GAINS")
|
||||
var g3 = initLeadGainGun()
|
||||
check "alias: TR_BITBRAIN_NET=1 makes the legacy names inert for this gun",
|
||||
g3.memMode == lgPerRound and not g3.logEnabled and
|
||||
g3.cands == @[0.0, 0.25, 0.5, 0.75, 1.0]
|
||||
# ... and then the NEW names still work
|
||||
putEnv("TR_LEADGAIN_MEM", "retained")
|
||||
var g4 = initLeadGainGun()
|
||||
check "alias: TR_LEADGAIN_* is always read, in both modes",
|
||||
g4.memMode == lgRetained
|
||||
clearEnv()
|
||||
|
||||
# ── rack alias ───────────────────────────────────────────────────────────────
|
||||
|
||||
proc testRackAlias() =
|
||||
clearEnv()
|
||||
putEnv(LegacyRackEnvName, "both")
|
||||
let m = loadRackMembership()
|
||||
check "rack: TR_RACK_BITBRAIN=both still selects LEADGAIN (id 16)",
|
||||
m[LeadGainId] == rmBoth and
|
||||
vBulletAdmitted(LeadGainId, rm1v1, m, true)
|
||||
putEnv(LG_NET_SWITCH_ENV, "1")
|
||||
let m2 = loadRackMembership()
|
||||
check "rack: TR_BITBRAIN_NET=1 hands the name to the new ADE+SBC gun (id 17)",
|
||||
m2[LeadGainId] == rmOff
|
||||
clearEnv()
|
||||
putEnv("TR_RACK_LEADGAIN", "both")
|
||||
putEnv(LegacyRackEnvName, "off")
|
||||
let m3 = loadRackMembership()
|
||||
check "rack: the new name wins over the legacy alias",
|
||||
m3[LeadGainId] == rmBoth
|
||||
clearEnv()
|
||||
|
||||
# ── the deprecation line ─────────────────────────────────────────────────────
|
||||
|
||||
proc testDeprecationLine() =
|
||||
clearEnv()
|
||||
check "depr: no legacy knobs => no deprecation line",
|
||||
lgDeprecationLine().len == 0
|
||||
putEnv("TR_BITBRAIN_GAINS", "1.0")
|
||||
let line = lgDeprecationLine()
|
||||
check "depr: a legacy knob produces exactly one line naming the new name",
|
||||
line.startsWith("[depr]") and
|
||||
line.contains("TR_BITBRAIN_GAINS -> TR_LEADGAIN_GAINS") and
|
||||
line.contains(NewPrefix)
|
||||
putEnv(LG_NET_SWITCH_ENV, "1")
|
||||
check "depr: the line is silent once the switch is flipped",
|
||||
lgDeprecationLine().len == 0
|
||||
clearEnv()
|
||||
|
||||
# ── shipped-default parity ───────────────────────────────────────────────────
|
||||
|
||||
proc testDefaultParity() =
|
||||
clearEnv()
|
||||
let m = loadRackMembership()
|
||||
var onlyPattern = true
|
||||
for i in 0..<RackGunNames.len:
|
||||
let want = if i == 5: rmBoth else: rmOff
|
||||
if m[i] != want: onlyPattern = false
|
||||
check "parity: a clean env still loads the shipped onlyPattern rack", onlyPattern
|
||||
check "parity: the rack is still 17 guns at id 16 = LEADGAIN",
|
||||
RackGunNames.len == 17 and RackGunNames[LeadGainId] == "LEADGAIN"
|
||||
var g = initLeadGainGun()
|
||||
check "parity: a clean env resolves the shipped candidate set",
|
||||
g.cands == @[0.0, 0.25, 0.5, 0.75, 1.0] and g.memMode == lgPerRound
|
||||
clearEnv()
|
||||
|
||||
testAliasTableIsDisjoint()
|
||||
testSwitch()
|
||||
testKnobAlias()
|
||||
testRackAlias()
|
||||
testDeprecationLine()
|
||||
testDefaultParity()
|
||||
|
||||
if failures > 0:
|
||||
echo "\n", failures, " check(s) FAILED"
|
||||
quit(1)
|
||||
echo "\nAll LEADGAIN legacy-alias checks passed."
|
||||
@@ -0,0 +1,89 @@
|
||||
## Default-parity + registration guard for the LEADGAIN gun (id 16).
|
||||
##
|
||||
## No Java, no battle, no network build. Covers:
|
||||
## * the rack table carries LEADGAIN at id 16 and it defaults to `off`;
|
||||
## * the shipped rack still admits exactly Pattern;
|
||||
## * TR_RACK_LEADGAIN=both is what admits it, and the spawn gate honours it;
|
||||
## (the LEGACY TR_RACK_BITBRAIN alias is pinned by
|
||||
## `test_lead_gain_legacy.nim`);
|
||||
## * `initLeadGainGun()` is LAZY (no learner) and does NOT touch the global
|
||||
## RNG, so the default path cannot perturb the selector's random draws;
|
||||
## * the mode parser defaults to `perRound`.
|
||||
##
|
||||
## Run: nim c -r common_libs/tests/test_lead_gain_registration.nim
|
||||
|
||||
import std/[random, os, math]
|
||||
import gun_harness/virtual_bullets
|
||||
import gun_harness/selector
|
||||
import guns/lead_gain
|
||||
|
||||
const LeadGainId = 16
|
||||
const PatternId = 5
|
||||
|
||||
var failures = 0
|
||||
proc check(name: string, ok: bool) =
|
||||
if ok: echo "PASS: ", name
|
||||
else: echo "FAIL: ", name; inc failures
|
||||
|
||||
proc clearRackEnv() =
|
||||
for name in RackGunNames: delEnv("TR_RACK_" & name)
|
||||
|
||||
proc testTable() =
|
||||
check "rack: LEADGAIN is registered at id 16", RackGunNames[LeadGainId] == "LEADGAIN"
|
||||
check "rack: LEADGAIN defaults to `off`", DefaultRackMembership[LeadGainId] == rmOff
|
||||
var onlyPattern = true
|
||||
for i in 0..<RackGunNames.len:
|
||||
let want = if i == PatternId: rmBoth else: rmOff
|
||||
if DefaultRackMembership[i] != want: onlyPattern = false
|
||||
check "rack: the shipped default is still the onlyPattern rack", onlyPattern
|
||||
check "rack: the default rack admits only Pattern (1v1)",
|
||||
admittedGuns(17, rm1v1, DefaultRackMembership) == @[PatternId]
|
||||
check "gate: LEADGAIN is NOT spawned under the default rack",
|
||||
not vBulletAdmitted(LeadGainId, rm1v1, DefaultRackMembership, true)
|
||||
|
||||
proc testEnvOverride() =
|
||||
clearRackEnv()
|
||||
putEnv("TR_RACK_LEADGAIN", "both")
|
||||
let m = loadRackMembership()
|
||||
check "env: TR_RACK_LEADGAIN=both admits LEADGAIN",
|
||||
m[LeadGainId] == rmBoth and
|
||||
vBulletAdmitted(LeadGainId, rm1v1, m, true)
|
||||
check "env: admitting LEADGAIN leaves Pattern as the only other member",
|
||||
admittedGuns(17, rm1v1, m) == @[PatternId, LeadGainId]
|
||||
clearRackEnv()
|
||||
|
||||
proc testLazyAndRngClean() =
|
||||
delEnv("TR_LEADGAIN_MEM"); delEnv("TR_BITBRAIN_MEM")
|
||||
var g = initLeadGainGun()
|
||||
check "lazy: constructing the gun does NOT build the learner (0 bytes)",
|
||||
g.networkBytes == 0
|
||||
check "default: unset TR_LEADGAIN_MEM is perRound",
|
||||
g.memMode == lgPerRound
|
||||
check "parse: retained/decay/unknown",
|
||||
parseLgMemMode("retained") == lgRetained and
|
||||
parseLgMemMode("decay") == lgDecay and
|
||||
parseLgMemMode("banana") == lgPerRound
|
||||
# Global RNG parity: constructing the gun must not consume global randomness.
|
||||
randomize(1234)
|
||||
let a = rand(1_000_000)
|
||||
randomize(1234)
|
||||
var g2 = initLeadGainGun()
|
||||
discard g2
|
||||
let b = rand(1_000_000)
|
||||
check "parity: initLeadGainGun() does not perturb the global RNG", a == b
|
||||
|
||||
proc testGeometry() =
|
||||
check "geometry: class 0 centre is the low edge + half a bin",
|
||||
abs(lgCenterDeg(0, 32, 40.0) - (-40.0 + 0.5 * 80.0 / 32.0)) < 1e-9
|
||||
check "geometry: lgClassOf round-trips the centre",
|
||||
lgClassOf(degToRad(lgCenterDeg(17, 32, 40.0)), 32, 40.0) == 17
|
||||
|
||||
testTable()
|
||||
testEnvOverride()
|
||||
testLazyAndRngClean()
|
||||
testGeometry()
|
||||
|
||||
if failures > 0:
|
||||
echo "\n", failures, " check(s) FAILED"
|
||||
quit(1)
|
||||
echo "\nAll LEADGAIN registration checks passed."
|
||||
Reference in New Issue
Block a user