feat(ModularBot): acceleration predictor gun, 9 guns total
- Accel gun: tracks velocity+heading changes, extrapolates with acceleration - Handles speed-up, slow-down, and combined accel+turn - Battle-tested vs Crazy, RandomMover, Walls
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
## Acceleration predictor gun: tracks velocity and heading changes across frames,
|
||||
## extrapolates with accel+turn for bots that speed up/slow down.
|
||||
## Needs 3+ frames for stable accel; falls back to linear (2 frames) or head-on (1 frame).
|
||||
## Coordinate system: 0° = East, CCW positive (Tank Royale standard).
|
||||
|
||||
import std/math
|
||||
import gun_harness/gun_interface
|
||||
|
||||
const MaxSpeed = 8.0 ## TR bot max speed (px/tick)
|
||||
|
||||
type AccelGun* = object
|
||||
prevSpeed: float
|
||||
prevHeading: float
|
||||
prevPrevSpeed: float
|
||||
prevTick: int
|
||||
frames: int ## how many observations we have
|
||||
cachedAccel: float ## accel (px/tick²) cached per tick
|
||||
cachedTurnRate: float ## turn rate (deg/tick) cached per tick
|
||||
cachedTick: int
|
||||
|
||||
proc initAccelGun*(): AccelGun =
|
||||
AccelGun()
|
||||
|
||||
proc predict*(g: var AccelGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
||||
if bulletSpeed <= 0.0:
|
||||
return GunPrediction(x: state.enemyX, y: state.enemyY)
|
||||
|
||||
# Update observation window on new tick
|
||||
if state.tick > g.cachedTick:
|
||||
if g.frames >= 1:
|
||||
# Compute turn rate (normalised to [-180, 180])
|
||||
var tr = state.enemyHeading - g.prevHeading
|
||||
if tr > 180.0: tr -= 360.0
|
||||
elif tr < -180.0: tr += 360.0
|
||||
let tickDelta = max(1, state.tick - g.prevTick).float
|
||||
g.cachedTurnRate = tr / tickDelta
|
||||
|
||||
if g.frames >= 2:
|
||||
# Compute acceleration
|
||||
g.cachedAccel = (state.enemySpeed - g.prevSpeed) / tickDelta
|
||||
else:
|
||||
g.cachedAccel = 0.0
|
||||
else:
|
||||
g.cachedTurnRate = 0.0
|
||||
g.cachedAccel = 0.0
|
||||
|
||||
# Advance window
|
||||
g.prevPrevSpeed = g.prevSpeed
|
||||
g.prevSpeed = state.enemySpeed
|
||||
g.prevHeading = state.enemyHeading
|
||||
g.prevTick = state.tick
|
||||
g.cachedTick = state.tick
|
||||
inc g.frames
|
||||
|
||||
# fallback: not enough history
|
||||
if g.frames < 2:
|
||||
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)
|
||||
var t = dist / bulletSpeed # initial ticks estimate
|
||||
|
||||
let accel = g.cachedAccel
|
||||
let turnRate = g.cachedTurnRate # deg/tick
|
||||
|
||||
# Iterative convergence (same pattern as circular gun)
|
||||
var ex = state.enemyX
|
||||
var ey = state.enemyY
|
||||
for _ in 0..4:
|
||||
var vx = state.enemySpeed
|
||||
var hd = state.enemyHeading # degrees
|
||||
var px = state.enemyX
|
||||
var py = state.enemyY
|
||||
let iters = int(t + 0.5)
|
||||
for _ in 0..<iters:
|
||||
vx = clamp(vx + accel, -MaxSpeed, MaxSpeed)
|
||||
hd += turnRate
|
||||
let hr = degToRad(hd)
|
||||
px += vx * cos(hr)
|
||||
py += vx * sin(hr)
|
||||
# Clamp to arena
|
||||
ex = clamp(px, BotRadius, state.arenaWidth - BotRadius)
|
||||
ey = clamp(py, BotRadius, state.arenaHeight - BotRadius)
|
||||
let ndx = ex - state.selfX
|
||||
let ndy = ey - state.selfY
|
||||
t = sqrt(ndx*ndx + ndy*ndy) / bulletSpeed
|
||||
|
||||
GunPrediction(x: ex, y: ey)
|
||||
|
||||
proc onResult*(g: var AccelGun, e: FeedbackEvent) =
|
||||
discard # analytical gun — no learning
|
||||
Reference in New Issue
Block a user