Files
SirRoboGarage/PPO_Bot/enemy_tracker.nim
T
2026-08-16 15:12:13 +02:00

95 lines
3.2 KiB
Nim

## Enemy tracker — deterministic radar lock + dead reckoning for PPO_Bot.
## No bot API imports; takes plain floats.
import std/math
type
EnemyState* = object
x*, y*: float64
direction*: float64
speed*: float64
energy*: float64
ticksSinceLastScan*: int
hasFired*: bool
lastFirePower*: float64
EnemyTracker* = object
current*: EnemyState
history*: array[5, tuple[x, y, direction, speed: float64]] # sliding window
historyCount*: int # valid entries 0-5
prevEnergy*: float64
hasContact*: bool
## For radar overshoot reversal
lastOvershootDir*: float64 # +1 or -1
proc initEnemyTracker*(): EnemyTracker =
result.lastOvershootDir = 1.0
proc update*(tracker: var EnemyTracker;
scanX, scanY, scanDir, scanSpeed, scanEnergy: float64) =
## Call on ScannedBotEvent. Detects enemy fire from energy delta.
# Shift history window
if tracker.historyCount > 0:
for i in countdown(min(tracker.historyCount, 4), 1):
tracker.history[i] = tracker.history[i - 1]
tracker.history[0] = (tracker.current.x, tracker.current.y,
tracker.current.direction, tracker.current.speed)
if tracker.historyCount < 5:
inc tracker.historyCount
# Detect firing: energy drop in [0.1, 3.0] means enemy fired
let delta = tracker.prevEnergy - scanEnergy
if tracker.hasContact and delta >= 0.1 and delta <= 3.0:
tracker.current.hasFired = true
tracker.current.lastFirePower = delta
else:
tracker.current.hasFired = false
tracker.prevEnergy = scanEnergy
tracker.current.x = scanX
tracker.current.y = scanY
tracker.current.direction = scanDir
tracker.current.speed = scanSpeed
tracker.current.energy = scanEnergy
tracker.current.ticksSinceLastScan = 0
tracker.hasContact = true
proc deadReckon*(tracker: var EnemyTracker) =
## Call on missed ticks. Predict position from last known velocity.
if not tracker.hasContact:
return
let rad = tracker.current.direction * PI / 180.0
tracker.current.x += tracker.current.speed * sin(rad)
tracker.current.y += tracker.current.speed * cos(rad)
inc tracker.current.ticksSinceLastScan
proc normalizeRelative(angle: float64): float64 {.inline.} =
result = angle mod 360.0
if result >= 180.0: result -= 360.0
elif result < -180.0: result += 360.0
proc getRadarTurnRate*(tracker: EnemyTracker;
botX, botY, botDirection, radarDirection: float64): float64 =
## Returns radar turn rate (degrees/tick, positive = right).
## Before contact: full 45° sweep.
## After contact: lock with overshoot; widen if stale.
if not tracker.hasContact:
return 45.0
if tracker.current.ticksSinceLastScan >= 2:
# Lost lock — widen sweep proportional to staleness
return 45.0
# Bearing from radar to enemy
let dx = tracker.current.x - botX
let dy = tracker.current.y - botY
let absoluteDir = (180.0 * arctan2(dy, dx) / PI + 360.0) mod 360.0
let radarBearing = normalizeRelative(absoluteDir - radarDirection)
# Overshoot by 10°, alternate direction each tick
# ponytail: simple fixed overshoot; adaptive sweep if enemy is fast-turning
let overshoot = 10.0
let target = radarBearing + tracker.lastOvershootDir * overshoot
result = target.clamp(-45.0, 45.0)