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