sots-engine/src/game/ai/tasks.h

141 lines
6.4 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, the sort and the two flagged
// priorities (650 / 750 -- constants with one reader and no writer in the original). The
// creation order is the call order of the per-family creators, not a claim about what each
// creates. What bit 0 of a task's flag byte MEANS is unknown; only its effect is.
#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 two priorities the Invade / EscortGateInvade overrides use in place of the table's. In the
// original these are two standalone integers rather than table entries; they are constants, not
// configuration -- each has exactly one reader in the whole image (its own override) and no
// writer anywhere, so there is nothing that could change them at runtime. Kept as a struct so a
// caller can still substitute them in a test.
struct TaskPriorityPolicy {
int flaggedInvade = 650;
int flaggedEscortGateInvade = 750;
};
// One task, as far as ranking is concerned.
struct RankedTask {
TaskType type = TaskType::Steamroll;
// Bit 0 of the task's flag byte. When SET, Invade and EscortGateInvade take the higher
// priority from TaskPriorityPolicy instead of the table's; when clear they fall through to
// the table. Deliberately named after the bit and not after a meaning: an earlier reading
// called this "committed" AND had the polarity backwards, and what the bit actually means is
// still unknown -- the two bytes on an Invade task that look like commitment state live
// elsewhere on the object.
bool priorityFlagBit0 = false;
// 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