154 lines
6.8 KiB
C++
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;
|
|
}
|