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

88 lines
3.6 KiB
Nim
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## State vector builder — produces 42-float normalized tensor for PPO policy.
## No bot API imports; takes plain BotState + EnemyTracker structs.
import std/math
import arraymancer
import ./enemy_tracker
type
BotStateData* = object
## Plain data mirror of the bot's observable state.
x*, y*: float64
direction*: float64
speed*: float64
energy*: float64
gunDirection*: float64
gunHeat*: float64
arenaWidth*, arenaHeight*: float64
proc buildStateVector*(bot: BotStateData; enemy: EnemyTracker): Tensor[float32] =
## Build the 42-float normalized state tensor.
## All values clipped to roughly [-1, 1] via division by physical maxima.
result = zeros[float32](42)
let aW = bot.arenaWidth
let aH = bot.arenaHeight
let diag = sqrt(aW * aW + aH * aH) # ≈ 1700 for 1200×800
let wallMax = max(aW, aH)
# --- Current tick: own bot (indices 0-6) ---
result[0] = float32(bot.x / aW)
result[1] = float32(bot.y / aH)
result[2] = float32(bot.direction / 360.0)
result[3] = float32(bot.speed / 8.0)
result[4] = float32(bot.energy / 100.0)
result[5] = float32(bot.gunDirection / 360.0)
result[6] = float32(bot.gunHeat / 1.8)
# --- Current tick: enemy (indices 7-13) ---
if enemy.hasContact:
result[7] = float32(enemy.current.x / aW)
result[8] = float32(enemy.current.y / aH)
result[9] = float32(enemy.current.direction / 360.0)
result[10] = float32(enemy.current.speed / 8.0)
result[11] = float32(enemy.current.energy / 100.0)
result[12] = float32(if enemy.current.hasFired: 1.0 else: 0.0)
result[13] = float32(enemy.current.lastFirePower / 3.0)
# else: remain 0.0
# --- Derived features (indices 14-21) ---
if enemy.hasContact:
# Enemy acceleration (speed delta from last history entry)
# Max delta is ±8 (stopped ↔ full speed); divide by 8 to normalize
if enemy.historyCount >= 1:
result[14] = float32((enemy.current.speed - enemy.history[0].speed) / 8.0)
# Enemy turn rate (direction delta from last history entry, normalized to [-1,1])
# Divide by 180 (max possible relative rotation) rather than 10 (max body turn rate)
# ponytail: /180 covers all cases; use /10 if you want sensitivity to small turns
if enemy.historyCount >= 1:
let dirDelta = ((enemy.current.direction - enemy.history[0].direction) + 540.0) mod 360.0 - 180.0
result[15] = float32(dirDelta / 180.0)
# Relative bearing to enemy (signed, from bot perspective)
let dx = enemy.current.x - bot.x
let dy = enemy.current.y - bot.y
let absDir = (180.0 * arctan2(dy, dx) / PI + 360.0) mod 360.0
let relBearing = ((absDir - bot.direction) + 540.0) mod 360.0 - 180.0
result[16] = float32(relBearing / 180.0)
# Distance to enemy
let dist = sqrt(dx * dx + dy * dy)
result[17] = float32(dist / diag)
# Wall distances (indices 18-21): top, bottom, left, right
# top = distance from bot to top wall (y=aH), bottom = distance to bottom (y=0)
# left = distance to left (x=0), right = distance to right (x=aW)
result[18] = float32((aH - bot.y) / wallMax) # top
result[19] = float32(bot.y / wallMax) # bottom
result[20] = float32(bot.x / wallMax) # left
result[21] = float32((aW - bot.x) / wallMax) # right
# --- History: 5 ticks × 4 floats = 20 floats (indices 22-41) ---
for i in 0 ..< 5:
let base = 22 + i * 4
if i < enemy.historyCount:
result[base + 0] = float32(enemy.history[i].x / aW)
result[base + 1] = float32(enemy.history[i].y / aH)
result[base + 2] = float32(enemy.history[i].direction / 360.0)
result[base + 3] = float32(enemy.history[i].speed / 8.0)
# else: remain 0.0 (pad)