## Recency-weighted GF gun: exponential decay on histogram bins. ## decay=0.998/tick gives ~350-tick half-life — adapts to mid-battle strategy shifts. ## Everything else identical to guess_factor.nim, including per-power-bin wave queues. import std/math import gun_harness/gun_interface import gun_harness/virtual_bullets as vb # PowerBins const GFBins = 31 GFPrior = 0.1 DecayRate = 0.998 # ponytail: single global decay, tune if adaptation too slow/fast DecayWaveCompactAt = 64 type DWave = object fireX, fireY: float fireBearing: float DecayGFGun* = object bins: array[GFBins, float] # One wave queue per power bin; a resolved bullet only learns from a wave # queued for its own bin (matched on bulletSpeed / bulletPower). waves: array[len(vb.PowerBins), seq[DWave]] waveHead: array[len(vb.PowerBins), int] # O(1) pop cursor waveStoredTick: array[len(vb.PowerBins), int] # last tick a wave was queued for this bin cachedTick: int # last tick bins were decayed wavePushes*: int waveStarved*: int debugGraphics*: bool proc initDecayGFGun*(): DecayGFGun = result.cachedTick = -1 result.debugGraphics = false for b in 0.. g.bins[best]: best = i best proc binForSpeed(spd: float): int {.inline.} = for i in 0..= g.waves[binIdx].len: return (false, DWave()) result = (true, g.waves[binIdx][g.waveHead[binIdx]]) inc g.waveHead[binIdx] if g.waveHead[binIdx] >= DecayWaveCompactAt and g.waveHead[binIdx] * 2 >= g.waves[binIdx].len: g.waves[binIdx] = g.waves[binIdx][g.waveHead[binIdx] .. g.waves[binIdx].high] g.waveHead[binIdx] = 0 proc predict*(g: var DecayGFGun, state: WorldState, bulletSpeed: float): GunPrediction = if bulletSpeed <= 0.0: return GunPrediction(x: state.enemyX, y: state.enemyY) let dx = state.enemyX - state.selfX let dy = state.enemyY - state.selfY let dist = sqrt(dx*dx + dy*dy) let bearing = arctan2(dy, dx) let mea = arcsin(clamp(8.0 / bulletSpeed, -1.0, 1.0)) if state.tick != g.cachedTick: # Decay all bins once per tick for i in 0..= 0 and g.waveStoredTick[binIdx] != state.tick: g.waves[binIdx].add DWave(fireX: state.selfX, fireY: state.selfY, fireBearing: bearing) g.waveStoredTick[binIdx] = state.tick inc g.wavePushes let peak = g.peakBin() let peakGF = indexToGF(peak) let gfAngle = bearing + peakGF * mea let px = state.selfX + cos(gfAngle) * dist let py = state.selfY + sin(gfAngle) * dist GunPrediction( x: clamp(px, BotRadius, state.arenaWidth - BotRadius), y: clamp(py, BotRadius, state.arenaHeight - BotRadius), ) proc onResult*(g: var DecayGFGun, e: FeedbackEvent) = let binIdx = binForPower(e.bulletPower) if binIdx < 0: return let (found, w) = g.takeOldestWave(binIdx) if not found: inc g.waveStarved return let speed = bulletSpeed(e.bulletPower) let mea = arcsin(clamp(8.0 / speed, -1.0, 1.0)) let actualDx = e.actualX - w.fireX let actualDy = e.actualY - w.fireY let actualBearing = arctan2(actualDy, actualDx) var bearingDelta = actualBearing - w.fireBearing while bearingDelta > PI: bearingDelta -= 2.0*PI while bearingDelta < -PI: bearingDelta += 2.0*PI let gf = if mea > 1e-10: clamp(bearingDelta / mea, -1.0, 1.0) else: 0.0 let centerIdx = gfToIndex(gf) for i in 0..