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