From 802e0e0056aedf265734b5f4d8375be322e12d30 Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Wed, 16 Sep 2026 21:48:06 +0200 Subject: [PATCH] feat(SNNBot): physics-based input encoding for lead generalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace (bearing, velDir, speed) with (relVelDir, speed, distance). Raw bearing is irrelevant to lead offset — the correction depends on how the target crosses the line of fire, not where it is. This lets exemplars from one position generalize to all positions with similar geometry. --- SNNBot_garage/src/SNNBot.nim | 62 +++++++++++++++------------------ SNNBot_garage/src/reservoir.nim | 2 +- 2 files changed, 29 insertions(+), 35 deletions(-) diff --git a/SNNBot_garage/src/SNNBot.nim b/SNNBot_garage/src/SNNBot.nim index d2010f7..7490b64 100644 --- a/SNNBot_garage/src/SNNBot.nim +++ b/SNNBot_garage/src/SNNBot.nim @@ -1,6 +1,6 @@ # SNNBot — SNN core + aiming loop prototype (issue #152). # 80-input population-coded → 12 LIF hidden → polar-coded (sin/cos) output decoded via atan2. -# Inputs: bearing [0-35], velocity direction [36-71], speed [72-79]. +# Inputs: relVelDir [0-35], speed [36-38], distance thermometer [39-46]. Raw bearing removed (irrelevant to lead). # No movement, no firing. SuperSpike three-factor rule on all weights (issue #158). # Binary reservoir alternative toggled via USE_RESERVOIR const (issue #159). # @@ -333,45 +333,39 @@ method onBulletHit*(bot: SNNBot, e: BulletHitBotEvent) = # ── Reservoir helpers ───────────────────────────────────────────────────────── -proc toBinaryInput(bearing: float, velDir: float, velSpeed: float, hasVel: bool): BitVec80 = - ## Binary-native encoding using relative velocity direction for cross-round generalization. - ## Bits 0-35: bearing (absolute, for range estimate) - ## Bits 36-71: relative velocity direction = (velDir - bearing) mod 360 - ## This encodes lateral vs. radial motion regardless of which wall Walls is on. - ## Bits 72-79: speed bands - ## ponytail: relative-vel encoding; revert to absolute if aimer convergence regresses +proc toBinaryInput(bearing: float, velDir: float, velSpeed: float, distance: float, hasVel: bool): BitVec80 = + ## Physics-based encoding for lead generalization (issue #152). + ## Raw bearing removed — irrelevant to lead offset. + ## Bits 0-35: relVelDir = (velDir - bearing) normalized to [-180,180), 36 bins × 10°, 5 active (center ± 2 neighbors). + ## Bits 36-38: speed level (3 bits, center ± 1 neighbor). + ## Bits 39-46: distance thermometer, 8 bands of ~125px in [0,1000]. + ## Bits 47-79: unused (sparse is fine for popcount similarity). + ## ponytail: physics-based; revert to absolute bearing if convergence regresses result = [0'u64, 0'u64] - # Bearing: bits 0-35 (36 bits, 10° bands) - # Activate center band + 2 neighbors on each side = 5 bits - let bNorm = (bearing + 180.0) / 10.0 # 0..36 - let bCenter = int(bNorm) mod 36 - for offset in -2 .. 2: - let idx = (bCenter + offset + 36) mod 36 - let word = idx div 64 - let bit = idx mod 64 - result[word] = result[word] or (1'u64 shl bit) - if hasVel: - # Relative velocity direction: (velDir - bearing + 360) mod 360 - # Encodes lateral/radial motion direction independent of which wall Walls is on - let relVel = ((velDir - bearing) + 360.0) mod 360.0 - let vNorm = relVel / 10.0 + # Relative velocity direction: how the target crosses our line of fire + var relVelDir = (velDir - bearing) + while relVelDir >= 180.0: relVelDir -= 360.0 + while relVelDir < -180.0: relVelDir += 360.0 + let vNorm = (relVelDir + 180.0) / 10.0 # 0..36 let vCenter = int(vNorm) mod 36 for offset in -2 .. 2: - let idx = 36 + (vCenter + offset + 36) mod 36 - let word = idx div 64 - let bit = idx mod 64 - result[word] = result[word] or (1'u64 shl bit) + let idx = (vCenter + offset + 36) mod 36 # bits 0-35 + result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64)) - # Speed: bits 72-79 (8 bits, 1 unit/tick bands) - # Activate center + 1 neighbor on each side = 3 bits - let sCenter = clamp(int(velSpeed), 0, 7) + # Speed: bits 36-38, center ± 1 neighbor clamped to [0,2] + let sCenter = clamp(int(velSpeed), 0, 2) for offset in -1 .. 1: - let idx = 72 + clamp(sCenter + offset, 0, 7) - let word = idx div 64 - let bit = idx mod 64 - result[word] = result[word] or (1'u64 shl bit) + let idx = 36 + clamp(sCenter + offset, 0, 2) + result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64)) + + # Distance thermometer: bits 39-46, 8 bands of ~125px over [0,1000] + # All bits up to the current band set (inclusive) + let band = clamp(int(distance / 125.0), 0, 7) + for b in 0 .. band: + let idx = 39 + b + result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64)) # ── Main loop ───────────────────────────────────────────────────────────────── @@ -396,7 +390,7 @@ method run*(bot: SNNBot) = let absBearing = bot.enemyBearing bot.lastRelBearing = normalizeRelativeAngle(absBearing - gunDir) when USE_RESERVOIR: - let binInput = toBinaryInput(absBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos) + let binInput = toBinaryInput(absBearing, bot.velDirDeg, bot.velSpeed, bot.enemyDist, bot.hasLastPos) let aimOffset = bot.res.forward(binInput) # lead correction offset or -999.0 sentinel bot.lastBinInput = binInput bot.lastDecideGunDir = gunDir diff --git a/SNNBot_garage/src/reservoir.nim b/SNNBot_garage/src/reservoir.nim index 926439e..eb7c305 100644 --- a/SNNBot_garage/src/reservoir.nim +++ b/SNNBot_garage/src/reservoir.nim @@ -5,7 +5,7 @@ import std/[bitops, math] const INPUT_BITS* = 80 WORDS_IN* = 2 # 80 bits → 2 × uint64 (128 bits, only 80 used) - MAX_K* = 128 + MAX_K* = 64 MIN_SIM* = 3 type