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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user