feat(SNNBot): replace aim smoothing with bin hysteresis

Smoothing masked jitter, hysteresis removes it at the source.
Gun stays on current bin unless a new bin wins by >1 point.
Removes all EMA/smoothing code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 23:28:50 +02:00
parent 9cb2727fef
commit be032e4ebd
+13 -17
View File
@@ -46,9 +46,7 @@ const
# ponytail: uniform exploration noise; upgrade to annealed Gaussian if convergence needs tuning # 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 FIRE_POWER = 1.0 # fixed firing power; bullet speed = 20 - 3*FIRE_POWER
BULLET_SPEED = 20.0 - 3.0 * FIRE_POWER BULLET_SPEED = 20.0 - 3.0 * FIRE_POWER
AIM_SMOOTH_MIN = 0.3 # close range — responsive HYSTERESIS = 1 # minimum score advantage to switch bins
AIM_SMOOTH_MAX = 0.9 # long range — heavy smoothing
SMOOTH_DIST_MAX = 600.0 # distance at which max smoothing kicks in
# ── SNN types ───────────────────────────────────────────────────────────────── # ── SNN types ─────────────────────────────────────────────────────────────────
@@ -218,8 +216,7 @@ type
velSpeed: float64 # speed (units/tick) from last scan delta velSpeed: float64 # speed (units/tick) from last scan delta
lastDecideGunDir: float # gun heading captured at DECIDE time for EVALUATE lastDecideGunDir: float # gun heading captured at DECIDE time for EVALUATE
lastBinInput: BitVec80 # DECIDE-time binary input, reused in EVALUATE lastBinInput: BitVec80 # DECIDE-time binary input, reused in EVALUATE
smoothSin: float # exponential smoothing of sin channel lastWinBin: int # last winning bin for hysteresis
smoothCos: float # exponential smoothing of cos channel
# ── aimTo helper ────────────────────────────────────────────────────────────── # ── aimTo helper ──────────────────────────────────────────────────────────────
@@ -284,8 +281,7 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) =
method onGameStarted*(bot: SNNBot, e: GameStartedEventForBot) = method onGameStarted*(bot: SNNBot, e: GameStartedEventForBot) =
initSNN(bot.snn) initSNN(bot.snn)
bot.res = initAimer() bot.res = initAimer()
bot.smoothSin = 0.0 bot.lastWinBin = -1
bot.smoothCos = 1.0
# ── Reservoir helpers ───────────────────────────────────────────────────────── # ── Reservoir helpers ─────────────────────────────────────────────────────────
@@ -347,18 +343,18 @@ method run*(bot: SNNBot) =
when USE_RESERVOIR: when USE_RESERVOIR:
let binInput = toBinaryInput(relBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos) let binInput = toBinaryInput(relBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
let winBin = bot.res.forward(binInput) let winBin = bot.res.forward(binInput)
let aimAngle = bot.res.interpolatedAngle(winBin) # ponytail: hysteresis=1 prevents bin flickering; raise if still jittery, lower if gun lags
# Far → more smoothing (less jitter). Close → less smoothing (faster tracking). let activeBin =
# ponytail: linear ramp; tune MIN/MAX if gun lags close or jitters far if bot.lastWinBin >= 0 and bot.res.scores[bot.lastWinBin] + HYSTERESIS >= bot.res.scores[winBin]:
let alpha = clamp(bot.enemyDist / SMOOTH_DIST_MAX, AIM_SMOOTH_MIN, AIM_SMOOTH_MAX) bot.lastWinBin # stick with current bin
let aimRad = degToRad(aimAngle) else:
bot.smoothSin = alpha * bot.smoothSin + (1.0 - alpha) * sin(aimRad) winBin # switch to new winner
bot.smoothCos = alpha * bot.smoothCos + (1.0 - alpha) * cos(aimRad) bot.lastWinBin = activeBin
let smoothedAim = radToDeg(arctan2(bot.smoothSin, bot.smoothCos)) let aimAngle = bot.res.interpolatedAngle(activeBin)
bot.lastBinInput = binInput bot.lastBinInput = binInput
bot.targetAngle = gunDir + smoothedAim bot.targetAngle = gunDir + aimAngle
bot.lastDecideGunDir = gunDir bot.lastDecideGunDir = gunDir
echo "RES tick=" & $bot.tick & " bin=" & $winBin & " aim=" & formatFloat(aimAngle, ffDecimal, 1) & " smoothed=" & formatFloat(smoothedAim, ffDecimal, 1) echo "RES tick=" & $bot.tick & " bin=" & $activeBin & " aim=" & formatFloat(aimAngle, ffDecimal, 1)
else: else:
let inputs = encodeInputFull(relBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos) let inputs = encodeInputFull(relBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
# Multi-tick inference: accumulate sin/cos and spike counts over N_INFER ticks # Multi-tick inference: accumulate sin/cos and spike counts over N_INFER ticks