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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user