diff --git a/SNNBot_garage/src/SNNBot.nim b/SNNBot_garage/src/SNNBot.nim index 690f358..4895295 100644 --- a/SNNBot_garage/src/SNNBot.nim +++ b/SNNBot_garage/src/SNNBot.nim @@ -215,6 +215,7 @@ type lastDecideGunDir: float # gun heading captured at DECIDE time for EVALUATE lastBinInput: BitVec80 # DECIDE-time binary input, reused in EVALUATE lastInput: BitVec80 # previous EVALUATE-time input (for change detection) + lastAbsBearing: float # absolute bearing to enemy captured at DECIDE time # ── aimTo helper ────────────────────────────────────────────────────────────── @@ -340,17 +341,18 @@ method run*(bot: SNNBot) = bot.lastRelBearing = normalizeRelativeAngle(absBearing - gunDir) when USE_RESERVOIR: let binInput = toBinaryInput(absBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos) - let aimRaw = bot.res.forward(binInput) # [0,360) or -1.0 sentinel + let aimOffset = bot.res.forward(binInput) # lead correction offset or -999.0 sentinel bot.lastBinInput = binInput bot.lastDecideGunDir = gunDir - if aimRaw < 0.0: - # cold start: aim directly at enemy + bot.lastAbsBearing = absBearing + if aimOffset <= -999.0: + # cold start: aim directly at enemy (no lead) bot.targetAngle = absBearing echo "RES tick=" & $bot.tick & " cold-start aim=" & formatFloat(absBearing, ffDecimal, 1) else: - # aimRaw is absolute [0,360): use directly as target - bot.targetAngle = aimRaw - echo "RES tick=" & $bot.tick & " aim=" & formatFloat(aimRaw, ffDecimal, 1) + # apply lead correction offset to current bearing + bot.targetAngle = (absBearing + aimOffset + 360.0) mod 360.0 + echo "RES tick=" & $bot.tick & " offset=" & formatFloat(aimOffset, ffDecimal, 1) & " aim=" & formatFloat(bot.targetAngle, ffDecimal, 1) else: let inputs = encodeInputFull(absBearing, bot.velDirDeg, bot.velSpeed, bot.hasLastPos) # Multi-tick inference: accumulate sin/cos and spike counts over N_INFER ticks @@ -410,24 +412,26 @@ method run*(bot: SNNBot) = let adaptiveDeadZone = 0.5 + (0.3 - 0.5) * t # lerp(0.5, 0.3, t) when USE_RESERVOIR: - let aimRaw = bot.res.forward(bot.lastBinInput) + # correctOffset: how much lead to apply from current bearing + let correctOffset = normalizeRelativeAngle(correctAngle - bot.lastAbsBearing) + let producedOffset = bot.res.forward(bot.lastBinInput) # Detect significant input change via Hamming distance let hammingDist = popcount(bot.lastBinInput[0] xor bot.lastInput[0]) + popcount(bot.lastBinInput[1] xor bot.lastInput[1]) let inputChanged = hammingDist > 2 # 3+ bits flipped = situation changed # Learn if: error > dead zone OR input pattern changed significantly - if aimRaw >= 0.0: - let aimErr = abs(normalizeRelativeAngle(aimRaw - correctAngle)) + if producedOffset > -999.0: + let aimErr = abs(normalizeRelativeAngle(producedOffset - correctOffset)) if aimErr > adaptiveDeadZone or inputChanged: - bot.res.learn(bot.lastBinInput, correctAngle) + bot.res.learn(bot.lastBinInput, correctOffset) else: # Cold start: always learn - bot.res.learn(bot.lastBinInput, correctAngle) - let aimErr = if aimRaw >= 0.0: abs(normalizeRelativeAngle(aimRaw - correctAngle)) else: -1.0 + bot.res.learn(bot.lastBinInput, correctOffset) + let aimErr = if producedOffset > -999.0: abs(normalizeRelativeAngle(producedOffset - correctOffset)) else: -1.0 echo "RES tick=" & $bot.tick & - " aim=" & (if aimRaw >= 0.0: formatFloat(aimRaw, ffDecimal, 1) else: "cold") & - " correct=" & formatFloat(correctAngle, ffDecimal, 1) & - " err=" & (if aimRaw >= 0.0: formatFloat(aimErr, ffDecimal, 1) else: "n/a") & + " offset=" & (if producedOffset > -999.0: formatFloat(producedOffset, ffDecimal, 1) else: "cold") & + " correctOffset=" & formatFloat(correctOffset, ffDecimal, 1) & + " err=" & (if producedOffset > -999.0: formatFloat(aimErr, ffDecimal, 1) else: "n/a") & " hammingDist=" & $hammingDist & " inputChanged=" & $inputChanged & " exemplars=" & $bot.res.count diff --git a/SNNBot_garage/src/reservoir.nim b/SNNBot_garage/src/reservoir.nim index f11959d..f14f305 100644 --- a/SNNBot_garage/src/reservoir.nim +++ b/SNNBot_garage/src/reservoir.nim @@ -13,7 +13,7 @@ type Exemplar* = object pattern*: BitVec80 - angle*: float # degrees 0-360 + offset*: float # lead correction in degrees, typically [-30, +30] active*: bool BinaryAimer* = object @@ -27,7 +27,7 @@ proc initBinaryAimer*(): BinaryAimer = proc forward*(aimer: var BinaryAimer, input: BitVec80): float = ## Kernel regression: weighted circular mean over active exemplars. ## Weights by similarity^2 and exponential recency decay (0.95^age). - ## Returns angle in [0,360) degrees, or -1.0 sentinel when cold (no weight). + ## Returns offset (lead correction) in degrees, or -999.0 sentinel when cold (no weight). var sinSum = 0.0 var cosSum = 0.0 var totalW = 0.0 @@ -41,18 +41,16 @@ proc forward*(aimer: var BinaryAimer, input: BitVec80): float = let age = (aimer.nextSlot - 1 - i + MAX_K) mod MAX_K let recency = pow(0.95, age.float) let weight = float(excess * excess) * recency - let rad = degToRad(aimer.exemplars[i].angle) + let rad = degToRad(aimer.exemplars[i].offset) sinSum += weight * sin(rad) cosSum += weight * cos(rad) totalW += weight if totalW == 0.0: - return -1.0 - var angle = radToDeg(arctan2(sinSum, cosSum)) - if angle < 0.0: angle += 360.0 - result = angle + return -999.0 + result = radToDeg(arctan2(sinSum, cosSum)) -proc learn*(aimer: var BinaryAimer, input: BitVec80, correctAngle: float) = +proc learn*(aimer: var BinaryAimer, input: BitVec80, offset: float) = ## Ring-buffer store: write exemplar at nextSlot, advance mod MAX_K. - aimer.exemplars[aimer.nextSlot] = Exemplar(pattern: input, angle: correctAngle, active: true) + aimer.exemplars[aimer.nextSlot] = Exemplar(pattern: input, offset: offset, active: true) aimer.nextSlot = (aimer.nextSlot + 1) mod MAX_K if aimer.count < MAX_K: inc aimer.count