## Stop-shot gun: predicts the point where a decelerating enemy will stop. ## Many bots pause briefly when reversing direction — catches them at the stop. ## Falls back to linear prediction when enemy is not decelerating. ## Coordinate system: 0° = East, CCW positive (Tank Royale standard). import std/math import gun_harness/gun_interface # TR deceleration constants (px/tick²) const BrakeDecel = 2.0 ## same-direction stop const CoastDecel = 1.0 ## cross-zero coasting stop type StopShotGun* = object prevSpeed: float prevHeading: float prevTick: int frames: int cachedTick: int cachedPredX: float cachedPredY: float proc initStopShotGun*(): StopShotGun = StopShotGun() proc predict*(g: var StopShotGun, state: WorldState, bulletSpeed: float): GunPrediction = if bulletSpeed <= 0.0: return GunPrediction(x: state.enemyX, y: state.enemyY) # Per-tick cache: all power bins share one prediction if state.tick == g.cachedTick: return GunPrediction(x: g.cachedPredX, y: g.cachedPredY) defer: g.cachedTick = state.tick g.cachedPredX = result.x g.cachedPredY = result.y # Update history let isNew = state.tick > g.prevTick if isNew: g.prevSpeed = state.enemySpeed g.prevHeading = state.enemyHeading g.prevTick = state.tick inc g.frames # Need 2+ frames to detect deceleration if g.frames < 2: result = GunPrediction(x: state.enemyX, y: state.enemyY) return let speed = state.enemySpeed let prev = g.prevSpeed # Detect deceleration: |speed| is shrinking let decelerating = abs(speed) < abs(prev) and abs(speed) > 0.01 if not decelerating: # Linear fallback let headRad = degToRad(state.enemyHeading) let dist = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY) let t = dist / bulletSpeed var px = state.enemyX + cos(headRad) * speed * t var py = state.enemyY + sin(headRad) * speed * t result = GunPrediction( x: clamp(px, 0.0, state.arenaWidth), y: clamp(py, 0.0, state.arenaHeight) ) return # Pick decel rate: braking (speed toward zero on same sign) = 2, else 1 # ponytail: simplified; TR has exact rules per direction but this is close enough let decel = if speed * prev > 0.0: BrakeDecel else: CoastDecel # Simulate stop position let headRad = degToRad(state.enemyHeading) var v = speed var sx = state.enemyX var sy = state.enemyY while abs(v) > 0.001: sx += v * cos(headRad) sy += v * sin(headRad) let step = if v > 0.0: -decel else: decel v += step if (v > 0.0) != (speed > 0.0): v = 0.0 # crossed zero # Bullet travel time to current pos, check against ticks to stop let dist = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY) let bulletTicks = dist / bulletSpeed let stopTicks = abs(speed) / decel # If bullet arrives well after stop, aim at stop; otherwise linear blend let px = if bulletTicks >= stopTicks: sx else: state.enemyX + cos(headRad) * speed * bulletTicks let py = if bulletTicks >= stopTicks: sy else: state.enemyY + sin(headRad) * speed * bulletTicks result = GunPrediction( x: clamp(px, BotRadius, state.arenaWidth - BotRadius), y: clamp(py, BotRadius, state.arenaHeight - BotRadius) ) proc onResult*(g: var StopShotGun, e: FeedbackEvent) = discard # analytical gun — no learning