Files
SirRoboGarage/PPO_Bot/enemy_tracker.nim
T
SirStone 6ad51148f4 fix(PPO_Bot): SIGSEGV crash fixes + static buffers for thread safety
- bullets: seq[InFlightBullet] → array[4, InFlightBullet] + bulletCount
  (eliminates cross-thread heap realloc under ORC)
- hasFired: edge-triggered (cleared after state build, not level-triggered)
- round_counter parseInt: wrapped for empty/torn file → 0
- Static SVG + intent buffers to kill cross-thread heap realloc
- Tick-local alive/bulletData also fixed arrays

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-20 14:23:08 +02:00

101 lines
3.6 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
proc initEnemyTracker*(): EnemyTracker = discard
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:
# Cold-start: pre-fill all slots with the incoming scan so indices 22-41
# are never zero-padded on tick 1. Accel/turn-rate correctly stay 0 (no delta yet).
for i in 0 ..< 5:
tracker.history[i] = (scanX, scanY, scanDir, scanSpeed)
tracker.historyCount = 5
else:
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 >= 8:
# Lost lock — widen sweep
return 45.0
# Bearing from radar to enemy.
# Tank Royale radar directions use standard math convention (0=east, CCW+).
# arctan2(dy, dx) gives the standard math angle matching radarDirection units.
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
var radarTurn = normalizeRelative(absoluteDir - radarDirection)
# Width Lock: overshoot proportional to arctan(36 / distance)
let distance = sqrt(dx * dx + dy * dy)
let extraTurn = min(arctan(36.0 / distance) * 180.0 / PI, 45.0)
if radarTurn < 0.0: radarTurn -= extraTurn
else: radarTurn += extraTurn
result = radarTurn.clamp(-45.0, 45.0)