feat(testing): gun rack gauntlet + analysis reports

- fix(ModularBot): onBulletHitBot → onBulletHit (real hits were never tracked)
- feat(ModularBot): per-round gun stats dump to /tmp/gun_stats.jsonl
- feat(ModularBot): gun selection counter per round
- fix(tests): adversary paths _garage suffix removed from 7 test files
- feat(tests): test_gauntlet_5bots.nim — 10-round gauntlet vs all 5 adversaries
- feat(tests): analyze_gun_stats.nim — JSONL parser for gun performance tables
- docs: gun_rack_analysis.md — full per-gun performance report
- docs: gun_rack_summary.md — TL;DR verdict table (keep/drop/tune)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 21:14:12 +02:00
parent 2cc2a3bd87
commit c034eb9d25
17 changed files with 519 additions and 17 deletions
+71 -3
View File
@@ -3,7 +3,7 @@
## Radar: RadarLockModule (1v1) / MeleeScanModule (2+ enemies), auto-switched per tick.
## Movement: OscillatorModule (perpendicular strafing).
import std/[math, os, strformat, tables]
import std/[math, os, strformat, tables, json]
import robocode_tankroyale_botapi
import radar_harness/radar_interface
import radars/radar_lock_module
@@ -89,6 +89,11 @@ type
prevGun: int
prevRadar: int
prevTarget: int
roundNumber: int
realShotsFired: int
realHits: int
gunSelectionCount: array[14, int]
lastKnownTargetId: int ## persists through death, used for round-end stats
proc printConfig(bot: ModularBot, changed: string = "") =
let gunName = GunNames[bot.currentGun]
@@ -136,10 +141,12 @@ method onScannedBot*(bot: ModularBot, e: ScannedBotEvent) =
method onBulletFired*(bot: ModularBot, e: BulletFiredEvent) =
inc bot.realShotsFired
bot.moveTracker.shadows.addBullet(e.bullet.x, e.bullet.y,
degToRad(e.bullet.direction), e.bullet.power)
method onBulletHitBot*(bot: ModularBot, e: BulletHitBotEvent) =
method onBulletHit*(bot: ModularBot, e: BulletHitBotEvent) =
inc bot.realHits
bot.moveTracker.shadows.removeBulletNear(e.bullet.x, e.bullet.y)
method onBulletHitWall*(bot: ModularBot, e: BulletHitWallEvent) =
@@ -154,9 +161,67 @@ method onHitByBullet*(bot: ModularBot, e: HitByBulletEvent) =
bot.moveTracker.registerHit(e.bullet.power, e.bullet.direction, getX(), getY())
method onRoundEnded*(bot: ModularBot, e: RoundEndedEventForBot) =
discard
## Dump per-gun virtual bullet stats to /tmp/gun_stats.jsonl (one line per round).
# Use lastKnownTargetId: currentTargetId is -1 if enemy died before round end
let targetId = if bot.currentTargetId >= 0: bot.currentTargetId else: bot.lastKnownTargetId
# Aggregate fitness across all tracked enemies (or just the primary target if known).
# Use aggregate (targetId=-1) so we always have data even if target switched.
let fit = block:
var f: seq[vb.GunFitness]
if targetId >= 0 and targetId in bot.tracker.fitness:
f = bot.tracker.fitness[targetId]
else:
# aggregate across all enemies
f = newSeq[vb.GunFitness](bot.tracker.numGuns)
for perEnemy in bot.tracker.fitness.values:
for gid in 0..<bot.tracker.numGuns:
for binIdx in 0..<len(vb.PowerBins):
let src = perEnemy[gid].bins[binIdx]
let n = min(src.count, vb.WindowSize)
for k in 0..<n:
f[gid].bins[binIdx].hits[(f[gid].bins[binIdx].head + f[gid].bins[binIdx].count) mod vb.WindowSize] = src.hits[k]
inc f[gid].bins[binIdx].count
f
var gunsArr = newJArray()
for gid in 0..<14:
var totalShots = 0
var totalHits = 0
for binIdx in 0..<len(vb.PowerBins):
let fw = fit[gid].bins[binIdx]
let n = min(fw.count, vb.WindowSize)
totalShots += n
for k in 0..<n:
if fw.hits[k]: inc totalHits
let hitRate = if totalShots > 0: totalHits.float / totalShots.float else: 0.0
gunsArr.add(%*{
"id": gid,
"name": GunNames[gid],
"vShots": totalShots,
"vHits": totalHits,
"hitRate": (hitRate * 100.0).int,
"selected": bot.gunSelectionCount[gid]
})
let row = %*{
"round": bot.roundNumber,
"targetId": targetId,
"guns": gunsArr,
"realShots": bot.realShotsFired,
"realHits": bot.realHits,
"score": e.results.totalScore,
"bulletDmg": e.results.bulletDamage
}
let f = open("/tmp/gun_stats.jsonl", fmAppend)
f.writeLine($row)
f.close()
method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
bot.roundNumber = e.roundNumber
bot.realShotsFired = 0
bot.realHits = 0
bot.lastKnownTargetId = -1
for i in 0..<14: bot.gunSelectionCount[i] = 0
setAdjustGunForBodyTurn(true)
setAdjustRadarForBodyTurn(true)
setAdjustRadarForGunTurn(true)
@@ -253,6 +318,8 @@ method run*(bot: ModularBot) =
bot.currentTargetId = candidateId
bot.targetSwitchTick = bot.tick
bot.printConfig("target")
if bot.currentTargetId >= 0:
bot.lastKnownTargetId = bot.currentTargetId
# --- Refresh target state FIRST, before ram decision ---
let tid = bot.currentTargetId
@@ -419,6 +486,7 @@ method run*(bot: ModularBot) =
# Gun selection + fire
let (selectedGun, _, power) = selectShot(bot.tracker, tid)
bot.gunSelectionCount[selectedGun] += 1
if selectedGun != bot.currentGun:
bot.currentGun = selectedGun
case selectedGun