// B4 adapter tests: a StarFleet snapshot -> one movement step, and the fleet list -> the // turn's pass schedule and per-player gate traffic. // // Hand-written fixtures only. #include "shim/hooks/movement_inputs.h" #include "check.h" using namespace shim::hooks; using sots::sim::WaypointKind; namespace { FleetStepSnapshot Fleet(double distance, int waypointType, int ships, float range) { FleetStepSnapshot s; s.fleetId = 42; s.ownerIndex = 1; s.waypointType = waypointType; s.waypointCount = 1; s.dt = 1.0f; s.speed = 10.0f; s.pos[0] = 0; s.dest[0] = distance; s.shipCount = ships; for (int i = 0; i < ships; ++i) s.ships[i].range = range; return s; } void test_straight_step() { simtest::ScriptedRng rng; FleetStepSnapshot s = Fleet(100, 0, 2, 50.f); FleetStepResult r = StepFleet(s, 10.0, rng); CHECK_NEAR(r.distance, 100.0, 1e-9); CHECK_NEAR(r.moved, 10.0, 1e-6); CHECK_NEAR(r.pos[0], 10.0, 1e-5); CHECK(!r.arrived); CHECK(!r.stranded); CHECK_EQ(r.rngDraws, 0); CHECK_NEAR(r.shipRanges[0], 40.0, 1e-5); CHECK_NEAR(r.shipRanges[1], 40.0, 1e-5); CHECK_NEAR(r.fraction, 1.0, 0.0); // a straight leg reports a full pass // an exempt ship still clamps the fleet but pays nothing s.ships[1].range = 3.f; s.ships[1].exempt = 1; r = StepFleet(s, 10.0, rng); CHECK_NEAR(r.moved, 3.05, 1e-5); // the tanker's 3 + the 0.05 grace margin CHECK_NEAR(r.shipRanges[1], 3.0, 0.0); CHECK_NEAR(r.shipRanges[0], 50.0 - 3.05, 1e-4); // out of fuel entirely s.ships[0].range = 0.f; s.ships[1].range = 0.f; s.ships[1].exempt = 0; r = StepFleet(s, 10.0, rng); CHECK(r.stranded); CHECK_NEAR(r.moved, 0.0, 0.0); CHECK_NEAR(r.pos[0], 0.0, 0.0); } void test_arrival_snap() { simtest::ScriptedRng rng; FleetStepSnapshot s = Fleet(4, 0, 1, 100.f); s.dest[1] = 3; // a 3-4-5 leg, distance 5 const FleetStepResult r = StepFleet(s, 10.0, rng); CHECK(r.arrived); CHECK_NEAR(r.pos[0], 4.0, 0.0); // copied verbatim, not extrapolated CHECK_NEAR(r.pos[1], 3.0, 0.0); CHECK_NEAR(r.moved, 5.0, 1e-9); } void test_node_waypoint_fraction() { simtest::ScriptedRng rng; FleetStepSnapshot s = Fleet(4, static_cast(WaypointKind::NodeRoute), 1, 100.f); const FleetStepResult r = StepFleet(s, 10.0, rng); CHECK(r.arrived); CHECK_NEAR(r.fraction, 0.4, 1e-6); // only a node waypoint reports a partial pass } void test_gate_teleport() { simtest::ScriptedRng rng; FleetStepSnapshot s = Fleet(1000, static_cast(WaypointKind::GateTeleport), 1, 1.f); const FleetStepResult r = StepFleet(s, 10.0, rng); CHECK(r.arrived); CHECK_NEAR(r.pos[0], 1000.0, 0.0); // no distance clamp at all CHECK_NEAR(r.shipRanges[0], 1.0, 0.0); // and no fuel is charged CHECK_EQ(r.rngDraws, 0); } void test_probabilistic_jump() { FleetStepSnapshot s = Fleet(1000, static_cast(WaypointKind::ProbabilisticJump), 1, 1.f); s.castEfficiency = 2.f; s.castThreshold = 1.f; { // 0.3 x 2 = 0.6 <= 1: it arrives, on one draw, with no fuel charged simtest::ScriptedRng rng({0.3f}); const FleetStepResult r = StepFleet(s, 10.0, rng); CHECK(r.arrived); CHECK_NEAR(r.pos[0], 1000.0, 0.0); CHECK_EQ(r.rngDraws, 1); CHECK_NEAR(r.shipRanges[0], 1.0, 0.0); } { // 0.9 x 2 = 1.8 > 1: a misjump, two draws, scattered by 1.8 simtest::ScriptedRng rng({0.9f}, {7u}); const FleetStepResult r = StepFleet(s, 10.0, rng); CHECK(!r.arrived); CHECK_EQ(r.rngDraws, 2); CHECK_NEAR(r.moved, 1.8, 1e-6); } } void test_held_fleet() { simtest::ScriptedRng rng; FleetStepSnapshot s = Fleet(100, 0, 1, 50.f); s.heldFlag = 1; const FleetStepResult r = StepFleet(s, 10.0, rng); CHECK_NEAR(r.moved, 0.0, 0.0); CHECK(!r.arrived); CHECK_EQ(r.rngDraws, 0); FleetStepSnapshot empty = Fleet(100, -1, 1, 50.f); const FleetStepResult e = StepFleet(empty, 10.0, rng); CHECK_NEAR(e.moved, 0.0, 0.0); // no waypoints: nothing happens } void test_gate_traffic_and_schedule() { std::vector f(4); f[0] = {1, 0, 2, 0, static_cast(WaypointKind::GateTeleport), 10, 0}; f[1] = {2, 0, 0, 0, static_cast(WaypointKind::ProbabilisticJump), 5, 0}; f[2] = {3, 1, 0, 0, static_cast(WaypointKind::NodeLine), 100, 0}; f[3] = {4, 1, 3, 2, static_cast(WaypointKind::GateTeleport), -4, 0}; const std::vector g = GateTraffic(f, 2); CHECK_EQ(g.size(), std::size_t{2}); CHECK_EQ(g[0], 15); // both gate-transit kinds count CHECK_EQ(g[1], -4); // the node-line fleet contributes nothing; the word is signed // fleet 1 chases fleet 2 (relation 0), fleet 4 follows fleet 3 (relation 2) const std::vector p = PassSchedule(f); CHECK_EQ(p.size(), std::size_t{6}); CHECK_EQ(p[0].fleetId, 2); CHECK_EQ(p[0].pass, 1); CHECK_EQ(p[1].fleetId, 1); CHECK_EQ(p[1].pass, 2); CHECK_EQ(p[2].fleetId, 2); CHECK_EQ(p[2].pass, 3); CHECK_EQ(p[3].fleetId, 1); CHECK_EQ(p[3].pass, 4); // the chase failed: a second half // fleet 3 is only a *follower's* target, which is not the prey set, so it takes a full // turn in pass 4 like any unengaged fleet CHECK_EQ(p[4].fleetId, 3); CHECK_EQ(p[4].pass, 4); CHECK_NEAR(p[4].dt, 1.0, 0.0); CHECK_EQ(p[5].fleetId, 4); CHECK_EQ(p[5].pass, 5); } void test_ship_ranges_helper() { FleetStepSnapshot s = Fleet(10, 0, 3, 1.f); s.ships[1].range = 2.f; s.ships[2].range = 3.f; const std::vector r = ShipRanges(s); CHECK_EQ(r.size(), std::size_t{3}); CHECK_NEAR(r[2], 3.0, 0.0); s.shipCount = kMaxTracedShips + 10; // a garbage count is truncated, not overrun CHECK_EQ(ShipRanges(s).size(), std::size_t{kMaxTracedShips}); } } // namespace int main() { test_straight_step(); test_arrival_snap(); test_node_waypoint_fraction(); test_gate_teleport(); test_probabilistic_jump(); test_held_fleet(); test_gate_traffic_and_schedule(); test_ship_ranges_helper(); return simtest::finish("shim_movement_unit"); }