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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user