From 22aefbddb635e54d4cfa0b00f4bee1ccc2d97bd0 Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Thu, 17 Sep 2026 23:49:59 +0200 Subject: [PATCH] =?UTF-8?q?feat(BNNBot):=20Hebbian=20weight=20matrix=20wit?= =?UTF-8?q?h=20virtual=20bullet=20learning=20=E2=80=94=20870=E2=86=927=20b?= =?UTF-8?q?it=20forward=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- BNNBot_garage/src/BNNBot.nim | 99 ++++++++++++++++++++++----- BNNBot_garage/src/binary_encoding.nim | 27 ++++++++ BNNBot_garage/src/hebbian.nim | 51 ++++++++++++++ 3 files changed, 159 insertions(+), 18 deletions(-) create mode 100644 BNNBot_garage/src/hebbian.nim diff --git a/BNNBot_garage/src/BNNBot.nim b/BNNBot_garage/src/BNNBot.nim index 2c21e3f..6c82d99 100644 --- a/BNNBot_garage/src/BNNBot.nim +++ b/BNNBot_garage/src/BNNBot.nim @@ -1,14 +1,22 @@ -# BNNBot — binary encoding data collector. -# Radar lock on enemy, encodes a 10-tick sliding window (870 bits), prints it. -# No aiming, no firing — pure scan visualization for BNN research. +# 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. -import std/[math, os, strutils] +import std/[math, os, strutils, random] import robocode_tankroyale_botapi import radar_lock/radar_lock as radar_lock import binary_encoding +import hebbian const botJsonPath = currentSourcePath().parentDir / "BNNBot.json" +const + BULLET_SLOTS = 50 + BULLET_SPEED = 14.0 # power 2 + EPSILON_START = 0.2 + EPSILON_MIN = 0.05 + EPSILON_DECAY = 0.9995 + type BNNBot = ref object of Bot hasContact: bool @@ -24,6 +32,12 @@ type hasPrev: bool frameBuffer: array[WINDOW_SIZE, array[FRAME_BITS, uint8]] bufferCount: int + net: HebbianNet + bullets: array[BULLET_SLOTS, VirtualBullet] + bulletHead: int # ring-buffer write index + virtualHits: int + virtualMiss: int + epsilon: float method onScannedBot*(bot: BNNBot, e: ScannedBotEvent) = let bx = getX(); let by = getY() @@ -36,25 +50,22 @@ method onScannedBot*(bot: BNNBot, e: ScannedBotEvent) = bot.hasLastPos = true bot.hasContact = true - let arenaW = getArenaWidth().float - let arenaH = getArenaHeight().float - let enemyX = e.x - let enemyY = e.y + let arenaW = getArenaWidth().float + let arenaH = getArenaHeight().float let frame = EnemyScanFrame( bearing: bot.enemyBearing, distance: bot.distance, velocity: bot.velocity, heading: bot.heading, - enemyWallN: arenaH - enemyY, - enemyWallS: enemyY, - enemyWallE: arenaW - enemyX, - enemyWallW: enemyX, + enemyWallN: arenaH - e.y, + enemyWallS: e.y, + enemyWallE: arenaW - e.x, + enemyWallW: e.x, enemyEnergy: e.energy, ) let encoded = encodeFrame(frame) - # 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 @@ -62,7 +73,7 @@ method onScannedBot*(bot: BNNBot, e: ScannedBotEvent) = if bot.bufferCount < WINDOW_SIZE: inc bot.bufferCount if bot.bufferCount < WINDOW_SIZE: - return # still filling + return let selfState = SelfState( myWallN: arenaH - by, @@ -75,10 +86,59 @@ method onScannedBot*(bot: BNNBot, e: ScannedBotEvent) = let selfEncoded = encodeSelf(selfState) let vec = encodeFullVector(bot.frameBuffer, selfEncoded) + # ── age & settle existing virtual bullets ──────────────────────────── + for idx in 0..= b.fireDist or b.trace.age >= TRACE_MAX_AGE: + let bulletX = b.fireX + sin(degToRad(b.aimAngleDeg)) * bulletDist + let bulletY = b.fireY + cos(degToRad(b.aimAngleDeg)) * bulletDist + let enemyDist = hypot(bulletX - bot.lastEnemyX, bulletY - bot.lastEnemyY) + if enemyDist < 36.0: + bot.net.learn(b.trace, HIT_REWARD) + inc bot.virtualHits + else: + bot.net.learn(b.trace, MISS_PENALTY) + inc bot.virtualMiss + b.active = false + + # ── forward pass → aim angle ───────────────────────────────────────── + var outVec = bot.net.forward(vec) + + # epsilon-greedy exploration + if rand(1.0) < bot.epsilon: + for j in 0.. 0: + binary = binary xor mask + mask = mask shr 1 + result = AIM_MIN + (float(binary) / 127.0) * (AIM_MAX - AIM_MIN) diff --git a/BNNBot_garage/src/hebbian.nim b/BNNBot_garage/src/hebbian.nim new file mode 100644 index 0000000..e6d4d01 --- /dev/null +++ b/BNNBot_garage/src/hebbian.nim @@ -0,0 +1,51 @@ +import binary_encoding +import std/math + +const + N_IN* = TOTAL_BITS # 870 + N_OUT* = OUTPUT_BITS # 7 + + TRACE_MAX_AGE* = 40 + LEARNING_RATE* = 0.1 + MISS_PENALTY* = -0.03 + HIT_REWARD* = 0.1 + W_CLAMP* = 5.0 + +type + HebbianNet* = object + W*: array[N_IN * N_OUT, float] + + EligibilityTrace* = object + input*: BinaryVector + output*: OutputVector + age*: int + alive*: bool + + VirtualBullet* = object + trace*: EligibilityTrace + fireX*: float + fireY*: float + aimAngleDeg*: float + fireDist*: float + bulletSpeed*: float + active*: bool + +proc initHebbianNet*(): HebbianNet = + discard # zero-init by default + +proc forward*(net: HebbianNet, input: BinaryVector): OutputVector = + for j in 0.. 0.0: 1'u8 else: 0'u8 + +proc learn*(net: var HebbianNet, trace: EligibilityTrace, reward: float) = + let decayedReward = reward * pow(0.95, float(trace.age)) + for i in 0..