feat(BNNBot): scaffold binary neural network bot with scan data collection and binary encoding visualization
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Executable
BIN
Binary file not shown.
@@ -0,0 +1,12 @@
|
|||||||
|
# Package
|
||||||
|
version = "0.1.0"
|
||||||
|
author = "Davide Cappellini"
|
||||||
|
description = "Binary Neural Network Bot — learns aiming with pure binary operations"
|
||||||
|
license = "MIT"
|
||||||
|
srcDir = "src"
|
||||||
|
bin = @["BNNBot"]
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
requires "nim >= 2.0.0"
|
||||||
|
requires "robocode_tankroyale_botapi >= 1.0.7"
|
||||||
|
# radar_lock is vendored in-tree (common_libs/) and wired via config.nims --path
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
--path:"../common_libs"
|
||||||
|
switch("outdir", "out")
|
||||||
|
switch("path", thisDir() & "/src")
|
||||||
|
# begin Nimble config (version 2)
|
||||||
|
when withDir(thisDir(), system.fileExists("nimble.paths")):
|
||||||
|
include "nimble.paths"
|
||||||
|
# end Nimble config
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "BNNBot",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"authors": ["Davide Cappellini"],
|
||||||
|
"description": "Binary Neural Network Bot — learns aiming with pure binary operations",
|
||||||
|
"homepage": "",
|
||||||
|
"countryCodes": ["IT"],
|
||||||
|
"gameTypes": ["classic", "1v1"],
|
||||||
|
"platform": "Nim",
|
||||||
|
"programmingLang": "Nim"
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# BNNBot — binary encoding data collector.
|
||||||
|
# Radar lock on enemy, converts each scan to a 112-bit BinaryVector, logs + prints it.
|
||||||
|
# No aiming, no firing — pure scan visualization for BNN research.
|
||||||
|
|
||||||
|
import std/[math, strformat, os]
|
||||||
|
import robocode_tankroyale_botapi
|
||||||
|
import radar_lock/radar_lock as radar_lock
|
||||||
|
import binary_encoding
|
||||||
|
|
||||||
|
const botJsonPath = currentSourcePath().parentDir / "BNNBot.json"
|
||||||
|
const scanLog = "/tmp/bnnbot_scan.log"
|
||||||
|
|
||||||
|
proc appendLog(s: string) =
|
||||||
|
try:
|
||||||
|
let f = open(scanLog, fmAppend)
|
||||||
|
f.writeLine(s)
|
||||||
|
f.close()
|
||||||
|
except:
|
||||||
|
discard
|
||||||
|
|
||||||
|
type
|
||||||
|
BNNBot = ref object of Bot
|
||||||
|
hasContact: bool
|
||||||
|
enemyBearing: float
|
||||||
|
lastEnemyX: float
|
||||||
|
lastEnemyY: float
|
||||||
|
hasLastPos: bool
|
||||||
|
velocity: float # signed velocity (from heading + speed)
|
||||||
|
heading: float # enemy heading (degrees)
|
||||||
|
speed: float # enemy speed (units/tick)
|
||||||
|
distance: float
|
||||||
|
tick: int
|
||||||
|
|
||||||
|
method onScannedBot*(bot: BNNBot, e: ScannedBotEvent) =
|
||||||
|
let bx = getX(); let by = getY()
|
||||||
|
bot.enemyBearing = directionTo(bx, by, e.x, e.y)
|
||||||
|
bot.distance = distanceTo(bx, by, e.x, e.y)
|
||||||
|
bot.heading = e.direction
|
||||||
|
bot.speed = abs(e.speed)
|
||||||
|
bot.velocity = e.speed # signed: positive = forward, negative = reverse
|
||||||
|
if bot.hasLastPos:
|
||||||
|
discard # velocity from e.speed is more accurate; position delta kept for future use
|
||||||
|
bot.lastEnemyX = e.x
|
||||||
|
bot.lastEnemyY = e.y
|
||||||
|
bot.hasLastPos = true
|
||||||
|
bot.hasContact = true
|
||||||
|
|
||||||
|
let scan = ScanData(
|
||||||
|
bearing: bot.enemyBearing,
|
||||||
|
distance: bot.distance,
|
||||||
|
velocity: bot.velocity,
|
||||||
|
heading: bot.heading,
|
||||||
|
speed: bot.speed,
|
||||||
|
)
|
||||||
|
let vec = encode(scan)
|
||||||
|
let fmt = formatBinary(vec)
|
||||||
|
let line = fmt"t={bot.tick} bear={bot.enemyBearing:.1f} dist={bot.distance:.0f} vel={bot.velocity:.2f} head={bot.heading:.1f} spd={bot.speed:.2f} pop={popcount(vec)} {fmt}"
|
||||||
|
echo line
|
||||||
|
appendLog(line)
|
||||||
|
|
||||||
|
method onRoundStarted*(bot: BNNBot, e: RoundStartedEvent) =
|
||||||
|
setAdjustGunForBodyTurn(true)
|
||||||
|
setAdjustRadarForBodyTurn(true)
|
||||||
|
setAdjustRadarForGunTurn(true)
|
||||||
|
radar_lock.init()
|
||||||
|
bot.hasContact = false
|
||||||
|
bot.hasLastPos = false
|
||||||
|
bot.tick = 0
|
||||||
|
setTargetSpeed(0.0)
|
||||||
|
setTurnRate(0.0)
|
||||||
|
|
||||||
|
method onGameStarted*(bot: BNNBot, e: GameStartedEventForBot) =
|
||||||
|
# Truncate log on game start
|
||||||
|
try:
|
||||||
|
let f = open(scanLog, fmWrite)
|
||||||
|
f.close()
|
||||||
|
except:
|
||||||
|
discard
|
||||||
|
|
||||||
|
method run*(bot: BNNBot) =
|
||||||
|
while isRunning():
|
||||||
|
inc bot.tick
|
||||||
|
setTargetSpeed(0.0)
|
||||||
|
setTurnRate(0.0)
|
||||||
|
|
||||||
|
if not bot.hasContact:
|
||||||
|
setRadarTurnRate(45.0)
|
||||||
|
go()
|
||||||
|
continue
|
||||||
|
|
||||||
|
setRadarTurnRate(radar_lock.doRadar(getRadarDirection(), bot.enemyBearing))
|
||||||
|
go()
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
var bot = BNNBot()
|
||||||
|
start(bot, botJsonPath)
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
## binary_encoding — scan data → binary vector for BNNBot.
|
||||||
|
## Encoding scheme:
|
||||||
|
## bearing/heading: population code (2 adjacent bits, triangular interpolation)
|
||||||
|
## distance/speed: thermometer (all bits below threshold active)
|
||||||
|
## velocity: signed thermometer (first half = negative, second half = positive)
|
||||||
|
|
||||||
|
import std/math
|
||||||
|
|
||||||
|
const
|
||||||
|
BEARING_BITS* = 36 # 360° / 10° per bit
|
||||||
|
DISTANCE_BITS* = 16 # 0-2000 units, ~125 per bit (thermometer)
|
||||||
|
VELOCITY_BITS* = 16 # -8 to +8, signed thermometer
|
||||||
|
HEADING_BITS* = 36 # 360° / 10° per bit
|
||||||
|
SPEED_BITS* = 8 # 0-8, 1 per bit (thermometer)
|
||||||
|
TOTAL_BITS* = BEARING_BITS + DISTANCE_BITS + VELOCITY_BITS + HEADING_BITS + SPEED_BITS # 112
|
||||||
|
|
||||||
|
type
|
||||||
|
BinaryVector* = array[TOTAL_BITS, uint8] # 0 or 1
|
||||||
|
|
||||||
|
ScanData* = object
|
||||||
|
bearing*: float # absolute bearing to enemy (degrees)
|
||||||
|
distance*: float # distance to enemy
|
||||||
|
velocity*: float # enemy velocity (negative = moving backward)
|
||||||
|
heading*: float # enemy heading (degrees)
|
||||||
|
speed*: float # enemy speed (always positive)
|
||||||
|
|
||||||
|
proc encodeBearing*(degrees: float, bits: int): seq[uint8] =
|
||||||
|
## Population code: 2 adjacent bits active via triangular interpolation.
|
||||||
|
result = newSeq[uint8](bits)
|
||||||
|
let norm = ((degrees mod 360.0) + 360.0) mod 360.0
|
||||||
|
let bandWidth = 360.0 / float(bits)
|
||||||
|
let idx = norm / bandWidth
|
||||||
|
let lo = int(idx) mod bits
|
||||||
|
let hi = (lo + 1) mod bits
|
||||||
|
result[lo] = 1
|
||||||
|
result[hi] = 1
|
||||||
|
|
||||||
|
proc encodeThermometer*(value, minVal, maxVal: float, bits: int): seq[uint8] =
|
||||||
|
## Thermometer code: all bits below threshold are 1.
|
||||||
|
result = newSeq[uint8](bits)
|
||||||
|
let clamped = clamp(value, minVal, maxVal)
|
||||||
|
let norm = (clamped - minVal) / (maxVal - minVal)
|
||||||
|
let active = int(norm * float(bits))
|
||||||
|
for i in 0..<active:
|
||||||
|
result[i] = 1
|
||||||
|
|
||||||
|
proc encodeSignedThermometer*(value, maxAbs: float, bits: int): seq[uint8] =
|
||||||
|
## Signed thermometer: first half = negative magnitude, second half = positive magnitude.
|
||||||
|
result = newSeq[uint8](bits)
|
||||||
|
let halfBits = bits div 2
|
||||||
|
let clamped = clamp(value, -maxAbs, maxAbs)
|
||||||
|
if clamped >= 0:
|
||||||
|
let norm = clamped / maxAbs
|
||||||
|
let active = int(norm * float(halfBits))
|
||||||
|
for i in 0..<active:
|
||||||
|
result[halfBits + i] = 1
|
||||||
|
else:
|
||||||
|
let norm = -clamped / maxAbs
|
||||||
|
let active = int(norm * float(halfBits))
|
||||||
|
for i in countdown(halfBits - 1, halfBits - active):
|
||||||
|
result[i] = 1
|
||||||
|
|
||||||
|
proc encode*(scan: ScanData): BinaryVector =
|
||||||
|
## Encode full scan data into a 112-bit binary vector.
|
||||||
|
var offset = 0
|
||||||
|
|
||||||
|
let bearBits = encodeBearing(scan.bearing, BEARING_BITS)
|
||||||
|
for i in 0..<BEARING_BITS:
|
||||||
|
result[offset + i] = bearBits[i]
|
||||||
|
offset += BEARING_BITS
|
||||||
|
|
||||||
|
let distBits = encodeThermometer(scan.distance, 0.0, 2000.0, DISTANCE_BITS)
|
||||||
|
for i in 0..<DISTANCE_BITS:
|
||||||
|
result[offset + i] = distBits[i]
|
||||||
|
offset += DISTANCE_BITS
|
||||||
|
|
||||||
|
let velBits = encodeSignedThermometer(scan.velocity, 8.0, VELOCITY_BITS)
|
||||||
|
for i in 0..<VELOCITY_BITS:
|
||||||
|
result[offset + i] = velBits[i]
|
||||||
|
offset += VELOCITY_BITS
|
||||||
|
|
||||||
|
let headBits = encodeBearing(scan.heading, HEADING_BITS)
|
||||||
|
for i in 0..<HEADING_BITS:
|
||||||
|
result[offset + i] = headBits[i]
|
||||||
|
offset += HEADING_BITS
|
||||||
|
|
||||||
|
let spdBits = encodeThermometer(scan.speed, 0.0, 8.0, SPEED_BITS)
|
||||||
|
for i in 0..<SPEED_BITS:
|
||||||
|
result[offset + i] = spdBits[i]
|
||||||
|
|
||||||
|
proc formatBinary*(vec: BinaryVector): string =
|
||||||
|
## Pretty-print binary vector with labeled sections.
|
||||||
|
result = "BIN ["
|
||||||
|
result &= "B:"
|
||||||
|
for i in 0..<BEARING_BITS:
|
||||||
|
result &= (if vec[i] == 1: "█" else: "░")
|
||||||
|
result &= "|"
|
||||||
|
var offset = BEARING_BITS
|
||||||
|
result &= "D:"
|
||||||
|
for i in 0..<DISTANCE_BITS:
|
||||||
|
result &= (if vec[offset + i] == 1: "█" else: "░")
|
||||||
|
result &= "|"
|
||||||
|
offset += DISTANCE_BITS
|
||||||
|
result &= "V:"
|
||||||
|
for i in 0..<VELOCITY_BITS:
|
||||||
|
result &= (if vec[offset + i] == 1: "█" else: "░")
|
||||||
|
result &= "|"
|
||||||
|
offset += VELOCITY_BITS
|
||||||
|
result &= "H:"
|
||||||
|
for i in 0..<HEADING_BITS:
|
||||||
|
result &= (if vec[offset + i] == 1: "█" else: "░")
|
||||||
|
result &= "|"
|
||||||
|
offset += HEADING_BITS
|
||||||
|
result &= "S:"
|
||||||
|
for i in 0..<SPEED_BITS:
|
||||||
|
result &= (if vec[offset + i] == 1: "█" else: "░")
|
||||||
|
result &= "]"
|
||||||
|
|
||||||
|
proc popcount*(vec: BinaryVector): int =
|
||||||
|
## Count active bits.
|
||||||
|
for b in vec:
|
||||||
|
result += int(b)
|
||||||
|
|
||||||
|
proc hammingDistance*(a, b: BinaryVector): int =
|
||||||
|
## XOR + popcount.
|
||||||
|
for i in 0..<TOTAL_BITS:
|
||||||
|
result += int(a[i] xor b[i])
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
--path:"../../common_libs"
|
||||||
Reference in New Issue
Block a user