282 lines
12 KiB
C++
282 lines
12 KiB
C++
#include "game/sim/movement.h"
|
|
|
|
#include "check.h"
|
|
|
|
using namespace sots::sim;
|
|
|
|
static void test_vectors() {
|
|
CHECK_NEAR(Distance({0, 0, 0}, {3, 4, 0}), 5.0, 1e-12);
|
|
Vec3 p = AdvanceToward({0, 0, 0}, {10, 0, 0}, 4);
|
|
CHECK_NEAR(p.x, 4.0, 1e-12);
|
|
CHECK_NEAR(p.y, 0.0, 0.0);
|
|
p = AdvanceToward({0, 0, 0}, {10, 0, 0}, 20); // snaps to the destination
|
|
CHECK_NEAR(p.x, 10.0, 0.0);
|
|
p = AdvanceToward({1, 2, 3}, {1, 2, 3}, 5); // already there
|
|
CHECK_NEAR(p.z, 3.0, 0.0);
|
|
p = AdvanceToward({0, 0, 0}, {3, 4, 0}, 2.5); // half way along a 3-4-5
|
|
CHECK_NEAR(p.x, 1.5, 1e-12);
|
|
CHECK_NEAR(p.y, 2.0, 1e-12);
|
|
}
|
|
|
|
static void test_steps() {
|
|
CHECK_NEAR(StraightStep(10, 0.5), 5.0, 0.0);
|
|
CHECK_NEAR(StraightStep(10, kFullStep), 10.0, 0.0);
|
|
CHECK_NEAR(StraightStep(0, kHalfStep), 0.0, 0.0);
|
|
|
|
TuningTable t;
|
|
t.STUTTER_SYSTEM_INFLUENCE_RADIUS = 100;
|
|
t.STUTTER_MIN_SPEED = 0.2;
|
|
t.STUTTER_MAX_SPEED = 1.0;
|
|
CHECK_NEAR(NodeLineSpeed(10, 50, t), 6.0, 1e-12); // 10 x (0.8 x 0.5 + 0.2)
|
|
CHECK_NEAR(NodeLineSpeed(10, 0, t), 2.0, 1e-12); // at a system: min profile
|
|
CHECK_NEAR(NodeLineSpeed(10, 100, t), 10.0, 1e-12); // at the radius: max profile
|
|
CHECK_NEAR(NodeLineSpeed(10, 200, t), 18.0, 1e-12); // no clamp (never reached in practice)
|
|
TuningTable zero;
|
|
CHECK_NEAR(NodeLineSpeed(10, 50, zero), 0.0, 0.0); // no tuning -> no speed
|
|
}
|
|
static void test_stutter_segments() {
|
|
CHECK_NEAR(DistPointToSegment({5, 3, 0}, {0, 0, 0}, {10, 0, 0}), 3.0, 1e-12);
|
|
CHECK_NEAR(DistPointToSegment({-5, 3, 0}, {0, 0, 0}, {10, 0, 0}), std::sqrt(34.0), 1e-12);
|
|
CHECK_NEAR(DistPointToSegment({15, 0, 0}, {0, 0, 0}, {10, 0, 0}), 5.0, 1e-12);
|
|
CHECK_NEAR(DistPointToSegment({3, 4, 0}, {1, 1, 1}, {1, 1, 1}), std::sqrt(4 + 9 + 1), 1e-12);
|
|
|
|
TuningTable t;
|
|
t.STUTTER_SYSTEM_INFLUENCE_RADIUS = 50;
|
|
t.STUTTER_MIN_SPEED = 0.2;
|
|
t.STUTTER_MAX_SPEED = 1.0;
|
|
const Vec3 from{0, 0, 0}, to{100, 0, 0};
|
|
|
|
// One system 30 off the line: chord where (x-50)^2 + 900 <= 2500 -> x in [10, 90]
|
|
std::vector<StutterSegment> s = BuildStutterSegments(from, to, {{50, 30, 0}}, t);
|
|
CHECK_EQ(s.size(), std::size_t{1});
|
|
CHECK_NEAR(s[0].start, 10.0, 1e-4);
|
|
CHECK_NEAR(s[0].end, 90.0, 1e-4);
|
|
CHECK_EQ(s[0].systemIndex, 0);
|
|
CHECK_NEAR(s[0].speedFactor, 0.8 * 30 / 50 + 0.2, 1e-6); // 0.68 for the whole chord
|
|
|
|
// Out of reach, and exactly tangent (zero-length chord): no segments
|
|
CHECK(BuildStutterSegments(from, to, {{50, 200, 0}}, t).empty());
|
|
CHECK(BuildStutterSegments(from, to, {{50, 50, 0}}, t).empty());
|
|
|
|
// B4: the overlap rule is NOT a midpoint. Second system near the end gives [35, 100];
|
|
// the earlier chord ends at 90, so both boundaries become 90 + 0.5 x (90 - 35) = 117.5 --
|
|
// pushed forward past both chords, which leaves the second segment inverted.
|
|
s = BuildStutterSegments(from, to, {{50, 30, 0}, {85, 0, 0}}, t);
|
|
CHECK_EQ(s.size(), std::size_t{2});
|
|
CHECK_NEAR(s[0].start, 10.0, 1e-4);
|
|
CHECK_NEAR(s[0].end, 117.5, 1e-3);
|
|
CHECK_EQ(s[0].systemIndex, 0);
|
|
CHECK_NEAR(s[1].start, 117.5, 1e-3);
|
|
CHECK_NEAR(s[1].end, 100.0, 1e-4);
|
|
CHECK(s[1].start > s[1].end); // inverted, and left that way
|
|
CHECK_EQ(s[1].systemIndex, 1);
|
|
|
|
// Input order does not matter: segments come back sorted by start
|
|
std::vector<StutterSegment> r = BuildStutterSegments(from, to, {{85, 0, 0}, {50, 30, 0}}, t);
|
|
CHECK_EQ(r.size(), std::size_t{2});
|
|
CHECK_EQ(r[0].systemIndex, 1);
|
|
CHECK_EQ(r[1].systemIndex, 0);
|
|
|
|
// A chord swallowed by an earlier one is not dropped either: [0,100] and [20,80]
|
|
// become [0, 140] and [140, 80].
|
|
s = BuildStutterSegments(from, to, {{50, 0, 0}, {50, 40, 0}}, t);
|
|
CHECK_EQ(s.size(), std::size_t{2});
|
|
CHECK_NEAR(s[0].end, 140.0, 1e-3);
|
|
CHECK_NEAR(s[1].start, 140.0, 1e-3);
|
|
CHECK_NEAR(s[1].end, 80.0, 1e-4);
|
|
|
|
TuningTable zero;
|
|
CHECK(BuildStutterSegments(from, to, {{50, 0, 0}}, zero).empty()); // no radius: plain line
|
|
|
|
// Walk the [10, 90] x0.68 profile at node speed 20.
|
|
s = BuildStutterSegments(from, to, {{50, 30, 0}}, t);
|
|
NodeLineStepResult n = NodeLineStep(20, 1.0, 100, s);
|
|
CHECK_NEAR(n.along, 16.8, 1e-4); // half a turn to reach 10, then 0.5 x 13.6
|
|
CHECK(!n.arrived);
|
|
n = NodeLineStep(20, 10.0, 100, s); // plenty of time: reaches the far end
|
|
CHECK_NEAR(n.along, 100.0, 1e-6);
|
|
CHECK(n.arrived);
|
|
n = NodeLineStep(20, 1.0, 100, {}); // no spheres: plain speed the whole way
|
|
CHECK_NEAR(n.along, 20.0, 1e-9);
|
|
CHECK(!n.arrived);
|
|
n = NodeLineStep(20, 0.0, 100, s); // no time at all
|
|
CHECK_NEAR(n.along, 0.0, 0.0);
|
|
CHECK(!n.arrived);
|
|
n = NodeLineStep(0, 1.0, 100, s); // no speed: the fleet does not move
|
|
CHECK_NEAR(n.along, 0.0, 0.0);
|
|
}
|
|
|
|
static void test_resolve() {
|
|
// range = minShipRange + 0.05 (B4: the grace margin is ADDED, not subtracted)
|
|
MoveStepResult r = ResolveMoveStep(5, 10, 20);
|
|
CHECK_NEAR(r.moved, 5.0, 0.0);
|
|
CHECK(!r.arrived);
|
|
CHECK(!r.stranded);
|
|
CHECK_NEAR(PassFraction(0, 20, 5), 1.0, 0.0); // a straight leg reports a full pass
|
|
|
|
r = ResolveMoveStep(5, 3, 20); // 3.05 of range limits the step
|
|
CHECK_NEAR(r.moved, 3.05, 1e-6);
|
|
CHECK_NEAR(r.range, 3.05, 1e-6);
|
|
|
|
r = ResolveMoveStep(5, 0, 20); // no fuel at all: the RANGE is zeroed
|
|
CHECK_NEAR(r.moved, 0.0, 0.0);
|
|
CHECK_NEAR(r.range, 0.0, 0.0);
|
|
CHECK(r.stranded);
|
|
CHECK(!r.arrived);
|
|
// ... and the step survives, so it is still the divisor of the pass fraction
|
|
CHECK_NEAR(BlockedPassFraction(r.moved, 5), 0.0, 0.0);
|
|
|
|
r = ResolveMoveStep(5, 0, 0.01); // within the grace margin: it moves
|
|
CHECK_NEAR(r.moved, 0.01, 1e-9);
|
|
CHECK(!r.stranded);
|
|
CHECK(r.arrived);
|
|
|
|
r = ResolveMoveStep(50, 100, 20); // arrives with step to spare
|
|
CHECK_NEAR(r.moved, 20.0, 0.0);
|
|
CHECK(r.arrived);
|
|
|
|
r = ResolveMoveStep(5, -1, 20); // a negative range moves it BACKWARDS
|
|
CHECK(r.moved < 0); // there is no floor at zero
|
|
|
|
r = ResolveMoveStep(0, 10, 20); // zero step
|
|
CHECK_NEAR(r.moved, 0.0, 0.0);
|
|
|
|
r = ResolveMoveStep(5, 10, 0); // already at the destination
|
|
CHECK(r.arrived);
|
|
|
|
// an empty fleet is unconstrained rather than stranded
|
|
CHECK(FleetMinShipRange({}, 0.0) > 1e30);
|
|
CHECK_NEAR(FleetMinShipRange({10, 3, 7}, 0.05), 3.05, 1e-6);
|
|
CHECK_NEAR(FleetMinShipRange({10, 3, 7}, 0.0), 3.0, 1e-6);
|
|
|
|
CHECK_NEAR(ConsumeShipRange(10, 3, false), 7.0, 0.0);
|
|
CHECK_NEAR(ConsumeShipRange(2, 3, false), 0.0, 0.0);
|
|
CHECK_NEAR(ConsumeShipRange(10, 3, true), 10.0, 0.0); // range-exempt: pays nothing
|
|
|
|
CHECK_NEAR(RemainingPassTime(0.4, 1.0), 0.6, 1e-7);
|
|
CHECK_NEAR(RemainingPassTime(0.4, 0.5), 0.3, 1e-7);
|
|
CHECK_NEAR(RemainingPassTime(0.99995, 1.0), 0.0, 0.0);
|
|
CHECK_NEAR(RemainingPassTime(1.0, 1.0), 0.0, 0.0);
|
|
// the threshold is the widened float literal and the test is strict
|
|
CHECK_NEAR(RemainingPassTime(kPassCompleteFraction, 1.0), 0.0, 0.0);
|
|
CHECK(RemainingPassTime(0.99989, 1.0) > 0);
|
|
|
|
// only a node waypoint (type 3) reports a partial fraction
|
|
CHECK_NEAR(PassFraction(3, 4, 10), 0.4, 1e-7);
|
|
CHECK_NEAR(PassFraction(3, 40, 10), 1.0, 0.0); // clamped
|
|
CHECK_NEAR(PassFraction(2, 4, 10), 1.0, 0.0); // node LINE is type 2: full pass
|
|
CHECK(IsNodeWaypoint(3));
|
|
CHECK(!IsNodeWaypoint(2));
|
|
CHECK(IsGateTransitWaypoint(4) && IsGateTransitWaypoint(5));
|
|
CHECK(!IsGateTransitWaypoint(3) && !IsGateTransitWaypoint(0) && !IsGateTransitWaypoint(9));
|
|
}
|
|
|
|
static void test_multi_waypoint_turn() {
|
|
// A fleet with speed 10 and plenty of range covers a 4-unit node leg, then continues
|
|
// with the remaining 0.6 of the turn onto the next leg.
|
|
double dt = kFullStep;
|
|
MoveStepResult a = ResolveMoveStep(StraightStep(10, dt), 100, 4);
|
|
CHECK(a.arrived);
|
|
dt = RemainingPassTime(PassFraction(3, 4, StraightStep(10, dt)), dt);
|
|
CHECK_NEAR(dt, 0.6, 1e-6);
|
|
MoveStepResult b = ResolveMoveStep(StraightStep(10, dt), 96, 20);
|
|
CHECK_NEAR(b.moved, 6.0, 1e-6);
|
|
CHECK(!b.arrived);
|
|
CHECK_NEAR(RemainingPassTime(PassFraction(0, 20, StraightStep(10, dt)), dt), 0.0, 0.0);
|
|
}
|
|
|
|
static void test_jump() {
|
|
{ // 0.7 x 1.0 > 0.5: the jump MISSES and scatters by 0.7 in a random direction,
|
|
// which costs a second draw
|
|
simtest::ScriptedRng rng({0.7f}, {0x1234u});
|
|
JumpResult j = RollProbabilisticJump(1.0, 0.5, rng);
|
|
CHECK(!j.arrived);
|
|
CHECK_NEAR(j.scatter, 0.7, 1e-7);
|
|
CHECK_EQ(j.draws, 2);
|
|
CHECK_EQ(rng.floatDraws(), std::size_t{1});
|
|
CHECK_EQ(rng.intDraws(), std::size_t{1});
|
|
}
|
|
{
|
|
simtest::ScriptedRng rng({0.3f});
|
|
JumpResult j = RollProbabilisticJump(1.0, 0.5, rng);
|
|
CHECK(j.arrived);
|
|
CHECK_NEAR(j.scatter, 0.0, 0.0);
|
|
CHECK_EQ(j.draws, 1); // an arrival costs exactly one word
|
|
CHECK_EQ(rng.intDraws(), std::size_t{0});
|
|
}
|
|
{ // efficiency scales the roll: 0.9 x 0.5 = 0.45 <= 0.5 arrives
|
|
simtest::ScriptedRng rng({0.9f});
|
|
JumpResult j = RollProbabilisticJump(0.5, 0.5, rng);
|
|
CHECK(j.arrived);
|
|
}
|
|
{ // a product exactly equal to the threshold is not "greater": it arrives
|
|
simtest::ScriptedRng rng({0.5f});
|
|
JumpResult j = RollProbabilisticJump(1.0, 0.5, rng);
|
|
CHECK(j.arrived);
|
|
}
|
|
{ // determinism: the same script gives the same outcome
|
|
auto run = [] {
|
|
simtest::ScriptedRng rng({0.6f, 0.2f, 0.95f}, {1u, 2u, 3u});
|
|
std::vector<double> out;
|
|
for (int i = 0; i < 3; ++i) out.push_back(RollProbabilisticJump(1.0, 0.5, rng).scatter);
|
|
return out;
|
|
};
|
|
CHECK(run() == run());
|
|
}
|
|
}
|
|
|
|
static void test_pass_schedule() {
|
|
// fleet 1 chases fleet 2 (no relation); fleet 3 follows allied fleet 4; fleet 5 idles.
|
|
std::vector<FleetMovementEntry> f = {
|
|
{1, 2, 0, false}, {2, 0, 0, false}, {3, 4, 1, false}, {4, 0, 0, false}, {5, 0, 0, false},
|
|
};
|
|
std::vector<MovementPass> p = PlanFleetMovement(f);
|
|
// pass 1 prey, pass 2 pursuer, pass 3 prey again, pass 4 the rest, pass 5 followers
|
|
CHECK_EQ(p.size(), std::size_t{7});
|
|
CHECK_EQ(p[0].fleetId, 2); CHECK_EQ(p[0].pass, 1); CHECK_NEAR(p[0].dt, 0.5, 0.0);
|
|
CHECK_EQ(p[1].fleetId, 1); CHECK_EQ(p[1].pass, 2); CHECK_NEAR(p[1].dt, 0.5, 0.0);
|
|
CHECK_EQ(p[2].fleetId, 2); CHECK_EQ(p[2].pass, 3); CHECK_NEAR(p[2].dt, 0.5, 0.0);
|
|
CHECK_EQ(p[3].fleetId, 1); CHECK_EQ(p[3].pass, 4); CHECK_NEAR(p[3].dt, 0.5, 0.0);
|
|
CHECK_EQ(p[4].fleetId, 4); CHECK_EQ(p[4].pass, 4); CHECK_NEAR(p[4].dt, 1.0, 0.0);
|
|
CHECK_EQ(p[5].fleetId, 5); CHECK_EQ(p[5].pass, 4); CHECK_NEAR(p[5].dt, 1.0, 0.0);
|
|
CHECK_EQ(p[6].fleetId, 3); CHECK_EQ(p[6].pass, 5); CHECK_NEAR(p[6].dt, 1.0, 0.0);
|
|
|
|
// when the pursuer catches its prey, both retire after pass 2
|
|
f[0].caught = true;
|
|
p = PlanFleetMovement(f);
|
|
CHECK_EQ(p.size(), std::size_t{5});
|
|
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, 4); CHECK_EQ(p[2].pass, 4);
|
|
CHECK_EQ(p[3].fleetId, 5); CHECK_EQ(p[3].pass, 4);
|
|
CHECK_EQ(p[4].fleetId, 3); CHECK_EQ(p[4].pass, 5);
|
|
}
|
|
|
|
static void test_gate_traffic() {
|
|
std::vector<GateTrafficEntry> f = {
|
|
{0, 4, 10}, // gate transit
|
|
{0, 5, 5}, // probabilistic jump also counts
|
|
{0, 2, 100}, // node line does not
|
|
{1, 4, -3}, // the traffic word is SIGNED
|
|
{2, -1, 99}, // no waypoints at all
|
|
};
|
|
std::vector<int> g = GateTrafficTotals(f, 3);
|
|
CHECK_EQ(g.size(), std::size_t{3});
|
|
CHECK_EQ(g[0], 15);
|
|
CHECK_EQ(g[1], -3);
|
|
CHECK_EQ(g[2], 0);
|
|
// an owner index past the player count is dropped rather than corrupting memory
|
|
CHECK_EQ(GateTrafficTotals({{9, 4, 10}}, 3)[0], 0);
|
|
}
|
|
|
|
int main() {
|
|
test_vectors();
|
|
test_steps();
|
|
test_stutter_segments();
|
|
test_resolve();
|
|
test_multi_waypoint_turn();
|
|
test_jump();
|
|
test_pass_schedule();
|
|
test_gate_traffic();
|
|
return simtest::finish("test_movement");
|
|
}
|