589a230106
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.
110 lines
4.0 KiB
Nim
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()
|