From be032e4ebde0c19a83930141335dba676f8621c4 Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Mon, 14 Sep 2026 23:28:50 +0200 Subject: [PATCH] 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 --- SNNBot_garage/src/SNNBot.nim | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/SNNBot_garage/src/SNNBot.nim b/SNNBot_garage/src/SNNBot.nim index de9781b..0919f9a 100644 --- a/SNNBot_garage/src/SNNBot.nim +++ b/SNNBot_garage/src/SNNBot.nim @@ -46,9 +46,7 @@ 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 - AIM_SMOOTH_MIN = 0.3 # close range — responsive - AIM_SMOOTH_MAX = 0.9 # long range — heavy smoothing - SMOOTH_DIST_MAX = 600.0 # distance at which max smoothing kicks in + HYSTERESIS = 1 # minimum score advantage to switch bins # ── SNN types ───────────────────────────────────────────────────────────────── @@ -218,8 +216,7 @@ 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 - smoothSin: float # exponential smoothing of sin channel - smoothCos: float # exponential smoothing of cos channel + lastWinBin: int # last winning bin for hysteresis # ── aimTo helper ────────────────────────────────────────────────────────────── @@ -284,8 +281,7 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) = method onGameStarted*(bot: SNNBot, e: GameStartedEventForBot) = initSNN(bot.snn) bot.res = initAimer() - bot.smoothSin = 0.0 - bot.smoothCos = 1.0 + bot.lastWinBin = -1 # ── Reservoir helpers ───────────────────────────────────────────────────────── @@ -347,18 +343,18 @@ method run*(bot: SNNBot) = when USE_RESERVOIR: let binInput = toBinaryInput(relBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos) let winBin = bot.res.forward(binInput) - let aimAngle = bot.res.interpolatedAngle(winBin) - # Far → more smoothing (less jitter). Close → less smoothing (faster tracking). - # ponytail: linear ramp; tune MIN/MAX if gun lags close or jitters far - let alpha = clamp(bot.enemyDist / SMOOTH_DIST_MAX, AIM_SMOOTH_MIN, AIM_SMOOTH_MAX) - let aimRad = degToRad(aimAngle) - bot.smoothSin = alpha * bot.smoothSin + (1.0 - alpha) * sin(aimRad) - bot.smoothCos = alpha * bot.smoothCos + (1.0 - alpha) * cos(aimRad) - let smoothedAim = radToDeg(arctan2(bot.smoothSin, bot.smoothCos)) + # 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) bot.lastBinInput = binInput - bot.targetAngle = gunDir + smoothedAim + bot.targetAngle = gunDir + aimAngle 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: let inputs = encodeInputFull(relBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos) # Multi-tick inference: accumulate sin/cos and spike counts over N_INFER ticks