588c9ebc2f
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
88 lines
3.6 KiB
Nim
88 lines
3.6 KiB
Nim
## 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)
|