## Circular gun: predicts enemy position assuming constant turn rate. ## Turn rate estimated from delta between last two observed headings. ## Needs 2+ frames; falls back to current position on first observation. import std/math import gun_harness/gun_interface type CircularGun* = object prevHeading: float ## enemy heading from the tick before current prevTick: int ## tick of prevHeading observation hasPrev: bool cachedOmega: float ## omega (rad/tick) computed on first call this tick cachedTick: int ## tick for which cachedOmega was computed debugGraphics*: bool proc predict*(g: var CircularGun, 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 ticks = dist / bulletSpeed # initial estimate if not g.hasPrev: # ponytail: first-frame fallback to head-on, needs 2 frames for turn rate g.prevHeading = state.enemyHeading g.prevTick = state.tick g.hasPrev = true return GunPrediction(x: state.enemyX, y: state.enemyY) # On new tick: recompute cachedOmega and advance the heading window. # On same-tick calls (multiple power bins): reuse cachedOmega so omega # doesn't collapse to zero on bins 1+. if state.tick > g.prevTick: var turnRate = state.enemyHeading - g.prevHeading if turnRate > 180.0: turnRate -= 360.0 elif turnRate < -180.0: turnRate += 360.0 let tickDelta = max(1, state.tick - g.prevTick) g.cachedOmega = degToRad(turnRate / tickDelta.float) g.cachedTick = state.tick g.prevHeading = state.enemyHeading g.prevTick = state.tick let omega = g.cachedOmega let theta = degToRad(state.enemyHeading) let v = state.enemySpeed # Closed-form integrated trajectory (Robowiki circular targeting) # 0°=East CCW+: dx/dt = v*cos(θ+ω*t), dy/dt = v*sin(θ+ω*t) # → x(t) = x₀ + (v/ω)*[sin(θ+ω*t) - sin(θ)] # → y(t) = y₀ - (v/ω)*[cos(θ+ω*t) - cos(θ)] var ex = state.enemyX var ey = state.enemyY var t = ticks for _ in 0..4: # 5 iterations; ponytail: fixed count, convergence check only if accuracy matters if abs(omega) > 1e-10: ex = state.enemyX + (v / omega) * (sin(theta + omega * t) - sin(theta)) ey = state.enemyY - (v / omega) * (cos(theta + omega * t) - cos(theta)) else: ex = state.enemyX + v * cos(theta) * t ey = state.enemyY + v * sin(theta) * t let ndx = ex - state.selfX let ndy = ey - state.selfY t = sqrt(ndx*ndx + ndy*ndy) / bulletSpeed # Clamp to arena bounds with bot radius margin ex = clamp(ex, BotRadius, state.arenaWidth - BotRadius) ey = clamp(ey, BotRadius, state.arenaHeight - BotRadius) GunPrediction(x: ex, y: ey) proc onResult*(g: var CircularGun, e: FeedbackEvent) = discard # analytical gun — no learning