fix(SNNBot): freeze DECIDE-time snapshot for EVALUATE — eliminates training signal noise

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 08:36:09 +02:00
parent 26438395a8
commit 2d7413666c
+21 -11
View File
@@ -247,6 +247,11 @@ type
lastDecideGunDir: float # gun heading captured at DECIDE time for EVALUATE lastDecideGunDir: float # gun heading captured at DECIDE time for EVALUATE
lastVPerp: float # DECIDE-time vPerp, reused in EVALUATE lastVPerp: float # DECIDE-time vPerp, reused in EVALUATE
lastAbsBearing: float # absolute bearing to enemy captured at DECIDE time lastAbsBearing: float # absolute bearing to enemy captured at DECIDE time
decideEnemyX: float # enemy X frozen at DECIDE time
decideEnemyY: float # enemy Y frozen at DECIDE time
decideVelDirDeg: float # enemy vel direction frozen at DECIDE time
decideVelSpeed: float # enemy speed frozen at DECIDE time
decideDist: float # distance to enemy frozen at DECIDE time
roundTick: int # ticks elapsed in current round (reset each round) roundTick: int # ticks elapsed in current round (reset each round)
bulletsFired: int # count of bullets fired this round bulletsFired: int # count of bullets fired this round
bulletsHit: int # count of bullets that hit this round bulletsHit: int # count of bullets that hit this round
@@ -379,6 +384,11 @@ method run*(bot: SNNBot) =
bot.lastVPerp = vPerp bot.lastVPerp = vPerp
bot.lastDecideGunDir = gunDir bot.lastDecideGunDir = gunDir
bot.lastAbsBearing = absBearing bot.lastAbsBearing = absBearing
bot.decideEnemyX = bot.lastEnemyX
bot.decideEnemyY = bot.lastEnemyY
bot.decideVelDirDeg = bot.velDirDeg
bot.decideVelSpeed = bot.velSpeed
bot.decideDist = bot.enemyDist
let rate = if bot.bulletsFired > 0: float(bot.bulletsHit) / float(bot.bulletsFired) * 100.0 else: 0.0 let rate = if bot.bulletsFired > 0: float(bot.bulletsHit) / float(bot.bulletsFired) * 100.0 else: 0.0
bot.gridHasData = gridOffset > -999.0 bot.gridHasData = gridOffset > -999.0
if gridOffset <= -999.0: if gridOffset <= -999.0:
@@ -437,33 +447,33 @@ method run*(bot: SNNBot) =
of EVALUATE: of EVALUATE:
# Predictive error signal: extrapolate enemy position at bullet impact time. # Predictive error signal: extrapolate enemy position at bullet impact time.
if bot.hasLastPos: if bot.hasLastPos:
# correctOffset: correct lead angle at BULLET_SPEED # correctOffset: correct lead angle at BULLET_SPEED, using DECIDE-time snapshot
let travelTime = bot.enemyDist / BULLET_SPEED let travelTime = bot.decideDist / BULLET_SPEED
let velRad = degToRad(bot.velDirDeg) let velRad = degToRad(bot.decideVelDirDeg)
let futureX = bot.lastEnemyX + cos(velRad) * bot.velSpeed * travelTime let futureX = bot.decideEnemyX + cos(velRad) * bot.decideVelSpeed * travelTime
let futureY = bot.lastEnemyY + sin(velRad) * bot.velSpeed * travelTime let futureY = bot.decideEnemyY + sin(velRad) * bot.decideVelSpeed * travelTime
# correctAngle: absolute world-frame bearing [0,360) to predicted enemy position # correctAngle: absolute world-frame bearing [0,360) to predicted enemy position
let correctAngle = directionTo(myX, myY, futureX, futureY) let correctAngle = directionTo(myX, myY, futureX, futureY)
# Adaptive learning dead zone: scale by enemy velocity # Adaptive learning dead zone: scale by DECIDE-time enemy velocity
let t = (bot.velSpeed / 8.0).clamp(0.0, 1.0) let t = (bot.decideVelSpeed / 8.0).clamp(0.0, 1.0)
let adaptiveDeadZone = 0.5 + (0.3 - 0.5) * t # lerp(0.5, 0.3, t) let adaptiveDeadZone = 0.5 + (0.3 - 0.5) * t # lerp(0.5, 0.3, t)
when USE_RESERVOIR: when USE_RESERVOIR:
# correctOffset: angle offset from bare bearing that a bullet at BULLET_SPEED needs # correctOffset: angle offset from bare bearing that a bullet at BULLET_SPEED needs
let correctOffset = normalizeRelativeAngle(correctAngle - bot.lastAbsBearing) let correctOffset = normalizeRelativeAngle(correctAngle - bot.lastAbsBearing)
let producedOffset = bot.res.forward(bot.lastVPerp, bot.enemyDist) let producedOffset = bot.res.forward(bot.lastVPerp, bot.decideDist)
# Bias diagnostics: log grid offset vs correct offset to detect systematic error # Bias diagnostics: log grid offset vs correct offset to detect systematic error
let biasVal = producedOffset - correctOffset let biasVal = producedOffset - correctOffset
appendLog(biasDebugLog, fmt"BIAS tick={bot.roundTick} gridOffset={producedOffset:.3f} correctOffset={correctOffset:.3f} bias={biasVal:.3f} vPerp={bot.lastVPerp:.3f} dist={bot.enemyDist:.1f}") appendLog(biasDebugLog, fmt"BIAS tick={bot.roundTick} gridOffset={producedOffset:.3f} correctOffset={correctOffset:.3f} bias={biasVal:.3f} vPerp={bot.lastVPerp:.3f} dist={bot.decideDist:.1f}")
# Learn if: cold start OR error > dead zone # Learn if: cold start OR error > dead zone
if producedOffset > -999.0: if producedOffset > -999.0:
let aimErr = abs(normalizeRelativeAngle(producedOffset - correctOffset)) let aimErr = abs(normalizeRelativeAngle(producedOffset - correctOffset))
if aimErr > adaptiveDeadZone: if aimErr > adaptiveDeadZone:
bot.res.learn(bot.lastVPerp, bot.enemyDist, correctOffset) bot.res.learn(bot.lastVPerp, bot.decideDist, correctOffset)
else: else:
bot.res.learn(bot.lastVPerp, bot.enemyDist, correctOffset) bot.res.learn(bot.lastVPerp, bot.decideDist, correctOffset)
let aimErr = if producedOffset > -999.0: abs(normalizeRelativeAngle(producedOffset - correctOffset)) else: -1.0 let aimErr = if producedOffset > -999.0: abs(normalizeRelativeAngle(producedOffset - correctOffset)) else: -1.0
let rate = if bot.bulletsFired > 0: (float(bot.bulletsHit) / float(bot.bulletsFired) * 100.0) else: 0.0 let rate = if bot.bulletsFired > 0: (float(bot.bulletsHit) / float(bot.bulletsFired) * 100.0) else: 0.0
echo "RES tick=" & $bot.tick & echo "RES tick=" & $bot.tick &