## 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 "../enemy_tracker" import "../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 == [42], "state vector has 42 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 ..< 42: 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" echo "All tests passed"