## 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 exact (fireTick, ## powerBin)-keyed wave pairing (see guess_factor.nim for the measured FIFO defect). import std/math import gun_harness/gun_interface import gun_harness/virtual_bullets as vb # PowerBins import guns/lead_forecast const GFBins = 31 GFPrior = 0.1 DecayRate = 0.998 # ponytail: single global decay, tune if adaptation too slow/fast DecayWaveRingSlots = 1024 # (fireTick, powerBin) ring; see guess_factor.nim type DWave = object fireX, fireY: float fireBearing: float fireTick: int bin: int alive: bool DecayGFGun* = object bins: array[GFBins, float] # Exact (fireTick, powerBin)-keyed ring; a resolved bullet is matched to the # wave it actually fired, no matter how many other shots resolved first. # Heap-backed (seq): see guess_factor.nim. waves: seq[DWave] waveStoredTick: array[len(vb.PowerBins), int] # last tick a wave was queued for this bin vt: VelocityTracker # enemy velocity history (base selection) cachedTick: int # last tick bins were decayed wavePushes*: int waveStarved*: int waveResolved*: int waveMispaired*: int # ring-slot collision (impossible by design) debugGraphics*: bool proc initDecayGFGun*(): DecayGFGun = result.cachedTick = -1 result.debugGraphics = false result.waves = newSeq[DWave](DecayWaveRingSlots) for b in 0.. g.bins[best]: best = i best proc binForSpeed(spd: float): int {.inline.} = for i in 0..= 0 and g.waveStoredTick[binIdx] != state.tick: let slot = waveSlot(state.tick, binIdx) g.waves[slot] = DWave(fireX: state.selfX, fireY: state.selfY, fireBearing: f.bearing, fireTick: state.tick, bin: binIdx, alive: true) g.waveStoredTick[binIdx] = state.tick inc g.wavePushes let peak = g.peakBin() let peakGF = indexToGF(peak) let gfAngle = f.bearing + peakGF * mea let px = state.selfX + cos(gfAngle) * f.dist let py = state.selfY + sin(gfAngle) * f.dist GunPrediction( x: clamp(px, BotRadius, state.arenaWidth - BotRadius), y: clamp(py, BotRadius, state.arenaHeight - BotRadius), confidence: g.bins[peak], # class-sum max over GF bins (see guess_factor.nim) ) proc onResult*(g: var DecayGFGun, e: FeedbackEvent) = let binIdx = if e.powerBin >= 0 and e.powerBin < len(vb.PowerBins): e.powerBin else: binForPower(e.bulletPower) if binIdx < 0: return let slot = waveSlot(e.fireTick, binIdx) var w = addr g.waves[slot] if not w.alive: inc g.waveStarved return if w.fireTick != e.fireTick: inc g.waveMispaired inc g.waveStarved return inc g.waveResolved 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..