// Hand-computed cases for the fleet path-planning rules. // // Every expected value was worked out from the rule, not by running the code. The cases most // worth keeping are: // * the species drive table, because it is the entire answer to "why is kind 2 never seen"; // * the fuel check's asymmetric squaring, which is the one floating-point detail in this // subsystem that flips a decision; // * the gate transit clearing the grounded flag, which is a rule two separate subsystems // reach independently; // * the leading-destination drop shifting the output array, which is a defect in the // original that this module reproduces on purpose. #include "game/nav/pathplan.h" #include #include #include using namespace sots::nav; namespace { int g_checks = 0; int g_fails = 0; void check(bool ok, const char* what) { ++g_checks; if (!ok) { ++g_fails; std::fprintf(stderr, "FAIL: %s\n", what); } } MapObject System(int id, int idx, double x = 0, double y = 0, double z = 0) { MapObject o; o.kind = ObjectKind::System; o.id = id; o.systemIndex = idx; o.pos = {x, y, z}; o.ownedByUs = true; o.ownerIsFriendly = true; return o; } MapObject Point(int id, double x = 0, double y = 0, double z = 0) { MapObject o; o.kind = ObjectKind::Point; o.id = id; o.pos = {x, y, z}; return o; } MapObject Fleet(int id) { MapObject o; o.kind = ObjectKind::Fleet; o.id = id; return o; } FleetState NodeFleet() { FleetState f; f.ownerSpecies = Species::Human; f.rangeRemaining = 1000.0; f.rangeFull = 1000.0; f.tankCapacity = 1000.0; return f; } // A graph with a single line between two named system indices. struct OneLineGraph : NodeGraph { int a, b, index; bool boreSucceeds = false; mutable int boreCalls = 0; OneLineGraph(int a_, int b_, int i) : a(a_), b(b_), index(i) {} int FindLine(int x, int y) const override { if ((x == a && y == b) || (x == b && y == a)) return index; return -1; } bool BoreLine(int, int) const override { ++boreCalls; return boreSucceeds; } }; // ----------------------------------------------------------------------------------------- // The drive table -- the type-2 question // ----------------------------------------------------------------------------------------- void TestDriveTable() { check(DriveOf(Species::Human) == WaypointKind::NodeRoute, "Human flies the node drive"); check(DriveOf(Species::Hiver) == WaypointKind::None, "Hiver has no straight-line kind"); check(DriveOf(Species::Tarkas) == WaypointKind::StraightA, "Tarkas kind 1"); check(DriveOf(Species::Liir) == WaypointKind::Stutter, "Liir kind 2"); check(DriveOf(Species::NPC) == WaypointKind::None, "NPC kind 0"); check(DriveOf(Species::Zuul) == WaypointKind::NodeRoute, "Zuul flies the node drive"); check(DriveOf(Species::Morrigi) == WaypointKind::StraightB, "Morrigi kind 6"); check(DriveOf(static_cast(9)) == WaypointKind::None, "out-of-range species is 0"); // The reachability argument, stated as a test: no node-drive race can produce kind 2 and // the kind-2 race cannot produce kind 3, for ANY leg, because the drive is a pure function // of species and the node branch is entered only for the node drive. check(DriveOf(Species::Human) != WaypointKind::Stutter && DriveOf(Species::Zuul) != WaypointKind::Stutter, "neither node race can produce kind 2"); check(DriveOf(Species::Liir) != WaypointKind::NodeRoute, "the kind-2 race is not a node race"); check(IsNodeKind(3) && !IsNodeKind(2) && !IsNodeKind(4), "only kind 3 is a node waypoint"); check(IsGateTransitKind(4) && IsGateTransitKind(5) && !IsGateTransitKind(3) && !IsGateTransitKind(2) && !IsGateTransitKind(6), "only kinds 4 and 5 are gate transits"); check(!IsNodeKind(-1) && !IsGateTransitKind(99), "out-of-range kinds are neither"); } // ----------------------------------------------------------------------------------------- // A non-node species short-circuits // ----------------------------------------------------------------------------------------- void TestNonNodeSpeciesShortCircuit() { FleetState f = NodeFleet(); f.ownerSpecies = Species::Liir; f.rangeRemaining = 0.0; // no fuel at all f.tankCapacity = 0.0; const MapObject a = System(16, 0, 0, 0, 0); const MapObject b = System(32, 1, 1000, 0, 0); // absurdly far OneLineGraph g(0, 1, 7); const LegResult r = ClassifyLeg(f, a, b, f.rangeRemaining, &g); check(r.kind == WaypointKind::Stutter, "a stutter leg is kind 2 regardless of distance"); check(r.flags == 0u, "and raises no range complaint -- no range check is performed"); check(r.route.pathIndex == -1 && r.route.fromId == 0 && r.route.toId == 0, "a non-node leg records an empty route"); // The same geometry for a node race does complain. FleetState h = NodeFleet(); h.rangeRemaining = 0.0; h.tankCapacity = 0.0; const LegResult rh = ClassifyLeg(h, a, b, h.rangeRemaining, &g); check(rh.kind == WaypointKind::None, "the node race cannot make the same leg"); check((rh.flags & kNodeLegOutOfRange) != 0u, "and says why"); } // ----------------------------------------------------------------------------------------- // The route record, and the save-visible invariant // ----------------------------------------------------------------------------------------- void TestRouteRecord() { FleetState f = NodeFleet(); const MapObject a = System(80, 0, 0, 0, 0); const MapObject b = System(272, 1, 3, 4, 0); // distance 5 OneLineGraph g(0, 1, 53); const LegResult r = ClassifyLeg(f, a, b, f.rangeRemaining, &g); check(r.kind == WaypointKind::NodeRoute, "an existing line gives a node route"); check(r.route.pathIndex == 53, "the line's index is recorded"); check(r.route.fromId == 80 && r.route.toId == 272, "the endpoints' handles are recorded"); // The invariant every save must satisfy: a non-node kind records nothing. FleetState t = NodeFleet(); t.ownerSpecies = Species::Tarkas; const LegResult rt = ClassifyLeg(t, a, b, t.rangeRemaining, &g); check(rt.kind == WaypointKind::StraightA, "a straight-drive leg"); check(rt.route.pathIndex == -1 && rt.route.fromId == 0 && rt.route.toId == 0, "kind != 3 implies an empty route record"); } void TestBoredLineHasNoIndex() { FleetState f = NodeFleet(); f.ownerSpecies = Species::Zuul; f.canBoreNodeLines = true; const MapObject a = System(80, 0, 0, 0, 0); const MapObject b = System(272, 1, 3, 4, 0); OneLineGraph g(5, 6, 99); // no line between 0 and 1 g.boreSucceeds = true; const LegResult r = ClassifyLeg(f, a, b, f.rangeRemaining, &g); check(r.kind == WaypointKind::NodeRoute, "a bored line still gives a node route"); check(g.boreCalls == 1, "and it was actually bored"); check(r.route.pathIndex == -1, "a freshly bored line records index -1, not a real index"); check(r.route.fromId == 80 && r.route.toId == 272, "with the endpoints still recorded"); g.boreSucceeds = false; const LegResult rf = ClassifyLeg(f, a, b, f.rangeRemaining, &g); check(rf.kind == WaypointKind::None && (rf.flags & kBoreFailed) != 0u, "a failed bore says so"); FleetState nb = f; nb.canBoreNodeLines = false; const LegResult rn = ClassifyLeg(nb, a, b, nb.rangeRemaining, &g); check((rn.flags & kNoLineAndCannotBore) != 0u, "a fleet that cannot bore says so instead"); check((rn.flags & kBoreFailed) == 0u, "and does not also claim the bore failed"); } void TestBoredLineOutOfRangeIsADistinctFlag() { FleetState f = NodeFleet(); f.ownerSpecies = Species::Zuul; f.canBoreNodeLines = true; f.rangeRemaining = 1.0; f.tankCapacity = 1.0; const MapObject a = System(80, 0, 0, 0, 0); const MapObject b = System(272, 1, 3, 4, 0); // distance 5, well beyond 1 OneLineGraph missing(5, 6, 99); missing.boreSucceeds = true; const LegResult r = ClassifyLeg(f, a, b, f.rangeRemaining, &missing); check((r.flags & kBoredLineOutOfRange) != 0u, "out of range AFTER boring is its own flag"); check((r.flags & kNodeLegOutOfRange) == 0u, "and is not the existing-line flag"); OneLineGraph present(0, 1, 12); const LegResult r2 = ClassifyLeg(f, a, b, f.rangeRemaining, &present); check((r2.flags & kNodeLegOutOfRange) != 0u, "out of range on an EXISTING line is the other"); check((r2.flags & kBoredLineOutOfRange) == 0u, "and not the bored one"); } // ----------------------------------------------------------------------------------------- // The fuel check -- the one float that decides an outcome // ----------------------------------------------------------------------------------------- void TestRangeCheckSquaringAsymmetry() { // Exact case first: a 3-4-5 triangle is exact in binary, so range 5 must just reach. check(LegInRange(Vec3{0, 0, 0}, Vec3{3, 4, 0}, 5.0, 1e9), "distance exactly equal is in range"); check(!LegInRange(Vec3{0, 0, 0}, Vec3{3, 4, 0}, 4.999, 1e9), "a hair short is out of range"); // The capacity cap bites even when plenty of fuel remains. check(!LegInRange(Vec3{0, 0, 0}, Vec3{3, 4, 0}, 1e9, 4.0), "the tank capacity caps the range"); check(LegInRange(Vec3{0, 0, 0}, Vec3{3, 4, 0}, 1e9, 5.0), "and permits it when large enough"); // The asymmetry itself, and it must actually be exercised -- a case that silently skips // is a check that compared nothing. `100.00000762939453` is the float32 just above 100; // its exact square is 10000.001525878964 and the float32 rounding of that square is // 10000.001953125, i.e. it rounds UP by ~4.3e-4. Any squared distance strictly between the // two is IN range under a "narrow the square too" rule and OUT of range under the real one. const double r = static_cast(100.00000762939453f); const double exactSquare = r * r; const double narrowedSquare = static_cast(static_cast(exactSquare)); check(narrowedSquare > exactSquare, "the chosen range's float32 square really does round up (else the next two checks " "would be vacuous)"); const double between = 0.5 * (exactSquare + narrowedSquare); check(between > exactSquare && between < narrowedSquare, "and the probe sits between them"); check(!LegInRange(between, r, 1e9), "a squared distance above the EXACT square is out of range"); check(LegInRange(exactSquare, r, 1e9), "while the exact square itself is inclusive"); // Stated the other way round: a float32 squaring would have accepted the probe, so this // pins the direction of the disagreement, not merely its existence. check(between <= narrowedSquare, "a float32 squaring would have called the same probe in range"); // A degenerate fleet: zero capacity means only a zero-length leg is in range. check(LegInRange(0.0, 0.0, 0.0), "a zero leg is in range with no fuel"); check(!LegInRange(1e-12, 0.0, 0.0), "any leg at all is not"); } void TestLegLengthNarrowsTheDeltas() { // A pair whose exact difference is not representable in float32. The rule narrows each // difference before squaring, so the answer is the length of the NARROWED delta. const double big = 1.0; const double tiny = 1.0e-9; // lost when 1.0 + tiny is stored as a float const double got = LegLength(Vec3{big + tiny, 0, 0}, Vec3{0, 0, 0}); const double expected = static_cast(static_cast(std::sqrt(static_cast( static_cast(static_cast(static_cast(big + tiny - 0.0)) * static_cast(static_cast(big + tiny - 0.0))))))); check(got == expected, "the leg length narrows the delta, the sum and the root"); check(LegLength(Vec3{0, 0, 0}, Vec3{3, 4, 0}) == 5.0, "and is exact on an exact triangle"); check(LegLength(Vec3{1, 2, 3}, Vec3{1, 2, 3}) == 0.0, "a zero leg has zero length"); } // ----------------------------------------------------------------------------------------- // Gate transits // ----------------------------------------------------------------------------------------- FleetState GateFleet() { FleetState f; f.ownerSpecies = Species::Hiver; f.rangeRemaining = 1000.0; f.rangeFull = 1000.0; f.tankCapacity = 1000.0; f.gateProjectionRadius = 10.0; f.gateTrafficCapacity = 100; f.gateTrafficCost = 5; return f; } void TestGateKinds() { FleetState f = GateFleet(); MapObject a = System(16, 0, 0, 0, 0); MapObject b = System(32, 1, 3, 4, 0); // distance 5, inside the radius a.weHaveGateHere = true; b.weHaveGateHere = true; check(ClassifyLeg(f, a, b, f.rangeRemaining, nullptr).kind == WaypointKind::GateToGate, "gate at both ends is kind 4"); b.weHaveGateHere = false; check(ClassifyLeg(f, a, b, f.rangeRemaining, nullptr).kind == WaypointKind::GateProjected, "gate at one end, within the radius, is kind 5"); // Outside the radius there is no gate transit at all, so the leg falls back to the drive. MapObject far = System(48, 2, 100, 0, 0); const LegResult rf = ClassifyLeg(f, a, far, f.rangeRemaining, nullptr); check(rf.kind == WaypointKind::None, "beyond the radius the gate race falls back to kind 0"); // The radius is inclusive, and a zero radius disables projection entirely. f.gateProjectionRadius = 5.0; check(ClassifyLeg(f, a, b, f.rangeRemaining, nullptr).kind == WaypointKind::GateProjected, "the projection radius is inclusive"); f.gateProjectionRadius = 0.0; check(ClassifyLeg(f, a, b, f.rangeRemaining, nullptr).kind == WaypointKind::None, "a zero projection radius disables the throw"); } void TestGateTrafficCapacity() { FleetState f = GateFleet(); MapObject a = System(16, 0, 0, 0, 0); MapObject b = System(32, 1, 3, 4, 0); a.weHaveGateHere = true; b.weHaveGateHere = true; f.gateTrafficUsed = 96; // 96 + 5 > 100 const LegResult over = ClassifyLeg(f, a, b, f.rangeRemaining, nullptr); check((over.flags & kGateTrafficExceeded) != 0u, "over capacity raises the traffic flag"); check(over.kind == WaypointKind::None, "and the leg becomes kind 0"); check(!OrderRefused(over.flags), "but the order is NOT refused -- a plan can be installed over gate capacity"); f.gateTrafficUsed = 95; // 95 + 5 == 100, not over check(ClassifyLeg(f, a, b, f.rangeRemaining, nullptr).kind == WaypointKind::GateToGate, "exactly at capacity is allowed"); // A fleet already on a gate leg is already counted and must not be counted twice. f.gateTrafficUsed = 100; f.alreadyOnGateLeg = true; check(ClassifyLeg(f, a, b, f.rangeRemaining, nullptr).kind == WaypointKind::GateToGate, "a fleet already on a gate leg does not pay again"); } void TestGateWaivesTheGroundedRefusal() { FleetState f = GateFleet(); f.anyShipGrounded = true; MapObject a = System(16, 0, 0, 0, 0); MapObject b = System(32, 1, 3, 4, 0); a.weHaveGateHere = true; b.weHaveGateHere = true; const LegResult gate = ClassifyLeg(f, a, b, f.rangeRemaining, nullptr); check((gate.flags & kFleetGrounded) == 0u, "a gate transit clears the grounded flag"); check(!OrderRefused(gate.flags), "so a dead-drive fleet may still be thrown through a gate"); // Without a gate, the same fleet is refused. a.weHaveGateHere = false; b.weHaveGateHere = false; const LegResult walk = ClassifyLeg(f, a, b, f.rangeRemaining, nullptr); check((walk.flags & kFleetGrounded) != 0u, "without a gate the flag stands"); check(OrderRefused(walk.flags), "and the order is refused"); } // ----------------------------------------------------------------------------------------- // The three refusal bits, and the warnings that are not refusals // ----------------------------------------------------------------------------------------- void TestRefusalMask() { check(kOrderRefusalMask == 0x418u, "the refusal mask is exactly three bits"); check(OrderRefused(kCannotInterceptFleet), "cannot-intercept refuses"); check(OrderRefused(kFleetGrounded), "grounded refuses"); check(OrderRefused(kDestPointNotPermitted), "forbidden point refuses"); for (unsigned bit : {kShipActionsWillCancel, kNodeLegOutOfRange, kGateTrafficExceeded, kNoLineAndCannotBore, kBoredLineOutOfRange, kBoreFailed, kDestSystemNotFriendly, kSourceSystemNotFriend, kShipActionEight}) { check(!OrderRefused(bit), "every other bit is advisory"); } } void TestInterceptFlagOnlyForNodeTravellingTargets() { FleetState f = NodeFleet(); const MapObject a = System(16, 0, 0, 0, 0); MapObject target = Fleet(64); target.fleetOnNodeLeg = true; target.interceptSolved = false; const LegResult miss = ClassifyLeg(f, a, target, f.rangeRemaining, nullptr); check((miss.flags & kCannotInterceptFleet) != 0u, "an unmeetable node-travelling target refuses the order"); target.fleetOnNodeLeg = false; const LegResult idle = ClassifyLeg(f, a, target, f.rangeRemaining, nullptr); check((idle.flags & kCannotInterceptFleet) == 0u, "a target that is not node-travelling raises nothing -- there was nothing to solve"); MapObject dest = System(32, 1, 3, 4, 0); target.fleetOnNodeLeg = true; target.interceptSolved = true; target.interceptSystem = &dest; OneLineGraph g(0, 1, 21); const LegResult hit = ClassifyLeg(f, a, target, f.rangeRemaining, &g); check((hit.flags & kCannotInterceptFleet) == 0u, "a solved intercept raises nothing"); check(hit.kind == WaypointKind::NodeRoute && hit.route.pathIndex == 21, "and the leg is planned to the system the target was met at"); } void TestPointPermission() { FleetState f = NodeFleet(); const MapObject a = System(16, 0, 0, 0, 0); MapObject p = Point(200, 3, 4, 0); const LegResult denied = ClassifyLeg(f, a, p, f.rangeRemaining, nullptr); check((denied.flags & kDestPointNotPermitted) != 0u, "an unknown point is refused"); check(denied.kind == WaypointKind::None, "and a node race gets no kind for it"); // A straight-line race still reports its own kind even though the order will be refused. FleetState t = NodeFleet(); t.ownerSpecies = Species::Tarkas; const LegResult straight = ClassifyLeg(t, a, p, t.rangeRemaining, nullptr); check((straight.flags & kDestPointNotPermitted) != 0u, "same refusal"); check(straight.kind == WaypointKind::StraightA, "but a straight drive still reports its kind alongside the refusal"); p.pointKnownToUs = true; const LegResult allowed = ClassifyLeg(f, a, p, f.rangeRemaining, nullptr); check((allowed.flags & kDestPointNotPermitted) == 0u, "either mask permits it"); check(allowed.kind == WaypointKind::NodeRoute, "and a node race plans a leg to it"); check(allowed.route.pathIndex == -1, "a point endpoint records no line index"); check(allowed.route.fromId == 16 && allowed.route.toId == 200, "with both handles recorded"); } void TestFriendlinessFlags() { FleetState f = NodeFleet(); MapObject a = System(16, 0, 0, 0, 0); MapObject p = Point(200, 3, 4, 0); p.pointKnownToUs = true; a.ownedByUs = false; a.ownerIsFriendly = false; const LegResult r = ClassifyLeg(f, a, p, f.rangeRemaining, nullptr); check((r.flags & kSourceSystemNotFriend) != 0u, "an unfriendly source system, going to a point"); check(r.kind == WaypointKind::None, "and no leg"); MapObject dest = System(32, 1, 3, 4, 0); dest.ownedByUs = false; dest.ownerIsFriendly = false; MapObject fromPoint = Point(200, 0, 0, 0); fromPoint.pointKnownToUs = true; const LegResult r2 = ClassifyLeg(f, fromPoint, dest, f.rangeRemaining, nullptr); check((r2.flags & kDestSystemNotFriendly) != 0u, "an unfriendly destination, coming from a point"); } void TestAdvisoryBitsAreSeparate() { FleetState f = NodeFleet(); f.anyShipActionEight = true; const MapObject a = System(16, 0, 0, 0, 0); const MapObject b = System(32, 1, 3, 4, 0); OneLineGraph g(0, 1, 5); const LegResult only8 = ClassifyLeg(f, a, b, f.rangeRemaining, &g); check((only8.flags & kShipActionEight) != 0u, "the singled-out action raises its own bit"); check((only8.flags & kShipActionsWillCancel) == 0u, "and NOT the general one -- it is taken out of the mask first"); check(!OrderRefused(only8.flags), "neither refuses"); check(only8.kind == WaypointKind::NodeRoute, "and the leg is still planned"); f.anyShipActingOther = true; const LegResult both = ClassifyLeg(f, a, b, f.rangeRemaining, &g); check((both.flags & (kShipActionEight | kShipActionsWillCancel)) == (kShipActionEight | kShipActionsWillCancel), "both can be raised together"); } // ----------------------------------------------------------------------------------------- // The walk // ----------------------------------------------------------------------------------------- struct FullGraph : NodeGraph { int FindLine(int, int) const override { return 1; } }; void TestWalkAccumulates() { FleetState f = NodeFleet(); const MapObject start = System(16, 0, 0, 0, 0); const MapObject b = System(32, 1, 3, 4, 0); const MapObject c = System(48, 2, 6, 8, 0); FullGraph g; const std::vector dests{&b, &c}; const PathPlan plan = SolvePath(f, start, dests, &g); check(plan.kinds.size() == 2, "one kind per destination"); check(plan.kinds[0] == WaypointKind::NodeRoute && plan.kinds[1] == WaypointKind::NodeRoute, "both legs are node routes"); check(!plan.droppedLeadingDestination, "nothing was dropped"); check(plan.flags == 0u && plan.firstFailingLeg == -1 && !plan.refused, "and nothing complained"); // The route chain: each leg's origin is the previous leg's destination, and the first // leg's origin is where the fleet started. This is the shape a save file shows. check(plan.routes[0].fromId == 16 && plan.routes[0].toId == 32, "leg 0 runs start -> b"); check(plan.routes[1].fromId == 32 && plan.routes[1].toId == 48, "leg 1 runs b -> c"); check(plan.routes[1].fromId == plan.routes[0].toId, "so the chain closes"); } void TestWalkRecordsTheFirstFailureOnly() { FleetState f = NodeFleet(); f.rangeRemaining = 4.0; // not enough for a distance-5 leg f.rangeFull = 4.0; f.tankCapacity = 4.0; const MapObject start = System(16, 0, 0, 0, 0); const MapObject b = System(32, 1, 3, 4, 0); const MapObject c = System(48, 2, 6, 8, 0); FullGraph g; const PathPlan plan = SolvePath(f, start, {&b, &c}, &g); check(plan.firstFailingLeg == 0, "the FIRST failing leg is recorded, not the last"); check((plan.flags & kNodeLegOutOfRange) != 0u, "and the flags are the OR across all legs"); check(!plan.refused, "running out of fuel does not refuse the order"); } void TestRefuellingResetsTheBudget() { FleetState f = NodeFleet(); f.rangeRemaining = 6.0; f.rangeFull = 6.0; f.tankCapacity = 6.0; const MapObject start = System(16, 0, 0, 0, 0); MapObject b = System(32, 1, 3, 4, 0); // 5 from start const MapObject c = System(48, 2, 6, 8, 0); // 5 further FullGraph g; // Without a refuel at b, the second leg has only 1.0 left and fails. const PathPlan dry = SolvePath(f, start, {&b, &c}, &g); check((dry.flags & kNodeLegOutOfRange) != 0u, "the second leg runs dry"); check(dry.firstFailingLeg == 1, "and it is the second leg that failed, not the first"); // With one, the budget goes back to full and both legs pass. b.weCanRefuelHere = true; const PathPlan wet = SolvePath(f, start, {&b, &c}, &g); check(wet.flags == 0u, "refuelling at the intermediate system carries the fleet through"); } void TestLeadingDestinationDropShiftsTheOutput() { FleetState f = NodeFleet(); const MapObject start = System(16, 0, 0, 0, 0); f.currentSystem = &start; const MapObject b = System(32, 1, 3, 4, 0); const MapObject c = System(48, 2, 6, 8, 0); FullGraph g; // Ordering the fleet to the system it is already at, then onward. const PathPlan plan = SolvePath(f, start, {&start, &b, &c}, &g); check(plan.droppedLeadingDestination, "the leading destination was dropped"); check(plan.kinds.size() == 3, "but the output still has one slot per destination"); check(plan.kinds[0] == WaypointKind::NodeRoute && plan.kinds[1] == WaypointKind::NodeRoute, "the two real legs land in slots 0 and 1"); check(plan.kinds[2] == WaypointKind::None, "and the LAST slot is never written -- this is the original's off-by-one, reproduced"); check(plan.routes[2].fromId == 0 && plan.routes[2].toId == 0, "the trailing route record is likewise untouched"); check(plan.routes[0].fromId == 16 && plan.routes[0].toId == 32, "slot 0 describes the leg to the SECOND destination, not the first"); // Without the drop, everything lines up. FleetState g2 = f; g2.currentSystem = nullptr; const PathPlan ok = SolvePath(g2, start, {&b, &c}, &g); check(!ok.droppedLeadingDestination && ok.kinds.size() == 2, "no drop, no shift"); } void TestEmptyAndDegenerateInputs() { FleetState f = NodeFleet(); const MapObject start = System(16, 0, 0, 0, 0); FullGraph g; const PathPlan none = SolvePath(f, start, {}, &g); check(none.kinds.empty() && none.flags == 0u && !none.refused, "an empty order plans nothing"); check(none.firstFailingLeg == -1, "and reports no failing leg"); // A leg to the system we are standing on is not a leg at all. const LegResult self = ClassifyLeg(f, start, start, f.rangeRemaining, &g); check(self.kind == WaypointKind::None, "a node leg from a system to itself is not a crossing"); // A shipless fleet has no drive. FleetState empty = NodeFleet(); empty.hasShips = false; const MapObject b = System(32, 1, 3, 4, 0); check(ClassifyLeg(empty, start, b, empty.rangeRemaining, &g).kind == WaypointKind::None, "a fleet with no ships has no drive"); // A NaN budget clamps to zero rather than propagating. FleetState nan = NodeFleet(); nan.rangeRemaining = std::numeric_limits::quiet_NaN(); nan.tankCapacity = 10.0; const PathPlan p = SolvePath(nan, start, {&b}, &g); check((p.flags & kNodeLegOutOfRange) != 0u, "a NaN fuel budget clamps to zero, not to infinity"); } } // namespace int main() { TestDriveTable(); TestNonNodeSpeciesShortCircuit(); TestRouteRecord(); TestBoredLineHasNoIndex(); TestBoredLineOutOfRangeIsADistinctFlag(); TestRangeCheckSquaringAsymmetry(); TestLegLengthNarrowsTheDeltas(); TestGateKinds(); TestGateTrafficCapacity(); TestGateWaivesTheGroundedRefusal(); TestRefusalMask(); TestInterceptFlagOnlyForNodeTravellingTargets(); TestPointPermission(); TestFriendlinessFlags(); TestAdvisoryBitsAreSeparate(); TestWalkAccumulates(); TestWalkRecordsTheFirstFailureOnly(); TestRefuellingResetsTheBudget(); TestLeadingDestinationDropShiftsTheOutput(); TestEmptyAndDegenerateInputs(); std::printf("game_nav pathplan: %d checks, %d failures\n", g_checks, g_fails); return g_fails == 0 ? 0 : 1; }