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:
@@ -0,0 +1,80 @@
|
||||
## Reads /tmp/gun_stats.jsonl and produces per-gun performance summary.
|
||||
## nim c -r --path:../../common_libs tests/analyze_gun_stats.nim
|
||||
## Output: /tmp/gun_performance_summary.txt
|
||||
|
||||
import std/[json, tables, strformat, strutils, sequtils, algorithm, os]
|
||||
|
||||
const GunNames = ["HeadOn","Linear","Tsetlin","Circular","GuessFactor","Pattern","AntiSurf","WallBounce","Accel","StopShot","Displace","AvgLead","DecayGF","KNN"]
|
||||
|
||||
type
|
||||
GunStats = object
|
||||
vShots, vHits, selected: int
|
||||
|
||||
EnemyStats = object
|
||||
guns: array[14, GunStats]
|
||||
realShots, realHits, rounds: int
|
||||
name: string
|
||||
|
||||
var byEnemy: Table[string, EnemyStats]
|
||||
var currentEnemy = "unknown"
|
||||
|
||||
if not fileExists("/tmp/gun_stats.jsonl"):
|
||||
echo "No /tmp/gun_stats.jsonl found — run the gauntlet first."
|
||||
quit(1)
|
||||
|
||||
for line in lines("/tmp/gun_stats.jsonl"):
|
||||
let s = line.strip()
|
||||
if s.len == 0: continue
|
||||
let j = parseJson(s)
|
||||
if j.hasKey("session_start"):
|
||||
currentEnemy = j["session_start"].getStr()
|
||||
if currentEnemy notin byEnemy:
|
||||
byEnemy[currentEnemy] = EnemyStats(name: currentEnemy)
|
||||
continue
|
||||
if not j.hasKey("round"): continue
|
||||
var es = addr byEnemy[currentEnemy]
|
||||
inc es.rounds
|
||||
es.realShots += j["realShots"].getInt()
|
||||
es.realHits += j["realHits"].getInt()
|
||||
for g in j["guns"].getElems():
|
||||
let gid = g["id"].getInt()
|
||||
es.guns[gid].vShots += g["vShots"].getInt()
|
||||
es.guns[gid].vHits += g["vHits"].getInt()
|
||||
es.guns[gid].selected += g["selected"].getInt()
|
||||
|
||||
var output: seq[string]
|
||||
output.add "Gun Performance Summary"
|
||||
output.add "Generated from /tmp/gun_stats.jsonl"
|
||||
output.add ""
|
||||
|
||||
# Preserve insertion order via seq of names encountered
|
||||
var enemyOrder: seq[string]
|
||||
for line in lines("/tmp/gun_stats.jsonl"):
|
||||
let s = line.strip()
|
||||
if s.len == 0: continue
|
||||
let j = parseJson(s)
|
||||
if j.hasKey("session_start"):
|
||||
let n = j["session_start"].getStr()
|
||||
if n notin enemyOrder: enemyOrder.add n
|
||||
|
||||
for ename in enemyOrder:
|
||||
if ename notin byEnemy: continue
|
||||
let es = byEnemy[ename]
|
||||
let realPct = if es.realShots > 0: es.realHits * 100 div es.realShots else: 0
|
||||
output.add fmt"=== vs {es.name} ({es.rounds} rounds) ==="
|
||||
output.add fmt" Real shots: {es.realShots} Real hits: {es.realHits} Real hit%: {realPct}%"
|
||||
output.add ""
|
||||
output.add " " & alignLeft("Gun", 14) & align("vShots", 8) & align("vHits", 7) & align("vHit%", 7) & align("Selected", 9)
|
||||
output.add " " & "-".repeat(50)
|
||||
var ranked = toSeq(0..<14)
|
||||
ranked.sort(proc(a,b:int):int = es.guns[b].selected - es.guns[a].selected)
|
||||
for gid in ranked:
|
||||
let g = es.guns[gid]
|
||||
let rate = if g.vShots > 0: g.vHits * 100 div g.vShots else: 0
|
||||
output.add fmt" {GunNames[gid]:<14} {g.vShots:>8} {g.vHits:>7} {rate:>6}% {g.selected:>9}"
|
||||
output.add ""
|
||||
|
||||
let summary = output.join("\n")
|
||||
echo summary
|
||||
writeFile("/tmp/gun_performance_summary.txt", summary)
|
||||
echo "\nWrote /tmp/gun_performance_summary.txt"
|
||||
Reference in New Issue
Block a user