## 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: 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)