From 86dfd672b1506ab2e491ec05fd88f31e6403e8e2 Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Thu, 17 Sep 2026 08:18:59 +0200 Subject: [PATCH] fix(SNNBot): revert grid to working version, add file-based stats logging Reverts reservoir.nim to the neighbor-blending forward() + single-cell learn() version (pre-bilinear-interpolation). Adds /tmp/snnbot_round_stats.log and /tmp/snnbot_aim_debug.log for diagnostics independent of test framework stdout. Co-Authored-By: Claude Sonnet 4.6 --- SNNBot_garage/src/SNNBot.nim | 42 ++++++++++++++++++++- SNNBot_garage/src/reservoir.nim | 67 ++++++++++++++++++--------------- 2 files changed, 77 insertions(+), 32 deletions(-) diff --git a/SNNBot_garage/src/SNNBot.nim b/SNNBot_garage/src/SNNBot.nim index 7a18140..b9f284f 100644 --- a/SNNBot_garage/src/SNNBot.nim +++ b/SNNBot_garage/src/SNNBot.nim @@ -9,11 +9,39 @@ # 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, 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" + +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 ────────────────────────────────────────────────────────────────── @@ -303,9 +331,17 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) = 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) method onBulletFired*(bot: SNNBot, e: BulletFiredEvent) = @@ -353,9 +389,11 @@ method run*(bot: SNNBot) = if aimOffset <= -999.0: 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 + diagLog("DECIDE tick=" & $bot.tick & " offset=cold-start cells=" & $bot.res.totalCount) else: 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) & "° 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 @@ -391,6 +429,8 @@ method run*(bot: SNNBot) = # Fire gate: gun must be aimed and settled before firing let energy = getEnergy() let readyToFire = err < 2.0 and energy >= ENERGY_GUARD and bot.roundTick >= PATIENCE_TICKS + 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} fired={readyToFire}") if readyToFire: bot.lastBulletSpeed = P2_SPEED discard setFire(2.0) diff --git a/SNNBot_garage/src/reservoir.nim b/SNNBot_garage/src/reservoir.nim index da5bfb1..34377d0 100644 --- a/SNNBot_garage/src/reservoir.nim +++ b/SNNBot_garage/src/reservoir.nim @@ -11,7 +11,7 @@ type GridCell = object sumSin: float sumCos: float - count: float64 + count: int LeadGrid* = object cells: array[VPERP_BINS * DIST_BINS, GridCell] # 17 × 8 = 136 cells @@ -19,39 +19,44 @@ type proc initLeadGrid*(): LeadGrid = result = LeadGrid() +proc cellIndex(vPerp: float, distance: float): int {.inline.} = + let vBin = clamp(int(vPerp + 8.5), 0, VPERP_BINS - 1) + let dBin = clamp(int(distance / DIST_BAND), 0, DIST_BINS - 1) + result = vBin * DIST_BINS + dBin + proc forward*(grid: var LeadGrid, vPerp, distance: float): float = - ## Returns bilinear-interpolated circular mean offset in degrees, or -999.0 when no data. - let vCont = clamp(vPerp + 8.0, 0.0, 15.999) - let dCont = clamp(distance / DIST_BAND, 0.0, 6.999) - let v0 = int(floor(vCont)); let v1 = min(v0 + 1, VPERP_BINS - 1) - let d0 = int(floor(dCont)); let d1 = min(d0 + 1, DIST_BINS - 1) - let fv = vCont - float(v0) - let fd = dCont - float(d0) - let corners = [(v0, d0, (1-fv)*(1-fd)), - (v1, d0, fv*(1-fd)), - (v0, d1, (1-fv)*fd), - (v1, d1, fv*fd)] - var sinSum, cosSum, totalW = 0.0 - for (vi, di, w) in corners: - let cell = grid.cells[vi * DIST_BINS + di] - if cell.count == 0: continue - sinSum += w * cell.sumSin / cell.count - cosSum += w * cell.sumCos / cell.count - totalW += w - if totalW == 0.0: return -999.0 + ## Returns circular mean offset in degrees, or -999.0 when no data. + let idx = cellIndex(vPerp, distance) + let vBin = clamp(int(vPerp + 8.5), 0, VPERP_BINS - 1) + let dBin = clamp(int(distance / DIST_BAND), 0, DIST_BINS - 1) + + # Accumulate weighted contributions: direct cell (w=1), cardinal neighbors (w=0.5), diagonals (w=0.25) + var sinSum = 0.0 + var cosSum = 0.0 + var totalW = 0.0 + for dv in -1 .. 1: + for dd in -1 .. 1: + let vb = vBin + dv + let db = dBin + dd + if vb < 0 or vb >= VPERP_BINS or db < 0 or db >= DIST_BINS: continue + let c = grid.cells[vb * DIST_BINS + db] + if c.count == 0: continue + let w = if dv == 0 and dd == 0: 1.0 + elif dv == 0 or dd == 0: 0.5 + else: 0.25 + sinSum += w * c.sumSin / float(c.count) + cosSum += w * c.sumCos / float(c.count) + totalW += w + if totalW == 0.0: + return -999.0 result = radToDeg(arctan2(sinSum, cosSum)) proc learn*(grid: var LeadGrid, vPerp, distance, correctOffset: float) = - let s = sin(degToRad(correctOffset)) - let c = cos(degToRad(correctOffset)) - let vBin = clamp(int(floor(vPerp + 8.0)), 0, VPERP_BINS - 1) - let dBin = clamp(int(floor(distance / DIST_BAND)), 0, DIST_BINS - 1) - let i = vBin * DIST_BINS + dBin - grid.cells[i].sumSin += s - grid.cells[i].sumCos += c - grid.cells[i].count += 1.0 + let idx = cellIndex(vPerp, distance) + grid.cells[idx].sumSin += sin(degToRad(correctOffset)) + grid.cells[idx].sumCos += cos(degToRad(correctOffset)) + inc grid.cells[idx].count proc totalCount*(grid: LeadGrid): int = - var s = 0.0 - for c in grid.cells: s += c.count - result = int(s) + for c in grid.cells: + result += c.count