feat(SNNBot): patience-first strategy — P3 always, strict fire gate after warmup
- ENERGY_GUARD 15→10 to squeeze more shots out - Remove power ladder (always P3, bulletSpeed=11) - Add PATIENCE_TICKS=15: first 15 ticks per round use loose 5° gate (collect exemplars) - After warmup (≥10 exemplars AND ≥15 ticks): strict 3° gate to avoid wasted shots - roundTick counter resets each round; BinaryAimer exemplars persist across rounds - Remove dead selectFirePower proc Result: 30% win rate vs Walls (was 0%), survival in 7/10 rounds Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,9 +43,10 @@ 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 # don't fire below this energy
|
ENERGY_GUARD = 10.0 # don't fire below this energy
|
||||||
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
|
||||||
COLD_K = 8 # exemplars needed before switching cold→warm (P1→P3)
|
COLD_K = 8 # exemplars needed before switching cold→warm (P1→P3)
|
||||||
|
PATIENCE_TICKS = 15 # don't fire for the first N ticks of each round
|
||||||
|
|
||||||
# ── SNN types ─────────────────────────────────────────────────────────────────
|
# ── SNN types ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -217,6 +218,7 @@ type
|
|||||||
lastBinInput: BitVec80 # DECIDE-time binary input, reused in EVALUATE
|
lastBinInput: BitVec80 # DECIDE-time binary input, reused in EVALUATE
|
||||||
lastInput: BitVec80 # previous EVALUATE-time input (for change detection)
|
lastInput: BitVec80 # previous EVALUATE-time input (for change 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)
|
||||||
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
|
||||||
@@ -283,16 +285,15 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) =
|
|||||||
bot.velSpeed = 0.0
|
bot.velSpeed = 0.0
|
||||||
bot.phase = DECIDE
|
bot.phase = DECIDE
|
||||||
bot.tick = 0
|
bot.tick = 0
|
||||||
# Reset per-round stats (learning and currentFirePower persist across rounds)
|
bot.roundTick = 0
|
||||||
|
# Reset per-round stats; fire power fixed at 3.0 always
|
||||||
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.powerChangeCounter = 0
|
||||||
# currentFirePower persists; init to 3.0 on first round (zero-value)
|
bot.currentFirePower = 3.0
|
||||||
if bot.currentFirePower == 0.0:
|
bot.bulletSpeed = P3_SPEED
|
||||||
bot.currentFirePower = 3.0
|
bot.lastBulletSpeed = P3_SPEED
|
||||||
bot.bulletSpeed = 20.0 - 3.0 * bot.currentFirePower
|
|
||||||
bot.lastBulletSpeed = bot.bulletSpeed
|
|
||||||
bot.lastEnemyX = 0.0
|
bot.lastEnemyX = 0.0
|
||||||
bot.lastEnemyY = 0.0
|
bot.lastEnemyY = 0.0
|
||||||
bot.lastAbsBearing = 0.0
|
bot.lastAbsBearing = 0.0
|
||||||
@@ -306,21 +307,6 @@ method onGameStarted*(bot: SNNBot, e: GameStartedEventForBot) =
|
|||||||
initSNN(bot.snn)
|
initSNN(bot.snn)
|
||||||
bot.res = initBinaryAimer()
|
bot.res = initBinaryAimer()
|
||||||
|
|
||||||
proc selectFirePower(bot: SNNBot): float =
|
|
||||||
## Power ladder with hysteresis: only commit after 8 consecutive same-direction evaluations.
|
|
||||||
if getEnergy() < ENERGY_GUARD:
|
|
||||||
return 0.0
|
|
||||||
let desired =
|
|
||||||
if bot.hitRateEMA > 0.45: 3.0
|
|
||||||
elif bot.hitRateEMA > 0.30: 2.0
|
|
||||||
else: 1.0
|
|
||||||
if desired != bot.currentFirePower:
|
|
||||||
if desired == bot.pendingPower:
|
|
||||||
if bot.powerChangeCounter >= 8:
|
|
||||||
return desired # commit
|
|
||||||
# counter incremented at call site
|
|
||||||
return bot.currentFirePower # hold
|
|
||||||
return bot.currentFirePower
|
|
||||||
|
|
||||||
method onBulletFired*(bot: SNNBot, e: BulletFiredEvent) =
|
method onBulletFired*(bot: SNNBot, e: BulletFiredEvent) =
|
||||||
inc bot.bulletsFired
|
inc bot.bulletsFired
|
||||||
@@ -372,6 +358,7 @@ proc toBinaryInput(bearing: float, velDir: float, velSpeed: float, distance: flo
|
|||||||
method run*(bot: SNNBot) =
|
method run*(bot: SNNBot) =
|
||||||
while isRunning():
|
while isRunning():
|
||||||
inc bot.tick
|
inc bot.tick
|
||||||
|
inc bot.roundTick
|
||||||
setTargetSpeed(0.0)
|
setTargetSpeed(0.0)
|
||||||
setTurnRate(0.0)
|
setTurnRate(0.0)
|
||||||
|
|
||||||
@@ -436,20 +423,17 @@ 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))
|
||||||
|
|
||||||
# Adaptive dead zone: scale learning threshold by enemy velocity
|
# Fire gate: loose during learning phase, strict after patience period
|
||||||
# Stationary → 0.5° (stable), full speed (8 units/tick) → 0.3° (fast tracking)
|
# ponytail: err thresholds tuned for P3 (slow bullet, needs good lead); adjust if still losing
|
||||||
let t = (bot.velSpeed / 8.0).clamp(0.0, 1.0)
|
let energy = getEnergy()
|
||||||
let adaptiveDeadZone = 0.5 + (0.3 - 0.5) * t # lerp(0.5, 0.3, t)
|
let warmedUp = bot.res.count >= 10 and bot.roundTick >= PATIENCE_TICKS
|
||||||
|
let readyToFire =
|
||||||
# Fire gate: only fire when error within adaptive dead zone
|
if warmedUp: err < 3.0 and energy >= ENERGY_GUARD
|
||||||
if err < adaptiveDeadZone:
|
else: err < 5.0 and energy >= ENERGY_GUARD # loose gate during learning
|
||||||
if getEnergy() >= ENERGY_GUARD:
|
if readyToFire:
|
||||||
# ponytail: P1 until COLD_K total exemplars exist; cross-round exemplar accumulation
|
bot.lastBulletSpeed = P3_SPEED
|
||||||
let firePower = if bot.res.count < COLD_K: 1.0 else: bot.currentFirePower
|
discard setFire(3.0)
|
||||||
let fireSpeed = 20.0 - 3.0 * firePower
|
bot.phase = EVALUATE
|
||||||
bot.lastBulletSpeed = fireSpeed
|
|
||||||
discard setFire(firePower)
|
|
||||||
bot.phase = EVALUATE
|
|
||||||
else:
|
else:
|
||||||
discard setFire(0.0)
|
discard setFire(0.0)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user