// 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 #include 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 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 AiSteppingOrder(const std::vector& 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