Files
SirRoboGarage/SNNBot_garage/src/SNNBot.nim
T

527 lines
25 KiB
Nim
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SNNBot — SNN core + aiming loop prototype (issue #152).
# 80-input population-coded → 12 LIF hidden → polar-coded (sin/cos) output decoded via atan2.
# Inputs: relVelDir [0-35], speed thermometer [36-43], distance thermometer [44-51]. Raw bearing removed (irrelevant to lead).
# No movement, no firing. SuperSpike three-factor rule on all weights (issue #158).
# Binary reservoir alternative toggled via USE_RESERVOIR const (issue #159).
#
# State machine:
# DECIDE → feed bearing+velocity into SNN/reservoir, store targetAngle, → WAITING
# WAITING → aimTo() each tick; when error < 2° → EVALUATE
# EVALUATE → measure error, compute SuperSpike/reservoir update, log, → DECIDE
import std/[math, random, os, strutils, strformat]
import robocode_tankroyale_botapi
import radar_lock/radar_lock as radar_lock
import reservoir
# Diagnostic log file for bullet economy tracking (independent of test framework)
let diagnosticLog = "/tmp/snnbot_diagnostic.log"
let roundStatsLog = "/tmp/snnbot_round_stats.log"
let aimDebugLog = "/tmp/snnbot_aim_debug.log"
let biasDebugLog = "/tmp/snnbot_bias_debug.log"
proc diagLog(s: string) =
try:
let f = open(diagnosticLog, fmAppend)
f.writeLine(s)
f.close()
except:
discard
proc appendLog(path, s: string) =
try:
let f = open(path, fmAppend)
f.writeLine(s)
f.close()
except:
discard
proc truncateLog(path: string) =
try:
let f = open(path, fmWrite)
f.close()
except:
discard
const USE_RESERVOIR* = true # ponytail: kept for SNN path fallback; remove when binary aimer is validated
# ── Constants ──────────────────────────────────────────────────────────────────
const botJsonPath = currentSourcePath().parentDir / "SNNBot.json"
const
N_IN = 80 # input neurons: bearing(36) + vel_dir(36) + speed(8)
N_HID = 12 # hidden LIF neurons
BAND_DEG = 10.0 # degrees per input band
BEARING_OFFSET = 0 # neurons 0-35: relative bearing
VEL_DIR_OFFSET = 36 # neurons 36-71: velocity direction
SPEED_OFFSET = 72 # neurons 72-79: speed bands
N_SPEED_BANDS = 8
SPEED_BAND_WIDTH = 1.0 # units/tick per band
MAX_SPEED = 8.0
LEAK = 0.9 # LIF membrane leak factor
THRESH = 0.08 # ponytail: THRESH=0.08 tuned for 80-input layer; raise if firing saturates
MAX_GUN_TURN = 20.0 # max gun turn per tick (degrees)
ETA = 0.1 # SuperSpike learning rate for hidden→output weights (r_0 from paper, bumped 2x)
ETA_IH = 0.02 # ponytail: ETA_IH=0.02 bumped 4x for input→hidden (was 0.005, too conservative)
# ponytail: separate input→hidden rate; add RMaxProp optimizer if convergence still unstable
N_INFER = 10 # inference window ticks per DECIDE
# ponytail: N_INFER=10, increase if output still noisy; decrease if too slow per tick
TRACE_DECAY = 0.9 # pre-synaptic trace decay (exponential low-pass)
W_CLAMP = 1.0 # ponytail: W_CLAMP=1.0, paper says ±0.1 but that's for mV-scale voltages; our unitless THRESH=1.0 needs larger weights
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 = 5.0 # keep buffer; Walls hits us while we shoot
FIRE_POWER = 2.0 # fixed fire power
BULLET_SPEED = 14.0 # 20 - 3 * FIRE_POWER
COLD_K = 3 # exemplars needed before leaving cold start
PATIENCE_TICKS = 5 # ticks before first fire allowed
# ── SNN types ─────────────────────────────────────────────────────────────────
type
SNN = object
wih: array[N_IN * N_HID, float] # 80×12 input→hidden weights
wSin: array[N_HID, float] # hidden→sin-channel weights
wCos: array[N_HID, float] # hidden→cos-channel weights
vHid: array[N_HID, float] # hidden membrane potentials
preTrace: array[N_IN, float] # pre-synaptic traces (low-pass of input spikes)
# Random feedback weights B[h, o] for hidden error projection; fixed, never updated.
# Indexed as bFb[h * 2 + o], o=0 → sin, o=1 → cos.
bFb: array[N_HID * 2, float]
tick: int
lastSinOut: float # last forward pass sin output (weighted sum)
lastCosOut: float # last forward pass cos output (weighted sum)
lastSnnAngle: float # last atan2 result (degrees)
proc initSNN(snn: var SNN) =
randomize()
for w in snn.wih.mitems: w = rand(0.2) - 0.1 # init within clamp ±0.1, room to grow to ±1.0
for w in snn.wSin.mitems: w = rand(0.2) - 0.1
for w in snn.wCos.mitems: w = rand(0.2) - 0.1
for b in snn.bFb.mitems: b = rand(2.0) - 1.0 # N(0,1)-ish; fixed forever
for t in snn.preTrace.mitems: t = 0.0
snn.tick = 0
proc encodeBearing(inputs: var array[N_IN, float], bearing: float, offset: int) =
## Population-code a -180..+180 angle into 36 neurons starting at offset.
## Triangular interpolation between lo and hi band.
let norm = ((bearing + 180.0) / BAND_DEG) # 0..36
let lo = int(norm) mod 36
let hi = (lo + 1) mod 36
let frac = norm - float(int(norm))
inputs[offset + lo] = 1.0 - frac
inputs[offset + hi] = frac
proc encodeInput(bearing: float): array[N_IN, float] =
## Bearing-only encode for overlay (velocity channels stay 0).
encodeBearing(result, bearing, BEARING_OFFSET)
proc encodeInputFull(bearing: float, velDirDeg: float, speed: float,
hasVel: bool): array[N_IN, float] =
## Full 80-neuron encode: bearing + velocity direction + speed.
encodeBearing(result, bearing, BEARING_OFFSET)
if hasVel:
encodeBearing(result, velDirDeg, VEL_DIR_OFFSET)
# Speed: triangular interpolation over N_SPEED_BANDS bands, clamp to [0, MAX_SPEED]
let s = speed.clamp(0.0, MAX_SPEED)
let norm = s / SPEED_BAND_WIDTH
let lo = min(int(norm), N_SPEED_BANDS - 1)
let hi = min(lo + 1, N_SPEED_BANDS - 1)
let frac = norm - float(int(norm))
result[SPEED_OFFSET + lo] += 1.0 - frac
result[SPEED_OFFSET + hi] += frac
proc surrogateDerivative(v: float): float {.inline.} =
## σ'(U) = (1 + |β(U − ϑ)|)^{-2} — peaks at threshold, gives gradient direction.
let x = BETA * (v - THRESH)
result = 1.0 / ((1.0 + abs(x)) * (1.0 + abs(x)))
proc forward(snn: var SNN, inputs: array[N_IN, float],
spikesOut: var array[N_HID, float],
vHidSnap: var array[N_HID, float],
sinOut: var float, cosOut: var float) =
## One SNN tick. Writes hidden spikes, pre-spike voltages, and raw sin/cos outputs.
## Caller accumulates sinOut/cosOut across N_INFER ticks, then calls atan2.
inc snn.tick
# Pre-synaptic trace: low-pass of input spikes
for i in 0 ..< N_IN:
snn.preTrace[i] = TRACE_DECAY * snn.preTrace[i] + inputs[i]
# Hidden layer: LIF update
for h in 0 ..< N_HID:
var wsum = 0.0
for i in 0 ..< N_IN:
wsum += inputs[i] * snn.wih[i * N_HID + h]
# ponytail: noise removed — SuperSpike surrogate derivative provides gradient direction, no exploration needed
snn.vHid[h] = LEAK * snn.vHid[h] + wsum
vHidSnap[h] = snn.vHid[h] # snapshot voltage before reset (for surrogate)
if snn.vHid[h] >= THRESH:
spikesOut[h] = 1.0
snn.vHid[h] = 0.0
else:
spikesOut[h] = 0.0
# Output: polar-coded via sin/cos channels (raw; caller does atan2)
sinOut = 0.0
cosOut = 0.0
for h in 0 ..< N_HID:
sinOut += spikesOut[h] * snn.wSin[h]
cosOut += spikesOut[h] * snn.wCos[h]
snn.lastSinOut = sinOut
snn.lastCosOut = cosOut
snn.lastSnnAngle = arctan2(sinOut, cosOut) * 180.0 / PI
proc superSpikeUpdate(snn: var SNN,
spikes: array[N_HID, float],
vSnap: array[N_HID, float],
preTrace: array[N_IN, float],
targetAngle: float) =
## SuperSpike three-factor weight update.
## Δw = η × pre_trace × σ'(U) × error
## spikes/vSnap/preTrace: captured at DECIDE time for this inference window.
## Output error: target_rate − actual_rate (rate-coded target).
## Hidden error: projected via fixed random feedback weights B.
# Target rates for sin/cos channels: map [-1,1] → [0,1]
let tSin = (sin(degToRad(targetAngle)) + 1.0) / 2.0
let tCos = (cos(degToRad(targetAngle)) + 1.0) / 2.0
# Normalize accumulated spike counts to rates in [0,1]
# Actual output activity using spike rates
var sinAct = 0.0; var cosAct = 0.0
for h in 0 ..< N_HID:
let rate = spikes[h] / float(N_INFER)
sinAct += rate * snn.wSin[h]
cosAct += rate * snn.wCos[h]
# Map actual output to [0,1] for rate comparison
let sinActNorm = (sinAct.clamp(-1.0, 1.0) + 1.0) / 2.0
let cosActNorm = (cosAct.clamp(-1.0, 1.0) + 1.0) / 2.0
# Output error (target_rate − actual_rate)
let errSin = tSin - sinActNorm
let errCos = tCos - cosActNorm
# Update hidden→output weights: Δw = η × rate_h × σ'(U_h) × error_o
for h in 0 ..< N_HID:
let sg = surrogateDerivative(vSnap[h])
let rate = spikes[h] / float(N_INFER)
snn.wSin[h] += ETA * rate * sg * errSin
snn.wSin[h] = snn.wSin[h].clamp(-W_CLAMP, W_CLAMP)
snn.wCos[h] += ETA * rate * sg * errCos
snn.wCos[h] = snn.wCos[h].clamp(-W_CLAMP, W_CLAMP)
# Update input→hidden weights: Δw = η × preTrace_j × σ'(U_h) × error_h
# Hidden error projected via fixed random feedback: error_h = Σ_o B[h,o] × error_o
for h in 0 ..< N_HID:
let sg = surrogateDerivative(vSnap[h])
let errHid = snn.bFb[h * 2 + 0] * errSin + snn.bFb[h * 2 + 1] * errCos
for i in 0 ..< N_IN:
snn.wih[i * N_HID + h] += ETA_IH * preTrace[i] * sg * errHid
snn.wih[i * N_HID + h] = snn.wih[i * N_HID + h].clamp(-W_CLAMP, W_CLAMP)
# ── Bot state machine ─────────────────────────────────────────────────────────
type
Phase = enum DECIDE, WAITING, EVALUATE
SNNBot = ref object of Bot
snn: SNN
res: LeadGrid
phase: Phase
targetAngle: float # SNN output (absolute bearing)
enemyBearing: float # last known enemy bearing
enemyDist: float # last known distance to enemy
hasContact: bool
tick: int
lastSpikes: array[N_HID, float] # accumulated spike counts over N_INFER ticks
lastVSnap: array[N_HID, float] # hidden voltages before reset from last tick (for SuperSpike)
lastRelBearing: float # relative bearing at DECIDE time (fixed for learning)
lastEnemyX: float64 # previous tick enemy position (for velocity)
lastEnemyY: float64
hasLastPos: bool
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
lastVPerp: float # DECIDE-time vPerp, reused in EVALUATE
lastAbsBearing: float # absolute bearing to enemy captured at DECIDE time
decideEnemyX: float # enemy X frozen at DECIDE time
decideEnemyY: float # enemy Y frozen at DECIDE time
decideVelDirDeg: float # enemy vel direction frozen at DECIDE time
decideVelSpeed: float # enemy speed frozen at DECIDE time
decideDist: float # distance to enemy frozen 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
gridHasData: bool # false during cold-start (forward returned -999)
# ── aimTo helper ──────────────────────────────────────────────────────────────
proc aimTo(targetAngle, gunDir: float) {.inline.} =
let delta = normalizeRelativeAngle(targetAngle - gunDir)
setGunTurnRate(delta.clamp(-MAX_GUN_TURN, MAX_GUN_TURN))
# ── Debug overlay ─────────────────────────────────────────────────────────────
proc drawOverlay(bot: SNNBot, myX, myY, gunDir, enemyBearing, targetAngle: float) =
let LINE_LEN = bot.enemyDist + 50.0
# Convert degree bearing to SVG direction (SVG y-axis is inverted; 0=east, CCW+)
template toXY(bearing, len: float): (float, float) =
(myX + cos(degToRad(bearing)) * len,
myY + sin(degToRad(bearing)) * len)
# Green: enemy direction (truth)
setStrokeColor(GREEN)
setStrokeWidth(2.0)
let (ex, ey) = toXY(enemyBearing, LINE_LEN)
drawLine(myX, myY, ex, ey)
# Red: gun direction
setStrokeColor(RED)
let (gx, gy) = toXY(gunDir, LINE_LEN)
drawLine(myX, myY, gx, gy)
# Yellow: SNN target angle
setStrokeColor(YELLOW)
let (tx, ty) = toXY(targetAngle, LINE_LEN)
drawLine(myX, myY, tx, ty)
# ── Event handlers ────────────────────────────────────────────────────────────
method onScannedBot*(bot: SNNBot, e: ScannedBotEvent) =
let bx = getX(); let by = getY()
bot.enemyBearing = directionTo(bx, by, e.x, e.y)
bot.enemyDist = distanceTo(bx, by, e.x, e.y)
bot.hasContact = true
if bot.hasLastPos:
let dx = e.x - bot.lastEnemyX
let dy = e.y - bot.lastEnemyY
bot.velDirDeg = arctan2(dy, dx) * 180.0 / PI
bot.velSpeed = sqrt(dx * dx + dy * dy)
bot.lastEnemyX = e.x; bot.lastEnemyY = e.y
bot.hasLastPos = true
method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) =
setAdjustGunForBodyTurn(true)
setAdjustRadarForBodyTurn(true)
setAdjustRadarForGunTurn(true)
radar_lock.init()
bot.hasContact = false
bot.hasLastPos = false
bot.velDirDeg = 0.0
bot.velSpeed = 0.0
bot.phase = DECIDE
bot.tick = 0
bot.roundTick = 0
# Reset per-round stats
bot.bulletsFired = 0
bot.bulletsHit = 0
bot.hitRateEMA = 0.5 # optimistic start
bot.gridHasData = false
bot.lastEnemyX = 0.0
bot.lastEnemyY = 0.0
bot.lastAbsBearing = 0.0
bot.lastDecideGunDir = 0.0
bot.targetAngle = 0.0
setTargetSpeed(0.0)
setTurnRate(0.0)
method onRoundEnded*(bot: SNNBot, e: RoundEndedEventForBot) =
let rate = if bot.bulletsFired > 0: bot.bulletsHit.float / bot.bulletsFired.float * 100.0 else: 0.0
let msg = "ROUND_STATS round=" & $e.roundNumber & " fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit & " rate=" & formatFloat(rate, ffDecimal, 1) & "%"
diagLog(msg)
appendLog(roundStatsLog, fmt"ROUND fired={bot.bulletsFired} hit={bot.bulletsHit} rate={rate:.1f}% energy={getEnergy():.0f}")
method onGameStarted*(bot: SNNBot, e: GameStartedEventForBot) =
initSNN(bot.snn)
bot.res = initLeadGrid()
truncateLog(roundStatsLog)
truncateLog(aimDebugLog)
truncateLog(biasDebugLog)
method onBulletFired*(bot: SNNBot, e: BulletFiredEvent) =
inc bot.bulletsFired
# decay EMA toward miss on each confirmed fire
bot.hitRateEMA = bot.hitRateEMA * 0.93
method onBulletHit*(bot: SNNBot, e: BulletHitBotEvent) =
inc bot.bulletsHit
bot.hitRateEMA = bot.hitRateEMA * 0.85 + 1.0 * 0.15
# ── Main loop ─────────────────────────────────────────────────────────────────
method run*(bot: SNNBot) =
while isRunning():
inc bot.tick
inc bot.roundTick
setTargetSpeed(0.0)
setTurnRate(0.0)
if not bot.hasContact:
setRadarTurnRate(45.0)
go()
continue
let myX = getX()
let myY = getY()
let gunDir = getGunDirection()
case bot.phase
of DECIDE:
# absBearing: world-frame bearing [0,360) from directionTo — independent of bot heading
let absBearing = bot.enemyBearing
bot.lastRelBearing = normalizeRelativeAngle(absBearing - gunDir)
when USE_RESERVOIR:
var relVelDir = bot.velDirDeg - absBearing
while relVelDir >= 180.0: relVelDir -= 360.0
while relVelDir < -180.0: relVelDir += 360.0
let vPerp = bot.velSpeed * sin(degToRad(relVelDir))
let gridOffset = bot.res.forward(vPerp, bot.enemyDist)
bot.lastVPerp = vPerp
bot.lastDecideGunDir = gunDir
bot.lastAbsBearing = absBearing
bot.decideEnemyX = bot.lastEnemyX
bot.decideEnemyY = bot.lastEnemyY
bot.decideVelDirDeg = bot.velDirDeg
bot.decideVelSpeed = bot.velSpeed
bot.decideDist = bot.enemyDist
let rate = if bot.bulletsFired > 0: float(bot.bulletsHit) / float(bot.bulletsFired) * 100.0 else: 0.0
bot.gridHasData = gridOffset > -999.0
if gridOffset <= -999.0:
bot.targetAngle = absBearing
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)
else:
# Grid learned at BULLET_SPEED — use offset directly (no scaling needed)
bot.targetAngle = (absBearing + gridOffset + 360.0) mod 360.0
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
diagLog("DECIDE tick=" & $bot.tick & " offset=" & formatFloat(gridOffset, ffDecimal, 2) & "° cells=" & $bot.res.totalCount)
else:
let inputs = encodeInputFull(absBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
# Multi-tick inference: accumulate sin/cos and spike counts over N_INFER ticks
var totalSin = 0.0; var totalCos = 0.0
var tickSpikes: array[N_HID, float]
var tickVSnap: array[N_HID, float]
for h in 0 ..< N_HID: bot.lastSpikes[h] = 0.0
for _ in 0 ..< N_INFER:
var sinT, cosT: float
bot.snn.forward(inputs, tickSpikes, tickVSnap, sinT, cosT)
totalSin += sinT; totalCos += cosT
for h in 0 ..< N_HID:
bot.lastSpikes[h] += tickSpikes[h] # accumulate counts
# Store last-tick voltages for learning
bot.lastVSnap = tickVSnap
bot.snn.lastSinOut = totalSin; bot.snn.lastCosOut = totalCos
bot.snn.lastSnnAngle = arctan2(totalSin, totalCos) * 180.0 / PI
bot.targetAngle = gunDir + bot.snn.lastSnnAngle
bot.lastDecideGunDir = gunDir
# Log total spike count across inference window
var spikeCount = 0
var maxV = 0.0
for h in 0 ..< N_HID:
spikeCount += int(bot.lastSpikes[h])
maxV = max(maxV, bot.snn.vHid[h])
echo "tick=" & $bot.tick & " infer_spk=" & $spikeCount & "/" & $(N_INFER * N_HID) & " maxV=" & formatFloat(maxV, ffDecimal, 3)
bot.phase = WAITING
of WAITING:
aimTo(bot.targetAngle, gunDir)
let err = abs(normalizeRelativeAngle(bot.targetAngle - gunDir))
# Fire gate: gun aimed, enough energy, not in cold-start
let energy = getEnergy()
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}")
let readyToFire = err < 2.0 and energy >= ENERGY_GUARD and bot.roundTick >= PATIENCE_TICKS
if readyToFire:
discard setFire(FIRE_POWER)
bot.phase = EVALUATE
else:
discard setFire(0.0)
of EVALUATE:
# Predictive error signal: extrapolate enemy position at bullet impact time.
if bot.hasLastPos:
# correctOffset: correct lead angle at BULLET_SPEED, using DECIDE-time snapshot
let travelTime = bot.decideDist / BULLET_SPEED
let velRad = degToRad(bot.decideVelDirDeg)
let futureX = bot.decideEnemyX + cos(velRad) * bot.decideVelSpeed * travelTime
let futureY = bot.decideEnemyY + sin(velRad) * bot.decideVelSpeed * travelTime
# correctAngle: absolute world-frame bearing [0,360) to predicted enemy position
let correctAngle = directionTo(myX, myY, futureX, futureY)
# Adaptive learning dead zone: scale by DECIDE-time enemy velocity
let t = (bot.decideVelSpeed / 8.0).clamp(0.0, 1.0)
let adaptiveDeadZone = 0.5 + (0.3 - 0.5) * t # lerp(0.5, 0.3, t)
when USE_RESERVOIR:
# correctOffset: angle offset from bare bearing that a bullet at BULLET_SPEED needs
let correctOffset = normalizeRelativeAngle(correctAngle - bot.lastAbsBearing)
let producedOffset = bot.res.forward(bot.lastVPerp, bot.decideDist)
# Bias diagnostics: log grid offset vs correct offset to detect systematic error
let biasVal = producedOffset - correctOffset
appendLog(biasDebugLog, fmt"BIAS tick={bot.roundTick} gridOffset={producedOffset:.3f} correctOffset={correctOffset:.3f} bias={biasVal:.3f} vPerp={bot.lastVPerp:.3f} dist={bot.decideDist:.1f}")
# Learn if: cold start OR error > dead zone
if producedOffset > -999.0:
let aimErr = abs(normalizeRelativeAngle(producedOffset - correctOffset))
if aimErr > adaptiveDeadZone:
bot.res.learn(bot.lastVPerp, bot.decideDist, correctOffset)
else:
bot.res.learn(bot.lastVPerp, bot.decideDist, correctOffset)
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
echo "RES tick=" & $bot.tick &
" hitEMA=" & formatFloat(bot.hitRateEMA * 100.0, ffDecimal, 0) & "%" &
" fired=" & $bot.bulletsFired & " hit=" & $bot.bulletsHit &
" rate=" & (if bot.bulletsFired > 0: formatFloat(rate, ffDecimal, 1) else: "0.0") & "%" &
" energy=" & formatFloat(getEnergy(), ffDecimal, 0) &
" offset=" & (if producedOffset > -999.0: formatFloat(producedOffset, ffDecimal, 1) else: "cold") &
" correctOffset=" & formatFloat(correctOffset, ffDecimal, 1) &
" err=" & (if producedOffset > -999.0: formatFloat(aimErr, ffDecimal, 1) else: "n/a") &
" vPerp=" & formatFloat(bot.lastVPerp, ffDecimal, 2) &
" cells=" & $bot.res.totalCount
else:
let targetRel = normalizeRelativeAngle(correctAngle - bot.lastDecideGunDir)
bot.snn.superSpikeUpdate(bot.lastSpikes, bot.lastVSnap, bot.snn.preTrace, targetRel)
let err = abs(normalizeRelativeAngle(gunDir - bot.enemyBearing))
# Compute verbose logging metrics
var spikeCount = 0
var maxV = 0.0
for h in 0 ..< N_HID:
spikeCount += int(bot.lastSpikes[h])
maxV = max(maxV, bot.snn.vHid[h])
var meanWih = 0.0
for w in bot.snn.wih:
meanWih += abs(w)
meanWih /= float(N_IN * N_HID)
var meanWout = 0.0
for w in bot.snn.wSin:
meanWout += abs(w)
for w in bot.snn.wCos:
meanWout += abs(w)
meanWout /= float(2 * N_HID)
let rate = if bot.bulletsFired > 0: (float(bot.bulletsHit) / float(bot.bulletsFired) * 100.0) else: 0.0
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
# Radar lock
setRadarTurnRate(radar_lock.doRadar(getRadarDirection(), bot.enemyBearing))
# Debug overlay
drawOverlay(bot, myX, myY, gunDir, bot.enemyBearing, bot.targetAngle)
go()
# ── Entry point ───────────────────────────────────────────────────────────────
when isMainModule:
var bot = SNNBot(phase: DECIDE)
start(bot, botJsonPath)