feat(SNNBot): encode perpendicular velocity for unambiguous lead

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 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 23:15:34 +02:00
parent 929fe5a696
commit 4bc7baee04
+34 -25
View File
@@ -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