Files
SirRoboGarage/common_libs/tests/range_guns.nim
T
SirStone 974528d5cf feat(gun_harness): offline gun range, proven equivalent to live play
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.
2026-09-20 23:44:42 +02:00

43 lines
1.5 KiB
Nim

## Shared helper: build the 13 ModularBot guns as type-erased offline range
## drivers, in ModularBot's gun-id order, with the same readiness gate the live
## loop uses (Tsetlin only spawns once its 10-frame window is full).
import std/random
import gun_harness/offline_range
import guns/head_on
import guns/linear
import guns/circular
import guns/tsetlin
import guns/guess_factor
import guns/pattern_matcher
import guns/wall_bounce
import guns/accel_predictor
import guns/stop_shot
import guns/displacement
import guns/averaged_lead
import guns/decay_gf
import guns/knn_gun
proc buildAllGunDrivers*(seed = -1): seq[GunDriver] =
## seed >= 0 re-seeds the global RNG after constructing Tsetlin so the
## stochastic gun's learning is reproducible for offline runs. (Its
## constructor calls randomize(); we override that seed afterwards.)
var tsetlin = initTsetlinGun()
if seed >= 0:
randomize(seed)
result = @[
makeDriver("HeadOn", HeadOnGun()),
makeDriver("Linear", LinearGun()),
makeDriver("Tsetlin", tsetlin),
makeDriver("Circular", CircularGun()),
makeDriver("GuessFactor", initGFGun()),
makeDriver("Pattern", PatternMatcherGun()),
makeDriver("WallBounce", initWallBounceGun()),
makeDriver("Accel", initAccelGun()),
makeDriver("StopShot", initStopShotGun()),
makeDriver("Displace", initDisplacementGun()),
makeDriver("AvgLead", initAveragedLeadGun()),
makeDriver("DecayGF", initDecayGFGun()),
makeDriver("KNN", initKNNGun()),
]