101 lines
5.6 KiB
C++
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
|