// Stepping order and pass-model cases. // // Every expectation here was read off the original's instruction stream, not produced by running // this code. The cases worth keeping are: // * index order, because the whole point is that the order is the player array's own and not a // scheduling decision -- a port that sorted or bucketed the queue would reorder the save's // command blocks and move its modification counter; // * the dedup, because the original checks the queue before appending and a port that did not // would double-run a player under any path that raises the event twice; // * the eliminated/Status interaction, because the original's TWO walks over the same array // make a live player's Status irrelevant and an eliminated player's decisive; // * pass 0 emitting nothing, which is the halving of the ModCount arithmetic; // * pass 1 gathering BOTH tiers, because it redoes tier 0 rather than replacing it. #include "game/ai/turn_order.h" #include #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()); } } AiTurnPlayer Ai(int netId) { AiTurnPlayer p; p.netId = netId; p.aiControlled = true; return p; } AiTurnPlayer Human(int netId) { AiTurnPlayer p; p.netId = netId; return p; } // ---- the pass model ------------------------------------------------------------------------ void TestPasses() { check(kPasses[0] == TaskPass::Reserve, "the first pass is Reserve"); check(kPasses[1] == TaskPass::Fill, "the second pass is Fill"); check(static_cast(TaskPass::Reserve) == 0, "Reserve is the literal 0 the original passes"); check(static_cast(TaskPass::Fill) == 1, "Fill is the literal 1"); // The load-bearing one: pass 0 writes no commands. Every emitting path in the original -- // the route issuer, the fleet-order issuer and the build request -- returns early unless the // pass is 1. If this were wrong, every AI order would cost twice the ModCount it does. check(!PassEmitsCommands(TaskPass::Reserve), "Reserve emits no commands"); check(PassEmitsCommands(TaskPass::Fill), "Fill emits commands"); // The tier loop is `i = 0 .. pass` inclusive, so Fill REDOES tier 0 before adding tier 1. std::vector reserve = TiersForPass(TaskPass::Reserve); check(reserve.size() == 1, "Reserve gathers one tier"); check(reserve[0] == QuotaTier::First, "and it is the first quota"); std::vector fill = TiersForPass(TaskPass::Fill); check(fill.size() == 2, "Fill gathers two tiers"); check(fill[0] == QuotaTier::First, "Fill redoes the first quota"); check(fill[1] == QuotaTier::Second, "and then fills the second"); // The tiers are the two quota field selectors, in that numeric order. check(static_cast(QuotaTier::First) == 0, "tier 0 selects the first quota field"); check(static_cast(QuotaTier::Second) == 1, "tier 1 selects the second"); } // ---- who receives the resume event --------------------------------------------------------- void TestResumeEligibility() { AiTurnPlayer live = Ai(7); check(ResumeZeroesStatus(live), "a live player's Status is zeroed"); check(ReceivesResumeEvent(live), "and it therefore always receives the event"); // A live player's incoming Status is irrelevant: the first walk overwrites it. AiTurnPlayer liveDirty = Ai(7); liveDirty.status = 99; check(ReceivesResumeEvent(liveDirty), "a live player with a non-zero Status still qualifies"); // An eliminated player is skipped by the first walk, so its Status decides. AiTurnPlayer deadBusy = Ai(8); deadBusy.eliminated = true; deadBusy.status = 3; check(!ResumeZeroesStatus(deadBusy), "an eliminated player's Status is not zeroed"); check(!ReceivesResumeEvent(deadBusy), "and a non-zero Status keeps it out"); AiTurnPlayer deadIdle = Ai(9); deadIdle.eliminated = true; deadIdle.status = 0; check(!ResumeZeroesStatus(deadIdle), "still not zeroed"); check(ReceivesResumeEvent(deadIdle), "but a zero Status lets an eliminated player through"); } // ---- the stepping order -------------------------------------------------------------------- void TestSteppingOrder() { // Index order, not net-id order. The ids here are deliberately descending so a port that // sorted the queue would fail. std::vector players = {Ai(30), Ai(20), Ai(10)}; std::vector order = AiSteppingOrder(players); check(order.size() == 3, "three AI players give three entries"); check(order[0] == 30 && order[1] == 20 && order[2] == 10, "the queue is in player-array order, not sorted"); // Humans are delivered inline and never enter the queue. std::vector mixed = {Human(1), Ai(2), Human(3), Ai(4)}; std::vector mixedOrder = AiSteppingOrder(mixed); check(mixedOrder.size() == 2, "only the AI players queue"); check(mixedOrder[0] == 2 && mixedOrder[1] == 4, "and they keep their array positions"); // A zero net id is rejected before the AI test. std::vector zero = {Ai(0), Ai(5)}; std::vector zeroOrder = AiSteppingOrder(zero); check(zeroOrder.size() == 1 && zeroOrder[0] == 5, "net id 0 never queues"); // Eliminated AI players drop out on the Status rule, not on a separate check. AiTurnPlayer dead = Ai(6); dead.eliminated = true; dead.status = 1; std::vector withDead = {Ai(5), dead, Ai(7)}; std::vector deadOrder = AiSteppingOrder(withDead); check(deadOrder.size() == 2, "an eliminated AI with a live Status is skipped"); check(deadOrder[0] == 5 && deadOrder[1] == 7, "and the survivors keep their order"); // The dedup: the same net id twice queues once, and keeps its FIRST position. std::vector dup = {Ai(11), Ai(12), Ai(11)}; std::vector dupOrder = AiSteppingOrder(dup); check(dupOrder.size() == 2, "a repeated net id queues once"); check(dupOrder[0] == 11 && dupOrder[1] == 12, "and keeps the earlier position"); // Degenerate cases. check(AiSteppingOrder({}).empty(), "no players, no queue"); check(AiSteppingOrder({Human(1), Human(2)}).empty(), "an all-human game queues nothing"); // An NPC-species AI still queues -- it creates no tasks, but the queue does not know that. // Recording it so a later lane does not "fix" this module when it finds the NPC arm empty. check(AiSteppingOrder({Ai(1)}).size() == 1, "the queue does not filter on species"); } } // namespace int main() { TestPasses(); TestResumeEligibility(); TestSteppingOrder(); std::printf("game_ai/turn_order: %d checks, %d failures\n", g_checks, g_fails); return g_fails == 0 ? 0 : 1; }