## 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 # Per-tick derived state. It depends only on the ENEMY's motion (speed delta, # chosen deceleration, simulated stop point), never on the bullet speed, so it # is computed once per tick and shared by all four power bins. The speed- # dependent lead is recomputed from it on every call. derivedTick: int warmEnough: bool decelerating: bool decel: float stopX, stopY: float debugGraphics*: bool proc initStopShotGun*(): StopShotGun = StopShotGun(debugGraphics: false) proc predict*(g: var StopShotGun, state: WorldState, bulletSpeed: float): GunPrediction = if bulletSpeed <= 0.0: return GunPrediction(x: state.enemyX, y: state.enemyY) # Roll the observation window forward at most once per tick. prevSpeed must # hold the PREVIOUS tick's speed when deceleration is tested, so it is read # before being overwritten with the current tick's speed. The old code # overwrote it first, making `prev == speed` and the stop branch unreachable. if state.tick != g.derivedTick: g.derivedTick = state.tick let prev = g.prevSpeed if state.tick > g.prevTick: g.prevSpeed = state.enemySpeed g.prevHeading = state.enemyHeading g.prevTick = state.tick inc g.frames g.warmEnough = g.frames >= 2 # Detect deceleration: |speed| is shrinking toward zero. g.decelerating = g.warmEnough and abs(state.enemySpeed) < abs(prev) and abs(state.enemySpeed) > 0.01 if g.decelerating: # Pick decel rate: braking (speed toward zero on same sign) = 2, else 1 g.decel = if state.enemySpeed * prev > 0.0: BrakeDecel else: CoastDecel # Simulate the enemy coasting to a stop from its current position/heading. let headRad = degToRad(state.enemyHeading) let startSpeed = state.enemySpeed var v = startSpeed var sx = state.enemyX var sy = state.enemyY while abs(v) > 0.001: sx += v * cos(headRad) sy += v * sin(headRad) v += (if v > 0.0: -g.decel else: g.decel) if (v > 0.0) != (startSpeed > 0.0): v = 0.0 # crossed zero g.stopX = sx g.stopY = sy # Need 2+ frames to detect deceleration if not g.warmEnough: return GunPrediction(x: state.enemyX, y: state.enemyY) let speed = state.enemySpeed let headRad = degToRad(state.enemyHeading) let dist = hypot(state.enemyX - state.selfX, state.enemyY - state.selfY) # Speed-dependent lead — always recomputed, never cached across power bins. let bulletTicks = dist / bulletSpeed if not g.decelerating: # Linear fallback var px = state.enemyX + cos(headRad) * speed * bulletTicks var py = state.enemyY + sin(headRad) * speed * bulletTicks return GunPrediction( x: clamp(px, 0.0, state.arenaWidth), y: clamp(py, 0.0, state.arenaHeight) ) # If the bullet arrives well after the enemy stops, aim at the stop point; # otherwise blend a linear lead. stopTicks is speed-independent, so only this # comparison depends on the requested bulletSpeed. let stopTicks = abs(speed) / g.decel let px = if bulletTicks >= stopTicks: g.stopX else: state.enemyX + cos(headRad) * speed * bulletTicks let py = if bulletTicks >= stopTicks: g.stopY else: state.enemyY + sin(headRad) * speed * bulletTicks 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