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

101 lines
5.6 KiB
C++

// Which AI players run, in what order, and what the two Execute passes mean.
//
// The strategic AI does not run on a schedule of its own. Once the server resumes play it walks
// its player array ONCE, in index order, and raises a "resume playing" event at each player. For
// a human that event is delivered inline. For an AI it is instead appended to a pending queue on
// the application object -- deduplicated -- and the whole queue is drained back-to-back inside a
// single frame later on, in the order it was filled.
//
// So the order the AI players are stepped is not a scheduling decision and it is not affected by
// think time: it is the player array's own order, filtered twice. That matters because it is the
// order the players' command blocks are appended in, which is the order the save's modification
// counter advances in. It is computable from a save with no game state at all, which is what
// this header is.
//
// The second half is the two passes. Each selected task's Execute runs twice, with 0 and then 1.
// The argument is NOT a plan/act switch -- it is a tier index. Every fleet request carries two
// force quotas, a smaller one and a larger one; the tier selects which is in force, and the
// gathering loop runs `for (i = 0; i <= pass; ++i)`, so pass 1 redoes tier 0 and then tops up to
// tier 1. Separately, every path that actually writes a command is gated on `pass == 1`.
//
// The result is a two-phase allocation: pass 0 claims each task's minimum force in priority
// order and writes nothing; pass 1 tops each task up to its desired force, again in priority
// order, and issues the orders.
//
// Pure: no state, no I/O, no random draws.
// CONFIDENCE: high on the ordering, the dedup, and the tier semantics -- all read from the
// instruction stream. The one soft edge is IsAiControlled: in the original that decision reads
// two bytes on the player object that the SAVE DOES NOT CARRY, so a caller has to supply it from
// somewhere else. See AiTurnPlayer::aiControlled.
#pragma once
#include <cstddef>
#include <vector>
namespace sots::ai {
// The pass argument each task's Execute receives, in the order it receives them.
enum class TaskPass : int {
// Claim each task's minimum force requirement, in descending priority order. Writes no
// commands: every emitting path in the original returns early unless the pass is Fill.
Reserve = 0,
// Redo Reserve's tier and then top each task up to its larger requirement, again in
// descending priority order -- and issue the orders.
Fill = 1,
};
// The two passes, in order. There are exactly two and the original hard-codes both.
inline constexpr TaskPass kPasses[2] = {TaskPass::Reserve, TaskPass::Fill};
// Whether a pass may append to the player's turn-command block. Only Fill may.
bool PassEmitsCommands(TaskPass pass);
// Which of a fleet request's two quota fields a pass is filling. Reserve fills the first,
// Fill fills the second (having first refilled the first). Any other value selects neither,
// which the original represents as a quota of zero -- i.e. "already satisfied, do nothing".
enum class QuotaTier : int { First = 0, Second = 1, None = 2 };
// The tiers a pass gathers, in the order it gathers them: {First} for Reserve, {First, Second}
// for Fill. This is the `for (i = 0; i <= pass; ++i)` loop, made explicit.
std::vector<QuotaTier> TiersForPass(TaskPass pass);
// One player, as far as the stepping order is concerned. Every field is read straight off the
// player array in index order.
struct AiTurnPlayer {
// The player's network id. This is what actually goes into the pending queue, and what the
// drain matches clients on. Zero is rejected outright by the original.
int netId = 0;
// The save's Elim flag. A live player has its Status reset to zero and therefore always
// qualifies; an eliminated player keeps whatever Status it had.
bool eliminated = false;
// The save's Status field, as it stands BEFORE the resume walk. Only consulted for
// eliminated players, because a live player's is overwritten with zero first.
int status = 0;
// Whether this player's turn is run by the strategic AI. In the original this is two
// in-memory bytes on the player object that sit in a hole in the serialised layout -- they
// are set at load or setup time and are NOT in the save. A caller must supply it; this
// module will not guess.
bool aiControlled = false;
// Opaque to this module. Carried through so callers can recover their own player.
const void* handle = nullptr;
};
// The order the AI players are stepped, as a list of net ids.
//
// Reproduces the original exactly: walk the players in index order; a player is offered the
// resume event when it is not eliminated (its Status having just been zeroed) or when its Status
// was already zero; the event is queued only for AI-controlled players with a non-zero net id;
// and a net id already queued is not queued again.
std::vector<int> AiSteppingOrder(const std::vector<AiTurnPlayer>& players);
// Whether the resume walk offers this player the event at all -- i.e. before the AI filter.
// Exposed because it is also the predicate that decides which players get their Status zeroed,
// and a caller reproducing the save's Status field needs it.
bool ReceivesResumeEvent(const AiTurnPlayer& p);
// Whether the resume walk zeroes this player's Status. Every non-eliminated player, and only
// those. Split out from ReceivesResumeEvent because the two differ for an eliminated player
// whose Status is already zero: it is NOT written, but it IS offered the event.
bool ResumeZeroesStatus(const AiTurnPlayer& p);
} // namespace sots::ai