342 lines
16 KiB
C++
342 lines
16 KiB
C++
// Task vocabulary and ordering-policy cases.
|
|
//
|
|
// Every expected value here was read off the original's own tables, not produced by running
|
|
// this code. The cases worth keeping are:
|
|
// * the full priority table as a golden list, because it IS the ordering policy;
|
|
// * the two artifact tasks, whose real priority is nothing like their table entry -- a
|
|
// port that only copied the table would rank them last instead of fourth and fifth;
|
|
// * the tie order, because the original sorts a std::list (a stable merge sort) and the
|
|
// per-species creation order is therefore load-bearing;
|
|
// * the NPC species building nothing, which is a whole arm of the original's switch;
|
|
// * DefendGateIncoming being Hiver-only even when the policy gate is open.
|
|
#include "game/ai/tasks.h"
|
|
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
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, const void* h = nullptr) {
|
|
RankedTask r;
|
|
r.type = t;
|
|
r.handle = h;
|
|
return r;
|
|
}
|
|
|
|
bool Contains(const std::vector<TaskType>& v, TaskType t) {
|
|
for (TaskType x : v)
|
|
if (x == t) return true;
|
|
return false;
|
|
}
|
|
|
|
// ---- the priority table, verbatim ----------------------------------------------------------
|
|
void TestTable() {
|
|
struct {
|
|
TaskType t;
|
|
int prio;
|
|
const char* name;
|
|
} expect[] = {
|
|
{TaskType::Steamroll, 1250, "AITSteamroll"},
|
|
{TaskType::Explore, 600, "AITExplore"},
|
|
{TaskType::ExploreInForce, 550, "AITExploreInForce"},
|
|
{TaskType::EscortGate, 700, "AITEscortGate"},
|
|
{TaskType::EscortGateInvade, 400, "AITEscortGateInvade"},
|
|
{TaskType::EscortGateInvadeGoal, 950, "AITEscortGateInvadeGoal"},
|
|
{TaskType::DeployGateAt, 1400, "AITDeployGateAt"},
|
|
{TaskType::Colonize, 900, "AITColonize"},
|
|
{TaskType::ColonizeGoal, 970, "AITColonizeGoal"},
|
|
{TaskType::ColonizeAt, 1300, "AITColonizeAt"},
|
|
{TaskType::Invade, 500, "AITInvade"},
|
|
{TaskType::InvadeGate, 1000, "AITInvadeGate"},
|
|
{TaskType::InvadeGoal, 930, "AITInvadeGoal"},
|
|
{TaskType::Retired0d, 200, ""},
|
|
{TaskType::DefendColonyIncoming, 1100, "AITDefendColonyIncoming"},
|
|
{TaskType::Retired0f, 300, ""},
|
|
{TaskType::DefendGateIncoming, 1200, "AITDefendGateIncoming"},
|
|
{TaskType::KillEasterEgg, 800, "AITKillEasterEgg"},
|
|
{TaskType::InterceptEnemy, 850, "AITInterceptEnemy"},
|
|
{TaskType::Mining, 350, "AITMining"},
|
|
{TaskType::MiningReturn, 375, "AITMiningReturn"},
|
|
{TaskType::AttackBlockade, 100, "AITAttackBlockade"},
|
|
{TaskType::AdvanceIdleShips, 0, "AITAdvanceIdleShips"},
|
|
{TaskType::StockFreighters, 50, "AITStockFreighters"},
|
|
{TaskType::RespondAttackSystem, 980, "AITRespondAttackSystem"},
|
|
{TaskType::RespondDefendSystem, 990, "AITRespondDefendSystem"},
|
|
{TaskType::NodeBore, 1275, "AITNodeBore"},
|
|
{TaskType::BuildStations, 910, "AITBuildStations"},
|
|
{TaskType::BuildPoliceShips, 75, "AITBuildPoliceShips"},
|
|
{TaskType::BuildDeepScanShips, 60, "AITBuildDeepScanShips"},
|
|
{TaskType::Raid, 399, "AITRaid"},
|
|
{TaskType::RetrieveArtifact, 1, "AITRetrieveArtifact"},
|
|
{TaskType::ReturnArtifact, 2, "AITReturnArtifact"},
|
|
};
|
|
for (const auto& e : expect) {
|
|
check(TablePriority(e.t) == e.prio,
|
|
"table priority of id " + std::to_string(static_cast<int>(e.t)));
|
|
check(std::string(TaskTypeName(e.t)) == e.name,
|
|
"name of id " + std::to_string(static_cast<int>(e.t)));
|
|
}
|
|
check(sizeof(expect) / sizeof(expect[0]) == kTaskTypeCount, "table covers every id");
|
|
|
|
// Out of range yields 0, as the original's bounds check does.
|
|
check(TablePriority(static_cast<TaskType>(0x21)) == 0, "id 0x21 is out of range");
|
|
check(TablePriority(static_cast<TaskType>(-1)) == 0, "negative id is out of range");
|
|
|
|
check(IsRetiredTaskType(TaskType::Retired0d), "0x0d is retired");
|
|
check(IsRetiredTaskType(TaskType::Retired0f), "0x0f is retired");
|
|
check(!IsRetiredTaskType(TaskType::Raid), "Raid is not retired");
|
|
}
|
|
|
|
// ---- the five overrides --------------------------------------------------------------------
|
|
void TestOverrides() {
|
|
TaskPriorityPolicy pol;
|
|
check(pol.flaggedInvade == 650, "the flagged Invade priority defaults to the original's 650");
|
|
check(pol.flaggedEscortGateInvade == 750,
|
|
"the flagged EscortGateInvade priority defaults to the original's 750");
|
|
pol.flaggedInvade = 4242;
|
|
pol.flaggedEscortGateInvade = 777;
|
|
|
|
// The artifact tasks ignore their table entries entirely.
|
|
check(PriorityOf(T(TaskType::RetrieveArtifact), pol) == 1260, "RetrieveArtifact overrides to 1260");
|
|
check(PriorityOf(T(TaskType::ReturnArtifact), pol) == 1261, "ReturnArtifact overrides to 1261");
|
|
check(TablePriority(TaskType::RetrieveArtifact) == 1, "and its dead table entry is still 1");
|
|
|
|
// A port that only copied the table would put them last; they are actually fourth and fifth.
|
|
check(PriorityOf(T(TaskType::ReturnArtifact), pol) < TablePriority(TaskType::ColonizeAt),
|
|
"artifacts rank below ColonizeAt");
|
|
check(PriorityOf(T(TaskType::RetrieveArtifact), pol) > TablePriority(TaskType::Steamroll),
|
|
"artifacts rank above Steamroll");
|
|
|
|
// Invade / EscortGateInvade take the flagged value when bit 0 is SET. The polarity is the
|
|
// whole point of this block: the original tests `not al; test al,1; je <tunable>`, so the
|
|
// HIGHER priority is the flag-set state. Getting it backwards silently reorders the list.
|
|
RankedTask flagged = T(TaskType::Invade);
|
|
flagged.priorityFlagBit0 = true;
|
|
check(PriorityOf(flagged, pol) == 4242, "a flagged Invade takes the flagged priority");
|
|
check(PriorityOf(T(TaskType::Invade), pol) == 500, "an unflagged Invade takes the table");
|
|
|
|
RankedTask ug = T(TaskType::EscortGateInvade);
|
|
ug.priorityFlagBit0 = true;
|
|
check(PriorityOf(ug, pol) == 777, "a flagged EscortGateInvade takes the flagged priority");
|
|
check(PriorityOf(T(TaskType::EscortGateInvade), pol) == 400, "unflagged takes the table");
|
|
|
|
// Both flagged priorities RAISE the task above its table entry -- the reason the polarity
|
|
// matters is that the flag makes these tasks more urgent, not less.
|
|
TaskPriorityPolicy real;
|
|
RankedTask fi = T(TaskType::Invade);
|
|
fi.priorityFlagBit0 = true;
|
|
check(PriorityOf(fi, real) == 650, "the flag raises Invade 500 -> 650");
|
|
RankedTask fe = T(TaskType::EscortGateInvade);
|
|
fe.priorityFlagBit0 = true;
|
|
check(PriorityOf(fe, real) == 750, "the flag raises EscortGateInvade 400 -> 750");
|
|
|
|
// The flag is meaningless for every other type.
|
|
RankedTask other = T(TaskType::Raid);
|
|
other.priorityFlagBit0 = true;
|
|
check(PriorityOf(other, pol) == 399, "the priority flag does not affect Raid");
|
|
|
|
// AttackBlockade is the one whose priority the caller supplies.
|
|
RankedTask ab = T(TaskType::AttackBlockade);
|
|
check(PriorityOf(ab, pol) == 100, "AttackBlockade defaults to the table");
|
|
ab.overridePriority = true;
|
|
ab.priority = 1234;
|
|
check(PriorityOf(ab, pol) == 1234, "AttackBlockade takes a supplied priority");
|
|
|
|
// ...and only AttackBlockade does.
|
|
RankedTask notAb = T(TaskType::Mining);
|
|
notAb.overridePriority = true;
|
|
notAb.priority = 1234;
|
|
check(PriorityOf(notAb, pol) == 350, "a supplied priority is ignored for other types");
|
|
}
|
|
|
|
// ---- ranking --------------------------------------------------------------------------------
|
|
void TestRank() {
|
|
TaskPriorityPolicy pol;
|
|
|
|
std::vector<RankedTask> v = {
|
|
T(TaskType::AdvanceIdleShips), // 0
|
|
T(TaskType::DeployGateAt), // 1400
|
|
T(TaskType::Mining), // 350
|
|
T(TaskType::RetrieveArtifact), // 1260 by override
|
|
T(TaskType::Raid), // 399
|
|
T(TaskType::ColonizeAt), // 1300
|
|
};
|
|
Rank(v, pol);
|
|
check(v[0].type == TaskType::DeployGateAt, "rank[0] DeployGateAt 1400");
|
|
check(v[1].type == TaskType::ColonizeAt, "rank[1] ColonizeAt 1300");
|
|
check(v[2].type == TaskType::RetrieveArtifact, "rank[2] RetrieveArtifact 1260 (override)");
|
|
check(v[3].type == TaskType::Raid, "rank[3] Raid 399");
|
|
check(v[4].type == TaskType::Mining, "rank[4] Mining 350");
|
|
check(v[5].type == TaskType::AdvanceIdleShips, "rank[5] AdvanceIdleShips 0");
|
|
|
|
// Stability: three tasks of one type keep their input order. The original sorts a
|
|
// std::list, so this is not an implementation choice -- it is the behaviour.
|
|
const int a = 1, b = 2, c = 3;
|
|
std::vector<RankedTask> ties = {
|
|
T(TaskType::Raid, &a),
|
|
T(TaskType::DeployGateAt),
|
|
T(TaskType::Raid, &b),
|
|
T(TaskType::Raid, &c),
|
|
};
|
|
Rank(ties, pol);
|
|
check(ties[0].type == TaskType::DeployGateAt, "the higher priority still leads");
|
|
check(ties[1].handle == &a && ties[2].handle == &b && ties[3].handle == &c,
|
|
"ties keep creation order");
|
|
|
|
// Ranking is idempotent -- a second pass must not reshuffle the ties.
|
|
std::vector<RankedTask> again = ties;
|
|
Rank(again, pol);
|
|
for (std::size_t i = 0; i < ties.size(); ++i)
|
|
check(again[i].handle == ties[i].handle, "re-ranking is stable at index " + std::to_string(i));
|
|
|
|
// The flagged priorities participate in the ordering, so substituting them changes the rank.
|
|
TaskPriorityPolicy hot;
|
|
hot.flaggedInvade = 9999;
|
|
RankedTask flagged = T(TaskType::Invade);
|
|
flagged.priorityFlagBit0 = true;
|
|
std::vector<RankedTask> mixed = {T(TaskType::DeployGateAt), flagged};
|
|
Rank(mixed, hot);
|
|
check(mixed[0].type == TaskType::Invade, "a hot flagged Invade outranks DeployGateAt");
|
|
|
|
// With the real values a flagged Invade is 650: below EscortGate's 700, above its own 500.
|
|
TaskPriorityPolicy real;
|
|
std::vector<RankedTask> band = {T(TaskType::Invade), flagged, T(TaskType::EscortGate)};
|
|
Rank(band, real);
|
|
check(band[0].type == TaskType::EscortGate, "EscortGate 700 leads");
|
|
check(band[1].priorityFlagBit0, "the flagged Invade 650 is second");
|
|
check(band[2].type == TaskType::Invade && !band[2].priorityFlagBit0,
|
|
"and the unflagged Invade 500 is last");
|
|
|
|
std::vector<RankedTask> empty;
|
|
Rank(empty, pol);
|
|
check(empty.empty(), "ranking an empty list is a no-op");
|
|
}
|
|
|
|
// ---- per-species creation order --------------------------------------------------------------
|
|
void TestCreationOrder() {
|
|
check(BuildsNoTasks(Species::NPC), "the NPC species builds no tasks");
|
|
check(CreationOrder(Species::NPC, true).empty(), "...and its creation order is empty");
|
|
check(CreationOrder(Species::NPC, false).empty(), "...with the policy gate shut too");
|
|
|
|
for (Species s : {Species::Human, Species::Hiver, Species::Tarkas, Species::Liir,
|
|
Species::Zuul, Species::Morrigi}) {
|
|
check(!CreationOrder(s, true).empty(), "a playable species builds tasks");
|
|
// Steamroll is created first in every arm.
|
|
check(CreationOrder(s, true).front() == TaskType::Steamroll, "Steamroll leads every arm");
|
|
}
|
|
|
|
// Only the Hiver arm builds the gate families.
|
|
for (TaskType gate : {TaskType::DeployGateAt, TaskType::EscortGate, TaskType::EscortGateInvade,
|
|
TaskType::InvadeGate}) {
|
|
check(Contains(CreationOrder(Species::Hiver, true), gate), "Hiver builds a gate family");
|
|
check(!Contains(CreationOrder(Species::Human, true), gate), "Human does not");
|
|
check(!Contains(CreationOrder(Species::Zuul, true), gate), "Zuul does not");
|
|
}
|
|
|
|
// Only the Zuul arm builds NodeBore, and it is second, right after Steamroll.
|
|
check(CreationOrder(Species::Zuul, true)[1] == TaskType::NodeBore, "Zuul builds NodeBore second");
|
|
check(!Contains(CreationOrder(Species::Human, true), TaskType::NodeBore), "Human does not");
|
|
check(!Contains(CreationOrder(Species::Hiver, true), TaskType::NodeBore), "Hiver does not");
|
|
|
|
// The Zuul arm is also the one that skips BuildPoliceShips and plain Explore.
|
|
check(!Contains(CreationOrder(Species::Zuul, true), TaskType::BuildPoliceShips),
|
|
"Zuul builds no police ships");
|
|
check(!Contains(CreationOrder(Species::Zuul, true), TaskType::Explore),
|
|
"Zuul builds ExploreInForce but not Explore");
|
|
check(Contains(CreationOrder(Species::Zuul, true), TaskType::ExploreInForce), "...it does build that");
|
|
|
|
// The Hiver arm skips KillEasterEgg and the plain explore pair.
|
|
check(!Contains(CreationOrder(Species::Hiver, true), TaskType::KillEasterEgg),
|
|
"Hiver skips KillEasterEgg");
|
|
check(!Contains(CreationOrder(Species::Hiver, true), TaskType::Explore), "Hiver skips Explore");
|
|
|
|
// The four species that share the default arm produce identical orders.
|
|
const std::vector<TaskType> human = CreationOrder(Species::Human, true);
|
|
for (Species s : {Species::Tarkas, Species::Liir, Species::Morrigi})
|
|
check(CreationOrder(s, true) == human, "the default arm is shared");
|
|
|
|
// The policy gate suppresses both defensive families, in every species.
|
|
for (Species s : {Species::Human, Species::Hiver, Species::Zuul}) {
|
|
const std::vector<TaskType> off = CreationOrder(s, false);
|
|
check(!Contains(off, TaskType::DefendColonyIncoming), "policy 0 suppresses DefendColony");
|
|
check(!Contains(off, TaskType::DefendGateIncoming), "policy 0 suppresses DefendGate");
|
|
check(Contains(CreationOrder(s, true), TaskType::DefendColonyIncoming),
|
|
"policy non-zero restores DefendColony");
|
|
}
|
|
|
|
// DefendGateIncoming is Hiver-only even with the gate open.
|
|
check(Contains(CreationOrder(Species::Hiver, true), TaskType::DefendGateIncoming),
|
|
"Hiver gets DefendGateIncoming");
|
|
for (Species s : {Species::Human, Species::Tarkas, Species::Liir, Species::Zuul, Species::Morrigi})
|
|
check(!Contains(CreationOrder(s, true), TaskType::DefendGateIncoming),
|
|
"no one else gets DefendGateIncoming");
|
|
|
|
// Neither retired id is ever created.
|
|
for (Species s : {Species::Human, Species::Hiver, Species::Zuul}) {
|
|
check(!Contains(CreationOrder(s, true), TaskType::Retired0d), "0x0d is never created");
|
|
check(!Contains(CreationOrder(s, true), TaskType::Retired0f), "0x0f is never created");
|
|
}
|
|
|
|
// The goal group is created as a block, in order, in every arm that has it.
|
|
for (Species s : {Species::Human, Species::Hiver, Species::Zuul}) {
|
|
const std::vector<TaskType> v = CreationOrder(s, true);
|
|
std::size_t i = 0;
|
|
while (i < v.size() && v[i] != TaskType::ColonizeGoal) ++i;
|
|
check(i + 3 < v.size(), "the goal group is present");
|
|
if (i + 3 < v.size()) {
|
|
check(v[i + 1] == TaskType::EscortGateInvadeGoal, "goal group order 1");
|
|
check(v[i + 2] == TaskType::Invade, "goal group order 2");
|
|
check(v[i + 3] == TaskType::InvadeGoal, "goal group order 3");
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- the two together ------------------------------------------------------------------------
|
|
void TestCreationOrderBreaksTies() {
|
|
// A Zuul AI holding one of everything it can create: the ranking is fully determined by the
|
|
// table, and where the table ties, by the creation order. Nothing else is consulted.
|
|
TaskPriorityPolicy pol;
|
|
std::vector<RankedTask> v;
|
|
for (TaskType t : CreationOrder(Species::Zuul, true)) v.push_back(T(t));
|
|
const std::vector<RankedTask> before = v;
|
|
Rank(v, pol);
|
|
|
|
// ColonizeAt (1300) leads, not NodeBore (1275) -- the Zuul arm creates NodeBore second but
|
|
// it does not rank first, and no gate task (DeployGateAt 1400) exists in a Zuul list at all.
|
|
check(v.front().type == TaskType::ColonizeAt, "ColonizeAt (1300) leads a Zuul list");
|
|
check(v[1].type == TaskType::NodeBore, "NodeBore (1275) is second");
|
|
check(v.back().type == TaskType::AdvanceIdleShips, "AdvanceIdleShips (0) trails it");
|
|
|
|
for (std::size_t i = 1; i < v.size(); ++i)
|
|
check(PriorityOf(v[i - 1], pol) >= PriorityOf(v[i], pol), "the result is non-increasing");
|
|
|
|
// Same multiset in, same multiset out.
|
|
check(v.size() == before.size(), "ranking preserves the count");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
TestTable();
|
|
TestOverrides();
|
|
TestRank();
|
|
TestCreationOrder();
|
|
TestCreationOrderBreaksTies();
|
|
std::printf("game/ai tasks: %d checks, %d failures\n", g_checks, g_fails);
|
|
return g_fails == 0 ? 0 : 1;
|
|
}
|