j142 retire the ADE+SBC gun (rack id 17): the owner watched it, it does not learn, throw it away
Remove guns/bitbrain_net.nim (+README), test_bitbrain_net.nim,
measure_bitbrain_scaling.nim, rack id 17 and all of its plumbing in
selector.nim / ModularBot.nim / env_report.nim, the TR_BITBRAIN_NET switch
and the NEW-NETWORK TR_BITBRAIN_* knobs, and the BitBrainNet arm of
run_prediction_quality.nim.
With id 17 gone there is nothing to disambiguate, so the legacy namespace
becomes the ONLY one: TR_RACK_BITBRAIN always selects id 16 LEADGAIN and
every TR_BITBRAIN_<X> in the frozen 14-suffix alias set always means
TR_LEADGAIN_<X>. The alias layer and its [depr] line stay.
KEPT: the common_libs/bitbrain/ SBC library (learned_surfer imports
bitbrain/sbc), lead_gain at id 16 with env TR_LEADGAIN_* and log tag [lg],
and the c9b6753 crash fix (NumRackGuns widths + test_rack_stat_width).
Tombstone: docs/bitbrain_campaign.md ## RETIRED and one cross-reference line
in docs/gun_campaign.md. Shipped defaults unchanged: clean env -> rack
active 1v1 = PATTERN, movement default strafe.
This commit is contained in:
@@ -1,246 +0,0 @@
|
||||
## SCALING SWEEP for the ADE+SBC gun (rack id 17) — RAM vs ms/tick vs quality.
|
||||
##
|
||||
## THE QUESTION: "how do inference, timing and size of RAM scale with input and
|
||||
## output size?" This answers it on the REAL gun by replaying a recorded live
|
||||
## corpus through it, not from theory.
|
||||
##
|
||||
## For each setting it reports:
|
||||
## RAM — `memoryBytes` (ADs + SBC tensors) and the SBC tensor alone,
|
||||
## with the counted-vs-bitset factor spelled out;
|
||||
## ms/tick — wall time per RECORDED TICK over the real replay, against the
|
||||
## project's 13.16 ms/tick budget. One `predict` per power bin
|
||||
## per tick, which is what the live loop does, so the number
|
||||
## is directly comparable with the budget;
|
||||
## quality — the OFFLINE ruler: mean |angular error| against the true
|
||||
## interception point (`gun_harness/prediction_quality`), plus
|
||||
## the hit proxy (|err| <= atan(18/range)).
|
||||
##
|
||||
## VETO-CAPABLE CHECK ONLY. Per `docs/offline_harness_trust.md` the offline
|
||||
## harness is trustworthy for per-gun, single-tick prediction quality on a FIXED
|
||||
## trajectory and for NOTHING that flows through the closed loop. A win here is
|
||||
## NOT a live win and is never presented as one.
|
||||
##
|
||||
## Usage:
|
||||
## nim c -r -d:release --path:common_libs common_libs/tests/measure_bitbrain_scaling.nim \
|
||||
## [--corpus /tmp/tfil_ab2/out] [--limit N]
|
||||
##
|
||||
## Env knobs are set per SETTING by this program (putEnv), so the sweep is a
|
||||
## pure-env experiment: no recompile between arms.
|
||||
|
||||
import std/[os, strformat, strutils, times, math, sequtils, algorithm]
|
||||
import gun_harness/gun_interface
|
||||
import gun_harness/prediction_quality
|
||||
import gun_harness/virtual_bullets
|
||||
import guns/pattern_matcher
|
||||
import guns/bitbrain_net
|
||||
|
||||
const
|
||||
BudgetMsPerTick* = 13.16 ## the project's live tick budget
|
||||
|
||||
type
|
||||
Setting = object
|
||||
label: string
|
||||
inputWidth: int
|
||||
nClasses: int
|
||||
nAde: int
|
||||
widths: string
|
||||
features: string
|
||||
mode: string
|
||||
|
||||
Row = object
|
||||
s: Setting
|
||||
ramBytes: int
|
||||
inputWidth: int
|
||||
sbcBytes: int
|
||||
adBytes: int
|
||||
msPerTick: float
|
||||
meanAbsDeg: float
|
||||
hitProxy: float
|
||||
patternAbsDeg: float
|
||||
n: int
|
||||
|
||||
# ── the sweep grid ───────────────────────────────────────────────────────────
|
||||
|
||||
const FullFeatures = "epos:4,evel:3,eturn:2,eself:2,dist:5,bear:4,walls:2,bull:2,hzn:4"
|
||||
|
||||
proc smallFeatures(): string = "epos:2,evel:2,eturn:1,eself:1,dist:2,bear:2,walls:1,bull:1,hzn:2"
|
||||
proc largeFeatures(): string = "epos:6,evel:5,eturn:4,eself:4,dist:8,bear:6,walls:3,bull:3,hzn:6"
|
||||
|
||||
proc settings(): seq[Setting] =
|
||||
## Three points along the INPUT axis (classes/geometry held at the default) and
|
||||
## three along the OUTPUT axis (input held at the default 52), all in BOTH
|
||||
## storage modes, because the counted/bitset factor is part of the answer.
|
||||
let base = Setting(label: "default", inputWidth: 0, nClasses: 8, nAde: 256,
|
||||
widths: "4,5,6", features: FullFeatures, mode: "counted")
|
||||
var inp: seq[Setting]
|
||||
for (lbl, feat, w) in [("input-small", smallFeatures(), 0),
|
||||
("input-medium", FullFeatures, 0),
|
||||
("input-large", largeFeatures(), 0)]:
|
||||
inp.add Setting(label: lbl, inputWidth: w, nClasses: 8, nAde: 256,
|
||||
widths: "4,5,6", features: feat, mode: "counted")
|
||||
var outp: seq[Setting]
|
||||
for (lbl, nc) in [("classes-small", 2), ("classes-medium", 8),
|
||||
("classes-large", 64)]:
|
||||
outp.add Setting(label: lbl, inputWidth: 0, nClasses: nc, nAde: 256,
|
||||
widths: "4,5,6", features: FullFeatures, mode: "counted")
|
||||
# the nAde axis is the third one, because RAM is quadratic in it
|
||||
var ade: seq[Setting]
|
||||
for (lbl, n) in [("nAde-small", 64), ("nAde-medium", 256), ("nAde-large", 512)]:
|
||||
ade.add Setting(label: lbl, inputWidth: 0, nClasses: 8, nAde: n,
|
||||
widths: "4,5,6", features: FullFeatures, mode: "counted")
|
||||
var both: seq[Setting]
|
||||
for m in ["bitset", "counted"]:
|
||||
both.add Setting(label: "mode-" & m, inputWidth: 0, nClasses: 8, nAde: 256,
|
||||
widths: "4,5,6", features: FullFeatures, mode: m)
|
||||
result = inp & outp & ade & both
|
||||
discard base
|
||||
|
||||
# ── one arm ──────────────────────────────────────────────────────────────────
|
||||
|
||||
proc applySetting(s: Setting) =
|
||||
for n in BitbrainNetEnvNames: delEnv(n)
|
||||
putEnv(BBN_NET_ENV, "1")
|
||||
putEnv(BBN_FEATURES_ENV, s.features)
|
||||
if s.inputWidth > 0: putEnv(BBN_INPUT_ENV, $s.inputWidth)
|
||||
putEnv(BBN_CLASSES_ENV, $s.nClasses)
|
||||
putEnv(BBN_NADES_ENV, $s.nAde)
|
||||
putEnv(BBN_WIDTHS_ENV, s.widths)
|
||||
putEnv(BBN_MODE_ENV, s.mode)
|
||||
putEnv(BBN_MINOBS_ENV, "1")
|
||||
putEnv(BBN_DECAY_EVERY_ENV, "64")
|
||||
putEnv(BBN_DECAY_SHIFT_ENV, "3")
|
||||
|
||||
# ── the corpus, turned once into a fixed sample set ─────────────────────────
|
||||
#
|
||||
# The interception solve (the ruler) is INDEPENDENT of the arm, so it is done
|
||||
# ONCE and cached. That does two things: every arm is scored on byte-identical
|
||||
# labels, and the timed region contains ONLY the gun's `predict` calls — the
|
||||
# ruler's own cost cannot contaminate the ms/tick number.
|
||||
|
||||
type
|
||||
Sample = object
|
||||
st: WorldState
|
||||
speed: float
|
||||
targetLead: float
|
||||
range: float
|
||||
tol: float
|
||||
|
||||
proc buildSamples(runs: seq[string]): seq[Sample] =
|
||||
for rp in runs:
|
||||
let c = loadCorpus(rp)
|
||||
if c.n == 0: continue
|
||||
for r in 0 ..< c.rStart.len:
|
||||
let base = int(c.rStart[r]) - c.base
|
||||
let cnt = int(c.rCount[r])
|
||||
let iEnd = base + cnt
|
||||
for i in base ..< iEnd:
|
||||
let ox = c.sx(i)
|
||||
let oy = c.sy(i)
|
||||
let localTick = int(c.tick[i]) - int(c.rStart[r])
|
||||
let baseState = WorldState(
|
||||
arenaWidth: c.arenaW, arenaHeight: c.arenaH, tick: localTick,
|
||||
enemyX: c.ex(i), enemyY: c.ey(i), enemyHeading: c.eh(i),
|
||||
enemySpeed: c.es(i), enemyEnergy: c.ee(i),
|
||||
selfX: ox, selfY: oy, selfHeading: c.sh(i), selfSpeed: c.ss(i),
|
||||
selfEnergy: c.se(i), selfRadarHeading: c.sh(i))
|
||||
let los = bearingDeg(ox, oy, c.ex(i), c.ey(i))
|
||||
for bin in 0 ..< len(PowerBins):
|
||||
let speed = bulletSpeed(PowerBins[bin])
|
||||
let ib = interceptBearing(c, i, iEnd, ox, oy, speed, true)
|
||||
if not ib.ok: continue
|
||||
result.add Sample(st: baseState, speed: speed,
|
||||
targetLead: wrap180(ib.bearing - los),
|
||||
range: ib.range, tol: tolDeg(ib.range))
|
||||
|
||||
proc runArm(s: Setting, samples: seq[Sample]): Row =
|
||||
applySetting(s)
|
||||
var g = initBitbrainNetGun()
|
||||
var pat = PatternMatcherGun()
|
||||
var sumAbs = 0.0
|
||||
var sumPat = 0.0
|
||||
var hits = 0
|
||||
var n = 0
|
||||
# `samples` is ordered round-by-round, so the gun sees rounds in order.
|
||||
var t0 = epochTime()
|
||||
for smp in samples:
|
||||
let bp = predict(g, smp.st, smp.speed)
|
||||
let los = bearingDeg(smp.st.selfX, smp.st.selfY,
|
||||
smp.st.enemyX, smp.st.enemyY)
|
||||
let bl = wrap180(bearingDeg(smp.st.selfX, smp.st.selfY, bp.x, bp.y) - los)
|
||||
let err = abs(wrap180(bl - smp.targetLead))
|
||||
sumAbs += err
|
||||
if err <= smp.tol: inc hits
|
||||
let pp = predict(pat, smp.st, smp.speed)
|
||||
let pl = wrap180(bearingDeg(smp.st.selfX, smp.st.selfY, pp.x, pp.y) - los)
|
||||
sumPat += abs(wrap180(pl - smp.targetLead))
|
||||
inc n
|
||||
let elapsed = epochTime() - t0
|
||||
# ms per recorded TICK: `n` samples over `len(PowerBins)` samples per tick.
|
||||
let ticks = max(1, samples.len div len(PowerBins))
|
||||
result = Row(s: s, inputWidth: g.inputWidth, ramBytes: g.networkBytes(),
|
||||
sbcBytes: g.sbcBytes(),
|
||||
adBytes: g.networkBytes() - g.sbcBytes(),
|
||||
msPerTick: elapsed * 1000.0 / float(ticks),
|
||||
meanAbsDeg: if n > 0: sumAbs / float(n) else: NaN,
|
||||
hitProxy: if n > 0: float(hits) / float(n) else: NaN,
|
||||
patternAbsDeg: if n > 0: sumPat / float(n) else: NaN,
|
||||
n: n)
|
||||
|
||||
# ── driver ───────────────────────────────────────────────────────────────────
|
||||
|
||||
proc main() =
|
||||
var corpusRoot = "/tmp/tfil_ab2/out"
|
||||
var limit = 4
|
||||
var i = 1
|
||||
while i <= paramCount():
|
||||
case paramStr(i)
|
||||
of "--corpus": inc i; corpusRoot = paramStr(i)
|
||||
of "--limit": inc i; limit = parseInt(paramStr(i))
|
||||
else: stderr.writeLine("unknown arg: " & paramStr(i)); quit(2)
|
||||
inc i
|
||||
var runs = discoverRuns(corpusRoot)
|
||||
if limit > 0 and runs.len > limit: runs.setLen(limit)
|
||||
if runs.len == 0:
|
||||
stderr.writeLine("no runs under " & corpusRoot); quit(1)
|
||||
|
||||
echo "=".repeat(118)
|
||||
echo "BITBRAIN (ADE+SBC, rack id 17) SCALING -- RAM / ms-per-tick / offline prediction quality"
|
||||
echo "=".repeat(118)
|
||||
echo fmt"corpus : {corpusRoot} ({runs.len} recorded run(s))"
|
||||
echo fmt"nAde : ADEs per address decoder; the SBC tensor is nAde^2 x nClasses"
|
||||
echo " cells, so RAM is QUADRATIC in nAde and LINEAR in nClasses."
|
||||
echo fmt"budget : {BudgetMsPerTick} ms/tick (one predict per power bin per recorded tick)"
|
||||
echo "quality : mean |angular error| vs the true interception point, over every"
|
||||
echo " tick x power-bin. VETO-CAPABLE OFFLINE CHECK ONLY (docs/offline_harness_trust.md):"
|
||||
echo " a win here is NOT a live win."
|
||||
echo ""
|
||||
stderr.writeLine("building the ruler sample set once from " & $runs.len &
|
||||
" run(s)...")
|
||||
let tSamp = epochTime()
|
||||
let samples = buildSamples(runs)
|
||||
echo fmt"sample set : {samples.len} tick x power-bin samples " &
|
||||
fmt"({samples.len div max(1, len(PowerBins))} recorded ticks) in " &
|
||||
fmt"{epochTime()-tSamp:.1f}s — arm-independent, so the timed region below"
|
||||
echo " contains ONLY the gun's predict calls."
|
||||
echo ""
|
||||
var rows: seq[Row]
|
||||
for s in settings(): rows.add runArm(s, samples)
|
||||
# Pattern is the same on every arm (it never reads the net's knobs), so the
|
||||
# Pattern column is taken from one arm and is identical for all of them.
|
||||
let patRef = rows[1]
|
||||
|
||||
echo "arm in nCl nAde mode RAM B SBC B AD B " &
|
||||
"ms/tick %bud mean|err| hit% Pattern|err|"
|
||||
echo "-".repeat(118)
|
||||
for r in rows:
|
||||
echo fmt"{r.s.label:<15} {r.inputWidth:>4} {r.s.nClasses:>5} " &
|
||||
fmt"{r.s.nAde:>5} {r.s.mode:<8} {r.ramBytes:>8} {r.sbcBytes:>9} " &
|
||||
fmt"{r.adBytes:>7} {r.msPerTick:>8.3f} {100.0*r.msPerTick/BudgetMsPerTick:>6.2f} " &
|
||||
fmt"{r.meanAbsDeg:>10.3f} {100.0*r.hitProxy:>6.2f} {patRef.patternAbsDeg:>13.3f}"
|
||||
echo ""
|
||||
echo "(in = the resolved input width, i.e. the CONFIGURED feature-block total;"
|
||||
echo " Pattern|err| is the shipped Pattern gun on the same ticks and is identical"
|
||||
echo " across arms, since Pattern never reads any of these knobs.)"
|
||||
for n in BitbrainNetEnvNames: delEnv(n)
|
||||
|
||||
main()
|
||||
@@ -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, lead_gain, bitbrain_net]
|
||||
import guns/[head_on, pattern_matcher, tm_horizon, lead_gain]
|
||||
|
||||
# arm indices (fixed order = fixed output)
|
||||
const
|
||||
@@ -43,16 +43,12 @@ const
|
||||
# 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
|
||||
## The REAL ADE+SBC gun (rack id 17, `guns/bitbrain_net.nim`): Pattern base plus
|
||||
## a fine-grained, class-resolved angular correction from the network. The gun
|
||||
## is default OFF (`TR_BITBRAIN_NET=0`), so the arm turns its own switch on
|
||||
## here — the ruler is the one place that must exercise it.
|
||||
A_BBN* = 14
|
||||
BandGainTable* = [1.0, 1.0, 1.0, 0.0, 0.0]
|
||||
## (The ADE+SBC gun that used to be arm 14 — rack id 17, `guns/bitbrain_net.nim`
|
||||
## — was RETIRED and removed. See `docs/bitbrain_campaign.md` §RETIRED.)
|
||||
ArmNames* = ["Oracle", "OracleQuant", "HeadOn", "Pattern", "PatternGain1.5",
|
||||
"PatternGain2.0", "PatternGain3.0", "NaiveLinear", "TMHorizon", "LeadGain",
|
||||
"PatternGain0.25", "PatternGain0.50", "PatternGain0.75", "PatternBandGain",
|
||||
"BitBrainNet"]
|
||||
"PatternGain0.25", "PatternGain0.50", "PatternGain0.75", "PatternBandGain"]
|
||||
|
||||
const
|
||||
## The five sub-unity gain arms, in increasing order, resolved to arm indices.
|
||||
@@ -102,7 +98,6 @@ type Ctx = object
|
||||
naive: NaiveLinearGun
|
||||
tmh: TmHorizonGun
|
||||
lg: LeadGainGun
|
||||
bbn: BitbrainNetGun
|
||||
headon: HeadOnGun
|
||||
st: WorldState
|
||||
enemy: seq[EnemyInfo]
|
||||
@@ -180,10 +175,6 @@ proc runRound(ctx: var Ctx, arms: var seq[ArmAcc], r: int) =
|
||||
let bp = predict(ctx.lg, ctx.st, speed)
|
||||
let bl = wrap180(bearingDeg(ox, oy, bp.x, bp.y) - los)
|
||||
arms[A_LG].record(rng, wrap180(bl - targetLead), bl, targetLead)
|
||||
# BITBRAIN (ADE+SBC): Pattern base + the network's fine-grained correction
|
||||
let np2 = predict(ctx.bbn, ctx.st, speed)
|
||||
let nl2 = wrap180(bearingDeg(ox, oy, np2.x, np2.y) - los)
|
||||
arms[A_BBN].record(rng, wrap180(nl2 - targetLead), nl2, targetLead)
|
||||
|
||||
proc runOne(runPath: string, arms: var seq[ArmAcc], shotsCont, shotsQuant: var ShotStat,
|
||||
doShots: bool, timing: bool, cont: bool): int =
|
||||
@@ -197,7 +188,6 @@ proc runOne(runPath: string, arms: var seq[ArmAcc], shotsCont, shotsQuant: var S
|
||||
naive: NaiveLinearGun(lastTick: -1),
|
||||
tmh: initTmHorizonGun(),
|
||||
lg: initLeadGainGun(),
|
||||
bbn: initBitbrainNetGun(),
|
||||
headon: HeadOnGun(),
|
||||
st: WorldState(arenaWidth: c.arenaW, arenaHeight: c.arenaH),
|
||||
enemy: newSeq[EnemyInfo](1))
|
||||
@@ -313,10 +303,9 @@ proc main() =
|
||||
let mPat = overallMean(arms, A_PATTERN)
|
||||
let mTmh = overallMean(arms, A_TMH)
|
||||
let mLg = overallMean(arms, A_LG)
|
||||
let mBbn = overallMean(arms, A_BBN)
|
||||
let mLin = overallMean(arms, A_NAIVE)
|
||||
let ordOk = mHead > mPat and mHead > mTmh and mHead > mLg and mHead > mBbn
|
||||
echo fmt"3. HeadOn (static LOS) mean|err| = {mHead:.3f} deg vs Pattern {mPat:.3f} / TMHorizon {mTmh:.3f} / LeadGain {mLg:.3f} / BitBrainNet {mBbn:.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"
|
||||
@@ -335,7 +324,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 LeadGain hpx BitBrainNet 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:
|
||||
@@ -343,10 +332,8 @@ 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_LG].bands[b].hitProxy):>13} {fmt4(arms[A_BBN].bands[b].hitProxy):>15}"
|
||||
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 "BitBrainNet hpx = the ADE+SBC gun (rack id 17), Pattern base + a class-resolved"
|
||||
echo "angular correction. VETO-CAPABLE OFFLINE CHECK ONLY, never a live claim."
|
||||
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"
|
||||
echo "to a perfect predictor (the campaign is playing for a slice of this)."
|
||||
|
||||
@@ -1,325 +0,0 @@
|
||||
## Does the BITBRAIN gun (rack id 17) actually ENGAGE the ADE+SBC network?
|
||||
##
|
||||
## Linking is not engagement. These checks drive the real gun over synthetic
|
||||
## states and assert the observable consequences of a live network:
|
||||
## * the network is BUILT on first predict and is 0 bytes before that (lazy);
|
||||
## * two DIFFERENT inputs give DIFFERENT class outputs (the ADE layer is
|
||||
## discriminating, not a constant);
|
||||
## * learning a sample CHANGES the memory (the SBC head is writing);
|
||||
## * `learn` is idempotent on a bitset SBC (setting the same class twice is a
|
||||
## no-op) and monotone on a counted one;
|
||||
## * the shipped knobs really move RAM and the derived geometry;
|
||||
## * the input is the CONFIGURED block set: a disabled block shrinks it, a
|
||||
## wider block grows it, and `TR_BITBRAIN_INPUT` pins it exactly;
|
||||
## * construction is RNG-clean (the default path cannot perturb the selector).
|
||||
##
|
||||
## No Java, no battle, no fixtures.
|
||||
##
|
||||
## Run: nim c -r --path:common_libs common_libs/tests/test_bitbrain_net.nim
|
||||
|
||||
import std/[os, random, math, strutils, sequtils]
|
||||
import gun_harness/gun_interface
|
||||
import gun_harness/virtual_bullets
|
||||
import gun_harness/selector
|
||||
import guns/bitbrain_net
|
||||
|
||||
const BitbrainNetId = 17
|
||||
const PatternId = 5
|
||||
|
||||
var failures = 0
|
||||
proc check(name: string, ok: bool) =
|
||||
if ok: echo "PASS: ", name
|
||||
else: echo "FAIL: ", name; inc failures
|
||||
|
||||
proc clearEnv() =
|
||||
for n in BitbrainNetEnvNames: delEnv(n)
|
||||
# the master switch is shared with LEADGAIN, so it is NOT in
|
||||
# BitbrainNetEnvNames and must be cleared explicitly here
|
||||
delEnv("TR_BITBRAIN_NET")
|
||||
for n in RackGunNames: delEnv("TR_RACK_" & n)
|
||||
|
||||
proc mkState(t: int, ex, ey, sx, sy, eh: float): WorldState =
|
||||
WorldState(arenaWidth: 800, arenaHeight: 600, tick: t,
|
||||
enemyX: ex, enemyY: ey, enemyHeading: eh, enemySpeed: 8,
|
||||
selfX: sx, selfY: sy, selfHeading: 0, selfSpeed: 8,
|
||||
selfEnergy: 100, enemyEnergy: 100, selfRadarHeading: 0)
|
||||
|
||||
proc armNet() =
|
||||
## A small, fast, deterministic configuration: TR_BITBRAIN_NET must be on for
|
||||
## the gun to engage at all.
|
||||
putEnv(BBN_NET_ENV, "1")
|
||||
putEnv(BBN_NADES_ENV, "64")
|
||||
putEnv(BBN_CLASSES_ENV, "8")
|
||||
putEnv(BBN_MINOBS_ENV, "1")
|
||||
putEnv(BBN_INPUT_ENV, "24")
|
||||
|
||||
# ── registration / default-off ───────────────────────────────────────────────
|
||||
|
||||
proc testRegistration() =
|
||||
clearEnv()
|
||||
check "rack: BITBRAIN is registered at id 17 and defaults to off",
|
||||
RackGunNames.len == 18 and
|
||||
RackGunNames[BitbrainNetId] == "BITBRAIN" and
|
||||
DefaultRackMembership[BitbrainNetId] == rmOff
|
||||
check "rack: the shipped default still admits only Pattern",
|
||||
admittedGuns(18, rm1v1, DefaultRackMembership) == @[PatternId] and
|
||||
admittedGuns(18, rmMelee, DefaultRackMembership) == @[PatternId]
|
||||
check "gate: the gun is NOT spawned under the default rack",
|
||||
not vBulletAdmitted(BitbrainNetId, rm1v1, DefaultRackMembership, true)
|
||||
var g = initBitbrainNetGun()
|
||||
check "default OFF: TR_BITBRAIN_NET unset leaves the gun disabled",
|
||||
not g.enabled and g.networkBytes() == 0
|
||||
# With the switch on but the gun not built, predict must not spend anything.
|
||||
armNet()
|
||||
var g2 = initBitbrainNetGun()
|
||||
check "lazy: an enabled but unused gun still holds 0 bytes",
|
||||
g2.enabled and g2.networkBytes() == 0
|
||||
|
||||
proc testRackEnv() =
|
||||
clearEnv()
|
||||
putEnv("TR_RACK_BITBRAIN", "both")
|
||||
putEnv(BBN_NET_ENV, "1")
|
||||
let m = loadRackMembership()
|
||||
check "env: TR_RACK_BITBRAIN=both + TR_BITBRAIN_NET=1 admits id 17",
|
||||
m[BitbrainNetId] == rmBoth and
|
||||
vBulletAdmitted(BitbrainNetId, rm1v1, m, true)
|
||||
# A pre-rename .env carries TR_RACK_BITBRAIN=both WITHOUT the master switch.
|
||||
# That must not admit the gun: a disabled gun's placeholder predictions would
|
||||
# enter the shared VirtualTracker ring and shift every other gun's learning
|
||||
# order, i.e. it would break the default path.
|
||||
clearEnv()
|
||||
putEnv("TR_RACK_BITBRAIN", "both") # exactly what a pre-rename .env carries
|
||||
let mLegacy = loadRackMembership()
|
||||
check "env: TR_RACK_BITBRAIN with the switch OFF still means LEADGAIN (id 16)",
|
||||
mLegacy[BitbrainNetId] == rmOff and mLegacy[16] == rmBoth
|
||||
# TR_RACK_BITBRAIN and TR_BITBRAIN_NET are gated by the SAME switch, so there
|
||||
# is no env configuration that admits the gun without enabling it. Pin that:
|
||||
# a disabled gun must never reach the shared VirtualTracker ring, because its
|
||||
# placeholder predictions would shift every other gun's learning order.
|
||||
var consistent = true
|
||||
for (rackVal, netVal) in [("both", ""), ("both", "0"), ("both", "1"),
|
||||
("off", "1"), ("1v1", "1"), ("1v1", "")]:
|
||||
clearEnv()
|
||||
putEnv("TR_RACK_BITBRAIN", rackVal)
|
||||
if netVal.len > 0: putEnv(BBN_NET_ENV, netVal)
|
||||
let mm = loadRackMembership()
|
||||
let inRack = mm[BitbrainNetId] != rmOff
|
||||
let admitted = initBitbrainNetGun().gunAdmitted(inRack)
|
||||
if inRack != admitted: consistent = false
|
||||
if admitted and not initBitbrainNetGun().enabled: consistent = false
|
||||
check "env: TR_RACK_BITBRAIN alone can NEVER admit a disabled id 17 (6 settings)",
|
||||
consistent
|
||||
clearEnv()
|
||||
clearEnv()
|
||||
|
||||
# ── the knobs really resolve ─────────────────────────────────────────────────
|
||||
|
||||
proc testKnobs() =
|
||||
clearEnv()
|
||||
armNet()
|
||||
putEnv(BBN_INPUT_ENV, "33")
|
||||
putEnv(BBN_CLASSES_ENV, "17")
|
||||
putEnv(BBN_NADES_ENV, "128")
|
||||
putEnv(BBN_WIDTHS_ENV, "3,5,7,9")
|
||||
putEnv(BBN_SPAN_ENV, "25")
|
||||
putEnv(BBN_MODE_ENV, "counted")
|
||||
putEnv(BBN_DECAY_EVERY_ENV, "11")
|
||||
putEnv(BBN_DECAY_SHIFT_ENV, "2")
|
||||
putEnv(BBN_MINOBS_ENV, "7")
|
||||
var g = initBitbrainNetGun()
|
||||
check "knob: input width", g.inputWidth == 33
|
||||
check "knob: nClasses (output resolution)", g.nClasses == 17
|
||||
check "knob: nAde per AD", g.nAde == 128
|
||||
check "knob: clause widths (one AD per width)", g.widths == @[3, 5, 7, 9]
|
||||
check "knob: class half-range", g.maxDeg == 25.0
|
||||
check "knob: SBC mode", g.mode == smCounted
|
||||
check "knob: counted-mode decay knobs",
|
||||
g.decayEvery == 11 and g.decayShift == 2
|
||||
check "knob: minObs", g.minObs == 7
|
||||
putEnv(BBN_MODE_ENV, "bitset")
|
||||
check "knob: bitset mode", initBitbrainNetGun().mode == smBitset
|
||||
clearEnv()
|
||||
|
||||
# ── the input is a CONFIGURED SET OF BLOCKS ──────────────────────────────────
|
||||
|
||||
proc testFeatureBlocks() =
|
||||
clearEnv()
|
||||
armNet()
|
||||
delEnv(BBN_INPUT_ENV)
|
||||
var g = initBitbrainNetGun()
|
||||
check "features: the shipped block set is 47 slots",
|
||||
derivedSlots(g.blockWidths) == BB_DEFAULT_SLOTS and
|
||||
g.inputWidth == BB_DEFAULT_SLOTS
|
||||
check "features: every shipped block is enabled by default",
|
||||
g.blockWidths.allIt(it > 0)
|
||||
# disable two blocks -> the input shrinks by exactly their widths
|
||||
delEnv(BBN_INPUT_ENV)
|
||||
putEnv(BBN_FEATURES_ENV, "epos:0,bull:0")
|
||||
var g2 = initBitbrainNetGun()
|
||||
check "features: a disabled block removes its slots",
|
||||
g2.inputWidth == BB_DEFAULT_SLOTS - 8 - 4
|
||||
# widen one block -> the input grows by exactly the extra slots
|
||||
putEnv(BBN_FEATURES_ENV, "dist:9")
|
||||
var g3 = initBitbrainNetGun()
|
||||
check "features: a wider block adds its slots",
|
||||
g3.inputWidth == BB_DEFAULT_SLOTS - 10 + 18
|
||||
# TR_BITBRAIN_INPUT pins the width whatever the blocks say
|
||||
putEnv(BBN_FEATURES_ENV, "dist:9")
|
||||
putEnv(BBN_INPUT_ENV, "64")
|
||||
var g4 = initBitbrainNetGun()
|
||||
check "features: TR_BITBRAIN_INPUT pins the width",
|
||||
g4.inputWidth == 64
|
||||
putEnv(BBN_INPUT_ENV, "9")
|
||||
var g5 = initBitbrainNetGun()
|
||||
check "features: TR_BITBRAIN_INPUT can shrink below the block total",
|
||||
g5.inputWidth == 9
|
||||
# the unknown-block warning path must not change the widths
|
||||
putEnv(BBN_FEATURES_ENV, "banana,hzn:3")
|
||||
var g6 = initBitbrainNetGun()
|
||||
check "features: an unknown block is ignored, the rest still apply",
|
||||
g6.blockWidths[blockNameIndex("hzn")] == 3 and
|
||||
g6.blockWidths[blockNameIndex("epos")] == 4
|
||||
clearEnv()
|
||||
|
||||
# ── the network really engages ───────────────────────────────────────────────
|
||||
|
||||
proc testNetworkEngages() =
|
||||
clearEnv()
|
||||
armNet()
|
||||
var g = initBitbrainNetGun()
|
||||
let st = mkState(1, 600, 300, 100, 300, 0)
|
||||
var input = buildInput(g, st)
|
||||
check "engage: the input vector is exactly inputWidth long",
|
||||
input.len == g.inputWidth
|
||||
check "engage: the input is a 0/255 thermometer, not a dead constant",
|
||||
input.allIt(it == 0'u8 or it == BBN_SBC_VALUE) and
|
||||
input.anyIt(it == BBN_SBC_VALUE)
|
||||
check "engage: no network is built before the gun is used",
|
||||
g.networkBytes() == 0
|
||||
discard predict(g, st, 11.0)
|
||||
check "engage: the gun builds the network on first use",
|
||||
g.networkBytes() > 0
|
||||
# two different inputs -> different class outputs
|
||||
g.buildNet()
|
||||
let a = buildInput(g, mkState(1, 600, 300, 100, 300, 0))
|
||||
let b = buildInput(g, mkState(1, 200, 500, 700, 100, 90))
|
||||
check "engage: two different inputs give DIFFERENT bit vectors", a != b
|
||||
# An untrained SBC has no evidence, so the argmax is trivially 0. The real
|
||||
# engagement test is: teach it a rule, then check the readout separates.
|
||||
for i in 0 ..< 300:
|
||||
let half = (i mod 2 == 0)
|
||||
let x = buildInput(g, mkState(i, if half: 200.0 else: 700.0,
|
||||
150.0 + float(i mod 7), 100, 500, float(i mod 3)))
|
||||
g.learnSample(x, if half: 0 else: 7)
|
||||
var labels = newSeq[int](100)
|
||||
for i in 0 ..< 100:
|
||||
let half = (i mod 2 == 0)
|
||||
let x = buildInput(g, mkState(i, if half: 200.0 else: 700.0,
|
||||
150.0 + float(i mod 7), 100, 500, float(i mod 3)))
|
||||
labels[i] = g.inferClass(x)
|
||||
var differ = false
|
||||
for i in 1 ..< labels.len:
|
||||
if labels[i] != labels[0]: differ = true
|
||||
check "engage: a TRAINED ADE+SBC head returns DIFFERENT classes for different states",
|
||||
differ
|
||||
var agree = 0
|
||||
for i in 0 ..< labels.len:
|
||||
if (labels[i] == 0) == (i mod 2 == 0): inc agree
|
||||
check "engage: the readout separates the two taught populations",
|
||||
agree >= 80
|
||||
# learning changes the memory
|
||||
g.resetLearning()
|
||||
let before = g.sbcBytesOf()
|
||||
let beforeOcc = g.sbcsOf()[0].occupancy()
|
||||
g.learnSample(a, 3)
|
||||
check "engage: learning one sample WRITES the SBC memory",
|
||||
g.sbcsOf()[0].occupancy() > beforeOcc
|
||||
check "engage: memoryBytes is unchanged by a learn (no realloc)",
|
||||
g.sbcBytesOf() == before
|
||||
# bitset learn is idempotent
|
||||
clearEnv()
|
||||
armNet()
|
||||
putEnv(BBN_MODE_ENV, "bitset")
|
||||
putEnv(BBN_WIDTHS_ENV, "2,3,4") # denser clauses so coincidences actually fire
|
||||
var gb = initBitbrainNetGun()
|
||||
gb.buildNet()
|
||||
gb.learnSample(a, 2)
|
||||
let occ1 = gb.net.sbcs[0].occupancy()
|
||||
for _ in 0 ..< 49: gb.learnSample(a, 2)
|
||||
check "engage: a bitset learn really writes the memory", occ1 > 0.0
|
||||
check "engage: bitset learn is idempotent (50x learn == 1x)",
|
||||
abs(gb.net.sbcs[0].occupancy() - occ1) < 1e-12
|
||||
# counted learn is monotone: the same cell keeps gaining evidence
|
||||
putEnv(BBN_MODE_ENV, "counted")
|
||||
putEnv(BBN_WIDTHS_ENV, "2,3,4")
|
||||
var gc = initBitbrainNetGun()
|
||||
gc.buildNet()
|
||||
gc.learnSample(a, 2)
|
||||
let ev2 = gc.evidenceFor(a, 2)
|
||||
for _ in 0 ..< 4: gc.learnSample(a, 2)
|
||||
check "engage: counted learn is MONOTONE (5 learns beat 1)",
|
||||
ev2 > 0 and gc.evidenceFor(a, 2) > ev2
|
||||
check "engage: the counted mode really holds saturating counters",
|
||||
gc.sbcsOf()[0].mode == smCounted
|
||||
clearEnv()
|
||||
|
||||
# ── the readout is a fine-grained correction, and it is gated ────────────────
|
||||
|
||||
proc testReadout() =
|
||||
clearEnv()
|
||||
armNet()
|
||||
putEnv(BBN_MINOBS_ENV, "1000")
|
||||
var g = initBitbrainNetGun()
|
||||
let st = mkState(1, 600, 300, 100, 300, 0)
|
||||
discard predict(g, st, 11.0)
|
||||
check "readout: below minObs the correction is exactly zero",
|
||||
g.lastShiftDeg == 0.0 and not isWarmedUp(g)
|
||||
putEnv(BBN_MINOBS_ENV, "1")
|
||||
var g2 = initBitbrainNetGun()
|
||||
# feed a stream so labels resolve and the learner engages
|
||||
var t = 0
|
||||
for _ in 0 ..< 400:
|
||||
let ft = float(t)
|
||||
let st = mkState(t, 100.0 + 2.0 * ft, 200.0 + 1.1 * ft, 400, 300,
|
||||
20.0 * sin(ft * 0.2))
|
||||
discard predict(g2, st, 11.0)
|
||||
inc t
|
||||
check "readout: a resolved stream trains the net", g2.trained > 100
|
||||
check "readout: the net becomes warmed up past minObs", isWarmedUp(g2)
|
||||
check "readout: the correction is a bounded, fine-grained angle",
|
||||
g2.lastShiftDeg >= -g2.maxDeg - 1e-9 and
|
||||
g2.lastShiftDeg <= g2.maxDeg + 1e-9
|
||||
check "readout: the gun's output is a POINT (Pattern base + correction)",
|
||||
predict(g2, mkState(400, 500, 300, 400, 300, 0), 11.0).x != 0.0
|
||||
check "readout: ADE threshold adaptation ran (online calibration)",
|
||||
g2.adapts > 0
|
||||
clearEnv()
|
||||
|
||||
# ── RNG parity ───────────────────────────────────────────────────────────────
|
||||
|
||||
proc testRngClean() =
|
||||
clearEnv()
|
||||
randomize(1234)
|
||||
let a = rand(1_000_000)
|
||||
randomize(1234)
|
||||
armNet()
|
||||
var g = initBitbrainNetGun()
|
||||
g.buildNet()
|
||||
discard predict(g, mkState(1, 300, 300, 100, 100, 0), 11.0)
|
||||
let b = rand(1_000_000)
|
||||
check "parity: building + running the net does not perturb the global RNG",
|
||||
a == b
|
||||
clearEnv()
|
||||
|
||||
testRegistration()
|
||||
testRackEnv()
|
||||
testKnobs()
|
||||
testFeatureBlocks()
|
||||
testNetworkEngages()
|
||||
testReadout()
|
||||
testRngClean()
|
||||
|
||||
if failures > 0:
|
||||
echo "\n", failures, " check(s) FAILED"
|
||||
quit(1)
|
||||
echo "\nAll BITBRAIN (ADE+SBC) engagement checks passed."
|
||||
@@ -73,7 +73,7 @@ proc testKnownNames() =
|
||||
check "known set has no duplicates", known.len == s.len
|
||||
check "known set only holds TR_*/GUN_* names",
|
||||
known.allIt(it.startsWith("TR_") or it.startsWith("GUN_"))
|
||||
check "known set covers all 18 rack names",
|
||||
check "known set covers all 17 rack names",
|
||||
RackGunNames.allIt((RackEnvPrefix & it) in s)
|
||||
for name in ["TR_MOVEMENT", "TR_POWER_ENERGY_MIN", "TR_TMHORIZON_NSTATES",
|
||||
"GUN_VBULLET_METRIC", "TR_ENV_REPORT", "GUN_SELECTOR_SEED",
|
||||
|
||||
@@ -2,18 +2,17 @@
|
||||
##
|
||||
## 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:
|
||||
## address the corrector that is now called LEADGAIN, and they MUST keep working.
|
||||
##
|
||||
## * `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 ADE+SBC gun that briefly owned the `TR_BITBRAIN_*` prefix (rack id 17,
|
||||
## `guns/bitbrain_net.nim`) was RETIRED and removed, so there is nothing left to
|
||||
## disambiguate: this gun is the SOLE owner of that namespace and
|
||||
##
|
||||
## The two name sets are DISJOINT, so the mapping is total and deterministic.
|
||||
## No Java, no battle, no network build.
|
||||
## * every `TR_BITBRAIN_<X>` in the frozen `LegacyKnobEnvNames` set is an
|
||||
## alias for `TR_LEADGAIN_<X>`, unconditionally;
|
||||
## * `TR_RACK_BITBRAIN` selects rack id 16, unconditionally;
|
||||
## * a stale `TR_BITBRAIN_NET=1` left in a `.env` is an unrecognised
|
||||
## variable — the boot report warns about it and nothing reads it.
|
||||
##
|
||||
## Run: nim c -r common_libs/tests/test_lead_gain_legacy.nim
|
||||
|
||||
@@ -35,7 +34,7 @@ proc clearEnv() =
|
||||
delEnv(NewPrefix & suffix)
|
||||
delEnv(LegacyRackEnvName)
|
||||
delEnv("TR_RACK_" & RackGunNames[LeadGainId])
|
||||
delEnv(LG_NET_SWITCH_ENV)
|
||||
delEnv("TR_BITBRAIN_NET")
|
||||
|
||||
# ── the alias table itself ───────────────────────────────────────────────────
|
||||
|
||||
@@ -61,19 +60,29 @@ proc testAliasTableIsDisjoint() =
|
||||
|
||||
# ── the switch ───────────────────────────────────────────────────────────────
|
||||
|
||||
proc testSwitch() =
|
||||
proc testNamespaceIsUnconditional() =
|
||||
## There is no namespace switch any more. The ADE+SBC gun is gone, so a stale
|
||||
## `TR_BITBRAIN_NET` left in an old `.env` is inert: it must neither disown
|
||||
## the legacy knob aliases, nor the legacy rack knob, nor the deprecation line.
|
||||
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()
|
||||
putEnv("TR_BITBRAIN_MEM", "decay")
|
||||
putEnv("TR_BITBRAIN_LOG", "1")
|
||||
putEnv(LegacyRackEnvName, "both")
|
||||
var g = initLeadGainGun()
|
||||
check "ns: the legacy prefix is unconditional (no switch set at all)",
|
||||
g.memMode == lgDecay and g.logEnabled
|
||||
putEnv("TR_BITBRAIN_NET", "1")
|
||||
var g1 = initLeadGainGun()
|
||||
check "ns: a stale TR_BITBRAIN_NET=1 does not disown the legacy knob names",
|
||||
g1.memMode == lgDecay and g1.logEnabled
|
||||
putEnv("TR_BITBRAIN_NET", "0")
|
||||
var g2 = initLeadGainGun()
|
||||
check "ns: a stale TR_BITBRAIN_NET=0 is equally inert", g2.memMode == lgDecay
|
||||
putEnv("TR_BITBRAIN_NET", "1")
|
||||
check "ns: a stale TR_BITBRAIN_NET=1 does not disown the legacy rack knob",
|
||||
loadRackMembership()[LeadGainId] == rmBoth
|
||||
check "ns: a stale TR_BITBRAIN_NET=1 does not suppress the deprecation line",
|
||||
lgDeprecationLine().len > 0
|
||||
clearEnv()
|
||||
|
||||
# ── knob aliasing ────────────────────────────────────────────────────────────
|
||||
@@ -93,13 +102,13 @@ proc testKnobAlias() =
|
||||
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")
|
||||
# the legacy namespace is unconditional, even next to a stale NET switch
|
||||
putEnv("TR_BITBRAIN_NET", "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]
|
||||
check "alias: a stale TR_BITBRAIN_NET=1 does NOT make the legacy names inert",
|
||||
g3.memMode == lgDecay and g3.logEnabled and
|
||||
g3.cands == @[0.25, 0.5, 1.0]
|
||||
# ... and then the NEW names still work
|
||||
putEnv("TR_LEADGAIN_MEM", "retained")
|
||||
var g4 = initLeadGainGun()
|
||||
@@ -116,10 +125,10 @@ proc testRackAlias() =
|
||||
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")
|
||||
putEnv("TR_BITBRAIN_NET", "1")
|
||||
let m2 = loadRackMembership()
|
||||
check "rack: TR_BITBRAIN_NET=1 hands the name to the new ADE+SBC gun (id 17)",
|
||||
m2[LeadGainId] == rmOff and m2.len == 18
|
||||
check "rack: a stale TR_BITBRAIN_NET=1 no longer disowns the name — LEADGAIN (id 16) stays selected",
|
||||
m2[LeadGainId] == rmBoth and m2.len == NumRackGuns
|
||||
clearEnv()
|
||||
putEnv("TR_RACK_LEADGAIN", "both")
|
||||
putEnv(LegacyRackEnvName, "off")
|
||||
@@ -140,9 +149,9 @@ proc testDeprecationLine() =
|
||||
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
|
||||
putEnv("TR_BITBRAIN_NET", "1")
|
||||
check "depr: the line still fires with a stale TR_BITBRAIN_NET=1 present",
|
||||
lgDeprecationLine().len > 0
|
||||
clearEnv()
|
||||
|
||||
# ── shipped-default parity ───────────────────────────────────────────────────
|
||||
@@ -155,15 +164,15 @@ proc testDefaultParity() =
|
||||
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 18 guns at id 16 = LEADGAIN",
|
||||
RackGunNames.len == 18 and RackGunNames[LeadGainId] == "LEADGAIN"
|
||||
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()
|
||||
testNamespaceIsUnconditional()
|
||||
testKnobAlias()
|
||||
testRackAlias()
|
||||
testDeprecationLine()
|
||||
|
||||
@@ -37,7 +37,7 @@ proc testTable() =
|
||||
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(18, rm1v1, DefaultRackMembership) == @[PatternId]
|
||||
admittedGuns(NumRackGuns, rm1v1, DefaultRackMembership) == @[PatternId]
|
||||
check "gate: LEADGAIN is NOT spawned under the default rack",
|
||||
not vBulletAdmitted(LeadGainId, rm1v1, DefaultRackMembership, true)
|
||||
|
||||
@@ -49,7 +49,7 @@ proc testEnvOverride() =
|
||||
m[LeadGainId] == rmBoth and
|
||||
vBulletAdmitted(LeadGainId, rm1v1, m, true)
|
||||
check "env: admitting LEADGAIN leaves Pattern as the only other member",
|
||||
admittedGuns(18, rm1v1, m) == @[PatternId, LeadGainId]
|
||||
admittedGuns(NumRackGuns, rm1v1, m) == @[PatternId, LeadGainId]
|
||||
clearRackEnv()
|
||||
|
||||
proc testLazyAndRngClean() =
|
||||
|
||||
@@ -158,14 +158,14 @@ proc testDefaultsOnlyPattern() =
|
||||
DefaultRackMembership[12] == rmOff and DefaultRackMembership[13] == rmOff and
|
||||
DefaultRackMembership[14] == rmOff
|
||||
check "defaults: the default rack admits ONLY Pattern in 1v1",
|
||||
admittedGuns(18, rm1v1, DefaultRackMembership) == @[PatternId]
|
||||
admittedGuns(17, rm1v1, DefaultRackMembership) == @[PatternId]
|
||||
check "defaults: the default rack admits ONLY Pattern in melee",
|
||||
admittedGuns(18, rmMelee, DefaultRackMembership) == @[PatternId]
|
||||
admittedGuns(17, rmMelee, DefaultRackMembership) == @[PatternId]
|
||||
let loaded = loadRackMembership()
|
||||
check "defaults: with a clean environment loadRackMembership() == shipped table",
|
||||
loaded == DefaultRackMembership
|
||||
check "defaults: RackGunNames covers the shipped 18-gun rack",
|
||||
RackGunNames.len == 18 and DefaultRackMembership.len == 18
|
||||
check "defaults: RackGunNames covers the shipped 17-gun rack",
|
||||
RackGunNames.len == 17 and DefaultRackMembership.len == 17
|
||||
|
||||
proc testFloorRespectsAdmission() =
|
||||
## The FLOOR path (`bestRate <= 0` or below the floor rate) historically fell
|
||||
@@ -204,23 +204,23 @@ proc testRevertOverrideRestoresFullRack() =
|
||||
## stays `off`). This must reproduce the old full rack without a rebuild.
|
||||
clearRackEnv()
|
||||
putEnv("TR_RACK_PATTERN", "both")
|
||||
# The ADE+SBC gun (id 17) additionally needs TR_BITBRAIN_NET=1; without the
|
||||
# switch TR_RACK_BITBRAIN is the LEGACY name for LEADGAIN (id 16), so the
|
||||
# documented one-liner has to flip the namespace over as well.
|
||||
putEnv("TR_BITBRAIN_NET", "1")
|
||||
for name in RackGunNames:
|
||||
if name != "TMPATTERN": putEnv("TR_RACK_" & name, "both")
|
||||
let m = loadRackMembership()
|
||||
delEnv("TR_BITBRAIN_NET")
|
||||
var full = true
|
||||
for i in 0..<RackGunNames.len:
|
||||
let want = if i == 14: rmOff else: rmBoth
|
||||
if m[i] != want: full = false
|
||||
check "revert: the documented one-liner restores the all-`both` full rack", full
|
||||
check "revert: 1v1 rack admits every gun again (TMPATTERN excluded)",
|
||||
admittedGuns(18, rm1v1, m) == @[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17]
|
||||
check "revert: the legacy TR_RACK_BITBRAIN alone still selects LEADGAIN only",
|
||||
loadRackMembership()[17] == rmOff
|
||||
admittedGuns(17, rm1v1, m) == @[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16]
|
||||
clearRackEnv()
|
||||
# The ADE+SBC gun (old rack id 17) is RETIRED, so `TR_RACK_BITBRAIN` is now
|
||||
# unambiguously LEADGAIN's old name: alone, it admits LEADGAIN and nothing else.
|
||||
putEnv("TR_RACK_BITBRAIN", "both")
|
||||
let mLegacy = loadRackMembership()
|
||||
check "revert: the legacy TR_RACK_BITBRAIN alone selects LEADGAIN (16) only",
|
||||
mLegacy[16] == rmBoth and admittedGuns(17, rm1v1, mLegacy) == @[5, 16]
|
||||
clearRackEnv()
|
||||
|
||||
proc testEnvOverrides() =
|
||||
|
||||
@@ -14,17 +14,23 @@
|
||||
## The fix derives every gun-indexed width from `NumRackGuns` (== `len(RackGunNames)`).
|
||||
## This test is the guard that would have caught it:
|
||||
##
|
||||
## A. rack table — `NumRackGuns` is 18, 18 DISTINCT names, and the
|
||||
## A. rack table — `NumRackGuns` is 17, 17 DISTINCT names, and the
|
||||
## membership table and the legacy alias target are in range;
|
||||
## B. source shape — `ModularBot.nim` contains NO literal width/loop bound
|
||||
## narrower than `NumRackGuns` for the gun-indexed
|
||||
## accounting, and sizes them with `NumRackGuns`;
|
||||
## C. accounting — an in-process replay of the real shape (`array[NumRackGuns]`
|
||||
## indexed by every admitted id) never goes out of range,
|
||||
## and gun 17 IS accounted;
|
||||
## and the newest id IS accounted, even when the rack
|
||||
## grows by one more gun;
|
||||
## D. live (opt-in) — with a rack that admits ONLY the newest id, the bot
|
||||
## selects it, fires and lands hits (skipped without JARs).
|
||||
##
|
||||
## The ADE+SBC gun (rack id 17) was later RETIRED, so the rack is back to 17 and
|
||||
## the newest id is LEADGAIN (16). The guard is unchanged in shape and in
|
||||
## strength: C3 now proves the fix survives a rack that grows again, which is
|
||||
## the general form of the bug that shipped.
|
||||
##
|
||||
## Run: nim c -r common_libs/tests/test_rack_stat_width.nim
|
||||
## Live: TR_SERVER_JAR=... TR_BATTLE_RUNNER=... nim c -r common_libs/tests/test_rack_stat_width.nim
|
||||
|
||||
@@ -41,7 +47,7 @@ const
|
||||
adversaryDir = repoRoot / "common_libs" / "test_framework" / "adversaries" / "SittingDuck"
|
||||
statsPath = "/tmp/rack_stat_width_stats.jsonl"
|
||||
PatternId = 5
|
||||
NewestId = NumRackGuns - 1 ## 17, the id that regressed
|
||||
NewestId = NumRackGuns - 1 ## 16 (LEADGAIN), the newest rack id
|
||||
LegacyLeadGainId = 16
|
||||
|
||||
var failures = 0
|
||||
@@ -53,7 +59,7 @@ proc check(name: string, ok: bool) =
|
||||
|
||||
proc testRackTable() =
|
||||
check "A1: NumRackGuns is derived from RackGunNames",
|
||||
NumRackGuns == RackGunNames.len and NumRackGuns == 18
|
||||
NumRackGuns == RackGunNames.len and NumRackGuns == 17
|
||||
check "A2: rack membership table has one entry per gun",
|
||||
DefaultRackMembership.len == NumRackGuns
|
||||
var seen = initTable[string, bool]()
|
||||
@@ -156,21 +162,31 @@ proc testAccounting() =
|
||||
if gunRealHits[NewestId] != 50: inRange = false
|
||||
check "C2: accounting stays in range and records the newest gun", inRange
|
||||
|
||||
# The bug in one line: with the OLD `array[17, int]`, `NewestId` is out of
|
||||
# range. Prove the id that regressed is exactly the one that overflowed.
|
||||
check "C3: the regressed id (17) is out of range of the old array[17, int]",
|
||||
NewestId >= 17
|
||||
# The bug in one line, in its GENERAL form: an accounting array hard-wired to
|
||||
# a literal is out of range the moment the rack grows. Prove the fix
|
||||
# (`NumRackGuns`) tracks a rack that grows by one more gun, where the id that
|
||||
# regressed in j141 (17) would be a legal, accounted index.
|
||||
var grown: array[NumRackGuns + 1, int]
|
||||
var grownOk = true
|
||||
for gid in 0 .. NumRackGuns:
|
||||
try:
|
||||
inc grown[gid]
|
||||
except IndexDefect:
|
||||
grownOk = false
|
||||
check "C3: a rack one gun WIDER is still fully covered by the NumRackGuns width",
|
||||
grownOk and grown[NumRackGuns] == 1
|
||||
|
||||
# The legacy namespace: TR_RACK_BITBRAIN with TR_BITBRAIN_NET unset addresses
|
||||
# LEADGAIN (16), and id 17 is NOT admitted.
|
||||
delEnv("TR_BITBRAIN_NET")
|
||||
# The legacy namespace: TR_RACK_BITBRAIN is now unambiguously LEADGAIN's old
|
||||
# name (the ADE+SBC gun that briefly owned it was retired), so it always
|
||||
# admits LEADGAIN (16) — the newest id — and nothing else.
|
||||
for n in RackGunNames: delEnv("TR_RACK_" & n)
|
||||
delEnv("TR_BITBRAIN_NET")
|
||||
putEnv("TR_RACK_BITBRAIN", "both")
|
||||
let legacy = loadRackMembership()
|
||||
check "C4: legacy TR_RACK_BITBRAIN admits LEADGAIN (16)",
|
||||
legacy[LegacyLeadGainId] == rmBoth
|
||||
check "C5: legacy namespace never admits id 17",
|
||||
legacy[NewestId] == rmOff
|
||||
check "C5: legacy TR_RACK_BITBRAIN alone adds LEADGAIN to the shipped Pattern",
|
||||
admittedGuns(NumRackGuns, rm1v1, legacy) == @[PatternId, LegacyLeadGainId]
|
||||
delEnv("TR_RACK_BITBRAIN")
|
||||
|
||||
# ── D. live proof, only with the JARs ────────────────────────────────────────
|
||||
@@ -203,7 +219,7 @@ proc live() =
|
||||
var linesOut: seq[string]
|
||||
for l in readFile(backup).splitLines():
|
||||
if l.strip().startsWith("GUN_STATS_PATH"): linesOut.add "GUN_STATS_PATH=" & statsPath
|
||||
elif l.strip().startsWith("TR_RACK_BITBRAIN"): linesOut.add "TR_RACK_BITBRAIN=both"
|
||||
elif l.strip().startsWith("TR_RACK_BITBRAIN"): linesOut.add "TR_RACK_LEADGAIN=both"
|
||||
else: linesOut.add l
|
||||
writeFile(dotenv, linesOut.join("\n"))
|
||||
echo "NOTE: live section wrote a temporary out/.env (backup at out/.env.j141-backup)."
|
||||
|
||||
@@ -62,7 +62,7 @@ proc seedOldRack(t: var VirtualTracker, targetId: int) =
|
||||
# ── registration table ────────────────────────────────────────────────────────
|
||||
|
||||
proc testTable() =
|
||||
check "rack: RackGunNames has 18 entries", RackGunNames.len == 18
|
||||
check "rack: RackGunNames has 17 entries", RackGunNames.len == 17
|
||||
check "rack: the new gun is named TMPATTERN at id 14",
|
||||
RackGunNames[TmPatternId] == "TMPATTERN"
|
||||
check "rack: the new gun defaults to `off`",
|
||||
@@ -82,9 +82,9 @@ proc oldRackMembership(): array[15, RackMembership] =
|
||||
|
||||
proc testDefaultAdmitsOnlyPattern() =
|
||||
check "default membership admits only PATTERN (1v1)",
|
||||
admittedGuns(18, rm1v1, DefaultRackMembership) == @[PatternId]
|
||||
admittedGuns(NumRackGuns, rm1v1, DefaultRackMembership) == @[PatternId]
|
||||
check "default membership admits only PATTERN (melee)",
|
||||
admittedGuns(18, rmMelee, DefaultRackMembership) == @[PatternId]
|
||||
admittedGuns(NumRackGuns, rmMelee, DefaultRackMembership) == @[PatternId]
|
||||
|
||||
proc testEnvOverride() =
|
||||
for name in RackGunNames: delEnv("TR_RACK_" & name)
|
||||
|
||||
Reference in New Issue
Block a user