The behavioural compare found 8 of 45 StrategyServer::MoveFleet calls diverging by one ULP on a position component. Read off the instruction stream, the cause is that the engine's vector normalise narrows to float32 four separate times and we kept everything in double: delta.c = f32(dest.c - pos.c) stored back to a float32 slot before normalising sumsq = f32(x*x + y*y + z*z) products/adds in 53-bit regs, only the SUM stored len = f32(sqrt(sumsq)) inv = f32(1.0 / len) a reciprocal, MULTIPLIED through, not three divides dir.c = f32(delta.c * inv) and the same call returns the leg distance, so it is never recomputed in a wider precision either. The position tail was already right, which is why the error was a constant absolute ~1.2e-7 (half an ULP of the inputs) rather than a formula error. Adds NormalizeVec3 / StraightLeg / StraightLegDistance / AdvanceAlongUnitDirection and rebuilds AdvanceAlongDirection on them; the movement hook now takes both the direction and the distance from one StraightLeg call, as the original does. The arrival test is an exact float compare, so the distance has to be that same float32. Tests pin float32 BIT PATTERNS, not tolerances: one case per narrowing plus four independent legs component by component. A CHECK_NEAR would pass against the old arithmetic. sim::Distance is left in double on purpose and flagged at its declaration: it now serves only the node-line/stutter geometry, which very likely needs the same treatment but has zero behavioural coverage to correct it against. Live, same VM/save/workload, run twice by this lane: control recap-7584bad-20260908T0615Z 45 calls, 45 compared, 8 diverged, exit 1 fixed mf-45bdf7d-dirty-20260908T0721Z 45 calls, 45 compared, 0 diverged, exit 0 with identical arguments, identical pos.before and identical ORIGINAL pos.after on all 45 calls. The control reproduced the eight divergent call_ids exactly. Coverage unchanged and still thin: all 15 moving calls are the same straight-run waypoint type; types 2-5 were attempted and could not be reached (the only player that would travel a node line has no ships on this save). See docs/M-movefleet.md. ctest 32/32; tools/clean_room_check.sh OK.
406 lines
18 KiB
C++
406 lines
18 KiB
C++
#include "game/sim/movement.h"
|
|
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
|
|
#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);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// The straight-run normalise, pinned to the bit.
|
|
//
|
|
// Every one of these expectations is a float32 BIT PATTERN, because the whole point of the
|
|
// module is which precision each intermediate is held in. A `CHECK_NEAR` here would pass
|
|
// against the wrong arithmetic -- the error this pins down is one ULP.
|
|
//
|
|
// Regression: the behavioural compare caught 8 of 45 live `MoveFleet` calls diverging by
|
|
// exactly one ULP on a position component, and the cause was this file computing the
|
|
// direction in double where the original narrows to float32 four separate times.
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
static std::uint32_t f32bits(double v) {
|
|
const float f = static_cast<float>(v);
|
|
std::uint32_t u = 0;
|
|
std::memcpy(&u, &f, sizeof u);
|
|
return u;
|
|
}
|
|
|
|
static void check_pos(const Vec3& got, std::uint32_t bx, std::uint32_t by, std::uint32_t bz,
|
|
int line) {
|
|
::simtest::report(f32bits(got.x) == bx, "pos.x bits", __FILE__, line,
|
|
"got 0x" + std::to_string(f32bits(got.x)) + ", expected 0x" + std::to_string(bx));
|
|
::simtest::report(f32bits(got.y) == by, "pos.y bits", __FILE__, line,
|
|
"got 0x" + std::to_string(f32bits(got.y)) + ", expected 0x" + std::to_string(by));
|
|
::simtest::report(f32bits(got.z) == bz, "pos.z bits", __FILE__, line,
|
|
"got 0x" + std::to_string(f32bits(got.z)) + ", expected 0x" + std::to_string(bz));
|
|
}
|
|
|
|
static void test_normalise_precision() {
|
|
// 1. The sum of squares is narrowed to float32 BEFORE the square root. 1e-4 squares to
|
|
// 1e-8, which is representable, but the sum 1 + 1e-8 is not: it rounds to 1.0f, so
|
|
// the length comes out exactly 1 rather than 1.000000005.
|
|
{
|
|
const NormalizeResult n = NormalizeVec3({1.0, 1e-4, 0.0});
|
|
CHECK_EQ(f32bits(n.length), f32bits(1.0f));
|
|
CHECK(n.length == 1.0);
|
|
}
|
|
|
|
// 2. The square root is narrowed to float32 too. sqrt(2) as a double is
|
|
// 1.4142135623730951; as a float32 it is 1.41421356201171875.
|
|
{
|
|
const NormalizeResult n = NormalizeVec3({1.0, 1.0, 0.0});
|
|
CHECK_EQ(f32bits(n.length), std::uint32_t{0x3FB504F3u});
|
|
CHECK(n.length == static_cast<double>(1.41421353816986083984375f));
|
|
}
|
|
|
|
// 3. The direction is a RECIPROCAL multiply through a float32 slot, not three divides.
|
|
// For this vector float32(1/len) x c and c / len land on different float32s.
|
|
{
|
|
const NormalizeResult n = NormalizeVec3({-12.7324999f, 11.0829000f, -16.4794998f});
|
|
// float32(1 / float32(sqrt(float32(sumsq))))
|
|
const double inv = static_cast<double>(1.0f / 23.590700149536133f);
|
|
CHECK_EQ(f32bits(n.dir.x), f32bits(static_cast<double>(-12.7324999f) * inv));
|
|
CHECK_EQ(f32bits(n.length), f32bits(23.590700149536133f));
|
|
}
|
|
|
|
// 4. Below the epsilon the direction is zeroed and the length reported as 0.
|
|
{
|
|
const NormalizeResult n = NormalizeVec3({1e-9, 0, 0});
|
|
CHECK(n.length == 0.0);
|
|
CHECK(n.dir.x == 0.0 && n.dir.y == 0.0 && n.dir.z == 0.0);
|
|
}
|
|
|
|
// 5. The leg delta is stored back to float32 before the normalise. Here the exact
|
|
// difference and its float32 rounding are different numbers, and the original uses
|
|
// the rounded one.
|
|
{
|
|
const Vec3 pos{1.8504999876022339, -2.4797000885009766, 11.430100440979004};
|
|
const Vec3 dest{-10.881999969482422, 8.60319995880127, -5.0493998527526855};
|
|
CHECK_EQ(f32bits(StraightLegDistance(pos, dest)), std::uint32_t{0x41BCB9C1u});
|
|
}
|
|
}
|
|
|
|
static void test_advance_bit_exact() {
|
|
// Four independent straight-run legs at step 2.0. Each expectation is the float32 the
|
|
// original produces; the previous double-precision direction got at least one of the
|
|
// three components one ULP wrong on every one of them.
|
|
struct Case {
|
|
Vec3 pos, dest;
|
|
std::uint32_t bx, by, bz;
|
|
};
|
|
const Case cases[] = {
|
|
{{1.8504999876022339, -2.4797000885009766, 11.430100440979004},
|
|
{-10.881999969482422, 8.60319995880127, -5.0493998527526855},
|
|
0x3F45637Au, 0xBFC52208u, 0x41208718u},
|
|
{{3.333899974822998, -3.0625, 1.145900011062622},
|
|
{-10.4931001663208, -10.569600105285645, -7.057000160217285},
|
|
0x3FE33EC5u, 0xC07A27DCu, 0x3E62996Cu},
|
|
{{4.329599857330322, -1.7378000020980835, -4.4604997634887695},
|
|
{2.053499937057495, -1.1236000061035156, -4.805600166320801},
|
|
0x401AD160u, 0xBF9C7244u, 0xC0980177u},
|
|
{{-11.458499908447266, -0.9193000197410583, -7.966800212860107},
|
|
{-9.18970012664795, -10.585100173950195, 6.437600135803223},
|
|
0xC1332FA2u, 0xC0018E2Bu, 0xC0CA3E13u},
|
|
};
|
|
for (const Case& c : cases) {
|
|
check_pos(AdvanceAlongDirection(c.pos, c.dest, 2.0), c.bx, c.by, c.bz, __LINE__);
|
|
// The two-step form the hook uses must agree with the one-shot form exactly.
|
|
const NormalizeResult n = StraightLeg(c.pos, c.dest);
|
|
check_pos(AdvanceAlongUnitDirection(c.pos, n.dir, 2.0), c.bx, c.by, c.bz, __LINE__);
|
|
}
|
|
|
|
// A leg shorter than the step arrives, and an arrival copies the destination words
|
|
// verbatim rather than stepping onto them.
|
|
{
|
|
const Vec3 pos{4.329599857330322, -1.7378000020980835, -4.4604997634887695};
|
|
const Vec3 dest{3.0, -1.5, -4.5999999046325684};
|
|
const double dist = StraightLegDistance(pos, dest);
|
|
CHECK(dist < 2.0);
|
|
const MoveStepResult m = ResolveMoveStep(2.0, 100.0, dist);
|
|
CHECK(m.arrived);
|
|
// `arrived` is an EXACT float comparison, so the distance the step is capped at has
|
|
// to be the same float32 the normalise produced -- a double-precision distance here
|
|
// would make `move == distance` a coincidence rather than an identity.
|
|
CHECK(m.moved == dist);
|
|
}
|
|
}
|
|
|
|
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();
|
|
test_normalise_precision();
|
|
test_advance_bit_exact();
|
|
return simtest::finish("test_movement");
|
|
}
|