sots-engine/tests/shim_movement/unit_tests.cpp

173 lines
6.1 KiB
C++

// 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<int>(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<int>(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<int>(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<FleetSummary> f(4);
f[0] = {1, 0, 2, 0, static_cast<int>(WaypointKind::GateTeleport), 10, 0};
f[1] = {2, 0, 0, 0, static_cast<int>(WaypointKind::ProbabilisticJump), 5, 0};
f[2] = {3, 1, 0, 0, static_cast<int>(WaypointKind::NodeLine), 100, 0};
f[3] = {4, 1, 3, 2, static_cast<int>(WaypointKind::GateTeleport), -4, 0};
const std::vector<int> 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<sots::sim::MovementPass> 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<double> 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");
}