// The turn's phase list and the two-pass task walk. // // The cases that matter: // * the dead tail. Five phases run after the turn is submitted and one of them issues a colonize // order; a port that lets them through is over on the modification counter every turn the AI // holds a colony ship. This is tested through the client, not by asserting a flag, because the // flag is the claim and the refusal is the behaviour. // * the species restriction on the one phase that has one, because it is the reason a whole // prologue gate has never been observed set; // * the first pass writing nothing even when the task action does not check the pass. The // original gates the writers, not the callers; a model that relied on every task being // well-behaved would be a different model. // * the second pass visiting the same tasks in the same order as the first -- the list is ranked // once and not re-sorted or pruned between passes. #include "game/ai/agent.h" #include #include #include using namespace sots::ai; using sots::sim::Species; 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()); } } RankedTask T(TaskType t) { RankedTask r; r.type = t; return r; } void TestPhaseTable() { const auto& phases = ProcessTurnPhases(); check(phases.size() == static_cast(kProcessTurnPhaseCount), "thirty-four phases"); for (int i = 0; i < kProcessTurnPhaseCount; ++i) { check(phases[static_cast(i)].index == i, "phase " + std::to_string(i) + " is in position"); } check(phases[kSubmitPhase].emits == PhaseEmission::Submit, "phase 28 submits"); check(!phases[kSubmitPhase].dead, "the submit phase is not itself dead"); for (int i = 0; i < kProcessTurnPhaseCount; ++i) { check(phases[static_cast(i)].dead == (i > kSubmitPhase), "phase " + std::to_string(i) + " deadness"); } check(phases[32].emits == PhaseEmission::Colonize, "phase 32 would issue a colonize order"); check(phases[32].dead, "but it runs after the submit and is refused"); } void TestPhaseCanEmit() { const auto& phases = ProcessTurnPhases(); // The species-restricted phase, from both sides. check(PhaseCanEmit(phases[2], Species::Hiver), "phase 2 emits for the one species that runs it"); check(!PhaseCanEmit(phases[2], Species::Human), "and for nobody else"); check(!PhaseCanEmit(phases[2], Species::Tarkas), "including the reference game's AI species"); // The dead tail is dead for every species. check(!PhaseCanEmit(phases[32], Species::Hiver), "the colonize phase is dead even for Hiver"); // The live emitters. check(PhaseCanEmit(phases[kTaskListPhase], Species::Tarkas), "the task phase emits"); check(PhaseCanEmit(phases[21], Species::Tarkas), "so does the system-rates phase"); check(PhaseCanEmit(phases[18], Species::Tarkas), "so does the research-target phase"); // A phase with no traced emission never claims one. check(!PhaseCanEmit(phases[1], Species::Tarkas), "an untraced phase emits nothing"); // The submit phase is not an emitter. check(!PhaseCanEmit(phases[kSubmitPhase], Species::Tarkas), "the submit phase is not an emitter"); } void TestDeadTailThroughTheClient() { OrderClient c(32); const auto& phases = ProcessTurnPhases(); int refused = 0; for (const auto& p : phases) { if (p.emits == PhaseEmission::Submit) { c.EndTurn(0.8f); continue; } if (p.emits == PhaseEmission::Colonize) { if (!c.OrderColonize(ColonizeOrder{})) ++refused; } } check(refused == 1, "walking the whole phase list refuses exactly the one late colonize"); check(c.block().ElementCount(CommandList::Colonize) == 0, "and the block carries no colonize order"); } void TestTwoPassSilence() { std::vector tasks = {T(TaskType::Colonize), T(TaskType::Explore), T(TaskType::AdvanceIdleShips)}; OrderClient client(32); TaskPriorityPolicy policy; std::vector passesSeen; std::vector> orderPerPass(2); int accepted = 0; // Deliberately a task action that does NOT check the pass: the original gates the writers. RunTaskList(tasks, policy, client, [&](const RankedTask& t, int pass, OrderClient& c) { if (orderPerPass[static_cast(pass)].empty()) passesSeen.push_back(pass); orderPerPass[static_cast(pass)].push_back(t.type); if (c.OrderBuild(BuildOrder{})) ++accepted; }); check(passesSeen.size() == 2 && passesSeen[0] == 0 && passesSeen[1] == 1, "two passes, 0 then 1"); check(accepted == 3, "only the second pass's three attempts were accepted"); check(client.block().ElementCount(CommandList::Build) == 3, "and only three builds reached the block"); check(orderPerPass[0] == orderPerPass[1], "the second pass visits the same tasks in the same order"); check(orderPerPass[0].size() == 3, "every task is visited in every pass"); // Ranked descending: Colonize 900, Explore 600, AdvanceIdleShips 0. check(orderPerPass[0][0] == TaskType::Colonize, "highest priority first"); check(orderPerPass[0][2] == TaskType::AdvanceIdleShips, "the sweep-up task last"); check(tasks[0].type == TaskType::Colonize, "the caller's list is left ranked"); } void TestTaskWalkAfterSubmit() { // A task walk that somehow ran after the submit still writes nothing: the latch outranks the // pass gate. std::vector tasks = {T(TaskType::Colonize)}; OrderClient client(32); client.EndTurn(0.8f); int accepted = 0; RunTaskList(tasks, TaskPriorityPolicy{}, client, [&](const RankedTask&, int, OrderClient& c) { if (c.OrderBuild(BuildOrder{})) ++accepted; }); check(accepted == 0, "the submit latch outranks the pass gate"); } void TestEmptyTaskList() { std::vector tasks; OrderClient client(32); int calls = 0; RunTaskList(tasks, TaskPriorityPolicy{}, client, [&](const RankedTask&, int, OrderClient&) { ++calls; }); check(calls == 0, "an empty task list runs no actions"); check(client.OrdersAccepted(), "and leaves the pass gate disengaged"); check(TaskWalkPasses().size() == 2, "there are exactly two passes"); } void TestNpcSpeciesBuildsNothing() { // The species that creates no tasks reaches the task phase and finds nothing to do. The phase // is still live -- it is the list that is empty, which is a different statement. check(CreationOrder(Species::NPC, true).empty(), "the NPC species builds no tasks"); check(PhaseCanEmit(ProcessTurnPhases()[kTaskListPhase], Species::NPC), "the task phase is still live for it"); } } // namespace int main() { TestPhaseTable(); TestPhaseCanEmit(); TestDeadTailThroughTheClient(); TestTwoPassSilence(); TestTaskWalkAfterSubmit(); TestEmptyTaskList(); TestNpcSpeciesBuildsNothing(); std::printf("game_ai/agent: %d checks, %d failures\n", g_checks, g_fails); return g_fails == 0 ? 0 : 1; }