Files
SirRoboGarage/common_libs/tests/test_tm_pattern_rack_live.nim
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

110 lines
4.0 KiB
Nim

## MINIMAL LIVE CHECK for the rack-registered TM pattern gun (id 14).
##
## Proves the new gun is a first-class rack member that the LIVE ModularBot
## actually predicts, spawns and can be forced alone with env vars only:
##
## TR_RACK_HEADON=off ... TR_RACK_TMSELECT=off TR_RACK_TMPATTERN=both
##
## One 1-round battle (deliberately minimal — the full A/B runs in a later job).
## Reads /tmp/tm_pattern_rack_stats.jsonl (the bot's per-round gun dump) and
## asserts gun 14 was selected and spawned virtual bullets while every other gun
## was deselected.
##
## Run with:
## TR_SERVER_JAR=... TR_BATTLE_RUNNER=... \
## nim c -r common_libs/tests/test_tm_pattern_rack_live.nim
##
## Skips (exit 0) when the JARs are absent, matching the other live tests.
import std/[os, json, strutils, tables]
import test_framework/test_framework
const
repoRoot = currentSourcePath().parentDir.parentDir.parentDir
modularBotDir = repoRoot / "ModularBot_garage"
adversaryDir = repoRoot / "common_libs" / "test_framework" / "adversaries" / "SittingDuck"
statsPath = "/tmp/tm_pattern_rack_stats.jsonl"
const TmPatternId = 14
const RackGunNames = ["HEADON", "LINEAR", "TSETLIN", "CIRCULAR", "GUESSFACTOR",
"PATTERN", "WALLBOUNCE", "ACCEL", "STOPSHOT", "DISPLACE", "AVGLEAD",
"DECAYGF", "KNN", "TMSELECT", "TMPATTERN"]
var failures = 0
proc check(name: string, ok: bool) =
if ok: echo "PASS: ", name
else: echo "FAIL: ", name; inc failures
proc lastRound(path: string): JsonNode =
result = nil
for line in lines(path):
let s = line.strip()
if s.len == 0: continue
let node = parseJson(s)
if node.hasKey("guns"): result = node
proc main() =
let serverJar = getEnv("TR_SERVER_JAR", "")
let runnerJar = getEnv("TR_BATTLE_RUNNER", "")
if serverJar.len == 0 or runnerJar.len == 0:
echo "Skipping: TR_SERVER_JAR / TR_BATTLE_RUNNER not set"
quit(0)
if not fileExists(serverJar) or not fileExists(runnerJar):
echo "Skipping: TR JARs not found (server=", serverJar, ", runner=", runnerJar, ")"
quit(0)
if not fileExists(modularBotDir / "src" / "ModularBot.nim"):
echo "Skipping: ModularBot source not found"
quit(0)
if fileExists(statsPath): removeFile(statsPath)
# Force the new gun alone. Every other rack entry is removed; TMPATTERN is
# admitted in both racks. With no other candidate, the selector must pick it.
for i, name in RackGunNames:
putEnv("TR_RACK_" & name, if i == TmPatternId: "both" else: "off")
putEnv("GUN_VBULLET_METRIC", "point") # the metric the radial win lives on
putEnv("GUN_STATS_PATH", statsPath)
defer:
for name in RackGunNames: delEnv("TR_RACK_" & name)
delEnv("GUN_VBULLET_METRIC")
delEnv("GUN_STATS_PATH")
echo "=== live: ModularBot (only TMPATTERN racked) vs SittingDuck, 1 round ==="
let battle = runBattle(@[modularBotDir, adversaryDir], rounds = 1,
timeout = 180000, maxSpeed = true)
for res in battle.results:
echo " ", res.name, " rank=", res.rank, " score=", res.totalScore
if not fileExists(statsPath):
echo "FAIL: no gun_stats dump at ", statsPath
quit(1)
let round = lastRound(statsPath)
check "gun_stats.jsonl has a round record", round != nil
if round == nil: quit(1)
var tmShots = 0
var tmSelected = 0
var otherSelected = 0
for g in round["guns"]:
let id = g["id"].getInt()
if id == TmPatternId:
tmShots = g["vShots"].getInt()
tmSelected = g["selected"].getInt()
elif g["selected"].getInt() > 0:
otherSelected += g["selected"].getInt()
echo "gun 14 (TMPattern): vShots=", tmShots, " selected=", tmSelected,
" other-gun selections=", otherSelected
check "the live loop SPAWNED virtual bullets for gun 14 (vShots > 0)", tmShots > 0
check "the live selector SELECTED gun 14 (selected > 0)", tmSelected > 0
check "no other gun was selected while TMPATTERN was forced alone",
otherSelected == 0
if failures > 0:
echo "\n", failures, " check(s) FAILED"
quit(1)
echo "\nTM pattern rack live check passed."
when isMainModule:
main()