From 82ec84142cbbe11161a46ea7face314b566db8fd Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Wed, 16 Sep 2026 22:01:57 +0200 Subject: [PATCH] =?UTF-8?q?feat(SNNBot):=20patience-first=20strategy=20?= =?UTF-8?q?=E2=80=94=20P3=20always,=20strict=20fire=20gate=20after=20warmu?= =?UTF-8?q?p?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- SNNBot_garage/src/SNNBot.nim | 56 +++++++++++++----------------------- 1 file changed, 20 insertions(+), 36 deletions(-) diff --git a/SNNBot_garage/src/SNNBot.nim b/SNNBot_garage/src/SNNBot.nim index 7490b64..dddbea2 100644 --- a/SNNBot_garage/src/SNNBot.nim +++ b/SNNBot_garage/src/SNNBot.nim @@ -43,9 +43,10 @@ const BETA = 1.0 # surrogate steepness (unitless; potentials are unitless) NOISE_AMP = 0.3 # exploration noise amplitude # 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 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 ───────────────────────────────────────────────────────────────── @@ -217,6 +218,7 @@ type lastBinInput: BitVec80 # DECIDE-time binary input, reused in EVALUATE lastInput: BitVec80 # previous EVALUATE-time input (for change detection) 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 bulletsHit: int # count of bullets that hit this round hitRateEMA: float # exponential moving average of hit rate @@ -283,16 +285,15 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) = bot.velSpeed = 0.0 bot.phase = DECIDE 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.bulletsHit = 0 bot.hitRateEMA = 0.5 # optimistic start bot.powerChangeCounter = 0 - # currentFirePower persists; init to 3.0 on first round (zero-value) - if bot.currentFirePower == 0.0: - bot.currentFirePower = 3.0 - bot.bulletSpeed = 20.0 - 3.0 * bot.currentFirePower - bot.lastBulletSpeed = bot.bulletSpeed + bot.currentFirePower = 3.0 + bot.bulletSpeed = P3_SPEED + bot.lastBulletSpeed = P3_SPEED bot.lastEnemyX = 0.0 bot.lastEnemyY = 0.0 bot.lastAbsBearing = 0.0 @@ -306,21 +307,6 @@ method onGameStarted*(bot: SNNBot, e: GameStartedEventForBot) = initSNN(bot.snn) 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) = inc bot.bulletsFired @@ -372,6 +358,7 @@ proc toBinaryInput(bearing: float, velDir: float, velSpeed: float, distance: flo method run*(bot: SNNBot) = while isRunning(): inc bot.tick + inc bot.roundTick setTargetSpeed(0.0) setTurnRate(0.0) @@ -436,20 +423,17 @@ method run*(bot: SNNBot) = aimTo(bot.targetAngle, gunDir) let err = abs(normalizeRelativeAngle(bot.targetAngle - gunDir)) - # Adaptive dead zone: scale learning threshold by enemy velocity - # Stationary → 0.5° (stable), full speed (8 units/tick) → 0.3° (fast tracking) - 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) - - # Fire gate: only fire when error within adaptive dead zone - if err < adaptiveDeadZone: - if getEnergy() >= ENERGY_GUARD: - # ponytail: P1 until COLD_K total exemplars exist; cross-round exemplar accumulation - let firePower = if bot.res.count < COLD_K: 1.0 else: bot.currentFirePower - let fireSpeed = 20.0 - 3.0 * firePower - bot.lastBulletSpeed = fireSpeed - discard setFire(firePower) - bot.phase = EVALUATE + # Fire gate: loose during learning phase, strict after patience period + # ponytail: err thresholds tuned for P3 (slow bullet, needs good lead); adjust if still losing + let energy = getEnergy() + let warmedUp = bot.res.count >= 10 and bot.roundTick >= PATIENCE_TICKS + let readyToFire = + if warmedUp: err < 3.0 and energy >= ENERGY_GUARD + else: err < 5.0 and energy >= ENERGY_GUARD # loose gate during learning + if readyToFire: + bot.lastBulletSpeed = P3_SPEED + discard setFire(3.0) + bot.phase = EVALUATE else: discard setFire(0.0)