// Ship construction: the completion bookkeeping, and the two rules of the pass that the // corpus can and cannot show. // // The corpus fixtures at the bottom are NOT invented. They are the build queues of // `zuul-turn16-noderoute.sav` and the queues the same game carries one turn later in // `zuul-turn17-rollpending.sav` (frames 16 and 17, a genuine consecutive-turn pair). The // test does not assume the point totals: it SOLVES for them, and the solve is the // falsification -- a non-FIFO order, a per-order point budget, or a "skip and continue" // rule instead of "stop at the first order that cannot finish" each make the solve fail. #include "game/sim/construction.h" #include "check.h" using namespace sots::sim; // --------------------------------------------------------------------------------------- // The records a completion writes // --------------------------------------------------------------------------------------- static void test_records() { ShipRecords r; RecordShipBuilt(r, 608, 0); CHECK_EQ(r.built[0], 1); CHECK_EQ(r.designs.size(), std::size_t{1}); CHECK_EQ(r.designs[0].designKey, 608); CHECK_EQ(r.designs[0].hullClass, 0); CHECK_EQ(r.designs[0].built, 1); // A second hull of the same design finds the record rather than appending one. RecordShipBuilt(r, 608, 0); CHECK_EQ(r.designs.size(), std::size_t{1}); CHECK_EQ(r.designs[0].built, 2); CHECK_EQ(r.built[0], 2); // A different design appends, and the vector's order is first-seen. RecordShipBuilt(r, 576, 0); RecordShipBuilt(r, 1136, 2); CHECK_EQ(r.designs.size(), std::size_t{3}); CHECK_EQ(r.designs[1].designKey, 576); CHECK_EQ(r.designs[2].designKey, 1136); CHECK_EQ(r.designs[2].hullClass, 2); CHECK_EQ(r.built[0], 3); CHECK_EQ(r.built[2], 1); // Nothing here touches losses, kills or in-service. CHECK_EQ(r.lost[0], 0); CHECK_EQ(r.killed[0], 0); CHECK_EQ(r.inService[0], 0); // An out-of-range hull class leaves the class array alone but still gets its own record; // the original indexes the array unchecked, so this guard is ours and is stated as such. ShipRecords g; RecordShipBuilt(g, 7, 9); CHECK_EQ(g.built[0], 0); CHECK_EQ(g.designs.size(), std::size_t{1}); CHECK_EQ(g.designs[0].built, 1); } // --------------------------------------------------------------------------------------- // The pass // --------------------------------------------------------------------------------------- static void test_pass_reports_designs() { // {designId, orderId, con, conleft, money, moneyAvailable} std::vector q = {{608, 3, 1980, 1753, 0, true}, {576, 4, 1860, 1860, 0, true}, {576, 5, 1860, 1860, 0, true}}; const SystemConstructionResult r = RunSystemConstruction(q, 4182); CHECK_EQ(r.completed.size(), std::size_t{2}); CHECK_EQ(r.completed[0].orderId, 3); CHECK_EQ(r.completed[0].designId, 608); CHECK_EQ(r.completed[1].orderId, 4); CHECK_EQ(r.completed[1].designId, 576); CHECK_EQ(r.pointsSpent, 4182); CHECK_EQ(r.pointsLeft, 0); CHECK(r.advancedPartially); CHECK_EQ(r.ordersRemoved, 2); CHECK_EQ(q.size(), std::size_t{1}); CHECK_EQ(q[0].orderId, 5); CHECK_EQ(q[0].constructionLeft, 1291); } static void test_points_gate_skips_the_sweep() { // An order already at zero. This state cannot arise in the corpus -- the pass that // zeroes an order also erases it -- so the rule is a labelled hypothesis about a // zero-construction-cost design, and the test pins the behaviour, not a measurement. std::vector q = {{608, 3, 0, 0, 0, true}}; SystemConstructionResult r = RunSystemConstruction(q, 0); CHECK_EQ(q.size(), std::size_t{1}); // points <= 0: the whole body is skipped CHECK_EQ(r.pointsLeft, 0); CHECK(!r.sweepRan); r = RunSystemConstruction(q, -5); CHECK_EQ(q.size(), std::size_t{1}); CHECK_EQ(r.pointsLeft, -5); r = RunSystemConstruction(q, 1); // one point is enough to run the sweep CHECK(q.empty()); CHECK(r.sweepRan); CHECK_EQ(r.ordersRemoved, 1); } static void test_money_refusal_skips_not_stops() { std::vector q = {{608, 1, 100, 100, 500, false}, {576, 2, 100, 100, 500, true}}; const SystemConstructionResult r = RunSystemConstruction(q, 300); CHECK_EQ(r.completed.size(), std::size_t{1}); CHECK_EQ(r.completed[0].orderId, 2); CHECK_EQ(r.moneyCharged, 500); // The refused order keeps its points and survives the sweep; the pass did not stop at it. CHECK_EQ(q.size(), std::size_t{1}); CHECK_EQ(q[0].orderId, 1); CHECK_EQ(r.pointsLeft, 200); CHECK(!r.advancedPartially); } static void test_running_off_the_end() { std::vector q = {{608, 1, 100, 100, 0, true}}; const SystemConstructionResult r = RunSystemConstruction(q, 900); CHECK_EQ(r.pointsLeft, 800); CHECK(!r.advancedPartially); CHECK(q.empty()); } // --------------------------------------------------------------------------------------- // The corpus oracle: zuul-turn16-noderoute.sav -> zuul-turn17-rollpending.sav // --------------------------------------------------------------------------------------- // // System 80 (owner 32, an AI) and system 384 (owner 16) each hold a queue at frame 16 and a // different queue at frame 17. The AI appended one order (58) during the turn, so its // before-state is the frame-16 queue with that order pushed on the end -- the only fitted // element in this fixture, and it is fitted from the frame-17 file's own `con`/`ordID`, not // from the model. // // The test searches every point total in a wide range and asserts that the set of totals // that reproduce the observed after-state is non-empty and is a contiguous run of ONE value // per system (the transition is exact, not a band), then checks the completions against the // per-design `srb` deltas the two files carry. struct Fixture { const char* what; std::vector before; std::vector after; std::vector expectedCompletedDesigns; }; static int solve_points(const Fixture& f, int* solutions) { int found = -1; *solutions = 0; for (int p = 0; p <= 200000; ++p) { std::vector q = f.before; const SystemConstructionResult r = RunSystemConstruction(q, p); if (q.size() != f.after.size()) continue; bool same = true; for (std::size_t i = 0; i < q.size(); ++i) if (q[i].orderId != f.after[i].orderId || q[i].constructionLeft != f.after[i].constructionLeft) same = false; if (!same) continue; std::vector designs; for (const Completion& c : r.completed) designs.push_back(c.designId); if (designs != f.expectedCompletedDesigns) continue; ++*solutions; if (found < 0) found = p; } return found; } static void test_corpus_pair() { // System 384, owner 16. Frame 16: three orders. Frame 17: one, advanced by 569. Fixture human{"sys 384 / player 16", {{608, 3, 1980, 1753, 0, true}, {576, 4, 1860, 1860, 0, true}, {576, 5, 1860, 1860, 0, true}}, {{576, 5, 1860, 1291, 0, true}}, {608, 576}}; // System 80, owner 32. Frame 16: four orders of design 114. Frame 17: order 58 only, // which the AI appended during the turn (con 6974) and which was advanced by 3156. Fixture ai{"sys 80 / player 32", {{114, 54, 1889, 959, 0, true}, {114, 55, 1889, 1889, 0, true}, {114, 56, 1889, 1889, 0, true}, {114, 57, 1889, 1889, 0, true}, {816, 58, 6974, 6974, 0, true}}, {{816, 58, 6974, 3818, 0, true}}, {114, 114, 114, 114}}; for (const Fixture* f : {&human, &ai}) { int solutions = 0; const int p = solve_points(*f, &solutions); simtest::report(p >= 0, "a point total reproduces the observed transition", __FILE__, __LINE__, std::string(f->what)); simtest::report(solutions == 1, "the point total is unique", __FILE__, __LINE__, std::string(f->what) + " solutions=" + std::to_string(solutions)); } // The two totals the solve finds, stated so a change to the model is visible as a number. int n = 0; CHECK_EQ(solve_points(human, &n), 4182); CHECK_EQ(solve_points(ai, &n), 9782); // The per-design `srb` deltas the two saves carry, reproduced by feeding the solved // totals through the records model. Player 16 (index 0 on the wire): design 608 goes // 2 -> 3 and a record for 576 appears with 1. Player 32 (index 1): design 114 goes // 18 -> 22, and the class-0 counters go 2 -> 4 and 53 -> 57. { ShipRecords r; r.built[0] = 2; r.designs.push_back({656, 0, 0, 0, 2}); r.designs.push_back({608, 0, 2, 0, 2}); std::vector q = human.before; for (const Completion& c : RunSystemConstruction(q, 4182).completed) RecordShipBuilt(r, c.designId, 0); CHECK_EQ(r.built[0], 4); CHECK_EQ(r.designs.size(), std::size_t{3}); CHECK_EQ(r.designs[1].built, 3); // 608 CHECK_EQ(r.designs[2].designKey, 576); // appended, in first-seen order CHECK_EQ(r.designs[2].built, 1); } { ShipRecords r; r.built[0] = 53; r.designs.push_back({816, 0, 6, 0, 8}); r.designs.push_back({18, 0, 23, 0, 23}); r.designs.push_back({34, 0, 4, 0, 4}); r.designs.push_back({114, 0, 18, 0, 18}); r.designs.push_back({130, 0, 2, 0, 2}); std::vector q = ai.before; for (const Completion& c : RunSystemConstruction(q, 9782).completed) RecordShipBuilt(r, c.designId, 0); CHECK_EQ(r.built[0], 57); CHECK_EQ(r.designs.size(), std::size_t{5}); // nothing appended CHECK_EQ(r.designs[3].built, 22); // 114 CHECK_EQ(r.designs[0].built, 6); // 816 did not complete } } int main() { test_records(); test_pass_reports_designs(); test_points_gate_skips_the_sweep(); test_money_refusal_skips_not_stops(); test_running_off_the_end(); test_corpus_pair(); return simtest::finish("game_sim_construction"); }