feat(SNNBot): adaptive learning dead zone scaled by enemy velocity

Stationary targets get large dead zone (3°) for stable aim. Fast movers
get small dead zone (0.5°) for rapid adaptation. Replaces fixed
threshold and jump-reset hack.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 08:40:03 +02:00
parent 87bcd3df6b
commit 3066a0c72b
+14 -15
View File
@@ -33,7 +33,6 @@ const
LEAK = 0.9 # LIF membrane leak factor
THRESH = 0.08 # ponytail: THRESH=0.08 tuned for 80-input layer; raise if firing saturates
MAX_GUN_TURN = 20.0 # max gun turn per tick (degrees)
AIM_TOL = 2.0 # arrive tolerance (degrees)
ETA = 0.1 # SuperSpike learning rate for hidden→output weights (r_0 from paper, bumped 2x)
ETA_IH = 0.02 # ponytail: ETA_IH=0.02 bumped 4x for input→hidden (was 0.005, too conservative)
# ponytail: separate input→hidden rate; add RMaxProp optimizer if convergence still unstable
@@ -215,7 +214,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
lastError: float # last aiming error (for jump detection)
# ── aimTo helper ──────────────────────────────────────────────────────────────
@@ -274,7 +272,6 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) =
bot.velSpeed = 0.0
bot.phase = DECIDE
bot.tick = 0
bot.lastError = -1.0 # sentinel: no error yet
setTargetSpeed(0.0)
setTurnRate(0.0)
@@ -385,21 +382,18 @@ method run*(bot: SNNBot) =
aimTo(bot.targetAngle, gunDir)
let err = abs(normalizeRelativeAngle(bot.targetAngle - gunDir))
# Bidirectional fire gate: fire only when error within tolerance
if err < AIM_TOL:
# Adaptive dead zone: scale learning threshold by enemy velocity
# Stationary → 3° (stable), full speed (8 units/tick) → 0.5° (fast tracking)
let t = (bot.velSpeed / 8.0).clamp(0.0, 1.0)
let adaptiveDeadZone = 3.0 + (0.5 - 3.0) * t # lerp(3.0, 0.5, t)
# Fire gate: only fire when error within adaptive dead zone
if err < adaptiveDeadZone:
discard setFire(FIRE_POWER)
bot.phase = EVALUATE
else:
discard setFire(0.0)
# Fast recovery: detect error jump (regime change) and reset ring buffer
# ponytail: >5° jump triggers reset; tune threshold if target moves frequently
if bot.lastError >= 0.0 and err - bot.lastError > 5.0:
bot.res.count = 0
bot.res.nextSlot = 0
bot.lastError = err
of EVALUATE:
# Predictive error signal: extrapolate enemy position at bullet impact time.
if bot.hasLastPos:
@@ -409,12 +403,17 @@ method run*(bot: SNNBot) =
let futureY = bot.lastEnemyY + sin(velRad) * bot.velSpeed * travelTime
# correctAngle: absolute world-frame bearing [0,360) to predicted enemy position
let correctAngle = directionTo(myX, myY, futureX, futureY)
# Adaptive learning dead zone: scale by enemy velocity (same as fire gate)
let t = (bot.velSpeed / 8.0).clamp(0.0, 1.0)
let adaptiveDeadZone = 3.0 + (0.5 - 3.0) * t # lerp(3.0, 0.5, t)
when USE_RESERVOIR:
let aimRaw = bot.res.forward(bot.lastBinInput)
# Dead zone: only learn if aim error > 1.5° (prevents ring buffer churn)
# Only learn if aim error exceeds adaptive dead zone
if aimRaw >= 0.0:
let aimErr = abs(normalizeRelativeAngle(aimRaw - correctAngle))
if aimErr > 1.5:
if aimErr > adaptiveDeadZone:
bot.res.learn(bot.lastBinInput, correctAngle)
else:
# Cold start: always learn