feat(SNNBot): replace bin-based aimer with exemplar kernel regression

Eliminates aim jitter by switching from 360-bin WTA to weighted circular
mean over stored exemplars. Continuous float output, no bins, no
hysteresis, no saturation. K=128 ring buffer, Hamming similarity with
quadratic weighting.
This commit is contained in:
2026-09-15 07:52:32 +02:00
parent 445c873316
commit 844abe0bb7
2 changed files with 61 additions and 103 deletions
+22 -33
View File
@@ -46,7 +46,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
HYSTERESIS = 1 # minimum score advantage to switch bins
# ── SNN types ─────────────────────────────────────────────────────────────────
@@ -216,7 +215,6 @@ type
velSpeed: float64 # speed (units/tick) from last scan delta
lastDecideGunDir: float # gun heading captured at DECIDE time for EVALUATE
lastBinInput: BitVec80 # DECIDE-time binary input, reused in EVALUATE
lastWinBin: int # last winning bin for hysteresis
# ── aimTo helper ──────────────────────────────────────────────────────────────
@@ -280,8 +278,7 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) =
method onGameStarted*(bot: SNNBot, e: GameStartedEventForBot) =
initSNN(bot.snn)
bot.res = initAimer()
bot.lastWinBin = -1
bot.res = initBinaryAimer()
# ── Reservoir helpers ─────────────────────────────────────────────────────────
@@ -341,20 +338,19 @@ method run*(bot: SNNBot) =
let relBearing = normalizeRelativeAngle(bot.enemyBearing - gunDir)
bot.lastRelBearing = relBearing
when USE_RESERVOIR:
let binInput = toBinaryInput(relBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
let winBin = bot.res.forward(binInput)
# ponytail: hysteresis=1 prevents bin flickering; raise if still jittery, lower if gun lags
let activeBin =
if bot.lastWinBin >= 0 and bot.res.scores[bot.lastWinBin] + HYSTERESIS >= bot.res.scores[winBin]:
bot.lastWinBin # stick with current bin
else:
winBin # switch to new winner
bot.lastWinBin = activeBin
let aimAngle = bot.res.interpolatedAngle(activeBin)
let binInput = toBinaryInput(relBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
let aimRaw = bot.res.forward(binInput) # [0,360) or -1.0 sentinel
bot.lastBinInput = binInput
bot.targetAngle = gunDir + aimAngle
bot.lastDecideGunDir = gunDir
echo "RES tick=" & $bot.tick & " bin=" & $activeBin & " aim=" & formatFloat(aimAngle, ffDecimal, 1)
if aimRaw < 0.0:
# cold start: use raw bearing directly
bot.targetAngle = bot.enemyBearing
echo "RES tick=" & $bot.tick & " cold-start aim=" & formatFloat(bot.enemyBearing, ffDecimal, 1)
else:
# aimRaw is absolute angle [0,360); convert to -180..180 relative then add gunDir
let aimRel = normalizeRelativeAngle(aimRaw - 180.0) # exemplars stored as 0-360, relative bearing is -180..180
bot.targetAngle = gunDir + aimRel
echo "RES tick=" & $bot.tick & " aim=" & formatFloat(aimRaw, ffDecimal, 1) & " rel=" & formatFloat(aimRel, ffDecimal, 1)
else:
let inputs = encodeInputFull(relBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
# Multi-tick inference: accumulate sin/cos and spike counts over N_INFER ticks
@@ -400,23 +396,16 @@ method run*(bot: SNNBot) =
let absBearing = directionTo(myX, myY, futureX, futureY)
let targetRel = normalizeRelativeAngle(absBearing - bot.lastDecideGunDir)
when USE_RESERVOIR:
let winBin = block:
var best = 0; var bestS = -1
for k in 0 ..< N_BINS:
if bot.res.scores[k] > bestS: bestS = bot.res.scores[k]; best = k
best
let aimAngle = bot.res.interpolatedAngle(winBin)
bot.res.learn(bot.lastBinInput, targetRel)
let correctBin = angleToBin(targetRel)
let readoutPopCorrect = block:
var c = 0
for w in 0 ..< WORDS_IN: c += popcount(bot.res.readout[correctBin][w]).int
c
echo "RES tick=" & $bot.tick & " bin=" & $winBin &
" aim=" & formatFloat(aimAngle, ffDecimal, 1) &
" target=" & formatFloat(targetRel, ffDecimal, 1) &
" err=" & formatFloat(abs(normalizeRelativeAngle(aimAngle - targetRel)), ffDecimal, 1) &
" readout_pop_correct=" & $readoutPopCorrect
# correctAngle must be [0,360): targetRel is -180..180, shift it
var correctAngle = targetRel + 180.0
let aimRaw = bot.res.forward(bot.lastBinInput)
bot.res.learn(bot.lastBinInput, correctAngle)
let aimErr = if aimRaw >= 0.0: abs(normalizeRelativeAngle(aimRaw - correctAngle)) else: -1.0
echo "RES tick=" & $bot.tick &
" aim=" & (if aimRaw >= 0.0: formatFloat(aimRaw, ffDecimal, 1) else: "cold") &
" correct=" & formatFloat(correctAngle, ffDecimal, 1) &
" err=" & (if aimRaw >= 0.0: formatFloat(aimErr, ffDecimal, 1) else: "n/a") &
" exemplars=" & $bot.res.count
else:
let err = abs(normalizeRelativeAngle(gunDir - bot.enemyBearing))
bot.snn.superSpikeUpdate(bot.lastSpikes, bot.lastVSnap, bot.snn.preTrace, targetRel)