fix(guns): per-bin wave queues unbreak GF/DecayGF/KNN learning; fix vbullet drops

Wave queues (guess_factor, decay_gf, knn_gun): predict() stored ONE wave
per tick while onResult() popped one per resolved bullet (~4/tick), so the
queue drained to empty within a few dozen ticks, ~3 of every 4 resolutions
returned without learning, and the survivor paired with a same-tick wave
(bearingDelta ~= 0) pinning the histogram at centre. PROOF: GF.vHits ==
HeadOn.vHits and DecayGF.vHits == HeadOn.vHits byte-for-byte in every one
of 50 rounds — the guns had degenerated to HeadOn.

Now each gun keeps a per-bin FIFO with an O(1) head cursor. At most one
push per (tick, bin) so the fire site's 5th predict() call is a no-op, and
onResult pops the oldest wave of its OWN bin via e.bulletPower. Aiming
math untouched (it was already correct: 0 deg = East, CCW+).

maxBullets 2048 -> 8192: the rack spawns 52 bullets/tick so the ring wrapped
every ~39 ticks while a long power-3 shot needs ~90, silently discarding
unresolved bullets and biasing every measured hit rate by range. Added a
droppedBullets counter so a future overflow is measurable, and wavePushes/
waveStarved counters on the three guns. After the fix: vDropped = 0 and
vStarved = 0 across all 48 recorded rounds.

fitnessFor is now exported, deterministic (enemies iterated in ascending id
order) and shared by the selector and the stats dump, replacing a hand-rolled
merge in ModularBot that never advanced its window head.

Round lines gain additive keys: vDropped, vStarved.
This commit is contained in:
2026-09-20 22:27:52 +02:00
parent a90a0cc9b5
commit 0cc682152d
5 changed files with 205 additions and 75 deletions
+16 -19
View File
@@ -199,24 +199,10 @@ method onRoundEnded*(bot: ModularBot, e: RoundEndedEventForBot) =
## 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
# Per-target fitness when known, else a deterministic recency-weighted aggregate
# over all enemies (see VirtualTracker.fitnessFor). Using the shared proc keeps
# this stats dump identical to what the gun selector sees.
let fit = bot.tracker.fitnessFor(targetId)
var gunsArr = newJArray()
for gid in 0..<13:
@@ -251,7 +237,9 @@ method onRoundEnded*(bot: ModularBot, e: RoundEndedEventForBot) =
"realShots": bot.realShotsFired,
"realHits": bot.realHits,
"score": e.results.totalScore,
"bulletDmg": e.results.bulletDamage
"bulletDmg": e.results.bulletDamage,
"vDropped": bot.tracker.droppedBullets,
"vStarved": bot.guessFactor.waveStarved + bot.decayGF.waveStarved + bot.knnGun.waveStarved
}
let f = open("/tmp/gun_stats.jsonl", fmAppend)
f.writeLine($row)
@@ -269,6 +257,15 @@ method onRoundStarted*(bot: ModularBot, e: RoundStartedEvent) =
bot.gunSelectionCount[i] = 0
bot.gunRealShots[i] = 0
bot.gunRealHits[i] = 0
# Reset per-round integrity counters so each /tmp/gun_stats.jsonl line reports
# that round's numbers (summing the lines gives the session total).
bot.tracker.droppedBullets = 0
bot.guessFactor.wavePushes = 0
bot.guessFactor.waveStarved = 0
bot.decayGF.wavePushes = 0
bot.decayGF.waveStarved = 0
bot.knnGun.wavePushes = 0
bot.knnGun.waveStarved = 0
setAdjustGunForBodyTurn(true)
setAdjustRadarForBodyTurn(true)
setAdjustRadarForGunTurn(true)