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
76 lines
2.9 KiB
Nim
76 lines
2.9 KiB
Nim
## Circular gun: predicts enemy position assuming constant turn rate.
|
|
## Turn rate estimated from delta between last two observed headings.
|
|
## Needs 2+ frames; falls back to current position on first observation.
|
|
|
|
import std/math
|
|
import gun_harness/gun_interface
|
|
|
|
type CircularGun* = object
|
|
prevHeading: float ## enemy heading from the tick before current
|
|
prevTick: int ## tick of prevHeading observation
|
|
hasPrev: bool
|
|
cachedOmega: float ## omega (rad/tick) computed on first call this tick
|
|
cachedTick: int ## tick for which cachedOmega was computed
|
|
debugGraphics*: bool
|
|
|
|
proc predict*(g: var CircularGun, state: WorldState, bulletSpeed: float): GunPrediction =
|
|
if bulletSpeed <= 0.0:
|
|
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)
|
|
let ticks = dist / bulletSpeed # initial estimate
|
|
|
|
if not g.hasPrev:
|
|
# ponytail: first-frame fallback to head-on, needs 2 frames for turn rate
|
|
g.prevHeading = state.enemyHeading
|
|
g.prevTick = state.tick
|
|
g.hasPrev = true
|
|
return GunPrediction(x: state.enemyX, y: state.enemyY)
|
|
|
|
# On new tick: recompute cachedOmega and advance the heading window.
|
|
# On same-tick calls (multiple power bins): reuse cachedOmega so omega
|
|
# doesn't collapse to zero on bins 1+.
|
|
if state.tick > g.prevTick:
|
|
var turnRate = state.enemyHeading - g.prevHeading
|
|
if turnRate > 180.0: turnRate -= 360.0
|
|
elif turnRate < -180.0: turnRate += 360.0
|
|
let tickDelta = max(1, state.tick - g.prevTick)
|
|
g.cachedOmega = degToRad(turnRate / tickDelta.float)
|
|
g.cachedTick = state.tick
|
|
g.prevHeading = state.enemyHeading
|
|
g.prevTick = state.tick
|
|
|
|
let omega = g.cachedOmega
|
|
let theta = degToRad(state.enemyHeading)
|
|
let v = state.enemySpeed
|
|
|
|
# Closed-form integrated trajectory (Robowiki circular targeting)
|
|
# 0°=East CCW+: dx/dt = v*cos(θ+ω*t), dy/dt = v*sin(θ+ω*t)
|
|
# → x(t) = x₀ + (v/ω)*[sin(θ+ω*t) - sin(θ)]
|
|
# → y(t) = y₀ - (v/ω)*[cos(θ+ω*t) - cos(θ)]
|
|
var ex = state.enemyX
|
|
var ey = state.enemyY
|
|
var t = ticks
|
|
|
|
for _ in 0..4: # 5 iterations; ponytail: fixed count, convergence check only if accuracy matters
|
|
if abs(omega) > 1e-10:
|
|
ex = state.enemyX + (v / omega) * (sin(theta + omega * t) - sin(theta))
|
|
ey = state.enemyY - (v / omega) * (cos(theta + omega * t) - cos(theta))
|
|
else:
|
|
ex = state.enemyX + v * cos(theta) * t
|
|
ey = state.enemyY + v * sin(theta) * t
|
|
let ndx = ex - state.selfX
|
|
let ndy = ey - state.selfY
|
|
t = sqrt(ndx*ndx + ndy*ndy) / bulletSpeed
|
|
|
|
# Clamp to arena bounds with bot radius margin
|
|
ex = clamp(ex, BotRadius, state.arenaWidth - BotRadius)
|
|
ey = clamp(ey, BotRadius, state.arenaHeight - BotRadius)
|
|
|
|
GunPrediction(x: ex, y: ey)
|
|
|
|
proc onResult*(g: var CircularGun, e: FeedbackEvent) =
|
|
discard # analytical gun — no learning
|