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
@@ -0,0 +1,66 @@
## Gun gauntlet: ModularBot vs all 5 adversaries, 10 rounds each.
## Dumps per-gun stats via ModularBot's onRoundEnded to /tmp/gun_stats.jsonl.
## nim c -r --path:../../common_libs tests/test_gauntlet_5bots.nim
import std/[os, strutils, strformat, tables]
import test_framework/server_manager
import test_framework/bot_compiler
import test_framework/runner_process
import test_framework/battle_result
const
modularBotDir = currentSourcePath().parentDir.parentDir
adversariesDir = "/home/davide/Projects/SirRoboGarage/common_libs/test_framework/adversaries"
serverJar = "/home/davide/Projects/tank-royale/server/build/libs/robocode-tankroyale-server-0.35.5-all.jar"
runnerJar = "/home/davide/Projects/tank-royale/runner/examples/lib/robocode-tankroyale-runner.jar"
if not fileExists(serverJar):
echo "SKIP: server JAR not found at " & serverJar
quit(0)
if not fileExists(runnerJar):
echo "SKIP: runner JAR not found at " & runnerJar
quit(0)
putEnv("TR_SERVER_JAR", serverJar)
putEnv("TR_BATTLE_RUNNER", runnerJar)
putEnv("TR_BATTLE_RUNNER_DIR", "/home/davide/Projects/SirRoboGarage/tools/battle_runner")
# Pre-compile ModularBot once
discard compileBots(@[modularBotDir])
echo "[gauntlet] ModularBot compiled OK"
proc runBattle(label, enemyName, enemyDir: string) =
let sep = "=".repeat(60)
echo "\n" & sep
echo " " & label
echo sep
let bots = @[modularBotDir, enemyDir]
discard compileBots(@[enemyDir]) # ensure adversary is compiled
# Write session marker so analysis can map rounds to enemy name
block:
let lf = open("/tmp/gun_stats.jsonl", fmAppend)
lf.writeLine("{\"session_start\":\"" & enemyName & "\"}")
lf.close()
let raw = runBattleRunner(getServerUrl(), bots, 10, 300_000, true)
let r = parseServerOutput(raw)
var mbScore = 0; var enScore = 0
for b in r.results:
if b.name == "ModularBot": mbScore = b.totalScore
elif b.name == enemyName: enScore = b.totalScore
let winner = if mbScore >= enScore: "ModularBot" else: enemyName
echo fmt" ModularBot: {mbScore} {enemyName}: {enScore} -> {winner} wins"
echo fmt" Rounds played: {r.rounds.len}"
# Write summary line to gauntlet log
let logLine = fmt"[{label}] ModularBot={mbScore} {enemyName}={enScore} winner={winner} rounds={r.rounds.len}"
let lf = open("/tmp/gun_gauntlet_results.log", fmAppend)
lf.writeLine(logLine)
lf.close()
ensureServer()
runBattle("Battle 1/5: vs SittingDuck", "SittingDuck", adversariesDir / "SittingDuck")
runBattle("Battle 2/5: vs OscillatorBot","OscillatorBot", adversariesDir / "OscillatorBot")
runBattle("Battle 3/5: vs RandomMover", "RandomMover", adversariesDir / "RandomMover")
runBattle("Battle 4/5: vs PatternMover", "PatternMover", adversariesDir / "PatternMover")
runBattle("Battle 5/5: vs WaveSurfer", "WaveSurfer", adversariesDir / "WaveSurfer")
echo "\nGauntlet complete. Stats in /tmp/gun_stats.jsonl and /tmp/gun_gauntlet_results.log"