fix(SNNBot): hold aim when error is within dead zone

Adds aim-lock mechanism to prevent re-running forward() pass during DECIDE when the
error is already within 1.5° dead zone. Lock is set in EVALUATE when error < 1.5°,
reusing the previous targetAngle in DECIDE. Lock releases on fresh enemy scan or when
error exceeds threshold, eliminating the ±0.9° wobble from input pattern drift.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 08:14:28 +02:00
parent 8685f0e794
commit c05fb7a32b
+16 -2
View File
@@ -215,6 +215,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
aimLocked: bool # when true, skip forward() and reuse targetAngle
# ── aimTo helper ──────────────────────────────────────────────────────────────
@@ -254,6 +255,7 @@ method onScannedBot*(bot: SNNBot, e: ScannedBotEvent) =
bot.enemyBearing = directionTo(bx, by, e.x, e.y)
bot.enemyDist = distanceTo(bx, by, e.x, e.y)
bot.hasContact = true
bot.aimLocked = false # release lock on fresh enemy scan
if bot.hasLastPos:
let dx = e.x - bot.lastEnemyX
let dy = e.y - bot.lastEnemyY
@@ -273,6 +275,7 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) =
bot.velSpeed = 0.0
bot.phase = DECIDE
bot.tick = 0
bot.aimLocked = false
setTargetSpeed(0.0)
setTurnRate(0.0)
@@ -335,6 +338,7 @@ method run*(bot: SNNBot) =
case bot.phase
of DECIDE:
if not bot.aimLocked:
let relBearing = normalizeRelativeAngle(bot.enemyBearing - gunDir)
bot.lastRelBearing = relBearing
when USE_RESERVOIR:
@@ -404,18 +408,28 @@ method run*(bot: SNNBot) =
let aimErr = abs(normalizeRelativeAngle(aimRaw - correctAngle))
if aimErr > 1.5:
bot.res.learn(bot.lastBinInput, correctAngle)
bot.aimLocked = false # error still large, keep recomputing
else:
bot.aimLocked = true # error within dead zone, freeze aim
else:
# Cold start: always learn
bot.res.learn(bot.lastBinInput, correctAngle)
bot.aimLocked = false
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
" exemplars=" & $bot.res.count &
" lock=" & (if bot.aimLocked: "ON" else: "OFF")
else:
let err = abs(normalizeRelativeAngle(gunDir - bot.enemyBearing))
bot.snn.superSpikeUpdate(bot.lastSpikes, bot.lastVSnap, bot.snn.preTrace, targetRel)
# Aim lock: freeze when error within 1.5° threshold
if err < 1.5:
bot.aimLocked = true
else:
bot.aimLocked = false
# Compute verbose logging metrics
var spikeCount = 0
var maxV = 0.0
@@ -432,7 +446,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) & "° 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) & " lock=" & (if bot.aimLocked: "ON" else: "OFF")
bot.phase = DECIDE
# Radar lock