aea0724d3a
- enemy_tracker: toggle lastOvershootDir each tick; make getRadarTurnRate take var tracker - training: remove threadvar Adam globals; pass adamStates as var param to ppoUpdate; export ACAdamStates - PPO_Bot: carry ACAdamStates through TrainingArgs/TrainingResult; drop trainingDone bool and Lock — use resultChan.tryRecv() directly as synchronisation - weights: sort checkpoint dirs newest-first by mtime instead of hardcoded order - tests/test_training: pass explicit ACAdamStates to ppoUpdate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
96 lines
3.3 KiB
Nim
96 lines
3.3 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: var 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)
|
|
tracker.lastOvershootDir *= -1.0
|