Commit Graph

4 Commits

Author SHA1 Message Date
SirStone 81af5854df FIX an incomplete commit: TMHORIZON was admitted unconditionally at HEAD
MY ERROR. `aed579b` committed `ModularBot.nim` (which wires the new gun as id 15)
and `guns/tm_horizon.nim`, but I staged only three files and left job-72's RACK
REGISTRATION behind in the working tree. Consequence at HEAD:

- `common_libs/gun_harness/selector.nim` still had the 15-entry rack table
  (ids 0..14) with no TMHORIZON name, so
- `TR_RACK_TMHORIZON` was never read (a silent no-op), and
- id 15 is PAST the table, and `rackAdmitted` documents "an id past the
  membership table is admitted" -> **TMHORIZON was admitted unconditionally**.

So the committed default rack was `Pattern + TMHORIZON`, not the intended
`Pattern only`, and the gun could not be switched off by env at HEAD. This was
caught by a live A/B job that had to work around it with `GUN_RACK_DISABLE=15`.

FIX (this commit): the rack table goes to 16 entries with `TMHORIZON` appended at
id 15 defaulting to `rmOff`, plus the two tests whose length literals follow it
(`test_rack_membership` 15->16, `test_tm_pattern_registration` 15->16).
`DefaultRackMembership` is again Pattern-only with every other gun `off`.

NOTE ON SCOPE: `selector.nim` in the working tree also contains a CONCURRENT job's
power-policy threading (an `enemyEnergy` parameter). That work is still in flight
and is deliberately NOT included here - only the three rack hunks were staged.
The remainder stays unstaged for its own commit.

The lesson, recorded because it has bitten twice tonight in different forms: a
change is not committed until its registration/table counterpart is, and
`git add` of a hand-picked file list is exactly how a half-change ships.
2026-09-22 23:46:56 +02:00
SirStone 31c7c01d28 SHIPPED: the default rack is now Pattern-only (+49% hit rate, +66% damage on the boss)
`DefaultRackMembership` now admits Pattern (id 5) and marks all 14 other guns
`rmOff`. **The selector mechanism is untouched** - `chooseFromFit`, the floor/band
logic, the hysteresis and the virtual-fitness plumbing are all intact and
functional. Only the rack membership changed, so this is reverted by env alone.

Evidence (measured, replicated three times, 10 adversaries): Pattern alone gives
10.36% real hit rate / 264 damage per run vs the full rack's 6.93% / 159. Pattern
significantly wins on DrussGT, Corners, Crazy and PatternMover, ties on three, and
the full rack never significantly beats it on ANY adversary. Mechanism: the
virtual signal keeps ranking the wrong guns first (HeadOn 46% of ticks at 2.0%
real; Linear 57.7% at 6.0% real while Pattern sits at 11.1%).

**THIS CONTRADICTS THE USER'S STANDING DIRECTIVE** to keep virtual-fitness
selection. Recorded plainly in docs/selector_negative_value.md with a SHIPPED
DECISION banner rather than done quietly: the mechanism is retained and one env
var away, because the measurement says it is negative value on every rack size
tested and on 10/10 adversaries.

Revert one-liner (no rebuild):
  TR_RACK_PATTERN=both TR_RACK_HEADON=both TR_RACK_LINEAR=both TR_RACK_TSETLIN=both \
  TR_RACK_CIRCULAR=both TR_RACK_GUESSFACTOR=both TR_RACK_WALLBOUNCE=both \
  TR_RACK_ACCEL=both TR_RACK_STOPSHOT=both TR_RACK_DISPLACE=both TR_RACK_AVGLEAD=both \
  TR_RACK_DECAYGF=both TR_RACK_KNN=both TR_RACK_TMSELECT=both ./out/ModularBot
The unit test `testRevertOverrideRestoresFullRack` exercises exactly this table.

FLOOR PATH, verified not assumed: `chooseFromFit` already returns `admitted[0]` on
the floor path, so it respects admission by construction. Cold field + shipped
default -> floor returns Pattern (id 5), NOT HeadOn. With an explicit all-`both`
membership the same cold field returns gun 0 (HeadOn) - the old behaviour. Four
assertions in `testFloorRespectsAdmission`.

LIVENESS: one 1-round battle with NO overrides -> Pattern selected 105/105 = 100%,
every other gun 0 including TMPattern.

Honesty caveat retained in the doc: 4 of the 10 opponents were Tank Royale
sample-bot PORTS rather than the original classic jars (only DrussGT is a real
classic jar through the shim).

Guards: test_rack_membership 48 (was 38; new floor/revert/default checks),
test_tm_pattern_registration 20 (5 checks hard-coded the old default and were
updated to assert the new one, with the TMPATTERN parity proof moved onto an
explicit old-rack table), test_gun_harness 39, test_vbullet_metric 11,
test_power_selection 3, test_adaptive_radar 41, test_tfil_ring_weights 24,
test_power_policy 26, test_ram_decision 28, test_selector_tiebreak 19,
test_tm_pattern_rack_live 4, test_tm_pattern_learning 3,
acceptance_offline_vs_online 12/12 VERDICT PASS. ModularBot compiles.

FOLLOW-ON THIS EXPOSED: membership filters SELECTION but not virtual-bullet
SPAWNING, so under `onlyPattern` the 13 unselected guns still predict and spawn
every tick. Tsetlin alone is ~5.3 ms/tick (~41% of the 13.16 ms per-tick budget),
so we are still paying for it while never using it. Gating spawn on admission
would reclaim that; it was deliberately NOT done here because it would alter the
measurement protocol mid-A/B.
2026-09-22 02:10:07 +02:00
SirStone 589a230106 TM radial gun: registered (default OFF) + label-bias fix that removes the bias but
retracts its own earlier learning claim

=== TASK 1: REGISTERED AS GUN 14, DEFAULT `off` ===
The radial TM gun is now a first-class rack member (`TMPATTERN`, id 14), forceable
alone with `TR_RACK_TMPATTERN=both` plus every other `TR_RACK_*=off`.
DEFAULT IS `off`, and the justification matters: `both` would let it compete for
selection AND (because the shared VirtualTracker ring is order-sensitive) shift
every other gun's learning order, so it CANNOT leave the default path unchanged.
With `off` its predict and spawnBullets are additionally GATED on rack admission
(the only gun wired that way), so the shipped default never spawns it at all:
zero cost, zero ring perturbation.
Live proof: 1-round battle with only TMPATTERN racked ->
  `gun 14 (TMPattern): vShots=400 selected=104 other-gun selections=0`.
Default-path-unchanged proof: parity checks that the 15-gun default bestGun/
selectGun equals the old 14-gun rack RNG-draw-for-RNG-draw, that gun 14 is never
selected by default, and acceptance 12/12.
Cost: 0.36 ms/tick (predict 0.30 + onResult 0.05) ~= 3% of the 13.16 ms budget.
Tsetlin in the same harness is 1.62 ms/tick, so the new gun is ~4.5x cheaper.

=== TASK 2: THE LABEL-BIAS FIX - AND A RETRACTION ===
Root cause confirmed: under bmPoint a SHORT radial correction resolves the virtual
bullet BEFORE the base arrival tick, so the label was dropped (labelMisses).
Fix: defer the label in a pending queue and flush it once the arrival tick is
recorded; labels still come from the BASE arrival tick.
  labelMisses        4,281,695  ->  0
  training samples   1,071,824  ->  5,345,847  (x5)
  radial head acc         48.8% ->  57.0%   (shuffled control 20.0%)
  bmPoint hit rate     9.4/5.8% ->  9.1/5.7%  (unchanged, within noise)
So the fix IMPROVES LEARNING but NOT the metric.

**RETRACTION OF THE PREVIOUS JOB'S CLAIM.** It reported the radial head's 48.8%
against a 36.7% majority baseline and concluded "conditional learning, not a
constant bias". With the bias removed, the correctly-measured majority baseline is
**58.2%** - so the head at 57.0% is AT/BELOW majority. The earlier apparent
conditional learning was PARTLY AN ARTEFACT OF THE BIASED SAMPLE. The bmPoint
metric win is real (TMRadial > Linear early 16/2 p=0.0013, overall 18/0 p<0.0001;
> shuffled 18/0 p<0.0001) but it comes from a NET-POSITIVE AVERAGE RADIAL SHIFT,
not from beating a majority classifier. Recorded plainly rather than left standing.

Guards: test_tm_pattern_registration 20 (new), test_tm_pattern_rack_live 4 (new),
test_gun_harness 39, test_vbullet_metric 11, test_power_selection 3 (the SIGSEGV is
gone - the knn_gun rewrite is now committed), test_adaptive_radar 41,
test_tfil_ring_weights 24, test_power_policy 26, test_ram_decision 28,
test_rack_membership 38, test_selector_tiebreak 19, test_tm_pattern_learning 3,
acceptance_offline_vs_online 12/12. ModularBot compiles (release).

Note: `common_libs/tests/range_guns.nim` still builds 14 offline drivers (the
offline sweep constructs TmPatternGun directly and acceptance only inspects ids
0..13), so nothing breaks - but a future job wanting it in the offline rack must
add a 15th driver and mirror the live admission gating. gun_stats.jsonl now emits
15 rows; downstream tooling should ignore id 14.
2026-09-22 01:58:33 +02:00
SirStone a73de13458 racks: separate melee and 1v1 gun racks, plus per-mode real hit-rate data
The user's plan: "separate racks for melee and 1v1, so the bot switches from
those based on the situation, and we can put the guns we want in one or both
racks."

MECHANISM
- `RackMode` (rm1v1/rmMelee) derived from SERVER TRUTH: `rackMode(enemyCount)`
  = 1v1 when the count is 1, melee otherwise. This is the SAME `getEnemyCount()`
  value the radar already uses, so there is now ONE definition of the mode.
  (Using the tracker's known-enemy count was a previous bug in the radar: it
  read 1 before the second enemy was scanned.)
- `RackMembership` per gun: both (default) | 1v1 | melee | off.
- The selector ranks only admitted guns - including the floor path and the
  incumbent-hysteresis path.
- Empty filtered set FALLS BACK to the full rack, so the bot can never end up
  with no gun.
- Env-overridable at process start, no rebuild: `TR_RACK_<GUN>` for all 14 guns
  (TR_RACK_HEADON, TR_RACK_LINEAR, ... TR_RACK_TMSELECT), values
  both|1v1|melee|off. Empty/unknown -> both + a stderr warning, never fatal.
- `[rack] mode=<1v1|melee> active=<guns> overrides=<...>` logged once per mode
  change, never per tick.

DEFAULT IS UNCHANGED: every gun ships `rmBoth`, so behaviour is byte-identical
until the user re-racks anything. Verified by the unit test's default-config
selection parity (RNG draw for RNG draw) and by `test_gun_harness` 39 and
acceptance 12/12. `chooseFromFit` iterates the admitted list in ascending id
order, so the random tie-break draws are unchanged.

NO TUNING DONE, deliberately: we had no per-gun melee hit-rate data, and an
earlier 15-paired-run experiment found pruning neutral-to-negative on hit rate
(p=0.57/0.21). So all guns stay `both` and the membership pass waits for data.

PER-MODE DATA PLUMBING (this is what unblocks that pass): per-gun real shot
accounting is now split by the rack in force at fire time, adding to
gun_stats.jsonl: realShots1v1, realHits1v1, realHitRate1v1, realShotsMelee,
realHitsMelee, realHitRateMelee.

Verification: test_rack_membership 38/38 (new, pure, no battle); test_gun_harness
39, test_vbullet_metric 11, test_power_selection 3, test_adaptive_radar 41,
test_tfil_ring_weights 24, test_power_policy 26, test_ram_decision 28;
acceptance_offline_vs_online 12/12 VERDICT PASS; ModularBot compiles. The live
`[rack]` line was observed switching 1v1 -> melee when the enemy died.

The offline range never calls the selector (only spawnBullets/tickBullets/
reportFor), so mode filtering cannot change the offline result and no offline
mode parameter was needed - confirmed by reasoning over the source and by 12/12.
2026-09-22 00:45:10 +02:00