feat(SNNBot): physics-based input encoding for lead generalization

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.
This commit is contained in:
2026-09-16 21:48:06 +02:00
parent 57cbcc5699
commit 802e0e0056
2 changed files with 29 additions and 35 deletions
+28 -34
View File
@@ -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
+1 -1
View File
@@ -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