experiment(SNNBot): try power 3 for more damage per hit, revert adaptive complexity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -71,14 +71,11 @@ 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 = 15.0 # keep buffer; Walls hits us while we shoot
|
ENERGY_GUARD = 5.0 # keep buffer; Walls hits us while we shoot
|
||||||
P3_SPEED = 11.0 # bullet speed for P3 (20 - 3*3); used for exemplar learning
|
FIRE_POWER = 3.0 # fixed fire power
|
||||||
P1_SPEED = 17.0 # bullet speed for P1 (20 - 3*1)
|
BULLET_SPEED = 11.0 # 20 - 3 * FIRE_POWER
|
||||||
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 # ticks before first fire allowed
|
||||||
VPERP_TRANSITION = 2.0 # suppress fire when |vPerp change| > this (enemy changing direction)
|
|
||||||
|
|
||||||
# ── SNN types ─────────────────────────────────────────────────────────────────
|
# ── SNN types ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -248,17 +245,11 @@ 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
|
||||||
bulletsHit: int # count of bullets that hit this round
|
bulletsHit: int # count of bullets that hit this round
|
||||||
hitRateEMA: float # exponential moving average of hit rate
|
hitRateEMA: float # exponential moving average of hit rate
|
||||||
currentFirePower: float # dynamic fire power (1.0 / 2.0 / 3.0)
|
|
||||||
bulletSpeed: float # 20 - 3 * currentFirePower
|
|
||||||
lastBulletSpeed: float # bullet speed at last fire (for EVALUATE learning)
|
|
||||||
pendingPower: float # candidate new power level
|
|
||||||
powerChangeCounter: int # ticks the new power has been suggested
|
|
||||||
|
|
||||||
# ── aimTo helper ──────────────────────────────────────────────────────────────
|
# ── aimTo helper ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -318,18 +309,13 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) =
|
|||||||
bot.phase = DECIDE
|
bot.phase = DECIDE
|
||||||
bot.tick = 0
|
bot.tick = 0
|
||||||
bot.roundTick = 0
|
bot.roundTick = 0
|
||||||
# Reset per-round stats; fire power fixed at 2.0 (P2)
|
# Reset per-round stats
|
||||||
bot.bulletsFired = 0
|
bot.bulletsFired = 0
|
||||||
bot.bulletsHit = 0
|
bot.bulletsHit = 0
|
||||||
bot.hitRateEMA = 0.5 # optimistic start
|
bot.hitRateEMA = 0.5 # optimistic start
|
||||||
bot.powerChangeCounter = 0
|
|
||||||
bot.currentFirePower = 2.0
|
|
||||||
bot.bulletSpeed = P2_SPEED
|
|
||||||
bot.lastBulletSpeed = P2_SPEED
|
|
||||||
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)
|
||||||
@@ -392,18 +378,13 @@ method run*(bot: SNNBot) =
|
|||||||
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 gridOffset <= -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 & " 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.
|
# Grid learned at BULLET_SPEED — use offset directly (no scaling needed)
|
||||||
# sin(leadAngle) ∝ 1/bulletSpeed, so scale sin of the offset.
|
bot.targetAngle = (absBearing + gridOffset + 360.0) mod 360.0
|
||||||
let chosenSpeed = 20.0 - 3.0 * bot.currentFirePower
|
echo "RES tick=" & $bot.tick & " hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "% offset=" & formatFloat(gridOffset, 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
|
||||||
let sinGrid = sin(degToRad(gridOffset))
|
diagLog("DECIDE tick=" & $bot.tick & " offset=" & formatFloat(gridOffset, ffDecimal, 2) & "° cells=" & $bot.res.totalCount)
|
||||||
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
|
|
||||||
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) & "° 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
|
||||||
@@ -436,33 +417,14 @@ 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))
|
||||||
|
|
||||||
# Velocity transition detection: recompute current vPerp
|
# Fire gate: gun aimed, enough energy, not in cold-start
|
||||||
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 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} transition={vPerpTransition}")
|
appendLog(aimDebugLog, fmt"TICK t={bot.roundTick} err={err:.2f} offset={leadOffset:.2f} vPerp={bot.lastVPerp:.2f} dist={bot.enemyDist:.1f}")
|
||||||
|
|
||||||
# Adaptive fire power: high confidence = P3 (damage), low = P1 (cheap miss)
|
let readyToFire = err < 2.0 and energy >= ENERGY_GUARD and bot.roundTick >= PATIENCE_TICKS
|
||||||
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 = 20.0 - 3.0 * chosenPower
|
discard setFire(FIRE_POWER)
|
||||||
discard setFire(chosenPower)
|
|
||||||
bot.phase = EVALUATE
|
bot.phase = EVALUATE
|
||||||
else:
|
else:
|
||||||
discard setFire(0.0)
|
discard setFire(0.0)
|
||||||
@@ -470,19 +432,20 @@ method run*(bot: SNNBot) =
|
|||||||
of EVALUATE:
|
of EVALUATE:
|
||||||
# Predictive error signal: extrapolate enemy position at bullet impact time.
|
# Predictive error signal: extrapolate enemy position at bullet impact time.
|
||||||
if bot.hasLastPos:
|
if bot.hasLastPos:
|
||||||
# Use actual bullet speed for travel time (P1 = 17.0)
|
# correctOffset: correct lead angle at BULLET_SPEED
|
||||||
let travelTime = bot.enemyDist / bot.lastBulletSpeed
|
let travelTime = bot.enemyDist / BULLET_SPEED
|
||||||
let velRad = degToRad(bot.velDirDeg)
|
let velRad = degToRad(bot.velDirDeg)
|
||||||
let futureX = bot.lastEnemyX + cos(velRad) * bot.velSpeed * travelTime
|
let futureX = bot.lastEnemyX + cos(velRad) * bot.velSpeed * travelTime
|
||||||
let futureY = bot.lastEnemyY + sin(velRad) * bot.velSpeed * travelTime
|
let futureY = bot.lastEnemyY + sin(velRad) * bot.velSpeed * travelTime
|
||||||
# correctAngle: absolute world-frame bearing [0,360) to predicted enemy position
|
# correctAngle: absolute world-frame bearing [0,360) to predicted enemy position
|
||||||
let correctAngle = directionTo(myX, myY, futureX, futureY)
|
let correctAngle = directionTo(myX, myY, futureX, futureY)
|
||||||
|
|
||||||
# Adaptive learning dead zone: scale by enemy velocity (same as fire gate)
|
# Adaptive learning dead zone: scale by enemy velocity
|
||||||
let t = (bot.velSpeed / 8.0).clamp(0.0, 1.0)
|
let t = (bot.velSpeed / 8.0).clamp(0.0, 1.0)
|
||||||
let adaptiveDeadZone = 0.5 + (0.3 - 0.5) * t # lerp(0.5, 0.3, t)
|
let adaptiveDeadZone = 0.5 + (0.3 - 0.5) * t # lerp(0.5, 0.3, t)
|
||||||
|
|
||||||
when USE_RESERVOIR:
|
when USE_RESERVOIR:
|
||||||
|
# correctOffset: angle offset from bare bearing that a bullet at BULLET_SPEED needs
|
||||||
let correctOffset = normalizeRelativeAngle(correctAngle - bot.lastAbsBearing)
|
let correctOffset = normalizeRelativeAngle(correctAngle - bot.lastAbsBearing)
|
||||||
let producedOffset = bot.res.forward(bot.lastVPerp, bot.enemyDist)
|
let producedOffset = bot.res.forward(bot.lastVPerp, bot.enemyDist)
|
||||||
|
|
||||||
@@ -496,7 +459,6 @@ method run*(bot: SNNBot) =
|
|||||||
let aimErr = if producedOffset > -999.0: abs(normalizeRelativeAngle(producedOffset - correctOffset)) else: -1.0
|
let aimErr = if producedOffset > -999.0: abs(normalizeRelativeAngle(producedOffset - correctOffset)) else: -1.0
|
||||||
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
|
||||||
echo "RES tick=" & $bot.tick &
|
echo "RES tick=" & $bot.tick &
|
||||||
" pwr=" & formatFloat(bot.currentFirePower, ffDecimal, 1) &
|
|
||||||
" hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "%" &
|
" hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "%" &
|
||||||
" fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit &
|
" fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit &
|
||||||
" rate=" & (if bot.bulletsFired > 0: formatFloat(rate, ffDecimal, 1) else: "0.0") & "%" &
|
" rate=" & (if bot.bulletsFired > 0: formatFloat(rate, ffDecimal, 1) else: "0.0") & "%" &
|
||||||
@@ -527,7 +489,7 @@ method run*(bot: SNNBot) =
|
|||||||
meanWout += abs(w)
|
meanWout += abs(w)
|
||||||
meanWout /= float(2 * N_HID)
|
meanWout /= float(2 * N_HID)
|
||||||
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
|
||||||
echo "tick=" & $bot.tick & " pwr=" & formatFloat(bot.currentFirePower, ffDecimal, 1) & " hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "% fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit & " rate=" & formatFloat(rate, ffDecimal, 1) & "% energy=" & formatFloat(getEnergy(), ffDecimal, 0) & " 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 & " hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "% fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit & " rate=" & formatFloat(rate, ffDecimal, 1) & "% energy=" & formatFloat(getEnergy(), ffDecimal, 0) & " 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)
|
||||||
|
|
||||||
bot.phase = DECIDE
|
bot.phase = DECIDE
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user