sots-engine/src/game/ai/tasks.h
alex 29ea2b65fa game/ai: the strategic AI's task vocabulary and its ordering policy
First module of game/ai, and the first piece of Rung B that is not scaffolding. It is the two
halves of the AI's task selection that are pure: the 33-value task type enumeration and the
ranking that decides which goal the AI acts on first.

  * the priority table, verbatim -- higher runs first, and it is the entire default policy;
  * the five overrides, kept out of the table on purpose. The two artifact tasks ignore their
    table entries (1 and 2) and return 1260/1261; a port that only copied the table would rank
    them last instead of fourth and fifth. The two tuned invade priorities are INPUTS
    (TaskPriorityPolicy), not constants, because their loader is not yet identified;
  * Rank() as a stable descending sort. The original sorts a std::list, so stability is the
    behaviour, not a choice -- ties keep creation order;
  * CreationOrder(species, policyNonZero), because that is what breaks the ties. Four arms: the
    NPC species builds nothing, Hiver is the only arm with the gate families, Zuul the only one
    with NodeBore, everyone else shares a fourth. Both defensive families are gated on the
    player's policy value and DefendGateIncoming is Hiver-only on top of that.

193 checks in tests/game_ai, every expected value read off the original's tables rather than
produced by running this code. ctest 46/46 -> 47/47; clean-room check OK.

Derivation: sots-re findings/subsystems/ai-task-system.md (lane AI2), sections 1-3.
2026-09-08 14:43:51 -04:00

135 lines
5.9 KiB
C++

// The strategic AI's task vocabulary and its ordering policy.
//
// The AI does not search and it does not score. Once a turn it rebuilds a list of candidate
// tasks -- which task families it builds at all depends on the player's species -- sorts that
// list by a per-task-type priority, and then walks it twice, calling each task's Execute with
// pass 0 and then pass 1. This header is the two halves of that which are pure data: the task
// type enumeration and the priority function, plus a stable ranking that reproduces the
// original's sort exactly.
//
// Three details are easy to get wrong and are the reason this is a module rather than a table:
//
// * The priority function is a lookup on the task's type id, but five task types override it.
// Two of them (the artifact tasks) override with a CONSTANT that is nothing like their table
// entry -- port only the table and they rank last instead of near the top.
// * The sort is a stable list sort, descending. Ties therefore keep the order the tasks were
// created in, which makes the per-species creation order part of the answer, not an
// implementation detail. CreationOrder() carries it.
// * The NPC species creates no strategic tasks at all.
//
// Pure: no state, no I/O, no random draws.
// CONFIDENCE: high on the enumeration, the priority table and the sort; the two tuned
// priorities (see TaskPriorityPolicy) are inputs this module does not own, and the creation
// order is the call order of the per-family creators, not a claim about what each creates.
#pragma once
#include <cstddef>
#include <vector>
#include "game/sim/species.h"
namespace sots::ai {
// The complete task type space. Values are the ids the tasks report for themselves; they index
// the priority table directly, so the two ids with no surviving task type are kept as holes
// rather than closed up.
enum class TaskType : int {
Steamroll = 0x00,
Explore = 0x01,
ExploreInForce = 0x02,
EscortGate = 0x03,
EscortGateInvade = 0x04,
EscortGateInvadeGoal = 0x05,
DeployGateAt = 0x06,
Colonize = 0x07,
ColonizeGoal = 0x08,
ColonizeAt = 0x09,
Invade = 0x0a,
InvadeGate = 0x0b,
InvadeGoal = 0x0c,
Retired0d = 0x0d, // no task type survives with this id; the priority entry does
DefendColonyIncoming = 0x0e,
Retired0f = 0x0f, // likewise
DefendGateIncoming = 0x10,
KillEasterEgg = 0x11,
InterceptEnemy = 0x12,
Mining = 0x13,
MiningReturn = 0x14,
AttackBlockade = 0x15,
AdvanceIdleShips = 0x16,
StockFreighters = 0x17,
RespondAttackSystem = 0x18,
RespondDefendSystem = 0x19,
NodeBore = 0x1a,
BuildStations = 0x1b,
BuildPoliceShips = 0x1c,
BuildDeepScanShips = 0x1d,
Raid = 0x1e,
RetrieveArtifact = 0x1f,
ReturnArtifact = 0x20,
};
constexpr int kTaskTypeCount = 0x21;
// The name each task type reports for itself. Empty for the two retired ids.
const char* TaskTypeName(TaskType t);
// True for the two ids that have a priority but no task type.
constexpr bool IsRetiredTaskType(TaskType t) {
return t == TaskType::Retired0d || t == TaskType::Retired0f;
}
// The priority every task type gets from the shared table. This is the whole default ranking
// policy; higher runs first. Out-of-range ids yield 0, as the original's bounds check does.
//
// NOTE: for RetrieveArtifact and ReturnArtifact this is NOT the priority those tasks actually
// use -- they override it. Prefer PriorityOf(), which applies the overrides.
int TablePriority(TaskType t);
// The four inputs the priority function needs that are not this module's to know. Two are
// tunables held outside the task code; the other two are per-instance state.
struct TaskPriorityPolicy {
// The priority an Invade / EscortGateInvade task takes while its "committed" flag is clear.
// Held as two separate tunables in the original rather than in the shared table.
int uncommittedInvade = 0;
int uncommittedEscortGateInvade = 0;
};
// One task, as far as ranking is concerned.
struct RankedTask {
TaskType type = TaskType::Steamroll;
// Set for Invade / EscortGateInvade once the task has committed. When clear, those two
// types take the tuned priority from TaskPriorityPolicy instead of the table's.
bool committed = true;
// AttackBlockade is the one task whose priority depends on what other tasks exist; the
// caller supplies the result. Ignored for every other type.
bool overridePriority = false;
int priority = 0;
// Opaque to this module. Carried through the ranking so callers can recover their own task.
const void* handle = nullptr;
};
// The priority a task actually ranks by: the table, with the five overrides applied.
int PriorityOf(const RankedTask& t, const TaskPriorityPolicy& policy);
// Rank a candidate list the way the original does: a STABLE sort, descending by PriorityOf.
// Equal priorities keep their input order, which is why the input order matters -- see
// CreationOrder().
void Rank(std::vector<RankedTask>& tasks, const TaskPriorityPolicy& policy);
// Which task families a species builds candidates for, in the order they are built. Ties in
// Rank() are broken by this order, so it is part of the ordering policy.
//
// Four distinct arms: the NPC species builds nothing; the Hiver arm is the only one that builds
// the gate families; the Zuul arm is the only one that builds NodeBore; everyone else shares a
// fourth. Two families -- DefendColonyIncoming and DefendGateIncoming -- are additionally gated
// on the player's policy value being non-zero, and DefendGateIncoming is Hiver-only even inside
// the Hiver arm's own gate.
//
// `policyNonZero` is the player's policy field; pass false to suppress the defensive families.
std::vector<TaskType> CreationOrder(sim::Species species, bool policyNonZero);
// True when this species builds no strategic tasks at all.
constexpr bool BuildsNoTasks(sim::Species species) { return species == sim::Species::NPC; }
} // namespace sots::ai