sots-engine/tests/game_ai/test_turn_order.cpp

154 lines
6.8 KiB
C++

// 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 <cstdio>
#include <string>
#include <vector>
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<int>(TaskPass::Reserve) == 0, "Reserve is the literal 0 the original passes");
check(static_cast<int>(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<QuotaTier> 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<QuotaTier> 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<int>(QuotaTier::First) == 0, "tier 0 selects the first quota field");
check(static_cast<int>(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<AiTurnPlayer> players = {Ai(30), Ai(20), Ai(10)};
std::vector<int> 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<AiTurnPlayer> mixed = {Human(1), Ai(2), Human(3), Ai(4)};
std::vector<int> 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<AiTurnPlayer> zero = {Ai(0), Ai(5)};
std::vector<int> 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<AiTurnPlayer> withDead = {Ai(5), dead, Ai(7)};
std::vector<int> 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<AiTurnPlayer> dup = {Ai(11), Ai(12), Ai(11)};
std::vector<int> 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;
}