141 lines
6.4 KiB
C++
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
|