974528d5cf
Gun evaluation previously required a full end-to-end battle (Java server + battle runner + websocket IPC to 2 bot processes, 50 rounds, ~3.4 min) and yielded only ~300-900 REAL shots across 13 guns -- far too few to rank guns, which is why tuning needed many repetitions. VirtualTracker is already a pure function of (WorldState stream, gun list); the only reason it needed Java was where WorldState came from. So the range replays a seq[WorldState] through the SAME tracker: offline and online scores are the same metric by construction, not an approximation. ACCEPTANCE TEST (the point of the whole thing): record one live round, replay it offline, compare per-gun virtual hit rates. 12/12 deterministic guns match EXACTLY, reproduced twice. Tsetlin is compared separately because tmLearnOne calls rand(). Getting to 12/12 exposed two real ordering quirks in the live loop: run() calls go() before the aim/fire block, so tickBullets resolves against the NEXT tick's scan while the prediction used the previous one; and if the target dies during that go() the final tick's spawn+resolution is skipped entirely. The recorder emits an end marker for the second case. The 5th (selected-gun) predict call was verified to be a no-op. Measured cost: 8 fixtures (1770 ticks, ~92k virtual bullets, 13 guns) replay in 2.9 s, ~32k virtual bullets/s -- roughly 70x faster and 100x more samples than a live gauntlet. Also adds a per-tick WorldState recorder behind const RecordWorldState (default off, mirrors the ShotLog idiom) which records the state the bot ACTUALLY builds, staleness included, rather than true positions -- recording the latter would hand the guns perfect information and produce flattering scores. 9 new guard checks (33 total, all passing), including fixture round-trip, replay determinism, stationary->HeadOn 100%, constant-velocity->Linear>HeadOn, and the energy-threshold turner crossing at t=41.
67 lines
2.3 KiB
Nim
67 lines
2.3 KiB
Nim
## Task 4: offline gun-range runner. Replays one or more fixtures through the
|
|
## 13 ModularBot guns and prints a deterministic per-gun table (overall hit rate
|
|
## plus per-power-bin hits/shots) for each fixture.
|
|
##
|
|
## Usage:
|
|
## nim c -r common_libs/tests/run_range.nim # all tools/fixtures/*.jsonl
|
|
## nim c -r common_libs/tests/run_range.nim path/a.jsonl path/b.jsonl
|
|
## nim c -r common_libs/tests/run_range.nim --timing # add per-fixture wall time
|
|
##
|
|
## Tsetlin is stochastic; it is seeded (seed=1) here so this tool's output is
|
|
## reproducible run-to-run. Its numbers are NOT expected to match a live battle.
|
|
|
|
import std/[os, algorithm, strformat, times]
|
|
import gun_harness/offline_range
|
|
import range_guns
|
|
|
|
const fixturesDir = currentSourcePath().parentDir.parentDir.parentDir / "tools" / "fixtures"
|
|
|
|
proc fixtureFiles(args: seq[string]): seq[string] =
|
|
for a in args:
|
|
if a == "--timing": continue
|
|
if dirExists(a):
|
|
for f in walkFiles(a / "*.jsonl"): result.add f
|
|
elif fileExists(a):
|
|
result.add a
|
|
else:
|
|
echo "warning: not found: ", a
|
|
if result.len == 0:
|
|
for f in walkFiles(fixturesDir / "*.jsonl"): result.add f
|
|
result.sort()
|
|
|
|
proc main() =
|
|
var args: seq[string]
|
|
for i in 1..paramCount():
|
|
args.add paramStr(i)
|
|
let showTiming = "--timing" in args
|
|
let files = fixtureFiles(args)
|
|
if files.len == 0:
|
|
echo "no fixtures found under ", fixturesDir
|
|
echo "run: nim c -r common_libs/tests/gen_synthetic_fixtures.nim"
|
|
quit(1)
|
|
|
|
var grandShots, grandHits = 0
|
|
for path in files:
|
|
let fx = loadFixture(path)
|
|
var t0 = 0.0
|
|
if showTiming: t0 = epochTime()
|
|
let reports = replayFixture(fx, buildAllGunDrivers(seed = 1),
|
|
liveActual = (fx.meta.source == "live"))
|
|
let elapsed = if showTiming: epochTime() - t0 else: 0.0
|
|
stdout.write formatReports(fx, reports)
|
|
if showTiming:
|
|
echo fmt" replay: {elapsed*1000.0:.1f} ms"
|
|
var totShots, totHits = 0
|
|
for r in reports:
|
|
totShots += r.shots
|
|
totHits += r.hits
|
|
grandShots += totShots
|
|
grandHits += totHits
|
|
echo ""
|
|
if files.len > 1:
|
|
echo fmt"TOTAL across {files.len} fixtures: {grandHits}/{grandShots} ({grandHits.float/grandShots.float*100.0:.1f}%)"
|
|
echo fmt"(replayed {files.len} fixture(s))"
|
|
|
|
when isMainModule:
|
|
main()
|