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 <noreply@anthropic.com>
This commit is contained in:
@@ -9,11 +9,39 @@
|
|||||||
# WAITING → aimTo() each tick; when error < 2° → EVALUATE
|
# WAITING → aimTo() each tick; when error < 2° → EVALUATE
|
||||||
# EVALUATE → measure error, compute SuperSpike/reservoir update, log, → DECIDE
|
# 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 robocode_tankroyale_botapi
|
||||||
import radar_lock/radar_lock as radar_lock
|
import radar_lock/radar_lock as radar_lock
|
||||||
import reservoir
|
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
|
const USE_RESERVOIR* = true # ponytail: kept for SNN path fallback; remove when binary aimer is validated
|
||||||
|
|
||||||
# ── Constants ──────────────────────────────────────────────────────────────────
|
# ── Constants ──────────────────────────────────────────────────────────────────
|
||||||
@@ -303,9 +331,17 @@ method onRoundStarted*(bot: SNNBot, e: RoundStartedEvent) =
|
|||||||
setTargetSpeed(0.0)
|
setTargetSpeed(0.0)
|
||||||
setTurnRate(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) =
|
method onGameStarted*(bot: SNNBot, e: GameStartedEventForBot) =
|
||||||
initSNN(bot.snn)
|
initSNN(bot.snn)
|
||||||
bot.res = initLeadGrid()
|
bot.res = initLeadGrid()
|
||||||
|
truncateLog(roundStatsLog)
|
||||||
|
truncateLog(aimDebugLog)
|
||||||
|
|
||||||
|
|
||||||
method onBulletFired*(bot: SNNBot, e: BulletFiredEvent) =
|
method onBulletFired*(bot: SNNBot, e: BulletFiredEvent) =
|
||||||
@@ -353,9 +389,11 @@ method run*(bot: SNNBot) =
|
|||||||
if aimOffset <= -999.0:
|
if aimOffset <= -999.0:
|
||||||
bot.targetAngle = absBearing
|
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
|
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:
|
else:
|
||||||
bot.targetAngle = (absBearing + aimOffset + 360.0) mod 360.0
|
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
|
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:
|
else:
|
||||||
let inputs = encodeInputFull(absBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
|
let inputs = encodeInputFull(absBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos)
|
||||||
# Multi-tick inference: accumulate sin/cos and spike counts over N_INFER ticks
|
# 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
|
# Fire gate: gun must be aimed and settled before firing
|
||||||
let energy = getEnergy()
|
let energy = getEnergy()
|
||||||
let readyToFire = err < 2.0 and energy >= ENERGY_GUARD and bot.roundTick >= PATIENCE_TICKS
|
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:
|
if readyToFire:
|
||||||
bot.lastBulletSpeed = P2_SPEED
|
bot.lastBulletSpeed = P2_SPEED
|
||||||
discard setFire(2.0)
|
discard setFire(2.0)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ type
|
|||||||
GridCell = object
|
GridCell = object
|
||||||
sumSin: float
|
sumSin: float
|
||||||
sumCos: float
|
sumCos: float
|
||||||
count: float64
|
count: int
|
||||||
|
|
||||||
LeadGrid* = object
|
LeadGrid* = object
|
||||||
cells: array[VPERP_BINS * DIST_BINS, GridCell] # 17 × 8 = 136 cells
|
cells: array[VPERP_BINS * DIST_BINS, GridCell] # 17 × 8 = 136 cells
|
||||||
@@ -19,39 +19,44 @@ type
|
|||||||
proc initLeadGrid*(): LeadGrid =
|
proc initLeadGrid*(): LeadGrid =
|
||||||
result = 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 =
|
proc forward*(grid: var LeadGrid, vPerp, distance: float): float =
|
||||||
## Returns bilinear-interpolated circular mean offset in degrees, or -999.0 when no data.
|
## Returns circular mean offset in degrees, or -999.0 when no data.
|
||||||
let vCont = clamp(vPerp + 8.0, 0.0, 15.999)
|
let idx = cellIndex(vPerp, distance)
|
||||||
let dCont = clamp(distance / DIST_BAND, 0.0, 6.999)
|
let vBin = clamp(int(vPerp + 8.5), 0, VPERP_BINS - 1)
|
||||||
let v0 = int(floor(vCont)); let v1 = min(v0 + 1, VPERP_BINS - 1)
|
let dBin = clamp(int(distance / DIST_BAND), 0, DIST_BINS - 1)
|
||||||
let d0 = int(floor(dCont)); let d1 = min(d0 + 1, DIST_BINS - 1)
|
|
||||||
let fv = vCont - float(v0)
|
# Accumulate weighted contributions: direct cell (w=1), cardinal neighbors (w=0.5), diagonals (w=0.25)
|
||||||
let fd = dCont - float(d0)
|
var sinSum = 0.0
|
||||||
let corners = [(v0, d0, (1-fv)*(1-fd)),
|
var cosSum = 0.0
|
||||||
(v1, d0, fv*(1-fd)),
|
var totalW = 0.0
|
||||||
(v0, d1, (1-fv)*fd),
|
for dv in -1 .. 1:
|
||||||
(v1, d1, fv*fd)]
|
for dd in -1 .. 1:
|
||||||
var sinSum, cosSum, totalW = 0.0
|
let vb = vBin + dv
|
||||||
for (vi, di, w) in corners:
|
let db = dBin + dd
|
||||||
let cell = grid.cells[vi * DIST_BINS + di]
|
if vb < 0 or vb >= VPERP_BINS or db < 0 or db >= DIST_BINS: continue
|
||||||
if cell.count == 0: continue
|
let c = grid.cells[vb * DIST_BINS + db]
|
||||||
sinSum += w * cell.sumSin / cell.count
|
if c.count == 0: continue
|
||||||
cosSum += w * cell.sumCos / cell.count
|
let w = if dv == 0 and dd == 0: 1.0
|
||||||
totalW += w
|
elif dv == 0 or dd == 0: 0.5
|
||||||
if totalW == 0.0: return -999.0
|
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))
|
result = radToDeg(arctan2(sinSum, cosSum))
|
||||||
|
|
||||||
proc learn*(grid: var LeadGrid, vPerp, distance, correctOffset: float) =
|
proc learn*(grid: var LeadGrid, vPerp, distance, correctOffset: float) =
|
||||||
let s = sin(degToRad(correctOffset))
|
let idx = cellIndex(vPerp, distance)
|
||||||
let c = cos(degToRad(correctOffset))
|
grid.cells[idx].sumSin += sin(degToRad(correctOffset))
|
||||||
let vBin = clamp(int(floor(vPerp + 8.0)), 0, VPERP_BINS - 1)
|
grid.cells[idx].sumCos += cos(degToRad(correctOffset))
|
||||||
let dBin = clamp(int(floor(distance / DIST_BAND)), 0, DIST_BINS - 1)
|
inc grid.cells[idx].count
|
||||||
let i = vBin * DIST_BINS + dBin
|
|
||||||
grid.cells[i].sumSin += s
|
|
||||||
grid.cells[i].sumCos += c
|
|
||||||
grid.cells[i].count += 1.0
|
|
||||||
|
|
||||||
proc totalCount*(grid: LeadGrid): int =
|
proc totalCount*(grid: LeadGrid): int =
|
||||||
var s = 0.0
|
for c in grid.cells:
|
||||||
for c in grid.cells: s += c.count
|
result += c.count
|
||||||
result = int(s)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user