fix(SNNBot): apply weight update for every matured snapshot, not just last

Multiple snapshots can mature by EVALUATE time but only the last one
triggered a superSpikeUpdate. Earlier matured snapshots were silently
discarded — wasting learning signal. Now each matured snapshot with a
valid ring buffer hit gets its own weight update.
This commit is contained in:
2026-09-14 22:25:39 +02:00
parent a80fd8d656
commit d5777fba07
+5 -14
View File
@@ -464,13 +464,9 @@ method run*(bot: SNNBot) =
# Retroactive would-have-hit error signal: find the most recently matured snapshot. # Retroactive would-have-hit error signal: find the most recently matured snapshot.
# A snapshot matures when currentTick >= snapshotTick + ceil(distance / BULLET_SPEED). # A snapshot matures when currentTick >= snapshotTick + ceil(distance / BULLET_SPEED).
# Look up enemy position at impact tick from ring buffer (offset from most-recent write). # Look up enemy position at impact tick from ring buffer (offset from most-recent write).
var retroTarget = bot.lastRelBearing # fallback (unused if no matured snapshot) var retroTarget = bot.lastRelBearing # fallback; overwritten by each matured snapshot (last one used for logging)
var hasMatured = false var hasMatured = false
var keepIdx = 0 # first non-matured snapshot to keep var keepIdx = 0 # first non-matured snapshot to keep
# matureSnap holds the last matured snapshot's SNN state for the weight update.
var matureSpikes: array[N_HID, float]
var matureVSnap: array[N_HID, float]
var maturePreTrace: array[N_IN, float]
for i in 0 ..< bot.snapshots.len: for i in 0 ..< bot.snapshots.len:
let snap = bot.snapshots[i] let snap = bot.snapshots[i]
let travelTicks = int(ceil(snap.distance / BULLET_SPEED)) let travelTicks = int(ceil(snap.distance / BULLET_SPEED))
@@ -487,11 +483,10 @@ method run*(bot: SNNBot) =
let ex = bot.posBuf[foundSlot].x let ex = bot.posBuf[foundSlot].x
let ey = bot.posBuf[foundSlot].y let ey = bot.posBuf[foundSlot].y
let absBearing = directionTo(snap.botX, snap.botY, ex, ey) let absBearing = directionTo(snap.botX, snap.botY, ex, ey)
retroTarget = normalizeRelativeAngle(absBearing - snap.gunHeading) retroTarget = normalizeRelativeAngle(absBearing - snap.gunHeading)
matureSpikes = snap.spikes # Update weights for every matured snapshot immediately; earlier ones were previously discarded.
matureVSnap = snap.vSnap bot.snn.superSpikeUpdate(snap.spikes, snap.vSnap, snap.preTrace, retroTarget)
maturePreTrace = snap.preTrace hasMatured = true
hasMatured = true
keepIdx = i + 1 # discard matured snapshots up to and including this one keepIdx = i + 1 # discard matured snapshots up to and including this one
else: else:
break # snapshots are in order; stop at first non-matured break # snapshots are in order; stop at first non-matured
@@ -499,10 +494,6 @@ method run*(bot: SNNBot) =
# or points past the last matured entry; the rest of bot.snapshots is kept intact). # or points past the last matured entry; the rest of bot.snapshots is kept intact).
if keepIdx > 0: if keepIdx > 0:
bot.snapshots = bot.snapshots[keepIdx .. ^1] bot.snapshots = bot.snapshots[keepIdx .. ^1]
if hasMatured:
# Use SNN state captured at DECIDE time — not the stale bot.lastSpikes/lastVSnap
# which have been overwritten by subsequent DECIDE cycles.
bot.snn.superSpikeUpdate(matureSpikes, matureVSnap, maturePreTrace, retroTarget)
# else: skip weight update — no matured snapshot yet (early game) # else: skip weight update — no matured snapshot yet (early game)
# Compute verbose logging metrics # Compute verbose logging metrics
var spikeCount = 0 var spikeCount = 0