From 17178158b290366e60d6b03d8fb9adbd4dffdb3d Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Mon, 14 Sep 2026 22:39:28 +0200 Subject: [PATCH] refactor(SNNBot): replace retroactive error with predictive lead target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete ring buffer, aim snapshots, maturation loop (~80 lines). Replace with simple velocity extrapolation: predict enemy position at bullet impact time, use bearing to predicted position as SuperSpike target. Immediate error signal every EVALUATE — no delay, no snapshot state storage. Mathematically equivalent to retroactive for linear movers. --- SNNBot_garage/src/SNNBot.nim | 130 ++++++++--------------------------- 1 file changed, 27 insertions(+), 103 deletions(-) diff --git a/SNNBot_garage/src/SNNBot.nim b/SNNBot_garage/src/SNNBot.nim index 7c5558b..0a7fac8 100644 --- a/SNNBot_garage/src/SNNBot.nim +++ b/SNNBot_garage/src/SNNBot.nim @@ -42,7 +42,6 @@ const # ponytail: uniform exploration noise; upgrade to annealed Gaussian if convergence needs tuning FIRE_POWER = 1.0 # fixed firing power; bullet speed = 20 - 3*FIRE_POWER BULLET_SPEED = 20.0 - 3.0 * FIRE_POWER - POS_BUF_LEN = 100 # ring buffer length for enemy positions (~100 ticks) # ── SNN types ───────────────────────────────────────────────────────────────── @@ -193,38 +192,23 @@ proc superSpikeUpdate(snn: var SNN, type Phase = enum DECIDE, WAITING, EVALUATE - AimSnapshot = object - aimAngle: float # SNN output angle (relative, degrees) - distance: float # distance to enemy at snapshot time - tick: int # tick when snapshot was taken - botX: float # bot position at snapshot time - botY: float - gunHeading: float # absolute gun heading at snapshot time - spikes: array[N_HID, float] # accumulated spike counts from this inference window - vSnap: array[N_HID, float] # hidden voltages from this inference window - preTrace: array[N_IN, float] # pre-synaptic traces from this inference window - SNNBot = ref object of Bot - snn: SNN - phase: Phase - targetAngle: float # SNN output (absolute bearing) - enemyBearing: float # last known enemy bearing - enemyDist: float # last known distance to enemy - hasContact: bool - tick: int - lastSpikes: array[N_HID, float] # accumulated spike counts over N_INFER ticks - lastVSnap: array[N_HID, float] # hidden voltages before reset from last tick (for SuperSpike) - lastRelBearing: float # relative bearing at DECIDE time (fixed for learning) - lastEnemyX: float64 # previous tick enemy position (for velocity) - lastEnemyY: float64 - hasLastPos: bool - velDirDeg: float64 # velocity direction (degrees) from last scan delta - velSpeed: float64 # speed (units/tick) from last scan delta - # Retroactive would-have-hit error signal - posBuf: array[POS_BUF_LEN, tuple[x, y: float, tick: int]] # ring buffer of enemy (x,y,tick) - posBufWrite: int # next write index into posBuf - posBufTick: int # game tick of the most recently written slot - snapshots: seq[AimSnapshot] # pending aim snapshots awaiting maturation + snn: SNN + phase: Phase + targetAngle: float # SNN output (absolute bearing) + enemyBearing: float # last known enemy bearing + enemyDist: float # last known distance to enemy + hasContact: bool + tick: int + lastSpikes: array[N_HID, float] # accumulated spike counts over N_INFER ticks + lastVSnap: array[N_HID, float] # hidden voltages before reset from last tick (for SuperSpike) + lastRelBearing: float # relative bearing at DECIDE time (fixed for learning) + lastEnemyX: float64 # previous tick enemy position (for velocity) + lastEnemyY: float64 + hasLastPos: bool + velDirDeg: float64 # velocity direction (degrees) from last scan delta + velSpeed: float64 # speed (units/tick) from last scan delta + lastDecideGunDir: float # gun heading captured at DECIDE time for EVALUATE # ── aimTo helper ────────────────────────────────────────────────────────────── @@ -369,10 +353,6 @@ method onScannedBot*(bot: SNNBot, e: ScannedBotEvent) = bot.velSpeed = sqrt(dx * dx + dy * dy) bot.lastEnemyX = e.x; bot.lastEnemyY = e.y bot.hasLastPos = true - # Record enemy position in ring buffer - bot.posBuf[bot.posBufWrite] = (x: e.x, y: e.y, tick: bot.tick) - bot.posBufTick = bot.tick - bot.posBufWrite = (bot.posBufWrite + 1) mod POS_BUF_LEN method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) = setAdjustGunForBodyTurn(true) @@ -385,9 +365,6 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) = bot.velSpeed = 0.0 bot.phase = DECIDE bot.tick = 0 - bot.posBufWrite = 0 - bot.posBufTick = 0 - bot.snapshots = @[] setTargetSpeed(0.0) setTurnRate(0.0) @@ -432,18 +409,7 @@ method run*(bot: SNNBot) = bot.snn.lastSinOut = totalSin; bot.snn.lastCosOut = totalCos bot.snn.lastSnnAngle = arctan2(totalSin, totalCos) * 180.0 / PI bot.targetAngle = gunDir + bot.snn.lastSnnAngle - # Record aim snapshot for retroactive error signal; capture SNN state now - # before subsequent DECIDE cycles overwrite bot.lastSpikes / bot.lastVSnap. - bot.snapshots.add(AimSnapshot( - aimAngle: bot.snn.lastSnnAngle, - distance: bot.enemyDist, - tick: bot.tick, - botX: myX, - botY: myY, - gunHeading: gunDir, - spikes: bot.lastSpikes, - vSnap: bot.lastVSnap, - preTrace: bot.snn.preTrace)) + bot.lastDecideGunDir = gunDir # Log total spike count across inference window var spikeCount = 0 var maxV = 0.0 @@ -462,57 +428,15 @@ method run*(bot: SNNBot) = of EVALUATE: let err = abs(normalizeRelativeAngle(gunDir - bot.enemyBearing)) - # Retroactive would-have-hit error signal: find the most recently matured snapshot. - # 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). - var retroTarget = bot.lastRelBearing # fallback; overwritten after averaging (used for logging) - var hasMatured = false - var keepIdx = 0 # first non-matured snapshot to keep - # Accumulate retroTarget circular components across all matured snapshots. - var sumSin = 0.0; var sumCos = 0.0 - var matureCount = 0 - # Most-recent matured snapshot's SNN state (most relevant for weight update). - var bestSpikes: array[N_HID, float] - var bestVSnap: array[N_HID, float] - var bestPreTrace: array[N_IN, float] - for i in 0 ..< bot.snapshots.len: - let snap = bot.snapshots[i] - let travelTicks = int(ceil(snap.distance / BULLET_SPEED)) - let impactTick = snap.tick + travelTicks - if bot.tick >= impactTick: - # Scan backward from most-recent write slot to find closest tick <= impactTick - var foundSlot = -1 - for j in 0 ..< POS_BUF_LEN: - let s = ((bot.posBufWrite - 1) - j + POS_BUF_LEN) mod POS_BUF_LEN - if bot.posBuf[s].tick > 0 and bot.posBuf[s].tick <= impactTick: - foundSlot = s - break - if foundSlot >= 0: - let ex = bot.posBuf[foundSlot].x - let ey = bot.posBuf[foundSlot].y - let absBearing = directionTo(snap.botX, snap.botY, ex, ey) - let rt = normalizeRelativeAngle(absBearing - snap.gunHeading) - # Accumulate circular mean components - sumSin += sin(degToRad(rt)) - sumCos += cos(degToRad(rt)) - inc matureCount - # Keep most-recent (highest index) snapshot's SNN state - bestSpikes = snap.spikes - bestVSnap = snap.vSnap - bestPreTrace = snap.preTrace - hasMatured = true - keepIdx = i + 1 # discard matured snapshots up to and including this one - else: - break # snapshots are in order; stop at first non-matured - # One update per EVALUATE using circular-mean target — stable learning rate regardless of snapshot count. - if matureCount > 0: - retroTarget = arctan2(sumSin, sumCos) * 180.0 / PI - bot.snn.superSpikeUpdate(bestSpikes, bestVSnap, bestPreTrace, retroTarget) - # Discard matured snapshots; immature ones are preserved automatically (keepIdx stays 0 - # or points past the last matured entry; the rest of bot.snapshots is kept intact). - if keepIdx > 0: - bot.snapshots = bot.snapshots[keepIdx .. ^1] - # else: skip weight update — no matured snapshot yet (early game) + # Predictive error signal: extrapolate enemy position at bullet impact time. + if bot.hasLastPos: + let travelTime = bot.enemyDist / BULLET_SPEED + let velRad = degToRad(bot.velDirDeg) + let futureX = bot.lastEnemyX + cos(velRad) * bot.velSpeed * travelTime + let futureY = bot.lastEnemyY + sin(velRad) * bot.velSpeed * travelTime + let absBearing = directionTo(myX, myY, futureX, futureY) + let targetAngle = normalizeRelativeAngle(absBearing - bot.lastDecideGunDir) + bot.snn.superSpikeUpdate(bot.lastSpikes, bot.lastVSnap, bot.snn.preTrace, targetAngle) # Compute verbose logging metrics var spikeCount = 0 var maxV = 0.0 @@ -529,7 +453,7 @@ method run*(bot: SNNBot) = for w in bot.snn.wCos: meanWout += abs(w) meanWout /= float(2 * N_HID) - echo "tick=" & $bot.tick & " err=" & formatFloat(err, ffDecimal, 1) & "° matured=" & $hasMatured & " retroAngle=" & formatFloat(retroTarget, ffDecimal, 1) & " infer_spk=" & $spikeCount & "/" & $(N_INFER * N_HID) & " maxV=" & formatFloat(maxV, ffDecimal, 3) & " |wih|=" & formatFloat(meanWih, ffDecimal, 4) & " |wOut|=" & formatFloat(meanWout, ffDecimal, 4) & " sin=" & formatFloat(bot.snn.lastSinOut, ffDecimal, 3) & " cos=" & formatFloat(bot.snn.lastCosOut, ffDecimal, 3) & " snnAngle=" & formatFloat(bot.snn.lastSnnAngle, ffDecimal, 1) + echo "tick=" & $bot.tick & " err=" & formatFloat(err, ffDecimal, 1) & "° infer_spk=" & $spikeCount & "/" & $(N_INFER * N_HID) & " maxV=" & formatFloat(maxV, ffDecimal, 3) & " |wih|=" & formatFloat(meanWih, ffDecimal, 4) & " |wOut|=" & formatFloat(meanWout, ffDecimal, 4) & " sin=" & formatFloat(bot.snn.lastSinOut, ffDecimal, 3) & " cos=" & formatFloat(bot.snn.lastCosOut, ffDecimal, 3) & " snnAngle=" & formatFloat(bot.snn.lastSnnAngle, ffDecimal, 1) bot.phase = DECIDE # Radar lock