feat(ModularBot): pluggable bot with 4 guns, phantom meteor movement, radar harness

- Gun harness: virtual bullet tracker, rolling fitness, auto-selector
- Guns: head-on, linear (extrapolation), circular (integrated formula), tsetlin machine (learning)
- Movement: phantom meteor gravity engine (danger histograms, phantom bullets, fire detection)
- Radar: harness + radar_lock adapter
- Color-coded modules: turret/bullet color per gun, body per movement, scan per radar
- Beats Target, SpinBot, Crazy, TrackFire in 10-round battles
This commit is contained in:
2026-09-20 00:37:10 +02:00
parent c9191b7afb
commit 254c7dc997
39 changed files with 2630 additions and 30 deletions
+76 -4
View File
@@ -1,13 +1,16 @@
# BNNBot — Hebbian weight matrix with virtual bullet learning.
# 870-bit input → 7-bit aim angle output via forward pass.
# Learns from virtual bullets (no real firing) via three-factor Hebbian rule.
# CSV data collection: BNNBOT_CSV=1 writes per-round CSV to data/battle_{round}.csv
import std/[math, os, strutils, random]
import std/[math, os, strformat, strutils, random]
import robocode_tankroyale_botapi
import radar_lock/radar_lock as radar_lock
import binary_encoding
import hebbian
let csvEnabled = getEnv("BNNBOT_CSV", "0") == "1"
const botJsonPath = currentSourcePath().parentDir / "BNNBot.json"
const
@@ -17,7 +20,18 @@ const
EPSILON_MIN = 0.05
EPSILON_DECAY = 0.9995
# CSV: 9 raw decimal fields per frame (matches analysis/backtest.py column names)
# bearing_sin(0-199), bearing_cos(0-199), distance(0-99), velocity(0-15),
# heading_sin(0-199), heading_cos(0-199), enemy_x(px), enemy_y(px), enemy_energy(float)
type
FrameRaw = object
bSin, bCos: int # 0-199
dist: int # 0-99
vel: int # 0-15
hSin, hCos: int # 0-199
ex, ey: float # absolute pixel coords
energy: float
BNNBot = ref object of Bot
hasContact: bool
enemyBearing: float
@@ -31,13 +45,52 @@ type
prevVec: BinaryVector
hasPrev: bool
frameBuffer: array[WINDOW_SIZE, array[FRAME_BITS, uint8]]
frameRawBuf: array[WINDOW_SIZE, FrameRaw] # decimal mirror of frameBuffer
bufferCount: int
net: HebbianNet
bullets: array[BULLET_SLOTS, VirtualBullet]
bulletHead: int # ring-buffer write index
bulletHead: int
virtualHits: int
virtualMiss: int
epsilon: float
# CSV state
csvFile: File
csvOpen: bool
roundNum: int
# ── CSV helpers ──────────────────────────────────────────────────────────────
proc csvPath(roundNum: int): string =
getAppDir() / "data" / fmt"battle_{roundNum}.csv"
proc buildHeader(): string =
result = "tick"
for fr in 0..<WINDOW_SIZE:
for fn in ["bearing_sin", "bearing_cos", "distance", "velocity",
"heading_sin", "heading_cos", "enemy_x", "enemy_y", "enemy_energy"]:
result &= fmt",f{fr}_{fn}"
proc openCsv(bot: BNNBot) =
if not csvEnabled: return
createDir(csvPath(bot.roundNum).parentDir)
bot.csvFile = open(csvPath(bot.roundNum), fmWrite)
bot.csvOpen = true
bot.csvFile.writeLine(buildHeader())
proc closeCsv(bot: BNNBot) =
if bot.csvOpen:
bot.csvFile.close()
bot.csvOpen = false
proc writeRow(bot: BNNBot) =
if not bot.csvOpen: return
var line = $bot.tick
for i in 0..<WINDOW_SIZE:
let f = bot.frameRawBuf[i]
line &= fmt",{f.bSin},{f.bCos},{f.dist},{f.vel},{f.hSin},{f.hCos},{f.ex:.2f},{f.ey:.2f},{f.energy:.2f}"
bot.csvFile.writeLine(line)
# ── Bot methods ──────────────────────────────────────────────────────────────
method onScannedBot*(bot: BNNBot, e: ScannedBotEvent) =
let bx = getX(); let by = getY()
@@ -66,10 +119,23 @@ method onScannedBot*(bot: BNNBot, e: ScannedBotEvent) =
)
let encoded = encodeFrame(frame)
# Shift window — index 0 = newest
for i in countdown(WINDOW_SIZE - 1, 1):
bot.frameBuffer[i] = bot.frameBuffer[i - 1]
bot.frameRawBuf[i] = bot.frameRawBuf[i - 1]
bot.frameBuffer[0] = encoded
# Build raw decimal record for this frame
let bSin = int((sin(degToRad(bot.enemyBearing)) + 1.0) / 2.0 * 199.0)
let bCos = int((cos(degToRad(bot.enemyBearing)) + 1.0) / 2.0 * 199.0)
let dist = int(clamp(bot.distance / 1414.0 * 99.0, 0.0, 99.0))
let vel = int(clamp(bot.velocity + 8.0, 0.0, 16.0))
let hSin = int((sin(degToRad(bot.heading)) + 1.0) / 2.0 * 199.0)
let hCos = int((cos(degToRad(bot.heading)) + 1.0) / 2.0 * 199.0)
bot.frameRawBuf[0] = FrameRaw(bSin: bSin, bCos: bCos, dist: dist, vel: vel,
hSin: hSin, hCos: hCos,
ex: e.x, ey: e.y, energy: e.energy)
if bot.bufferCount < WINDOW_SIZE:
inc bot.bufferCount
if bot.bufferCount < WINDOW_SIZE:
@@ -99,11 +165,9 @@ method onScannedBot*(bot: BNNBot, e: ScannedBotEvent) =
let missDistance = hypot(bulletX - bot.lastEnemyX, bulletY - bot.lastEnemyY)
# Shaped reward: +1.0 for perfect hit, decays toward -1.0 as miss distance grows
# Using exponential decay: reward = 2.0 * exp(-missDistance / 36.0) - 1.0
let reward = 2.0 * exp(-missDistance / 36.0) - 1.0
bot.net.learn(b.trace, reward)
# Track hit/miss for display: hit if within 36px, miss otherwise
if missDistance < 36.0:
inc bot.virtualHits
else:
@@ -134,6 +198,9 @@ method onScannedBot*(bot: BNNBot, e: ScannedBotEvent) =
active: true,
)
# ── CSV row ───────────────────────────────────────────────────────────
bot.writeRow()
# ── stats & echo ─────────────────────────────────────────────────────
let hamming = if bot.hasPrev: hammingDistance(bot.prevVec, vec) else: 0
let similarity = if bot.hasPrev: TOTAL_BITS - hamming else: 0
@@ -159,8 +226,13 @@ method onRoundStarted*(bot: BNNBot, e: RoundStartedEvent) =
bot.tick = 0
bot.hasPrev = false
bot.bufferCount = 0
inc bot.roundNum
bot.openCsv()
# net and epsilon persist across rounds (learning carries over)
method onRoundEnded*(bot: BNNBot, e: RoundEndedEventForBot) =
bot.closeCsv()
method onGameStarted*(bot: BNNBot, e: GameStartedEventForBot) =
discard