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:
@@ -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