// The order the server applies a turn's commands in. // // This test cannot be written against an outcome, and saying so is half of its value. Both // captures the campaign holds put every non-empty list on ONE player and every command on ONE // system, so any permutation of the thirty steps produces the same save and the same counter. // The order is implemented because a later workload will need it, and the checks below are // therefore checks against the ORIGINAL'S INSTRUCTION STREAM, not against a result: // // * the six appliers the batch inlines each write the counter in place, so a watchpoint run // recovered their addresses; those addresses must increase in the same order as the steps // they belong to. That is nine of thirty positions independently pinned, and the test says // nine rather than pretending to thirty; // * the permutation must be a bijection of 1..27 and the gates must appear exactly once each, // which catches a transcription slip and nothing else; // * NEITHER the list sequence NOR the member-offset sequence may be ascending. A port that // loops `for (list = 1..27)`, or walks the block's members in memory order, is the obvious // wrong implementation and is the one thing an outcome-free test CAN rule out. #include "game/ai/apply_order.h" #include #include using namespace sots::ai; namespace { int g_checks = 0; int g_fails = 0; void check(bool ok, const std::string& what) { ++g_checks; if (!ok) { ++g_fails; std::fprintf(stderr, "FAIL: %s\n", what.c_str()); } } // Every list exactly once, every gate exactly once, and nothing else in the schedule. void TestSchedulePartitionsTheBlock() { int listSeen[kCommandListCount + 1] = {}; int gateSeen[kPrologueGateCount] = {}; int listSteps = 0, gateSteps = 0; for (int i = 0; i < kApplyStepCount; ++i) { const ApplyStep& s = kApplySchedule[i]; if (s.kind == ApplyStepKind::List) { ++listSteps; check(s.list >= 1 && s.list <= kCommandListCount, "list number in range"); if (s.list >= 1 && s.list <= kCommandListCount) ++listSeen[s.list]; } else { ++gateSteps; check(s.gateCount >= 1 && s.gateCount <= kMaxGatesPerStep, "gate count in range"); for (int g = 0; g < s.gateCount; ++g) ++gateSeen[static_cast(s.gates[g])]; } } check(listSteps == kCommandListCount, "twenty-seven list steps"); check(gateSteps == 3, "three gate loops"); for (int n = 1; n <= kCommandListCount; ++n) check(listSeen[n] == 1, "list " + std::to_string(n) + " appears exactly once"); // Five of the six gates have a located applier in this routine; the civilian-ratios gate has // none at all, and its ABSENCE from the schedule is the honest representation of that. A // schedule that quietly included it would be claiming a cost of zero for something whose cost // is unknown. for (int g = 0; g < kPrologueGateCount; ++g) { const bool located = static_cast(g) != PrologueGate::CivilianRatios; check(gateSeen[g] == (located ? 1 : 0), "gate " + std::to_string(g) + (located ? " applied once" : " has no applier here")); } } // The two orderings a wrong implementation would produce. void TestScheduleIsNeitherSortedOrder() { bool listAscending = true, offsetAscending = true; int prevList = 0, prevOffset = 0; for (int i = 0; i < kApplyStepCount; ++i) { const ApplyStep& s = kApplySchedule[i]; if (s.kind != ApplyStepKind::List) continue; if (s.list < prevList) listAscending = false; if (s.memberOffset < prevOffset) offsetAscending = false; prevList = s.list; prevOffset = s.memberOffset; } check(!listAscending, "the list sequence is NOT 1..27"); check(!offsetAscending, "the member-offset sequence is NOT ascending either"); // The two anchors a reader can check by eye against the published table. check(kApplySchedule[0].kind == ApplyStepKind::List && kApplySchedule[0].list == 6, "the batch starts at list 6"); check(kApplySchedule[kApplyStepCount - 1].kind == ApplyStepKind::GateLoop, "the batch ends on a gate loop"); check(StepIndexOfList(14) == 20, "list 14 is the twenty-first step"); check(StepIndexOfList(8) == 24, "list 8 is the twenty-fifth step"); check(StepIndexOfList(-1) == -1 && StepIndexOfList(28) == -1, "a bad list number has no step"); } // The one part of the sequence this module can re-derive rather than inherit. void TestInlinedBumpAddressesIncreaseWithTheSchedule() { unsigned prevAddr = 0; int prevStep = -1; for (int i = 0; i < kScheduleAnchorCount; ++i) { const ScheduleAnchor& a = kScheduleAnchors[i]; check(a.address > prevAddr, std::string("address increases at ") + a.what); check(a.step >= prevStep, std::string("step does not go backwards at ") + a.what); check(a.step >= 0 && a.step < kApplyStepCount, std::string("step in range at ") + a.what); prevAddr = a.address; prevStep = a.step; } // Every anchored step must be the kind the anchor claims: the four inlined LIST appliers sit // on list steps and the gate-loop anchors sit on gate steps. This is what would catch an // anchor that had been renumbered against a reordered schedule. check(kApplySchedule[18].kind == ApplyStepKind::List && kApplySchedule[18].list == 12, "anchor 0x008902fe is list 12"); check(kApplySchedule[19].kind == ApplyStepKind::List && kApplySchedule[19].list == 13, "anchor 0x008903b9 is list 13"); check(kApplySchedule[20].kind == ApplyStepKind::List && kApplySchedule[20].list == 14, "anchor 0x0089046c is list 14"); check(kApplySchedule[23].kind == ApplyStepKind::List && kApplySchedule[23].list == 7, "anchor 0x008905c8 is list 7"); check(kApplySchedule[9].kind == ApplyStepKind::GateLoop, "anchor 0x0088fdb0 is gate loop A"); check(kApplySchedule[28].kind == ApplyStepKind::GateLoop, "anchor 0x008907b1 is gate loop B"); check(kApplySchedule[29].kind == ApplyStepKind::GateLoop, "anchor 0x0089080a is gate loop C"); } // The gates are not one group, and the two that sit in the middle are the two research ones. void TestGatesAreSplitAcrossTheRun() { const ApplyStep& a = kApplySchedule[9]; check(a.kind == ApplyStepKind::GateLoop && a.gateCount == 3, "gate loop A tests three gates"); check(a.gates[0] == PrologueGate::Group5 && a.gates[1] == PrologueGate::ResearchTarget && a.gates[2] == PrologueGate::ResearchRate, "gate loop A order: group5, target, rate"); check(kApplySchedule[28].gates[0] == PrologueGate::ResearchBoost, "gate loop B is the boost"); check(kApplySchedule[29].gates[0] == PrologueGate::Group4, "gate loop C is group 4"); // Nine list steps run before any gate is tested, and eighteen run after. int before = 0; for (int i = 0; i < 9; ++i) before += kApplySchedule[i].kind == ApplyStepKind::List ? 1 : 0; check(before == 9, "nine lists are applied before the first gate is even read"); } // The cost half of the schedule, restated from the step rather than from the list number, so a // step whose `bumps` flag disagreed with the 1..16 rule would be caught here. void TestStepCostMatchesTheListCostRule() { for (int i = 0; i < kApplyStepCount; ++i) { const ApplyStep& s = kApplySchedule[i]; if (s.kind != ApplyStepKind::List) continue; check(s.bumps == ListAdvancesModCount(static_cast(s.list)), "step cost agrees with the list cost rule for list " + std::to_string(s.list)); } } } // namespace int main() { TestSchedulePartitionsTheBlock(); TestScheduleIsNeitherSortedOrder(); TestInlinedBumpAddressesIncreaseWithTheSchedule(); TestGatesAreSplitAcrossTheRun(); TestStepCostMatchesTheListCostRule(); std::printf("game_ai/apply_order: %d checks, %d failures\n", g_checks, g_fails); return g_fails == 0 ? 0 : 1; }