Commit Graph

425 Commits

Author SHA1 Message Date
SirStone 0df7763765 merge j160-ramfloor: the energy-reserve FIRING FLOOR + ENEMY-EXHAUSTION ram trigger (default off)
TR_RAM_FLOOR_ENERGY and TR_RAM_ENEMY_ENERGY, both default 0.0, plus
common_libs/movements/ram_decision.nim, the fire gate in ModularBot.nim, two
offline measure_* tools and the A/B record.

The j163 A/B (450 runs/arm, 0 env mis-set) is a clean negative: round-win
40.30% -> 39.70%, sign-flip p=0.7676, MDE 4.73 pp. DO NOT ADOPT;
TR_RAM_FLOOR_ENERGY stays 0.0. The mechanism barely fired (0.04% of ticks, not
the 9.6% the offline ruler predicted), so the null does not prove the knob
inert.

# Conflicts:
#	common_libs/tests/test_tfil_commit_env.nim
2026-09-27 14:00:06 +02:00
SirStone 07866c99bd merge j165-ring-commit: port tfil's arrival commitment to tfil_ring (default off)
TR_TFIL_RING_COMMIT_ARRIVAL (default off) and TR_TFIL_RING_NOREV_SPEED
(default 0.0), plus the tfil_ring replay ruler and its default-parity golden
fixture. Offline parity is byte-for-byte over 20,026 ticks with the knobs unset.
Never live-tested: the first live test of the_floor_is_lava_ring.nim under these
knobs should be a real experiment, not a formality.
2026-09-27 13:55:22 +02:00
SirStone 51bfa57067 j163: RESULT - the firing floor is a clean negative, and the offline energy corpus missed the live game by 200x
450 runs/arm x 2 arms (15 opponents x 30 runs x 3 rounds, conc=6, 0 failed,
0 never started, 0 env mis-set), frozen binary d9a39c3b8472, TR_MOVEMENT=tfil.
Round-win 40.30% -> 39.70% (-0.59 pp, CI -3.90..+2.72, sign-flip exact-2^15
p=0.7676, MDE 4.73 pp); wins/run 0.200 -> 0.182 (MDE 0.1420); damage/run
113.65 -> 112.70 (p=0.5298, MDE 4.19). Deviation disclosed: 30 runs/opponent
instead of 42 (throughput 14-22 runs/min vs 22.7-23.2 assumed); full 15-opponent
panel and both arms kept, runs reduced.

Headline is the mechanism failure: firing was suppressed on 0.04% of ticks,
not the 9.6% the offline ruler predicted (~200x smaller); only 4.8% of shots
happen in the low-energy zone and the floor removed 14% of those; rounds ending
at self energy <=0 were 40.6% live vs 61.2% implied by the offline corpus;
median self energy at death 14.1 -> 15.5. The pre-registered 'we cannot measure
the damage cost directly' call was correct.

DO NOT ADOPT. TR_RAM_FLOOR_ENERGY stays 0.0, do not re-test. The null does not
prove the knob inert - the mechanism barely fired.
2026-09-27 13:55:19 +02:00
SirStone fe77056459 j165: port tfil's arrival commitment to tfil_ring, default off
The_floor_is_lava_ring carried raw commitTicks with no arrival guard and no
no-reversal guard. Port the behaviour of tfil's j144 fix on RING-SPECIFIC env
names (TR_TFIL_RING_COMMIT_ARRIVAL, TR_TFIL_RING_NOREV_SPEED) so the two forks
never share a namespace. Both default OFF: with them unset the ring mover is
byte-for-byte the pre-change mover over the whole 20026-tick fixture replay
(golden generated from git show HEAD:..., checked by tfil_ring_replay.nim).

Structural differences from tfil, all noted in the code:
  * ring has no TfilTileReplanMode - the tile-crossing cancel is unconditional
    self-tile, so the arrival guard is just 'not TfilRingCommitArrival'.
  * ring has no replanReason enum, so the arrival/danger/expiry outcomes are a
    local bool; the default-off path keeps ring's original dec/no-dec exactly.
  * ring's MinCommitTicks is 0 (tfil's is 5), so the arrival branch is evaluated
    from the first committed tick. Left as is: changing it would change the
    default path.
  * ring's ScoredTile carries no turnDeg, so the no-reversal offsets are
    computed by ringTileOffTravel at the pick site.
TR_TFIL_COMMIT_MARGIN (tfil's hysteresis) is deliberately NOT ported: it is a
third knob, outside the two named, and inert at its 0.0 default.

No other tfil mechanism touched: no turn-cost tiebreak, TR_TFIL_ARRIVE_TICKS,
TR_FIRE_LAG, heat-field override, corridor bound, hold, or geometry weighting.
No default changed anywhere.
2026-09-27 13:27:02 +02:00
SirStone 6cfb1698f1 j163: PRE-REGISTER the 2-arm firing-floor A/B (A floor=0 vs B floor=5) before any battle 2026-09-27 12:59:51 +02:00
SirStone 64e23e29d1 j162: measure whether the bot ever runs out of energy (offline, no battles)
Decisive measurement for the j160 firing floor, on the recorded closed-loop
corpus (8149 recordings / 35163 rounds / 34.46M ticks, state only):

* 61.2% of rounds end with self energy crossing 0. Energy at death: median
  0.83, p90 8.90, max 24.83 -- the bot dies BROKE, so the floor's premise is
  real. Time at energy<=0 is a median of 1 tick: the round ends on the
  crossing tick, there is no recoverable disabled window.
* Reserve that would have absorbed the killing blow: median 0.40, p75 2.00,
  p90 6.90.
* Cannot climb back out: at energy<=5 the next tick brings a landed hit 0.232%
  of the time and death 0.663% (2.9x). At <=20 recovery is 2x more likely,
  which is why a floor at 20 is the wrong value.
* Cost: floor 5 blocks 9.58% of ticks, median run 53, mean run 118, banking
  ~9.9 energy against a p75 overshoot of 2.0.
* Measured caveat: the recorded ledger closes exactly (residual -0.00 over
  35065 rounds), so these captures do NOT charge firepower cost; the cost
  column is derived from the game rules, and the landed-hit power (mode 1.0,
  mean 1.42) is what sets the bracket.

Honest read: worth an A/B, materially different in magnitude from the geometry
arm (smaller damage cost, stronger and directly measured safety claim), so NOT
the clean 'protects against nothing' negative.

docs/ram_floor_exhaustion_ab.md: replaces the draft with the measurement plus a
re-sized A/B proposal (4 arms, 42 runs/opponent/arm, 630 runs/arm for MDE 0.10
wins/run, ~2.7 h at the measured 23 battles/min). NOT RUN -- no battle, server
or GUI was started. Both knobs remain default 0.0.
2026-09-27 12:58:14 +02:00
SirStone 23bce2dad5 j160 (default-off): the energy-reserve FIRING FLOOR + ENEMY-EXHAUSTION ram trigger
TR_RAM_FLOOR_ENERGY (0.0 = off): at/below this self energy we start no NEW
shot, holding back the reserve for a final ram exchange. Justified by the only
energy gain in the game being +3*power per bullet hit LANDED, so not firing
denies the enemy its only refill. Blocks only NEW shots (gunHeat already gates
committed ones) and is bypassed while ramming.

TR_RAM_ENEMY_ENERGY (0.0 = off): last-scanned enemy energy <= this -> ram
mode. Enemy energy IS observable (ScannedBotEvent.energy, schemas.nim:306),
1-8 ticks stale. This is the shipped finisher with its energy tolerance
promoted to a knob, keeping the self>enemy surplus guard because RAM_DAMAGE
0.6 applies to BOTH bots on every contact tick.

Open-loop measurement (measure_ramfloor_energy, 8149 recordings / 29871
rounds / 33.8M ticks): 'both low' is COMMON (10.4% of ticks below 20, 15.1%
below 25) but neither side goes low first (enemy 52.7% / us 47.3%), and the
owner's literal trigger - enemy so low it cannot fire (energy <= 1.95) - is
only 2.5% of ticks, 1.1% while we are healthy.

Guards 136 -> 147 in test_tfil_commit_env.nim, all green. A/B PRE-REGISTERED
in docs/ram_floor_exhaustion_ab.md and NOT RUN.
2026-09-27 12:46:46 +02:00
SirStone 4a1f3e1c88 j159: live 2-arm A/B result — geometry draw weighting costs 8.8 damage/run, wins null; keep TR_TFIL_GEO_MODE=off 2026-09-27 12:44:38 +02:00
SirStone 7c5bc9ccbd j159: PRE-REGISTER the tfil geo A/B (geo draw weighting vs uniform) before any battle 2026-09-27 12:16:36 +02:00
SirStone 2223ca667e j154 (default-off): the DERIVED bounded hold TR_TFIL_HOLD_MAX_TICKS + panic release
Budget derived from the server source (tank-royale 0.35.5): calcGunHeat(p) =
1 + p/5, coolDown 0.1/tick, fire only at heat == 0, calcBulletDamage(3.0) = 16
-> two max-power shots are 16 ticks apart and 32 damage is the most the enemy
can land in that window (brute force over the 0.1 power grid: 8/11/16/24/32
ticks -> 16/18/32/32/48). 16 = CommitTicks + 1, the first window that admits
the enemy's second shot.

Knob defaults to 0 = today's behaviour byte-for-byte (checked over 20026
ticks). Hold is taken only on a replan tick with an empty safe set, at most N
ticks per streak, released the tick a safe tile exists, counter reset on the
pick. PANIC RELEASE: a tracked bullet whose closest approach is within its core
at t* in [0, min(N,16)] overrides the hold immediately. The gun is untouched.

Also fixes a stray '&' that stopped test_tfil_commit_env.nim from compiling at
all, and enforces the 'a hold never interrupts a live commitment' invariant
j153 documented but did not implement. Registers TR_TFIL_HOLD_MAX_TICKS in
env_report + knownEnvNames + .env.example.

No battle, no A/B run.
2026-09-27 10:23:46 +02:00
SirStone 38fbc6ecd1 j152 (default-off): geometry shapes the tile DRAW (TR_TFIL_GEO_MODE/TAU) + the offline sweep with the diversity cost
The owner: choose the tile pool not only from the heat point but from the
geometric position too. Heat stays the hard filter; the draw over the
survivors is re-weighted by turn and/or distance. Three weighting forms
(soft softmax / top-K third / rejection band), one env name carrying both
axes. Default = off = today's uniform draw, byte-for-byte (golden parity).

WHAT DIFFERS FROM j9 (TR_TFIL_TURN_BIAS, a live null): that was a tiebreak
weight among the non-empty safe set only. This runs on the WHOLE pool the
draw already runs on, including the 2 promoted least-hot tiles the ~65%
forced picks choose from.

OFFLINE (8 fixtures x 3 seeds, no java): headline 'tile actually reached at
tta' 4.5% -> 16.9% at TR_TFIL_GEO_MODE=both-soft TR_TFIL_GEO_TAU=45, with
no diversity collapse (distinct tiles 220 -> 219, normalised entropy
0.87 -> 0.86, top-tile share 7.0% -> 8.6%). perpE — the perpendicular rate
in the FORCED population — does not move in ANY arm: that pool is 2 tiles
ranked by heat alone and the only lever is a coin flip.

Also registers j150's TR_TFIL_DIAG / TR_TFIL_DANGER_THRESHOLD in
env_report + knownEnvNames + .env.example (test_env_report was red) and
documents DANGER_THRESHOLD as quantisation-limited: heat comes in 5s, so
the effective steps are 10/15/20 and 10-14 admits zero extra tiles.

No battle, no server, no A/B run.
2026-09-27 10:22:24 +02:00
SirStone 94ffc63160 j153: the open-loop ruler for "hold when trapped" + the A/B proposal (NOT run)
Measures, on the recorded fixtures and with NO counterfactual replay: how long
until a safe tile appears at a forced pick, whether the enemy had just fired,
whether our own tile is already hot, and all three split by distance. Registers
nothing and changes no default: the knob TR_TFIL_HOLD_WHEN_TRAPPED and its
implementation live in the concurrently edited working tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-27 10:16:53 +02:00
SirStone a01141c959 j151 (default-off): hard arrival bound TR_TFIL_ARRIVE_TICKS + the offline per-pick ruler that found it
Measures the owner's two complaints per pick (turn angle, heat on the path
vs at the destination, time-to-arrive, and whether the destination is hot on
the recorded true future when we would arrive), split by empty vs non-empty
safe set. Result: the path IS scored (hard filter + least-hot fallback), but
there was NO time term at all -- 65% of picks outran the 15-tick commitment
and the chosen tile was reached 6.5% of the time. The bound refuses a
candidate we cannot reach in TR_TFIL_ARRIVE_TICKS (default 0 = off = today).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-27 09:57:25 +02:00
SirStone 0b74b01c4b j150 (diag only): picker loss histogram + sweepable heat cutoff
Measures WHERE the lava picker loses tiles, per pick:
reachable hull -> CoolestLevels=2 distinct-value filter -> path heat
filter -> draw set -> chosen. TR_TFIL_DIAG (default off) fills
TfilLoss*; TR_TFIL_DANGER_THRESHOLD (default 10.0, the shipped const)
makes the cutoff sweepable offline. No decision logic changed: the guard
test proves the diag-on move stream is byte-for-byte the diag-off one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-27 09:51:19 +02:00
SirStone 5e213df8d9 j148: bound the corridor by speed x ticks (TR_{TFIL,STRAFE}_CORRIDOR_TICKS, default 0 = to the wall); UNTESTED 2026-09-27 08:23:16 +02:00
SirStone 32ec040e90 j147 results: 180-battle live A/B on the frozen 15-opponent panel
Within-mover references (the only comparison that isolates the knob): tfil_lag1
-0.04 wins/run, strafe_lag1 +0.18 wins/run, both far under the reported MDEs
(0.33 / 0.41) -> NOT distinguishable, so TR_FIRE_LAG stays default 0. Incoming
hit rate also unmoved (+0.94 / +0.33 pp). The one significant result is the
mover (strafe_lag1 vs tfil_off +0.38 wins/run p=0.0386, hit rate -4.67pp
p=0.0074), which is the known strafe-over-tfil gap and exactly why the
within-mover reference was pre-registered.

Also a cosmetic no-op refactor of the two spawn sites (compute velX/velY once;
bit-identical, and it keeps the default-parity claim exact).
2026-09-26 23:20:36 +02:00
SirStone d21f7ce5f5 j147: the 1-tick fire-detection lag is OURS — measure it, then back-date it (TR_FIRE_LAG)
MEASURED LIVE (common_libs/tests/measure_fire_ghost_lag.py, 4 sessions, 1777
matched ghost spawns, both movers): the server dispatches a turn's fire AFTER
our go() for that same turn, so a turn-T shot's energy drop first reaches our
scan at turn T+1 — and a bullet takes its FIRST step during the turn it is
fired, so the true bullet is already one whole bullet step (11-20 px) downrange.
Both movers place the ghost at the SCANNED enemy position (where the bullet was
born), so the whole ghost trajectory is the true one shifted one turn later and
the arrival deadline is a full tick late.

MEASURED: detection lag +1 tick on 100% of 1777 matched spawns; ghost-vs-
observer displacement 19.06 px mean / 22.00 p90 (tfil) and 16.08 / 21.81
(strafe); arrival-deadline error 0.99 / 0.77 ticks. NOT a rendering artefact:
the draw/advance order is correct (advanceBullets -> detectFires -> build).

THE FIX: TR_FIRE_LAG (int, default 0 = today byte-for-byte) in the shared
fire_tracker, applied by both movers at spawn: x = origin + dir*speed*lag.
The deadline needs no separate change — both movers derive it from the ghost's
own position, so a correct position gives a correct deadline.
WITH IT: displacement 19.06 -> 5.37 px mean (the residue is the enemy's own
<=8 px scan staleness) and the deadline error 0.99 -> 0.06 ticks.

Guards: test_tfil_commit_env 77 -> 87 checks (default golden parity, exact
n-step back-date, deadline shortens by exactly lag, junk/negative degrade to 0,
reaped exactly one tick earlier); test_env_report + test_env_dotenv green.
TR_FIRE_LAG registered in env_report + knownEnvNames + .env.example +
docs/env_reference.md. Live A/B pre-registered in docs/movement_campaign.md
(Batch 8) with its MDE stated up front; arms tools/ab/arms_fire_lag.txt.
TR_FIRE_DIAG gains a per-round ROUND line (the tick->getTurn anchor) and a
per-spawn SPAWN line (the ghost's drawn position).
2026-09-26 23:08:57 +02:00
SirStone de5d02ba3f j146 ledger: the field shape restores a real safe set offline (filter broken 63.5%->30.4%) and is a live null on damage/run and round wins; 375 battles, 5 arms 2026-09-26 22:35:17 +02:00
SirStone 298ea6d586 j146 TFIL: the bullet's own heat becomes tunable (TR_TFIL_BULLET_CORE/AURA, default 10/5 = today) and the field-shape A/B is pre-registered 2026-09-26 22:16:06 +02:00
SirStone 5dd921c26b j145 ledger: 300 battles, the turn tiebreak is a real but small mechanism with an under-powered outcome null 2026-09-26 22:13:03 +02:00
SirStone 39c90fd930 j145 TFIL: a turn-cost tiebreak among the SAFE tiles (default-off)
The picker scored candidates on pathMaxHeat alone and then drew uniformly
among the survivors, so a mirror-side tile was as likely as a straight-ahead
one. Added a continuous turn cost as a DRAW WEIGHT applied only after the
hard heat filter:

  w = max(1, round(1 + TR_TFIL_TURN_BIAS * (1 - max(0,|turn| - REF)/180)))

- TR_TFIL_TURN_BIAS (default 0) is the odds ratio straight-ahead vs 180 deg;
  TR_TFIL_TURN_REF_DEG (default 45) is where the penalty starts. Both
  default-off-effect: the default-path golden in test_tfil_commit_env.nim is
  unchanged and still passes.
- Turn cost is NEVER folded into the heat score. The filter stays hard.
- The draw stays random (j51 measured an argmin worse); every weight is
  floored at 1, so the pool can never be emptied and bias 0 is exactly the
  shipped uniform draw.
- |turn| now travels on the ScoredTile, and the commit log gained turn /
  minturn / promote so a caller can measure the regret of the draw.

Guards: 51 -> 66 checks (an absurd 99:1 bias never rescues an over-threshold
tile; mean |turn|, draw regret, >90 and mirror-side shares all fall; path
heat does not rise). env_report + .env.example updated.
2026-09-26 21:56:54 +02:00
SirStone 0e7e124c7f j144 ledger: the live A/B result, 600 battles, two independent blocks
Pooled verdict on the pre-registered rule is NOT DISTINGUISHABLE: the primary
cross-opponent sign test on round wins is 11/14, p=0.05737 for 'arrive', over the
0.05 line. Block 1 alone passed (p=0.01294); block 2 alone did not (p=0.0654).
Every delta is positive in both blocks for all three arms and damage/run is UP
on all three, so this reads as an under-powered null at the MDE boundary
(MDE 0.31 wins/run, observed 0.28) - but the verdict layer is not
reinterpreted, exactly as gate v1 was not.

The MECHANISM is established cleanly and replicates in both blocks: incoming hit
rate 18.07% -> 14.92%, sign-flip p=0.0013, CI [-5.29, -1.53] pp, damage taken
-24.57/run. That is precisely the pathology the owner watched.

Recommended for his own .env: TR_TFIL_COMMIT_ARRIVAL=1 and
TR_TFIL_NOREV_SPEED=4; leave TR_TFIL_COMMIT_MARGIN at 0 (weakest arm in both
blocks). Shipped default untouched: TR_MOVEMENT=strafe, all three new knobs
default off.
2026-09-26 21:43:03 +02:00
SirStone d2005abee9 j144 TFIL: arrival-based commitment + hysteresis + no mid-flight reversal
The owner's live-GUI report was correct on all four counts, and all four are
one bug: the commitment is cancelled by our own tile-boundary crossing
(96.1% of picks, 3793/3946, mean hold 5.06 ticks) while the bot is still
accelerating, and the picker is an unconstrained uniform draw over every
safe tile, so the new target can land in the mirror direction at |speed| < 4.

New knobs, all env-gated and default = today's behaviour (byte-for-byte
default parity guard re-run and green, 51 checks):
  TR_TFIL_COMMIT_ARRIVAL  hold the committed tile until we are ON it; the
                          tick knob becomes a MINIMUM dwell. 0 = shipped.
  TR_TFIL_COMMIT_MARGIN   leave only if the best alternative is at least
                          this much cooler on the same pathMaxHeat scale.
                          0 = shipped.
  TR_TFIL_NOREV_SPEED     while |speed| is below this, a mid-flight switch
                          may not take a tile >90 deg off the travel
                          direction. 0 = shipped. norevPool() never returns
                          an empty pool: with every candidate behind us it
                          takes the least-bad turn.

Offline gate (recorded DrussGT fixture, 20026 ticks): mean hold 4.1 -> 24.0
ticks, abandoned-before-arrival 92.8% -> 40.5%, committed tile actually
reached 3.3% -> 17.2%, opposite-direction slow mid-flight switches 394 -> 64
(-84%). 'TR_TFIL_TILE_REPLAN=off' alone - what cc11ede's arm B already tried -
only gets the hold to 13.6, which is why that A/B could not find this.

strafe is untouched: it imports only heatDecay/bulletMagScale/Pillar*, none
of which this touches. TR_MOVEMENT default stays strafe. Registered in
env_report.nim + knownEnvNames() + .env.example. Arms pre-registered in
docs/movement_campaign.md and tools/ab/arms_tfil_commit.txt.
2026-09-26 21:07:09 +02:00
SirStone d4a5d100e7 j143 rebuild .env.example: every TR_*/GUN_* knob, its default, one-line comment (includes the owner's uncommitted edits) 2026-09-26 19:35:50 +02:00
SirStone 5e32ec16df 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.
2026-09-26 19:20:23 +02:00
SirStone c9b67530db j141 fix: the per-gun real-shot accounting was still array[17] after rack id 17 landed
REGRESSION: the live bot stopped firing entirely and moved degenerately
whenever a rack admitted rack id 17 (the ADE+SBC BITBRAIN gun added in
e9302bc).

ROOT CAUSE: ModularBot.nim declared the per-gun accounting arrays
(gunRealShots / gunRealHits / gunRealShotsByMode / gunRealHitsByMode /
gunSelectionCount) as array[17, int] — the rack size BEFORE id 17 existed.
The instant the selector picked gun 17, the accounting indexed one past
the end:

  * debug build -> IndexDefect out of run(): the bot stops, 0 shots;
  * -d:release (shipped) -> silent out-of-bounds write onto the adjacent
    lastPowerLogKey: string header, so the bot kept moving but never
    fired and never reported a shot.

Measured with the owner's exact out/.env, 1v1 SittingDuck:
  0dc5552 (pre-rename):  BitBrain(id16) selected 356+157 ticks,
                         realShots 24+9, realHits 23+9,  ModularBot wins 180/360
  HEAD (e9302bc..)   :  selected 0, vShots 0, realShots 0, realHits 0
  debug               : IndexDefect on the first tick that selects id 17
  -d:release         : same 0/0/0, bot scores 22 and dies

FIX: derive every gun-indexed width from the rack instead of a literal.
gun_harness/selector exports NumRackGuns* = len(RackGunNames) (18);
ModularBot uses it for the five accounting arrays, the per-round reset
loops, the gun_stats.jsonl dump loop, the  table and
initTracker(). Shipped defaults unchanged: clean env is still the
onlyPattern rack and movement is still strafe.

GUARD: common_libs/tests/test_rack_stat_width.nim (24 checks) — rack
table shape, a source scan proving no gun-indexed width/loop bound is
narrower than NumRackGuns, an in-process accounting replay that would
have overflowed array[17], and the legacy-namespace checks (id 16 via
TR_RACK_BITBRAIN, id 17 never admitted while legacy). It reports 7
failures on the pre-fix ModularBot.nim and passes after. Optional
--live section proves a rack admitting only the newest id fires and
lands hits.

Parity: test_env_report 25, test_rack_membership 49, test_lead_gain_
registration 13, test_lead_gain_legacy 24, test_bitbrain_net 44,
test_gun_harness 39, test_tfil_commit_env 30, test_bitbrain 56,
test_tm_pattern_registration 20 — all unchanged, 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-26 18:58:26 +02:00
SirStone 59c5af0499 j140 fix: a pre-rename .env must not admit the disabled BitBrain net gun
TR_RACK_BITBRAIN=both is what the owner's live .env carries, and with the new
ADE+SBC gun registered at id 17 under the SAME rack name that value was also
landing on id 17 - so a gun that was not enabled (TR_BITBRAIN_NET unset) was
admitted into the rack and its placeholder predictions were pushed into the
shared VirtualTracker ring, which shifts every other gun's learning order.

While the namespace is LEGACY, loadRackMembership now skips id 17's
TR_RACK_BITBRAIN entirely, so that value addresses ONLY the gun it always
addressed (LEADGAIN, id 16). ModularBot additionally gates admission on
BitbrainNetGun.gunAdmitted(), and test_bitbrain_net pins the truth table: over
6 (rack, switch) settings there is NO configuration that admits the gun while
leaving it disabled.

Guards: test_env_report 25, test_rack_membership 49 (was 48; the revert
one-liner now sets TR_BITBRAIN_NET=1 and one truth-table check was added),
test_tm_pattern_registration 20, test_bitbrain 56, test_gun_harness 39,
test_tfil_commit_env 30, test_lead_gain_registration 13,
test_lead_gain_legacy 24, test_bitbrain_net 44.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-26 16:54:43 +02:00
SirStone e9302bc9f6 j140 rebuild a real BitBrain gun: ADE+SBC at rack id 17, default off, and measure its scaling
The BITBRAIN name was sitting on a gun with no network in it. This is the gun
that actually runs the algorithm: an ADE layer (thresholded random projections
with ONLINE threshold adaptation) feeding the SBC head from
common_libs/bitbrain/, with the counted+decay mode available.

  common_libs/guns/bitbrain_net.nim     the gun
  rack name BITBRAIN, rack id 17 (rack 17 -> 18 guns), both new guns default OFF
  admitted by TR_RACK_BITBRAIN=both AND TR_BITBRAIN_NET=1 (the switch that also
  disowns LEADGAIN's legacy TR_BITBRAIN_* aliases)

OUTPUT: a fine-grained aim CORRECTION on top of Pattern - the probability-
weighted mean of the nClasses class centres under inferProb - not a direct aim
point from the argmax. That is the shape docs/bitbrain_gate.md measured, and
Pattern is already a strong predictor, so the net's job is the signed residual.
Below TR_BITBRAIN_MINOBS the shift is exactly 0 and Pattern is returned
unchanged.

INPUT: a CONFIGURED set of FEATURE BLOCKS (TR_BITBRAIN_FEATURES=name:W), each
block's width == its resolution, laid out as a thermometer code over 0/255 slots
(so an ADE synapse 'matches' when its polarity agrees with the slot and a random
ADE fires iff its w synapses all match, rate 2^-w). Default is 52 slots over 9
blocks. NO long temporal window, per docs/state_window_gate.md: the only history
is a 12-tick ring feeding three rate/turn quantities.

Every knob env-configurable: _INPUT (width), _NCLASSES, _NADES, _WIDTHS
(clause widths), _FEATURES, _SPAN, _MODE, _DECAY_EVERY, _DECAY_SHIFT,
_MINOBS, _ADAPT_EVERY, _TARGET, _NETSEED, _NETLOG, _NET_RESET_ON_TARGET.

MEASURED SCALING (measure_bitbrain_scaling.nim, 3 recorded runs, 37412 ticks,
-d:release, one predict per power bin per tick, timed region = predicts only):
  RAM 1.59 MB default (98.6% SBC tensors); linear in nClasses, QUADRATIC in
      nAde, FLAT in input width; counted/bitset = 7.30x on RAM, ~1x on time.
  ms/tick 2.70 default = 21% of the 13.16 ms budget; 64 classes busts it (149%),
      nAde 512 uses 74%, nAde 64 uses 2%.
  CAPACITY vs ACCURACY: over a 100x RAM range the offline mean |err| moves
      17.254 -> 17.115 deg around Pattern's 16.964, and the sign flips along the
      nClasses axis, so it is noise, not a trend. The corrector is consistently
      slightly WORSE than Pattern. The ceiling is the STATE, not the classifier.
      VETO-CAPABLE OFFLINE CHECK ONLY (docs/offline_harness_trust.md), never
      presented as a live win.

ENGAGEMENT is proven, not assumed: test_bitbrain_net.nim (42 checks) shows 0
bytes before first use, different inputs -> different class outputs, a learn
raises SBC occupancy, bitset learn idempotent while counted learn is monotone,
threshold adaptation runs, and the global RNG is untouched.

Parity: shipped rack still onlyPattern, shipped movement still strafe. Guards:
test_env_report 25, test_rack_membership 48, test_tm_pattern_registration 20,
test_lead_gain_registration 13, test_lead_gain_legacy 24, test_bitbrain 56,
test_gun_harness 39, test_tfil_commit_env 30, test_bitbrain_net 42.
Clean archive build: [SuccessX].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-26 16:50:21 +02:00
SirStone 7a6237ec20 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>
2026-09-26 15:54:00 +02:00
SirStone 0dc5552c73 j139 dotenv: strip trailing inline comments and warn on non-token values
A `#` preceded by whitespace and outside quotes now ends the value, so
`TR_DEBUG_DRAW=0      # hides the grid` resolves to `0` instead of the
whole tail. Values that are still not plain tokens (whitespace, `#`, an
unclosed quote) get one `[dotenv] WARNING` line naming file, key, raw value
and the fact that the reader falls back to its DEFAULT, instead of being
applied silently. Guard test 29 -> 47 checks.
2026-09-26 15:18:04 +02:00
SirStone 10473fc211 j138 draw-only: yellow triangle (enemy-heading other point, enemy, us) in the geometry overlay 2026-09-26 15:14:48 +02:00
SirStone 189d990812 j136 [modules] inventory: also list the always-on body API 2026-09-26 14:48:43 +02:00
SirStone 36c4334487 bitbrain gun README: the gain warning was wrong and contradicted its own verdict section (it recommended the direction measured decisively harmful) 2026-09-26 14:48:23 +02:00
SirStone 7516ca47c0 docs: quick-recap README for bitbrain gun (inputs/outputs, knobs, verdict) 2026-09-26 14:47:42 +02:00
SirStone 8f44140783 j136 add TR_MODULE_* on/off switches + single [modules] boot inventory 2026-09-26 14:46:29 +02:00
SirStone fd34e4ae4a j135 draw-only geometry overlay (TR_GEO_DEBUG) + global debug-draw switch (TR_DEBUG_DRAW) 2026-09-26 14:29:46 +02:00
SirStone 3903ec71b4 j134 ledger: note phantom_meteor is dead code (not selectable), out of scope 2026-09-26 13:01:36 +02:00
SirStone 3237b65f40 j134 ledger: propagated fire fix to all 5 movers via one shared fire_tracker (TR_FIRE_FIX), per-mover catch table 98.888->100, live tick alignment measured (correction belongs on event_tick+2) 2026-09-26 12:59:35 +02:00
SirStone 882733819b j134 fix: tracker must take the mover's switch per-call (instance flag was silently off on the object-literal construction path) + apply the correction on the reading that carries the server's turn-N+1 energy change (two-slot buffer); live-verified alignment 2026-09-26 12:56:26 +02:00
SirStone 6ad5d99922 j134 fire fix: share ONE fire_tracker across tfil/ring/strafe/learned/surf (TR_FIRE_FIX, default on); env-gated TR_FIRE_DIAG alignment trace 2026-09-26 12:40:19 +02:00
SirStone bd57db6d63 j133 strafe: correct the comment's measured fire count (67065, catch 98.888%->100.000%) 2026-09-26 12:25:06 +02:00
SirStone 3bee4353b2 j133 ledger: appended 'Missed fires + the label question' (catch 98.888->100%, 746/67065 shots were blind: 456 by the server +3*power bonus, 290 by our own same-tick damage; exact label still -0.230, state-conditional model -0.347 -> the observable state is the constraint) + report fixtures 2026-09-26 12:22:31 +02:00
SirStone 799438bbe9 j133 strafe fire detection: correct the enemy energy delta for the server's +3*power hit bonus and our own damage, never drop a too-large drop (TR_STRAFE_FIRE_FIX, default on); catch 98.89%->100% of enemy fires on the 70-battle corpus 2026-09-26 12:18:21 +02:00
SirStone 0566264b06 j132 strafe display check: also assert the chosen tile's ramp border and value plate reach the SVG 2026-09-26 11:54:14 +02:00
SirStone cc332138b3 j131 learned movement: real bullet-endpoint resolution (TR_LEARNED_REAL_EVENTS, default off) + exact-geometry Gate A/B (inversion NOT fixed; state still the constraint) 2026-09-26 11:54:11 +02:00
SirStone d5047598b5 j132 strafe display check: also assert the chosen tile's heat number reaches the SVG with the heat grid ON and OFF 2026-09-26 11:53:10 +02:00
SirStone 4db46f02dd j132 strafe pick quality: chosen tile's heat in the [strafe] log line (heat=h/10.0 alt=best-other ok=0|1) and ON the tile (green/yellow/red ramp + value), legible with TR_STRAFE_HEAT_GRID=0; deterministic display check vs pathMaxHeat 2026-09-26 11:51:33 +02:00
SirStone 8dd9b3b3b5 j130 learned movement outcome label: live panel results (no arm beats strafe; label swap is a dead heat; gap is information) + Gate A report 2026-09-26 11:35:47 +02:00
SirStone 61def1c3e9 j130 learned movement: outcome label (P(hit|state,g)) mode + Gate A; pre-registered outcome arms 2026-09-26 11:27:03 +02:00
SirStone 4c934a67c1 j129: add entry-point check for the virtual-bullet overlay draw proc 2026-09-26 11:03:14 +02:00