feat(SNNBot): adaptive fire power with lead scaling and velocity transition suppression

- P3 at err<0.5°, P1 at err<2.0° (was fixed P2); no fire at err>=2.0°
- Scale grid lead offset by bullet speed ratio (sin(θ) ∝ 1/v_bullet)
- ENERGY_GUARD 5→15 to survive Walls' sustained fire
- Suppress fire when |vPerp delta| > 2.0 (enemy changing direction)
- Store lastBulletSpeed = 20-3*chosenPower for correct EVALUATE travel time

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 08:23:13 +02:00
parent 86dfd672b1
commit faa3edbe32
+38 -9
View File
@@ -71,12 +71,14 @@ const
BETA = 1.0 # surrogate steepness (unitless; potentials are unitless) BETA = 1.0 # surrogate steepness (unitless; potentials are unitless)
NOISE_AMP = 0.3 # exploration noise amplitude NOISE_AMP = 0.3 # exploration noise amplitude
# ponytail: uniform exploration noise; upgrade to annealed Gaussian if convergence needs tuning # ponytail: uniform exploration noise; upgrade to annealed Gaussian if convergence needs tuning
ENERGY_GUARD = 5.0 # P1 costs 1/shot, be aggressive ENERGY_GUARD = 15.0 # keep buffer; Walls hits us while we shoot
P3_SPEED = 11.0 # bullet speed for P3 (20 - 3*3); used for exemplar learning P3_SPEED = 11.0 # bullet speed for P3 (20 - 3*3); used for exemplar learning
P1_SPEED = 17.0 # bullet speed for P1 (20 - 3*1) P1_SPEED = 17.0 # bullet speed for P1 (20 - 3*1)
P2_SPEED = 14.0 # bullet speed for P2 (20 - 3*2) P2_SPEED = 14.0 # bullet speed for P2 (20 - 3*2)
GRID_BULLET_SPEED = 14.0 # reference speed the grid learns at (power 2)
COLD_K = 3 # exemplars needed before leaving cold start COLD_K = 3 # exemplars needed before leaving cold start
PATIENCE_TICKS = 5 # P1 is forgiving, less patience needed PATIENCE_TICKS = 5 # P1 is forgiving, less patience needed
VPERP_TRANSITION = 2.0 # suppress fire when |vPerp change| > this (enemy changing direction)
# ── SNN types ───────────────────────────────────────────────────────────────── # ── SNN types ─────────────────────────────────────────────────────────────────
@@ -246,6 +248,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
lastVPerp: float # DECIDE-time vPerp, reused in EVALUATE lastVPerp: float # DECIDE-time vPerp, reused in EVALUATE
prevTickVPerp: float # vPerp from previous tick (transition detection)
lastAbsBearing: float # absolute bearing to enemy captured at DECIDE time lastAbsBearing: float # absolute bearing to enemy captured at DECIDE time
roundTick: int # ticks elapsed in current round (reset each round) roundTick: int # ticks elapsed in current round (reset each round)
bulletsFired: int # count of bullets fired this round bulletsFired: int # count of bullets fired this round
@@ -326,6 +329,7 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) =
bot.lastEnemyX = 0.0 bot.lastEnemyX = 0.0
bot.lastEnemyY = 0.0 bot.lastEnemyY = 0.0
bot.lastAbsBearing = 0.0 bot.lastAbsBearing = 0.0
bot.prevTickVPerp = 0.0
bot.lastDecideGunDir = 0.0 bot.lastDecideGunDir = 0.0
bot.targetAngle = 0.0 bot.targetAngle = 0.0
setTargetSpeed(0.0) setTargetSpeed(0.0)
@@ -381,19 +385,25 @@ method run*(bot: SNNBot) =
while relVelDir >= 180.0: relVelDir -= 360.0 while relVelDir >= 180.0: relVelDir -= 360.0
while relVelDir < -180.0: relVelDir += 360.0 while relVelDir < -180.0: relVelDir += 360.0
let vPerp = bot.velSpeed * sin(degToRad(relVelDir)) let vPerp = bot.velSpeed * sin(degToRad(relVelDir))
let aimOffset = bot.res.forward(vPerp, bot.enemyDist) let gridOffset = bot.res.forward(vPerp, bot.enemyDist)
bot.lastVPerp = vPerp bot.lastVPerp = vPerp
bot.lastDecideGunDir = gunDir bot.lastDecideGunDir = gunDir
bot.lastAbsBearing = absBearing bot.lastAbsBearing = absBearing
let rate = if bot.bulletsFired > 0: float(bot.bulletsHit) / float(bot.bulletsFired) * 100.0 else: 0.0 let rate = if bot.bulletsFired > 0: float(bot.bulletsHit) / float(bot.bulletsFired) * 100.0 else: 0.0
if aimOffset <= -999.0: if gridOffset <= -999.0:
bot.targetAngle = absBearing bot.targetAngle = absBearing
echo "RES tick=" & $bot.tick & " pwr=" & formatFloat(bot.currentFirePower, ffDecimal, 1) & " hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "% cold-start aim=" & formatFloat(absBearing, ffDecimal, 1) & " fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit & " rate=" & formatFloat(rate, ffDecimal, 1) & "% energy=" & formatFloat(getEnergy(), ffDecimal, 0) & " cells=" & $bot.res.totalCount echo "RES tick=" & $bot.tick & " pwr=" & formatFloat(bot.currentFirePower, ffDecimal, 1) & " hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "% cold-start aim=" & formatFloat(absBearing, ffDecimal, 1) & " fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit & " rate=" & formatFloat(rate, ffDecimal, 1) & "% energy=" & formatFloat(getEnergy(), ffDecimal, 0) & " cells=" & $bot.res.totalCount
diagLog("DECIDE tick=" & $bot.tick & " offset=cold-start cells=" & $bot.res.totalCount) diagLog("DECIDE tick=" & $bot.tick & " offset=cold-start cells=" & $bot.res.totalCount)
else: else:
# Scale grid offset (learned at GRID_BULLET_SPEED) to chosen fire power speed.
# sin(leadAngle) ∝ 1/bulletSpeed, so scale sin of the offset.
let chosenSpeed = 20.0 - 3.0 * bot.currentFirePower
let sinGrid = sin(degToRad(gridOffset))
let sinScaled = clamp(sinGrid * GRID_BULLET_SPEED / chosenSpeed, -1.0, 1.0)
let aimOffset = radToDeg(arcsin(sinScaled))
bot.targetAngle = (absBearing + aimOffset + 360.0) mod 360.0 bot.targetAngle = (absBearing + aimOffset + 360.0) mod 360.0
echo "RES tick=" & $bot.tick & " pwr=" & formatFloat(bot.currentFirePower, ffDecimal, 1) & " hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "% offset=" & formatFloat(aimOffset, ffDecimal, 1) & " aim=" & formatFloat(bot.targetAngle, ffDecimal, 1) & " fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit & " rate=" & formatFloat(rate, ffDecimal, 1) & "% energy=" & formatFloat(getEnergy(), ffDecimal, 0) & " cells=" & $bot.res.totalCount echo "RES tick=" & $bot.tick & " pwr=" & formatFloat(bot.currentFirePower, ffDecimal, 1) & " hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "% offset=" & formatFloat(aimOffset, ffDecimal, 1) & " aim=" & formatFloat(bot.targetAngle, ffDecimal, 1) & " fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit & " rate=" & formatFloat(rate, ffDecimal, 1) & "% energy=" & formatFloat(getEnergy(), ffDecimal, 0) & " cells=" & $bot.res.totalCount
diagLog("DECIDE tick=" & $bot.tick & " offset=" & formatFloat(aimOffset, ffDecimal, 2) & "° cells=" & $bot.res.totalCount) diagLog("DECIDE tick=" & $bot.tick & " offset=" & formatFloat(aimOffset, ffDecimal, 2) & "° gridOffset=" & formatFloat(gridOffset, ffDecimal, 2) & "° cells=" & $bot.res.totalCount)
else: else:
let inputs = encodeInputFull(absBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos) let inputs = encodeInputFull(absBearing, 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
@@ -426,14 +436,33 @@ method run*(bot: SNNBot) =
aimTo(bot.targetAngle, gunDir) aimTo(bot.targetAngle, gunDir)
let err = abs(normalizeRelativeAngle(bot.targetAngle - gunDir)) let err = abs(normalizeRelativeAngle(bot.targetAngle - gunDir))
# Fire gate: gun must be aimed and settled before firing # Velocity transition detection: recompute current vPerp
var curVPerp = bot.lastVPerp
if bot.hasLastPos:
var relVelDir2 = bot.velDirDeg - bot.enemyBearing
while relVelDir2 >= 180.0: relVelDir2 -= 360.0
while relVelDir2 < -180.0: relVelDir2 += 360.0
curVPerp = bot.velSpeed * sin(degToRad(relVelDir2))
let vPerpTransition = abs(curVPerp - bot.prevTickVPerp) > VPERP_TRANSITION
bot.prevTickVPerp = curVPerp
# Fire gate: gun aimed, enough energy, not in cold-start, enemy not changing direction
let energy = getEnergy() let energy = getEnergy()
let readyToFire = err < 2.0 and energy >= ENERGY_GUARD and bot.roundTick >= PATIENCE_TICKS
let leadOffset = normalizeRelativeAngle(bot.targetAngle - bot.lastAbsBearing) let leadOffset = normalizeRelativeAngle(bot.targetAngle - bot.lastAbsBearing)
appendLog(aimDebugLog, fmt"TICK t={bot.roundTick} err={err:.2f} offset={leadOffset:.2f} vPerp={bot.lastVPerp:.2f} dist={bot.enemyDist:.1f} fired={readyToFire}") appendLog(aimDebugLog, fmt"TICK t={bot.roundTick} err={err:.2f} offset={leadOffset:.2f} vPerp={bot.lastVPerp:.2f} dist={bot.enemyDist:.1f} transition={vPerpTransition}")
# Adaptive fire power: high confidence = P3 (damage), low = P1 (cheap miss)
let chosenPower =
if err < 0.5: 3.0
elif err < 2.0: 1.0
else: 0.0
bot.currentFirePower = if chosenPower > 0.0: chosenPower else: bot.currentFirePower
let readyToFire = chosenPower > 0.0 and energy >= ENERGY_GUARD and
bot.roundTick >= PATIENCE_TICKS and not vPerpTransition
if readyToFire: if readyToFire:
bot.lastBulletSpeed = P2_SPEED bot.lastBulletSpeed = 20.0 - 3.0 * chosenPower
discard setFire(2.0) discard setFire(chosenPower)
bot.phase = EVALUATE bot.phase = EVALUATE
else: else:
discard setFire(0.0) discard setFire(0.0)