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.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
## ModularBot — plugin gun architecture tracer bullet.
|
||||
## Guns: HeadOnGun (0), LinearGun (1), TsetlinGun (2), CircularGun (3), GFGun (4), PatternMatcherGun (5), WallBounceGun (6), AccelGun (7), StopShotGun (8), DisplacementGun (9), AveragedLeadGun (10), DecayGFGun (11), KNNGun (12), TmSelectorGun (13) via GunHarness.
|
||||
## Guns: HeadOnGun (0), LinearGun (1), TsetlinGun (2), CircularGun (3), GFGun (4), PatternMatcherGun (5), WallBounceGun (6), AccelGun (7), StopShotGun (8), DisplacementGun (9), AveragedLeadGun (10), DecayGFGun (11), KNNGun (12), TmSelectorGun (13), TmPatternGun (14) via GunHarness.
|
||||
## Radar: RadarLockModule (1v1) / AdaptiveMeleeRadarModule (2+ enemies), auto-switched per tick.
|
||||
## Movement: OscillatorModule (perpendicular strafing).
|
||||
|
||||
@@ -25,6 +25,7 @@ import guns/averaged_lead
|
||||
import guns/decay_gf
|
||||
import guns/knn_gun
|
||||
import guns/tm_selector
|
||||
import guns/tm_pattern
|
||||
import movements/phantom_meteor
|
||||
import movements/rammer
|
||||
import movements/ram_decision
|
||||
@@ -106,7 +107,14 @@ let ShotLogPath = getEnv("GUN_SHOTLOG_PATH", "/tmp/shot_log.jsonl")
|
||||
## shows why the cap moved. The policy itself lives in the shared gun harness
|
||||
## (`applyPowerPolicy`), so both live and offline paths see the same rule.
|
||||
let PowerLog = existsEnv("TR_POWER_LOG")
|
||||
const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor", "Pattern", "WallBounce", "Accel", "StopShot", "Displace", "AvgLead", "DecayGF", "KNN", "TMSelect"]
|
||||
const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor", "Pattern", "WallBounce", "Accel", "StopShot", "Displace", "AvgLead", "DecayGF", "KNN", "TMSelect", "TMPattern"]
|
||||
|
||||
## Rack id of the new TM pattern gun. It defaults to `TR_RACK_TMPATTERN=off`;
|
||||
## unlike the other guns, its virtual-bullet spawn is gated on rack admission
|
||||
## (see the aim block in `run`), so the shipped default never spawns it and the
|
||||
## shared VirtualTracker ring head — hence every other gun's learning order and
|
||||
## the default selection sequence — is byte-for-byte unchanged.
|
||||
const TmPatternId = 14
|
||||
|
||||
const
|
||||
CLR_GUN = "\e[33m" # yellow
|
||||
@@ -153,6 +161,7 @@ type
|
||||
decayGF: DecayGFGun
|
||||
knnGun: KNNGun
|
||||
tmSelector: TmSelectorGun
|
||||
tmPattern: TmPatternGun
|
||||
mover: TFILModule
|
||||
ringMover: TFILRingModule
|
||||
rammer: RammerModule
|
||||
@@ -179,18 +188,18 @@ type
|
||||
roundNumber: int
|
||||
realShotsFired: int
|
||||
realHits: int
|
||||
gunRealShots: array[14, int]
|
||||
gunRealHits: array[14, int]
|
||||
gunRealShots: array[15, int]
|
||||
gunRealHits: array[15, int]
|
||||
# Same real-shot accounting split by the rack in force at fire time, so a
|
||||
# later data-driven pass can rank guns per mode (1v1 vs melee).
|
||||
gunRealShotsByMode: array[vb.RackMode, array[14, int]]
|
||||
gunRealHitsByMode: array[vb.RackMode, array[14, int]]
|
||||
gunRealShotsByMode: array[vb.RackMode, array[15, int]]
|
||||
gunRealHitsByMode: array[vb.RackMode, array[15, int]]
|
||||
pendingFires: seq[PendingShot] ## FIFO of fired shots awaiting onBulletFired bulletId stamp
|
||||
bulletGun: Table[int, int] ## bulletId -> gun id, filled on onBulletFired, drained on resolution
|
||||
bulletMode: Table[int, vb.RackMode] ## bulletId -> rack at fire time
|
||||
bulletShot: Table[int, PendingShot] ## bulletId -> shot metadata (Task A shot log)
|
||||
pendingHitBullets: HashSet[int] ## hit bulletIds seen before their onBulletFired stamp
|
||||
gunSelectionCount: array[14, int]
|
||||
gunSelectionCount: array[15, int]
|
||||
lastPowerLogKey: string ## change detector for the TR_POWER_LOG line
|
||||
lastKnownTargetId: int ## persists through death, used for round-end stats
|
||||
# Radar measurement instrumentation (only touched when RadarScanLog is set).
|
||||
@@ -554,7 +563,7 @@ method onRoundEnded*(bot: ModularBot, e: RoundEndedEventForBot) =
|
||||
let fit = bot.tracker.fitnessFor(targetId)
|
||||
|
||||
var gunsArr = newJArray()
|
||||
for gid in 0..<14:
|
||||
for gid in 0..<15:
|
||||
var totalShots = 0
|
||||
var totalHits = 0
|
||||
for binIdx in 0..<len(vb.PowerBins):
|
||||
@@ -667,12 +676,12 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
|
||||
bot.radarAcquireTicks = 0
|
||||
bot.radarTrackTicks = 0
|
||||
bot.radarMeleeActive = false
|
||||
for i in 0..<14:
|
||||
for i in 0..<15:
|
||||
bot.gunSelectionCount[i] = 0
|
||||
bot.gunRealShots[i] = 0
|
||||
bot.gunRealHits[i] = 0
|
||||
for m in vb.RackMode:
|
||||
for i in 0..<14:
|
||||
for i in 0..<15:
|
||||
bot.gunRealShotsByMode[m][i] = 0
|
||||
bot.gunRealHitsByMode[m][i] = 0
|
||||
# Reset per-round integrity counters so each /tmp/gun_stats.jsonl line reports
|
||||
@@ -693,6 +702,10 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
|
||||
bot.mover.resetRound()
|
||||
bot.ringMover.resetRound()
|
||||
bot.enemyTracker.resetRound()
|
||||
# The TM pattern gun is cold every round (no cross-battle persistence): wipe
|
||||
# its clause teams and motion history explicitly, so a same-id opponent in the
|
||||
# next round cannot inherit the previous round's overfit net.
|
||||
bot.tmPattern.resetLearning()
|
||||
bot.isRamming = false
|
||||
bot.ramStuckTicks = 0
|
||||
bot.ramDurationTicks = 0
|
||||
@@ -981,6 +994,7 @@ method run*(bot: ModularBot) =
|
||||
var dgPreds: array[len(PowerBins), GunPrediction]
|
||||
var knnPreds: array[len(PowerBins), GunPrediction]
|
||||
var tmselPreds: array[len(PowerBins), GunPrediction]
|
||||
var tmpPreds: array[len(PowerBins), GunPrediction]
|
||||
for i in 0..<len(PowerBins):
|
||||
headsUp[i] = bot.headOn.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||
linPreds[i] = bot.linear.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||
@@ -997,6 +1011,10 @@ method run*(bot: ModularBot) =
|
||||
knnPreds[i] = bot.knnGun.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||
if EnableTmSelector:
|
||||
tmselPreds[i] = bot.tmSelector.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||
# TMPATTERN predict is gated on rack admission, not just spawn: with the
|
||||
# shipped `off` default it costs nothing and touches no shared state.
|
||||
if rackAdmitted(TmPatternId, bot.rackMode, ActiveRackMembership):
|
||||
tmpPreds[i] = bot.tmPattern.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||
|
||||
bot.tracker.spawnBullets(0, headsUp, bot.lastState, tid)
|
||||
if not gunDisabled(1): bot.tracker.spawnBullets(1, linPreds, bot.lastState, tid)
|
||||
@@ -1014,6 +1032,9 @@ method run*(bot: ModularBot) =
|
||||
if not gunDisabled(12): bot.tracker.spawnBullets(12, knnPreds, bot.lastState, tid)
|
||||
if EnableTmSelector and not gunDisabled(13) and bot.tmSelector.isWarmedUp():
|
||||
bot.tracker.spawnBullets(13, tmselPreds, bot.lastState, tid)
|
||||
if not gunDisabled(TmPatternId) and
|
||||
rackAdmitted(TmPatternId, bot.rackMode, ActiveRackMembership):
|
||||
bot.tracker.spawnBullets(TmPatternId, tmpPreds, bot.lastState, tid)
|
||||
|
||||
# Build slim enemy table for tickBullets
|
||||
var enemyPositions: Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]]
|
||||
@@ -1042,6 +1063,7 @@ method run*(bot: ModularBot) =
|
||||
of 11: bot.decayGF.onResult(fe)
|
||||
of 12: bot.knnGun.onResult(fe)
|
||||
of 13: bot.tmSelector.onResult(fe)
|
||||
of TmPatternId: bot.tmPattern.onResult(fe)
|
||||
else: discard
|
||||
if fe.hit: inc bot.virtualHits else: inc bot.virtualMiss
|
||||
when DebugVBullets:
|
||||
@@ -1083,6 +1105,7 @@ method run*(bot: ModularBot) =
|
||||
of 11: setTurretColor("#008888"); setBulletColor("#00AAAA")
|
||||
of 12: setTurretColor("#CC00CC"); setBulletColor("#FF44FF")
|
||||
of 13: setTurretColor("#00FFCC"); setBulletColor("#66FFDD")
|
||||
of TmPatternId: setTurretColor("#AAFF00"); setBulletColor("#CCFF66")
|
||||
else: discard
|
||||
|
||||
let pred = case selectedGun
|
||||
@@ -1099,6 +1122,7 @@ method run*(bot: ModularBot) =
|
||||
of 11: bot.decayGF.predict(bot.lastState, bulletSpeed(power))
|
||||
of 12: bot.knnGun.predict(bot.lastState, bulletSpeed(power))
|
||||
of 13: bot.tmSelector.predict(bot.lastState, bulletSpeed(power))
|
||||
of TmPatternId: bot.tmPattern.predict(bot.lastState, bulletSpeed(power))
|
||||
else: bot.headOn.predict(bot.lastState, bulletSpeed(power))
|
||||
let aimTarget = aimAngle(getX(), getY(), pred.x, pred.y)
|
||||
|
||||
@@ -1153,7 +1177,7 @@ proc seedSelectorRng() =
|
||||
|
||||
when isMainModule:
|
||||
var bot = ModularBot(
|
||||
tracker: vb.initTracker(14), # 0: HeadOn, 1: Linear, 2: Tsetlin, 3: Circular, 4: GuessFactor, 5: Pattern, 6: WallBounce, 7: Accel, 8: StopShot, 9: Displace, 10: AvgLead, 11: DecayGF, 12: KNN, 13: TMSelect
|
||||
tracker: vb.initTracker(15), # 0: HeadOn, 1: Linear, 2: Tsetlin, 3: Circular, 4: GuessFactor, 5: Pattern, 6: WallBounce, 7: Accel, 8: StopShot, 9: Displace, 10: AvgLead, 11: DecayGF, 12: KNN, 13: TMSelect, 14: TMPattern
|
||||
headOn: HeadOnGun(),
|
||||
linear: LinearGun(),
|
||||
circular: CircularGun(),
|
||||
@@ -1168,6 +1192,7 @@ when isMainModule:
|
||||
decayGF: initDecayGFGun(),
|
||||
knnGun: initKNNGun(),
|
||||
tmSelector: initTmSelectorGun(),
|
||||
tmPattern: initTmRadialGun(),
|
||||
radar: RadarLockModule(),
|
||||
meleeRadar: initAdaptiveMeleeRadar(),
|
||||
mover: TFILModule(debugGraphics: true),
|
||||
|
||||
Reference in New Issue
Block a user