Files
SirRoboGarage/PPO_Bot_garage/tests/test_state.nim
2026-08-27 18:42:03 +02:00

188 lines
7.5 KiB
Nim
Raw Permalink 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.
## Assert-based tests for EnemyTracker and StateVector.
## Run: nim c -r tests/test_state.nim
import std/[math, strformat]
import arraymancer
# Import from parent dir
import PPO_Bot/enemy_tracker
import PPO_Bot/state_vector
template check(cond: bool, msg: string) =
if not cond:
quit("FAIL: " & msg, 1)
# ---------------------------------------------------------------------------
# EnemyTracker tests
# ---------------------------------------------------------------------------
block testBasicUpdate:
var t = initEnemyTracker()
t.update(200.0, 300.0, 90.0, 5.0, 80.0)
check t.hasContact, "hasContact after update"
check t.current.x == 200.0, "x after update"
check t.current.y == 300.0, "y after update"
check t.current.direction == 90.0, "direction after update"
check t.current.speed == 5.0, "speed after update"
check t.current.energy == 80.0, "energy after update"
check t.current.ticksSinceLastScan == 0, "ticksSinceLastScan reset"
block testFireDetection:
var t = initEnemyTracker()
# First update sets prevEnergy
t.update(100.0, 100.0, 0.0, 0.0, 100.0)
# Second update: energy drop of 3.0 → enemy fired power 3.0
t.update(100.0, 100.0, 0.0, 0.0, 97.0)
check t.current.hasFired, "hasFired when energy drops by 3.0"
check abs(t.current.lastFirePower - 3.0) < 0.001, "lastFirePower == 3.0"
block testNoFireOnSmallDrop:
var t = initEnemyTracker()
t.update(100.0, 100.0, 0.0, 0.0, 100.0)
# Drop of 0.05 — below MIN_FIRE_POWER threshold
t.update(100.0, 100.0, 0.0, 0.0, 99.95)
check not t.current.hasFired, "no fire on small energy drop"
block testDeadReckoning:
var t = initEnemyTracker()
# direction=0° in Tank Royale means north (y increases)
t.update(100.0, 100.0, 0.0, 5.0, 100.0)
t.deadReckon()
# x unchanged (sin 0° = 0), y increases by speed (cos 0° = 1)
check abs(t.current.x - 100.0) < 0.001, "dead reckon: x unchanged for dir=0"
check abs(t.current.y - 105.0) < 0.001, "dead reckon: y += speed for dir=0"
check t.current.ticksSinceLastScan == 1, "ticksSinceLastScan incremented"
block testDeadReckonEast:
var t = initEnemyTracker()
# direction=90° → east (sin 90° = 1, cos 90° = 0)
t.update(100.0, 100.0, 90.0, 5.0, 100.0)
t.deadReckon()
check abs(t.current.x - 105.0) < 0.001, "dead reckon east: x += speed"
check abs(t.current.y - 100.0) < 0.001, "dead reckon east: y unchanged"
block testHistoryWindow:
var t = initEnemyTracker()
# Feed 6 updates — history should hold last 5
for i in 1 .. 6:
t.update(float64(i) * 10.0, float64(i) * 20.0, 0.0, float64(i), 100.0)
check t.historyCount == 5, "historyCount capped at 5"
# history[0] should be the second-to-last scan (i=5)
check abs(t.history[0].x - 50.0) < 0.001, "history[0].x == 50 (i=5)"
check abs(t.history[4].x - 10.0) < 0.001, "history[4].x == 10 (i=1)"
# ---------------------------------------------------------------------------
# StateVector tests
# ---------------------------------------------------------------------------
block testStateVectorLength:
var t = initEnemyTracker()
t.update(400.0, 300.0, 45.0, 3.0, 80.0)
let bot = BotStateData(
x: 200.0, y: 200.0, direction: 90.0, speed: 4.0, energy: 50.0,
gunDirection: 180.0, gunHeat: 0.5,
arenaWidth: 800.0, arenaHeight: 600.0,
)
let sv = buildStateVector(bot, t)
check sv.shape == [57], "state vector has 57 elements"
block testStateVectorRange:
var t = initEnemyTracker()
t.update(400.0, 300.0, 180.0, 8.0, 100.0)
let bot = BotStateData(
x: 800.0, y: 600.0, direction: 360.0, speed: 8.0, energy: 100.0,
gunDirection: 360.0, gunHeat: 1.8,
arenaWidth: 800.0, arenaHeight: 600.0,
)
let sv = buildStateVector(bot, t)
for i in 0 ..< 57:
check sv[i] >= -2.0f32 and sv[i] <= 2.0f32,
&"sv[{i}]={sv[i]} out of [-2,2] range"
block testWallDistances:
# Bot at (100, 200) in 800×600 arena
# wallMax = max(800, 600) = 800
# top = (600 - 200) / 800 = 400/800 = 0.5
# bottom = 200 / 800 = 0.25
# left = 100 / 800 = 0.125
# right = (800 - 100) / 800 = 700/800 = 0.875
var t = initEnemyTracker()
let bot = BotStateData(
x: 100.0, y: 200.0, direction: 0.0, speed: 0.0, energy: 100.0,
gunDirection: 0.0, gunHeat: 0.0,
arenaWidth: 800.0, arenaHeight: 600.0,
)
let sv = buildStateVector(bot, t)
check abs(sv[18] - 0.5f32) < 0.001f32, "top wall = 0.5"
check abs(sv[19] - 0.25f32) < 0.001f32, "bottom wall = 0.25"
check abs(sv[20] - 0.125f32) < 0.001f32, "left wall = 0.125"
check abs(sv[21] - 0.875f32) < 0.001f32, "right wall = 0.875"
block testRelativeBearing:
# Game convention: north=0°, CW. arctan2(dx,dy) used.
# Bot at (0,0) dir=0°. Enemy at (0,100) → due north → absDir=0°.
# relBearing = (0 - 0 + 540) mod 360 - 180 = 0°. normalized = 0/180 = 0.0
var t = initEnemyTracker()
t.update(0.0, 100.0, 0.0, 0.0, 100.0)
let bot = BotStateData(
x: 0.0, y: 0.0, direction: 0.0, speed: 0.0, energy: 100.0,
gunDirection: 0.0, gunHeat: 0.0,
arenaWidth: 800.0, arenaHeight: 600.0,
)
let sv = buildStateVector(bot, t)
check abs(sv[16] - 0.0f32) < 0.01f32, "relative bearing = 0.0 (due north), got " & $sv[16]
block testRelativeBearingEast:
# Enemy at (100,0) → due east → absDir=90°.
# relBearing = (90 - 0 + 540) mod 360 - 180 = 90°. normalized = 90/180 = 0.5
var t = initEnemyTracker()
t.update(100.0, 0.0, 0.0, 0.0, 100.0)
let bot = BotStateData(
x: 0.0, y: 0.0, direction: 0.0, speed: 0.0, energy: 100.0,
gunDirection: 0.0, gunHeat: 0.0,
arenaWidth: 800.0, arenaHeight: 600.0,
)
let sv = buildStateVector(bot, t)
check abs(sv[16] - 0.5f32) < 0.01f32, "relative bearing east = 0.5, got " & $sv[16]
block testHistoryPaddedWhenEmpty:
var t = initEnemyTracker()
let bot = BotStateData(
x: 400.0, y: 300.0, direction: 0.0, speed: 0.0, energy: 100.0,
gunDirection: 0.0, gunHeat: 0.0,
arenaWidth: 800.0, arenaHeight: 600.0,
)
let sv = buildStateVector(bot, t)
for i in 22 ..< 42:
check sv[i] == 0.0f32, &"history slot {i} should be 0 when no contact"
# indices 42-43 (goto inputs) default to 0 when not provided
check sv[42] == 0.0f32, "sv[42] (remainingGotoDistance) should default to 0"
check sv[43] == 0.0f32, "sv[43] (remainingGunAngle) should default to 0"
# indices 44-55 (bullet slots) default to 0 when no bullets provided
for i in 44 ..< 56:
check sv[i] == 0.0f32, &"bullet slot {i} should be 0 when no bullets"
# index 56 (staleness): no contact so ticksSinceLastScan=0 → 0/30 = 0
check sv[56] == 0.0f32, "sv[56] staleness should be 0 when no contact"
block testBulletSlots:
var t = initEnemyTracker()
t.update(400.0, 300.0, 0.0, 0.0, 100.0) # enemy at (400,300)
let bot = BotStateData(
x: 200.0, y: 200.0, direction: 0.0, speed: 0.0, energy: 100.0,
gunDirection: 0.0, gunHeat: 0.0,
arenaWidth: 800.0, arenaHeight: 600.0,
)
# Bullet at (300,250), power 1.0 → speed = 20-3 = 17
# relX = 300-200 = 100, relY = 250-200 = 50 (relative to bot, not enemy)
# dist to bot = sqrt(100^2+50^2) ≈ 111.8, ticks ≈ 111.8/17 ≈ 6.58
let b = BulletData(x: 300.0, y: 250.0, power: 1.0)
let sv = buildStateVector(bot, t, 0.0, 0.0, [b], 1)
check abs(sv[44] - (100.0/800.0).float32) < 0.001f32, "bullet relX"
check abs(sv[45] - (50.0/600.0).float32) < 0.001f32, "bullet relY"
check abs(sv[46] - (17.0/20.0).float32) < 0.001f32, "bullet speed norm"
# second slot should be zero-padded
for i in 48 ..< 57:
check sv[i] == 0.0f32, &"unused bullet slot {i} should be 0"
echo "All tests passed"