## Displacement-vector gun: predict using average velocity over a sliding window. ## Captures drift/tendency that instantaneous velocity misses ## (e.g. oscillating enemy slowly drifting east has v=0 at midpoints but ## displacement still pointing east). import std/math import gun_harness/gun_interface const WindowSize = 15 # ticks; N+1 frames stored type DisplacementGun* = object posX: array[WindowSize + 1, float] posY: array[WindowSize + 1, float] count: int # frames collected so far head: int # ring-buffer head # Per-tick derived state. The ring must advance exactly ONCE per tick and the # average per-tick velocity is speed-independent, so both are computed once # per tick and shared by all four power bins. The iterative bullet lead is # recomputed from (dx, dy) on every call. derivedTick: int dx, dy: float ready: bool debugGraphics*: bool proc predict*(g: var DisplacementGun, state: WorldState, bulletSpeed: float): GunPrediction = # Sample the enemy position exactly once per tick (the harness calls predict() # 4-5x/tick, once per power bin). Keying the old cache on bulletSpeed too made # every bin miss, so the nominal 15-tick window was actually advanced ~4x/tick. if state.tick != g.derivedTick: g.derivedTick = state.tick # Push current position into ring buffer g.head = (g.head + 1) mod (WindowSize + 1) g.posX[g.head] = state.enemyX g.posY[g.head] = state.enemyY if g.count < WindowSize + 1: inc g.count # Need at least N+1 frames; fall back to head-on if not enough if g.count < WindowSize + 1: g.ready = false else: g.ready = true # Oldest frame is (head + 1) mod (WindowSize + 1) let oldest = (g.head + 1) mod (WindowSize + 1) g.dx = (state.enemyX - g.posX[oldest]) / WindowSize.float g.dy = (state.enemyY - g.posY[oldest]) / WindowSize.float if not g.ready: return GunPrediction(x: state.enemyX, y: state.enemyY) # Iterate time estimate 5 times let dist0 = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY) var ticks = dist0 / bulletSpeed var px = state.enemyX var py = state.enemyY for _ in 0..4: px = state.enemyX + g.dx * ticks py = state.enemyY + g.dy * ticks ticks = hypot(px - state.selfX, py - state.selfY) / bulletSpeed px = clamp(px, 0.0, state.arenaWidth) py = clamp(py, 0.0, state.arenaHeight) GunPrediction(x: px, y: py) proc onResult*(g: var DisplacementGun, e: FeedbackEvent) = discard # analytical gun — no learning proc initDisplacementGun*(): DisplacementGun = DisplacementGun(count: 0, head: 0, derivedTick: -1, debugGraphics: false)