feat(ModularBot): stop-shot gun, 10 guns total

- Stop-shot gun: predicts enemy deceleration stop point
- Catches bots at direction reversal pauses
- Battle-tested vs WaveSurfer, Crazy, RandomMover

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 01:37:45 +02:00
parent 18ff2d45b3
commit a87be5f99f
2 changed files with 112 additions and 3 deletions
+100
View File
@@ -0,0 +1,100 @@
## 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