feat(guns): scale-aware power selection (+52% damage); TM classifier gun built, measured, DISABLED
TASK 2 - power selection, a clear win. bestPower used an ABSOLUTE MinHitRate = 0.40 bar. Measured per-bin virtual rates (rolling-100 fraction) show no bin ever clears 40%, so 11 of 14 guns were stuck at bin 0 (power 1.0) even where higher bins were comparable: Linear p1.0 44% p1.5 39% p2.0 30% p3.0 29% old bin 0 -> new bin 3 Accel p1.0 44% p1.5 40% p2.0 26% p3.0 29% old bin 1 -> new bin 3 Pattern p1.0 50% p1.5 40% p2.0 27% p3.0 12% old bin 1 -> new bin 2 Replaced with a scale-aware PowerBarFrac = 0.50 (a dimensionless FRACTION of the gun's own best bin rate). 13 of 14 selections now pick heavier bullets. Real effect vs DrussGT (8 rounds x 3 runs): hit rate unchanged (7.56% -> 7.47%) but damage dealt +52% (157 -> 239 per run) and rounds end faster. Same accuracy, half the shots, half again more damage. TASK 1 - the TM pattern-classifier gun does NOT earn its slot. It was built as a mixture of experts with a corrected-Granmo TM as a multi-class gate over HeadOn/Linear/Circular/WallBounce/Accel, labelled by which expert's prediction was closest to the actual enemy position (an exact, supervised, per-shot label - no delayed credit). Offline it loses to the best of its OWN experts on essentially every fixture, and against DrussGT it cost real performance: baseline (path+relative) 7.56% real hit rate, damage 157 + power fix 7.47%, damage 239 + power fix + TM gun 5.59%, damage 133 The gun was selected on 806 ticks and fired 24 real shots at 4.2%. So the tree ships with EnableTmSelector = false: code and wiring kept intact for re-enabling, but it is not in the active rack. Worth recording from the clause dump: the gate DOES latch onto meaningful structure. On energy-threshold-turner, HeadOn's clauses key on the energy bits (the rule's own driving variable) while Circular keys on distance/velocity. So the TM is learning something real and interpretable - it simply cannot beat 'always pick the best expert'. Root cause (INFERRED): the closest-expert label is noisy because several experts are near-tied, and under the path metric the winner varies by power bin while the gate sees one shared per-tick input, so a one-vs-rest gate over a saturated 870-bit clause space has no margin to exploit. (Zero-padding the 2-frame window was tried first and saturated every clause at 256-755 included literals; alternating the two real frames fixed that.) Also factors the corrected feedback into an exported tmLearnDir and exports the encoding/TM primitives; the Tsetlin tests still reproduce the documented mean=13.8 included literals, so the refactor is behaviour-preserving. Verified: 33/33 guard checks, tsetlin tests green, metric checks green, new power-selection guard green (13/14 selections change; relative bar still picks bin 1 and not bin 3 for a [30,25,12,5]% profile), 12/12 offline==online acceptance under the shipped default.
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) 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) via GunHarness.
|
||||
## Radar: RadarLockModule (1v1) / MeleeScanModule (2+ enemies), auto-switched per tick.
|
||||
## Movement: OscillatorModule (perpendicular strafing).
|
||||
|
||||
@@ -24,6 +24,7 @@ import guns/displacement
|
||||
import guns/averaged_lead
|
||||
import guns/decay_gf
|
||||
import guns/knn_gun
|
||||
import guns/tm_selector
|
||||
import movements/phantom_meteor
|
||||
import movements/rammer
|
||||
import movements/the_floor_is_lava
|
||||
@@ -35,6 +36,15 @@ import targeting/target_selector
|
||||
const botJsonPath = currentSourcePath().parentDir / "ModularBot.json"
|
||||
const DebugVBullets = false
|
||||
const DebugCircular = false
|
||||
## Registration switch for the TM selector gun (id 13). Set false to build a
|
||||
## rack without it (A/B runs); the tracker still reserves gun id 13 so the other
|
||||
## ids and per-gun stats are unchanged.
|
||||
##
|
||||
## VERDICT: left FALSE. The TM gate underperforms its own experts on every
|
||||
## offline fixture (energy-threshold 63% vs Circular 100%, random-walk 45% vs
|
||||
## WallBounce 68%, Corners 25% vs Accel 34%) and the DrussGT A/B showed real hit
|
||||
## rate 5.59% vs 7.47% for the power-fix-only rack. Flip to true to re-enable.
|
||||
const EnableTmSelector = false
|
||||
## Task A instrumentation: log every real shot + its eventual outcome to
|
||||
## /tmp/shot_log.jsonl. Set to false to compile all shot-log machinery out.
|
||||
const ShotLog = true
|
||||
@@ -45,7 +55,7 @@ const ShotLog = true
|
||||
## can enable recording for just the battle it spawns by exporting the env var.
|
||||
let RecordWorldState* = existsEnv("TR_RECORD_WORLDSTATE")
|
||||
const WorldStateRecordPath = "/tmp/worldstate_record.jsonl"
|
||||
const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor", "Pattern", "WallBounce", "Accel", "StopShot", "Displace", "AvgLead", "DecayGF", "KNN"]
|
||||
const GunNames = ["HeadOn", "Linear", "Tsetlin", "Circular", "GuessFactor", "Pattern", "WallBounce", "Accel", "StopShot", "Displace", "AvgLead", "DecayGF", "KNN", "TMSelect"]
|
||||
|
||||
const
|
||||
CLR_GUN = "\e[33m" # yellow
|
||||
@@ -90,6 +100,7 @@ type
|
||||
avgLead: AveragedLeadGun
|
||||
decayGF: DecayGFGun
|
||||
knnGun: KNNGun
|
||||
tmSelector: TmSelectorGun
|
||||
mover: TFILModule
|
||||
rammer: RammerModule
|
||||
isRamming: bool
|
||||
@@ -110,13 +121,13 @@ type
|
||||
roundNumber: int
|
||||
realShotsFired: int
|
||||
realHits: int
|
||||
gunRealShots: array[13, int]
|
||||
gunRealHits: array[13, int]
|
||||
gunRealShots: array[14, int]
|
||||
gunRealHits: array[14, 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
|
||||
bulletShot: Table[int, PendingShot] ## bulletId -> shot metadata (Task A shot log)
|
||||
pendingHitBullets: HashSet[int] ## hit bulletIds seen before their onBulletFired stamp
|
||||
gunSelectionCount: array[13, int]
|
||||
gunSelectionCount: array[14, int]
|
||||
lastKnownTargetId: int ## persists through death, used for round-end stats
|
||||
|
||||
proc writeShotLog(shot: PendingShot, hit: bool, unresolved: bool) =
|
||||
@@ -322,7 +333,7 @@ method onRoundEnded*(bot: ModularBot, e: RoundEndedEventForBot) =
|
||||
let fit = bot.tracker.fitnessFor(targetId)
|
||||
|
||||
var gunsArr = newJArray()
|
||||
for gid in 0..<13:
|
||||
for gid in 0..<14:
|
||||
var totalShots = 0
|
||||
var totalHits = 0
|
||||
for binIdx in 0..<len(vb.PowerBins):
|
||||
@@ -382,7 +393,7 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
|
||||
bot.bulletGun.clear()
|
||||
bot.bulletShot.clear()
|
||||
bot.pendingHitBullets.clear()
|
||||
for i in 0..<13:
|
||||
for i in 0..<14:
|
||||
bot.gunSelectionCount[i] = 0
|
||||
bot.gunRealShots[i] = 0
|
||||
bot.gunRealHits[i] = 0
|
||||
@@ -589,6 +600,7 @@ method run*(bot: ModularBot) =
|
||||
var alPreds: array[len(PowerBins), GunPrediction]
|
||||
var dgPreds: array[len(PowerBins), GunPrediction]
|
||||
var knnPreds: array[len(PowerBins), GunPrediction]
|
||||
var tmselPreds: 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]))
|
||||
@@ -603,6 +615,8 @@ method run*(bot: ModularBot) =
|
||||
alPreds[i] = bot.avgLead.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||
dgPreds[i] = bot.decayGF.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||
knnPreds[i] = bot.knnGun.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||
if EnableTmSelector:
|
||||
tmselPreds[i] = bot.tmSelector.predict(bot.lastState, bulletSpeed(PowerBins[i]))
|
||||
|
||||
bot.tracker.spawnBullets(0, headsUp, bot.lastState, tid)
|
||||
bot.tracker.spawnBullets(1, linPreds, bot.lastState, tid)
|
||||
@@ -618,6 +632,8 @@ method run*(bot: ModularBot) =
|
||||
bot.tracker.spawnBullets(10, alPreds, bot.lastState, tid)
|
||||
bot.tracker.spawnBullets(11, dgPreds, bot.lastState, tid)
|
||||
bot.tracker.spawnBullets(12, knnPreds, bot.lastState, tid)
|
||||
if EnableTmSelector and bot.tmSelector.isWarmedUp():
|
||||
bot.tracker.spawnBullets(13, tmselPreds, bot.lastState, tid)
|
||||
|
||||
# Build slim enemy table for tickBullets
|
||||
var enemyPositions: Table[int, tuple[x, y: float, lastSeenTick: int, alive: bool]]
|
||||
@@ -645,6 +661,7 @@ method run*(bot: ModularBot) =
|
||||
of 10: bot.avgLead.onResult(fe)
|
||||
of 11: bot.decayGF.onResult(fe)
|
||||
of 12: bot.knnGun.onResult(fe)
|
||||
of 13: bot.tmSelector.onResult(fe)
|
||||
else: discard
|
||||
if fe.hit: inc bot.virtualHits else: inc bot.virtualMiss
|
||||
when DebugVBullets:
|
||||
@@ -672,6 +689,7 @@ method run*(bot: ModularBot) =
|
||||
of 10: setTurretColor("#996633"); setBulletColor("#CC9966")
|
||||
of 11: setTurretColor("#008888"); setBulletColor("#00AAAA")
|
||||
of 12: setTurretColor("#CC00CC"); setBulletColor("#FF44FF")
|
||||
of 13: setTurretColor("#00FFCC"); setBulletColor("#66FFDD")
|
||||
else: discard
|
||||
|
||||
let pred = case selectedGun
|
||||
@@ -687,6 +705,7 @@ method run*(bot: ModularBot) =
|
||||
of 10: bot.avgLead.predict(bot.lastState, bulletSpeed(power))
|
||||
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))
|
||||
else: bot.headOn.predict(bot.lastState, bulletSpeed(power))
|
||||
let aimTarget = aimAngle(getX(), getY(), pred.x, pred.y)
|
||||
|
||||
@@ -718,7 +737,7 @@ method run*(bot: ModularBot) =
|
||||
|
||||
when isMainModule:
|
||||
var bot = ModularBot(
|
||||
tracker: vb.initTracker(13), # 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
|
||||
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
|
||||
headOn: HeadOnGun(),
|
||||
linear: LinearGun(),
|
||||
circular: CircularGun(),
|
||||
@@ -732,6 +751,7 @@ when isMainModule:
|
||||
avgLead: initAveragedLeadGun(),
|
||||
decayGF: initDecayGFGun(),
|
||||
knnGun: initKNNGun(),
|
||||
tmSelector: initTmSelectorGun(),
|
||||
radar: RadarLockModule(),
|
||||
meleeScan: initMeleeScan(),
|
||||
mover: TFILModule(debugGraphics: true),
|
||||
|
||||
Reference in New Issue
Block a user