2cc2a3bd87
- 30-tick cooldown after ghost-stuck/timeout ram exit prevents re-entry loop - enemy_tracker.update() skips dead bots to prevent same-tick scan resurrection - TFIL graphics cleared when ramming is active movement - [config] logs: white base with green-highlighted changes only - [ram:enter] logs trigger reason and key values on false→true transition - [death] and [target-invalid] logs retained for diagnostics
94 lines
2.9 KiB
Nim
94 lines
2.9 KiB
Nim
## 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
|
|
debugGraphics*: bool
|
|
|
|
proc initAccelGun*(): AccelGun =
|
|
AccelGun(debugGraphics: false)
|
|
|
|
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
|