refactor(SNNBot): replace reservoir with direct binary readout

Delete 1024-neuron reservoir — it added noise, not features.
Direct 80-bit input → 72 output bins via popcount + WTA Hebbian.
Same learning rule (OR reinforce, AND NOT punish), zero indirection.
Reservoir adds value for temporal features (step 3), not now.
This commit is contained in:
2026-09-14 23:13:01 +02:00
parent 2963917072
commit f70d9e6d1e
2 changed files with 62 additions and 132 deletions
+15 -8
View File
@@ -9,12 +9,12 @@
# WAITING → aimTo() each tick; when error < 2° → EVALUATE
# EVALUATE → measure error, compute SuperSpike/reservoir update, log, → DECIDE
import std/[math, random, os, strutils]
import std/[math, random, os, strutils, bitops]
import robocode_tankroyale_botapi
import radar_lock/radar_lock as radar_lock
import reservoir
const USE_RESERVOIR* = true
const USE_RESERVOIR* = true # ponytail: kept for SNN path fallback; remove when binary aimer is validated
# ── Constants ──────────────────────────────────────────────────────────────────
@@ -198,7 +198,7 @@ type
SNNBot = ref object of Bot
snn: SNN
res: Reservoir
res: BinaryAimer
phase: Phase
targetAngle: float # SNN output (absolute bearing)
enemyBearing: float # last known enemy bearing
@@ -214,6 +214,7 @@ type
velDirDeg: float64 # velocity direction (degrees) from last scan delta
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
# ── aimTo helper ──────────────────────────────────────────────────────────────
@@ -375,7 +376,7 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) =
method onGameStarted*(bot: SNNBot, e: GameStartedEventForBot) =
initSNN(bot.snn)
bot.res = initReservoir(42)
bot.res = initAimer()
# ── Reservoir helpers ─────────────────────────────────────────────────────────
@@ -413,7 +414,8 @@ method run*(bot: SNNBot) =
let binInput = toBitVec80(inputs)
let winBin = bot.res.forward(binInput)
let aimAngle = bot.res.interpolatedAngle(winBin)
bot.targetAngle = gunDir + aimAngle
bot.lastBinInput = binInput
bot.targetAngle = gunDir + aimAngle
bot.lastDecideGunDir = gunDir
echo "RES tick=" & $bot.tick & " bin=" & $winBin & " aim=" & formatFloat(aimAngle, ffDecimal, 1)
else:
@@ -460,18 +462,23 @@ method run*(bot: SNNBot) =
let absBearing = directionTo(myX, myY, futureX, futureY)
let targetRel = normalizeRelativeAngle(absBearing - bot.lastDecideGunDir)
when USE_RESERVOIR:
# state/scores already set by DECIDE's forward(); re-forwarding would corrupt them
let winBin = block:
var best = 0; var bestS = -1
for k in 0 ..< N_BINS:
if bot.res.scores[k] > bestS: bestS = bot.res.scores[k]; best = k
best
let aimAngle = bot.res.interpolatedAngle(winBin)
bot.res.learn(targetRel)
bot.res.learn(bot.lastBinInput, targetRel)
let correctBin = angleToBin(targetRel)
let readoutPopCorrect = block:
var c = 0
for w in 0 ..< WORDS_IN: c += popcount(bot.res.readout[correctBin][w]).int
c
echo "RES tick=" & $bot.tick & " bin=" & $winBin &
" aim=" & formatFloat(aimAngle, ffDecimal, 1) &
" target=" & formatFloat(targetRel, ffDecimal, 1) &
" err=" & formatFloat(abs(aimAngle - targetRel), ffDecimal, 1)
" err=" & formatFloat(abs(aimAngle - targetRel), ffDecimal, 1) &
" readout_pop_correct=" & $readoutPopCorrect
else:
let err = abs(normalizeRelativeAngle(gunDir - bot.enemyBearing))
bot.snn.superSpikeUpdate(bot.lastSpikes, bot.lastVSnap, bot.snn.preTrace, targetRel)