feat(BNNBot): 10-tick sliding window of enemy data + self state — 870 bits total
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# BNNBot — binary encoding data collector.
|
# BNNBot — binary encoding data collector.
|
||||||
# Radar lock on enemy, converts each scan to a 44-bit BinaryVector, prints it.
|
# Radar lock on enemy, encodes a 10-tick sliding window (870 bits), prints it.
|
||||||
# No aiming, no firing — pure scan visualization for BNN research.
|
# No aiming, no firing — pure scan visualization for BNN research.
|
||||||
|
|
||||||
import std/[math, os, strutils]
|
import std/[math, os, strutils]
|
||||||
@@ -11,56 +11,73 @@ const botJsonPath = currentSourcePath().parentDir / "BNNBot.json"
|
|||||||
|
|
||||||
type
|
type
|
||||||
BNNBot = ref object of Bot
|
BNNBot = ref object of Bot
|
||||||
hasContact: bool
|
hasContact: bool
|
||||||
enemyBearing: float
|
enemyBearing: float
|
||||||
lastEnemyX: float
|
lastEnemyX: float
|
||||||
lastEnemyY: float
|
lastEnemyY: float
|
||||||
hasLastPos: bool
|
hasLastPos: bool
|
||||||
velocity: float # signed velocity (from heading + speed)
|
velocity: float
|
||||||
heading: float # enemy heading (degrees)
|
heading: float
|
||||||
distance: float
|
distance: float
|
||||||
tick: int
|
tick: int
|
||||||
prevVec: BinaryVector
|
prevVec: BinaryVector
|
||||||
hasPrev: bool
|
hasPrev: bool
|
||||||
|
frameBuffer: array[WINDOW_SIZE, array[FRAME_BITS, uint8]]
|
||||||
|
bufferCount: int
|
||||||
|
|
||||||
method onScannedBot*(bot: BNNBot, e: ScannedBotEvent) =
|
method onScannedBot*(bot: BNNBot, e: ScannedBotEvent) =
|
||||||
let bx = getX(); let by = getY()
|
let bx = getX(); let by = getY()
|
||||||
bot.enemyBearing = directionTo(bx, by, e.x, e.y)
|
bot.enemyBearing = directionTo(bx, by, e.x, e.y)
|
||||||
bot.distance = distanceTo(bx, by, e.x, e.y)
|
bot.distance = distanceTo(bx, by, e.x, e.y)
|
||||||
bot.heading = e.direction
|
bot.heading = e.direction
|
||||||
bot.velocity = e.speed # signed: positive = forward, negative = reverse
|
bot.velocity = e.speed
|
||||||
if bot.hasLastPos:
|
bot.lastEnemyX = e.x
|
||||||
discard # velocity from e.speed is more accurate; position delta kept for future use
|
bot.lastEnemyY = e.y
|
||||||
bot.lastEnemyX = e.x
|
bot.hasLastPos = true
|
||||||
bot.lastEnemyY = e.y
|
bot.hasContact = true
|
||||||
bot.hasLastPos = true
|
|
||||||
bot.hasContact = true
|
|
||||||
|
|
||||||
let arenaW = getArenaWidth().float
|
let arenaW = getArenaWidth().float
|
||||||
let arenaH = getArenaHeight().float
|
let arenaH = getArenaHeight().float
|
||||||
let enemyX = e.x
|
let enemyX = e.x
|
||||||
let enemyY = e.y
|
let enemyY = e.y
|
||||||
let scan = ScanData(
|
|
||||||
bearing: bot.enemyBearing,
|
let frame = EnemyScanFrame(
|
||||||
distance: bot.distance,
|
bearing: bot.enemyBearing,
|
||||||
velocity: bot.velocity,
|
distance: bot.distance,
|
||||||
heading: bot.heading,
|
velocity: bot.velocity,
|
||||||
myWallN: arenaH - by,
|
heading: bot.heading,
|
||||||
myWallS: by,
|
enemyWallN: arenaH - enemyY,
|
||||||
myWallE: arenaW - bx,
|
enemyWallS: enemyY,
|
||||||
myWallW: bx,
|
enemyWallE: arenaW - enemyX,
|
||||||
enemyWallN: arenaH - enemyY,
|
enemyWallW: enemyX,
|
||||||
enemyWallS: enemyY,
|
enemyEnergy: e.energy,
|
||||||
enemyWallE: arenaW - enemyX,
|
|
||||||
enemyWallW: enemyX,
|
|
||||||
myEnergy: getEnergy(),
|
|
||||||
enemyEnergy: e.energy,
|
|
||||||
canFire: getGunHeat() <= 0.0,
|
|
||||||
)
|
)
|
||||||
let vec = encode(scan)
|
let encoded = encodeFrame(frame)
|
||||||
let hamming = if bot.hasPrev: hammingDistance(bot.prevVec, vec) else: 0
|
|
||||||
|
# Shift buffer: 0..8 → 1..9, newest at index 0
|
||||||
|
for i in countdown(WINDOW_SIZE - 1, 1):
|
||||||
|
bot.frameBuffer[i] = bot.frameBuffer[i - 1]
|
||||||
|
bot.frameBuffer[0] = encoded
|
||||||
|
|
||||||
|
if bot.bufferCount < WINDOW_SIZE:
|
||||||
|
inc bot.bufferCount
|
||||||
|
if bot.bufferCount < WINDOW_SIZE:
|
||||||
|
return # still filling
|
||||||
|
|
||||||
|
let selfState = SelfState(
|
||||||
|
myWallN: arenaH - by,
|
||||||
|
myWallS: by,
|
||||||
|
myWallE: arenaW - bx,
|
||||||
|
myWallW: bx,
|
||||||
|
myEnergy: getEnergy(),
|
||||||
|
canFire: getGunHeat() <= 0.0,
|
||||||
|
)
|
||||||
|
let selfEncoded = encodeSelf(selfState)
|
||||||
|
let vec = encodeFullVector(bot.frameBuffer, selfEncoded)
|
||||||
|
|
||||||
|
let hamming = if bot.hasPrev: hammingDistance(bot.prevVec, vec) else: 0
|
||||||
let similarity = if bot.hasPrev: TOTAL_BITS - hamming else: 0
|
let similarity = if bot.hasPrev: TOTAL_BITS - hamming else: 0
|
||||||
let overlap = if bot.hasPrev: popcount(bitwiseAnd(bot.prevVec, vec)) else: 0
|
let overlap = if bot.hasPrev: popcount(bitwiseAnd(bot.prevVec, vec)) else: 0
|
||||||
echo align($bot.tick, 4), " ", formatBinary(vec), " ", hamming, " ", similarity, " ", overlap
|
echo align($bot.tick, 4), " ", formatBinary(vec), " ", hamming, " ", similarity, " ", overlap
|
||||||
bot.prevVec = vec
|
bot.prevVec = vec
|
||||||
bot.hasPrev = true
|
bot.hasPrev = true
|
||||||
@@ -74,6 +91,7 @@ method onRoundStarted*(bot: BNNBot, e: RoundStartedEvent) =
|
|||||||
bot.hasLastPos = false
|
bot.hasLastPos = false
|
||||||
bot.tick = 0
|
bot.tick = 0
|
||||||
bot.hasPrev = false
|
bot.hasPrev = false
|
||||||
|
bot.bufferCount = 0
|
||||||
setTargetSpeed(0.0)
|
setTargetSpeed(0.0)
|
||||||
setTurnRate(0.0)
|
setTurnRate(0.0)
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,32 @@
|
|||||||
import std/math
|
import std/math
|
||||||
|
|
||||||
const
|
const
|
||||||
TOTAL_BITS* = 123
|
FRAME_BITS* = 83
|
||||||
|
SELF_BITS* = 40
|
||||||
|
WINDOW_SIZE* = 10
|
||||||
|
TOTAL_BITS* = FRAME_BITS * WINDOW_SIZE + SELF_BITS # 870
|
||||||
|
|
||||||
type
|
type
|
||||||
BinaryVector* = array[TOTAL_BITS, uint8]
|
BinaryVector* = array[TOTAL_BITS, uint8]
|
||||||
|
|
||||||
ScanData* = object
|
EnemyScanFrame* = object
|
||||||
bearing*: float # 0-360 degrees
|
bearing*: float # → sin/cos, 8+8 bits
|
||||||
distance*: float # 0-1414 pixels (will be converted to 0-100%)
|
distance*: float # → %, 7 bits
|
||||||
velocity*: float # -8 to +8 integer steps
|
velocity*: float # → 5 bits
|
||||||
heading*: float # 0-360 degrees
|
heading*: float # → sin/cos, 8+8 bits
|
||||||
myWallN*: float # our distance to north wall
|
enemyWallN*: float # 7 bits
|
||||||
myWallS*: float # our distance to south wall
|
enemyWallS*: float # 7 bits
|
||||||
myWallE*: float # our distance to east wall
|
enemyWallE*: float # 7 bits
|
||||||
myWallW*: float # our distance to west wall
|
enemyWallW*: float # 7 bits
|
||||||
enemyWallN*: float # enemy distance to north wall
|
enemyEnergy*: float # 11 bits
|
||||||
enemyWallS*: float # enemy distance to south wall
|
|
||||||
enemyWallE*: float # enemy distance to east wall
|
SelfState* = object
|
||||||
enemyWallW*: float # enemy distance to west wall
|
myWallN*: float # 7 bits
|
||||||
myEnergy*: float # 0-150
|
myWallS*: float # 7 bits
|
||||||
enemyEnergy*: float # 0-150
|
myWallE*: float # 7 bits
|
||||||
canFire*: bool # gun heat == 0
|
myWallW*: float # 7 bits
|
||||||
|
myEnergy*: float # 11 bits
|
||||||
|
canFire*: bool # 1 bit
|
||||||
|
|
||||||
const MAX_DISTANCE = 1414.0 # diagonal of 1000x1000 arena
|
const MAX_DISTANCE = 1414.0 # diagonal of 1000x1000 arena
|
||||||
|
|
||||||
@@ -34,73 +39,88 @@ proc toBits(value: int, bits: int): seq[uint8] =
|
|||||||
for i in 0..<bits:
|
for i in 0..<bits:
|
||||||
result[bits - 1 - i] = uint8((gray shr i) and 1)
|
result[bits - 1 - i] = uint8((gray shr i) and 1)
|
||||||
|
|
||||||
proc encode*(scan: ScanData): BinaryVector =
|
proc encodeFrame*(frame: EnemyScanFrame): array[FRAME_BITS, uint8] =
|
||||||
var offset = 0
|
var offset = 0
|
||||||
|
|
||||||
# 1. bearing_sin: sin(bearing), -1..+1 → 0..199 → 8 bits
|
# bearing sin: 8 bits
|
||||||
let bSin = int((sin(degToRad(scan.bearing)) + 1.0) / 2.0 * 199.0)
|
let bSin = int((sin(degToRad(frame.bearing)) + 1.0) / 2.0 * 199.0)
|
||||||
let bSinBits = toBits(clamp(bSin, 0, 199), 8)
|
let bSinBits = toBits(clamp(bSin, 0, 199), 8)
|
||||||
for i in 0..<8: result[offset + i] = bSinBits[i]
|
for i in 0..<8: result[offset + i] = bSinBits[i]
|
||||||
offset += 8
|
offset += 8
|
||||||
|
|
||||||
# 2. bearing_cos: cos(bearing), -1..+1 → 0..199 → 8 bits
|
# bearing cos: 8 bits
|
||||||
let bCos = int((cos(degToRad(scan.bearing)) + 1.0) / 2.0 * 199.0)
|
let bCos = int((cos(degToRad(frame.bearing)) + 1.0) / 2.0 * 199.0)
|
||||||
let bCosBits = toBits(clamp(bCos, 0, 199), 8)
|
let bCosBits = toBits(clamp(bCos, 0, 199), 8)
|
||||||
for i in 0..<8: result[offset + i] = bCosBits[i]
|
for i in 0..<8: result[offset + i] = bCosBits[i]
|
||||||
offset += 8
|
offset += 8
|
||||||
|
|
||||||
# 3. distance %: 0-100% of max → 0..99 → 7 bits
|
# distance %: 7 bits
|
||||||
let distPct = int(clamp(scan.distance / MAX_DISTANCE * 99.0, 0.0, 99.0))
|
let distPct = int(clamp(frame.distance / MAX_DISTANCE * 99.0, 0.0, 99.0))
|
||||||
let distBits = toBits(distPct, 7)
|
let distBits = toBits(distPct, 7)
|
||||||
for i in 0..<7: result[offset + i] = distBits[i]
|
for i in 0..<7: result[offset + i] = distBits[i]
|
||||||
offset += 7
|
offset += 7
|
||||||
|
|
||||||
# 4. velocity: -8..+8 → 0..16 → 5 bits
|
# velocity: 5 bits
|
||||||
let vel = int(clamp(scan.velocity + 8.0, 0.0, 16.0))
|
let vel = int(clamp(frame.velocity + 8.0, 0.0, 16.0))
|
||||||
let velBits = toBits(vel, 5)
|
let velBits = toBits(vel, 5)
|
||||||
for i in 0..<5: result[offset + i] = velBits[i]
|
for i in 0..<5: result[offset + i] = velBits[i]
|
||||||
offset += 5
|
offset += 5
|
||||||
|
|
||||||
# 5. heading_sin: sin(heading), -1..+1 → 0..199 → 8 bits
|
# heading sin: 8 bits
|
||||||
let hSin = int((sin(degToRad(scan.heading)) + 1.0) / 2.0 * 199.0)
|
let hSin = int((sin(degToRad(frame.heading)) + 1.0) / 2.0 * 199.0)
|
||||||
let hSinBits = toBits(clamp(hSin, 0, 199), 8)
|
let hSinBits = toBits(clamp(hSin, 0, 199), 8)
|
||||||
for i in 0..<8: result[offset + i] = hSinBits[i]
|
for i in 0..<8: result[offset + i] = hSinBits[i]
|
||||||
offset += 8
|
offset += 8
|
||||||
|
|
||||||
# 6. heading_cos: cos(heading), -1..+1 → 0..199 → 8 bits
|
# heading cos: 8 bits
|
||||||
let hCos = int((cos(degToRad(scan.heading)) + 1.0) / 2.0 * 199.0)
|
let hCos = int((cos(degToRad(frame.heading)) + 1.0) / 2.0 * 199.0)
|
||||||
let hCosBits = toBits(clamp(hCos, 0, 199), 8)
|
let hCosBits = toBits(clamp(hCos, 0, 199), 8)
|
||||||
for i in 0..<8: result[offset + i] = hCosBits[i]
|
for i in 0..<8: result[offset + i] = hCosBits[i]
|
||||||
offset += 8
|
offset += 8
|
||||||
|
|
||||||
# 7. My wall distances (7 bits each, 0-100% of arena dimension)
|
# enemy wall distances: 4×7 bits
|
||||||
for wall in [scan.myWallN, scan.myWallS, scan.myWallE, scan.myWallW]:
|
for wall in [frame.enemyWallN, frame.enemyWallS, frame.enemyWallE, frame.enemyWallW]:
|
||||||
let pct = int(clamp(wall / 1000.0 * 99.0, 0.0, 99.0))
|
let pct = int(clamp(wall / 1000.0 * 99.0, 0.0, 99.0))
|
||||||
let bits = toBits(pct, 7)
|
let bits = toBits(pct, 7)
|
||||||
for i in 0..<7: result[offset + i] = bits[i]
|
for i in 0..<7: result[offset + i] = bits[i]
|
||||||
offset += 7
|
offset += 7
|
||||||
|
|
||||||
# 8. Enemy wall distances (7 bits each)
|
# enemy energy: 11 bits
|
||||||
for wall in [scan.enemyWallN, scan.enemyWallS, scan.enemyWallE, scan.enemyWallW]:
|
let enE = int(clamp(frame.enemyEnergy * 10.0, 0.0, 1500.0))
|
||||||
|
let enEBits = toBits(enE, 11)
|
||||||
|
for i in 0..<11: result[offset + i] = enEBits[i]
|
||||||
|
# offset += 11 # last field, no need
|
||||||
|
|
||||||
|
proc encodeSelf*(self: SelfState): array[SELF_BITS, uint8] =
|
||||||
|
var offset = 0
|
||||||
|
|
||||||
|
# my wall distances: 4×7 bits
|
||||||
|
for wall in [self.myWallN, self.myWallS, self.myWallE, self.myWallW]:
|
||||||
let pct = int(clamp(wall / 1000.0 * 99.0, 0.0, 99.0))
|
let pct = int(clamp(wall / 1000.0 * 99.0, 0.0, 99.0))
|
||||||
let bits = toBits(pct, 7)
|
let bits = toBits(pct, 7)
|
||||||
for i in 0..<7: result[offset + i] = bits[i]
|
for i in 0..<7: result[offset + i] = bits[i]
|
||||||
offset += 7
|
offset += 7
|
||||||
|
|
||||||
# 9. My energy (11 bits, 0-150 → 0-1500 with 0.1 HP precision)
|
# my energy: 11 bits
|
||||||
let myE = int(clamp(scan.myEnergy * 10.0, 0.0, 1500.0))
|
let myE = int(clamp(self.myEnergy * 10.0, 0.0, 1500.0))
|
||||||
let myEBits = toBits(myE, 11)
|
let myEBits = toBits(myE, 11)
|
||||||
for i in 0..<11: result[offset + i] = myEBits[i]
|
for i in 0..<11: result[offset + i] = myEBits[i]
|
||||||
offset += 11
|
offset += 11
|
||||||
|
|
||||||
# 10. Enemy energy (11 bits, 0-150 → 0-1500 with 0.1 HP precision)
|
# canFire: 1 bit
|
||||||
let enE = int(clamp(scan.enemyEnergy * 10.0, 0.0, 1500.0))
|
result[offset] = if self.canFire: 1'u8 else: 0'u8
|
||||||
let enEBits = toBits(enE, 11)
|
|
||||||
for i in 0..<11: result[offset + i] = enEBits[i]
|
|
||||||
offset += 11
|
|
||||||
|
|
||||||
# 11. Can fire (1 bit)
|
proc encodeFullVector*(window: array[WINDOW_SIZE, array[FRAME_BITS, uint8]],
|
||||||
result[offset] = if scan.canFire: 1'u8 else: 0'u8
|
self: array[SELF_BITS, uint8]): BinaryVector =
|
||||||
|
var offset = 0
|
||||||
|
# LIFO: index 0 = newest frame
|
||||||
|
for i in 0..<WINDOW_SIZE:
|
||||||
|
for j in 0..<FRAME_BITS:
|
||||||
|
result[offset] = window[i][j]
|
||||||
|
inc offset
|
||||||
|
for j in 0..<SELF_BITS:
|
||||||
|
result[offset] = self[j]
|
||||||
|
inc offset
|
||||||
|
|
||||||
proc formatBinary*(vec: BinaryVector): string =
|
proc formatBinary*(vec: BinaryVector): string =
|
||||||
for b in vec:
|
for b in vec:
|
||||||
|
|||||||
Reference in New Issue
Block a user