From 4bc7baee04c2a2b619e17c3c1d60406d6306c8f7 Mon Sep 17 00:00:00 2001 From: Davide Cappellini Date: Wed, 16 Sep 2026 23:15:34 +0200 Subject: [PATCH] feat(SNNBot): encode perpendicular velocity for unambiguous lead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit relVelDir ±5° shared 80% of bits despite needing opposite lead. Now encodes v_perp = speed×sin(relVelDir) directly with signed thermometer coding — positive and negative crossing velocities have zero bit overlap. Also adds v_parallel for approach/recede. Co-Authored-By: Claude Sonnet 4.6 --- SNNBot_garage/src/SNNBot.nim | 59 +++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/SNNBot_garage/src/SNNBot.nim b/SNNBot_garage/src/SNNBot.nim index 6c354dc..6d7deb9 100644 --- a/SNNBot_garage/src/SNNBot.nim +++ b/SNNBot_garage/src/SNNBot.nim @@ -321,40 +321,46 @@ method onBulletHit*(bot: SNNBot, e: BulletHitBotEvent) = # ── Reservoir helpers ───────────────────────────────────────────────────────── -proc toBinaryInput(bearing: float, velDir: float, velSpeed: float, distance: float, hasVel: bool): BitVec80 = +proc toBinaryInput(relVelDir: 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-43: speed thermometer (8 bits): speedInt bits set from bit 36 upward, 0-8 active. - ## Adjacent speeds differ by 1 bit (Hamming-1), giving smooth generalization. - ## Bits 44-51: distance thermometer, 8 bands of ~125px in [0,1000]. - ## Bits 52-79: unused. - ## ponytail: physics-based; revert to absolute bearing if convergence regresses + ## Bits 0-15: v_perp signed thermometer — velSpeed*sin(relVelDir) in [-8,+8]. + ## Positive (target moves right): bits 0-7; negative (left): bits 8-15. + ## +3 sets bits 0,1,2; -3 sets bits 8,9,10. Zero overlap between signs. + ## Bits 16-31: v_parallel signed thermometer — velSpeed*cos(relVelDir) in [-8,+8]. + ## Positive = approaching (less lead needed), negative = receding. + ## Bits 32-39: distance thermometer, 8 bands of ~125px in [0,1000]. + ## Bits 40-47: total speed thermometer, 8 bands of 1 unit/tick. + ## Bits 48-79: unused. result = [0'u64, 0'u64] - if hasVel: - # 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 = (vCenter + offset + 36) mod 36 # bits 0-35 - result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64)) + template setThermo(baseIdx: int, value: float) = + ## Signed thermometer: positive → bits baseIdx..baseIdx+7, negative → bits baseIdx+8..baseIdx+15. + let mag = min(int(abs(value)), 8) + if value >= 0.0: + for b in 0 ..< mag: + let idx = baseIdx + b + result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64)) + else: + for b in 0 ..< mag: + let idx = baseIdx + 8 + b + result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64)) - # Speed thermometer: bits 36-43; set bits 36..(36+speedInt-1) - # speedInt=0 → no bits; speedInt=8 → all 8 bits set + if hasVel: + let vPerp = velSpeed * sin(degToRad(relVelDir)) # lateral speed, range [-8,+8] + let vPar = velSpeed * cos(degToRad(relVelDir)) # approach speed, range [-8,+8] + setThermo(0, vPerp) + setThermo(16, vPar) + + # Total speed thermometer: bits 40-47 let speedInt = clamp(int(velSpeed + 0.5), 0, 8) for b in 0 ..< speedInt: - let idx = 36 + b + let idx = 40 + b result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64)) - # Distance thermometer: bits 44-51, 8 bands of ~125px over [0,1000] - # All bits up to the current band set (inclusive) + # Distance thermometer: bits 32-39, 8 bands of ~125px over [0,1000] let band = clamp(int(distance / 125.0), 0, 7) for b in 0 .. band: - let idx = 44 + b + let idx = 32 + b result[idx div 64] = result[idx div 64] or (1'u64 shl (idx mod 64)) # ── Main loop ───────────────────────────────────────────────────────────────── @@ -381,7 +387,10 @@ 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.enemyDist, bot.hasLastPos) + var relVelDir = bot.velDirDeg - absBearing + while relVelDir >= 180.0: relVelDir -= 360.0 + while relVelDir < -180.0: relVelDir += 360.0 + let binInput = toBinaryInput(relVelDir, 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