// Retreat: what happens to the losers' ships after a battle resolves. // // This is the first of the post-battle sub-phases and it is the one with strategic // consequences: it decides where beaten fleets go, splits fleets whose ships did not all // run, deletes fleets that end up empty, and grants the retreating side knowledge of the // system it was beaten at. All of that lands in saved state. // // It is DRAW-FREE. The whole sub-tree contains no random draw of any kind, so this module // needs no generator and its output is a pure function of its input. // // Everything here is a PLAN, not a mutation. The functions below decide *what* should // happen; applying it -- creating a fleet, reparenting a ship, deleting an emptied fleet -- // needs an object store with id allocation, which lives above this layer. Keeping the two // apart is what makes the decision testable on the host. // // CONFIDENCE: the destination search, the eligibility gates, the grouping key and the // whole-versus-partial rule are all read from the instruction stream. See the campaign note // on the retreat pipeline for the per-claim breakdown. #pragma once #include #include namespace sots::combat { // ------------------------------------------------------------------------------------ // Inputs // ------------------------------------------------------------------------------------ // Positions are float32 in the original and the destination search compares SQUARED // distances accumulated in float32. Keeping the type narrow here is deliberate: widening it // changes which system wins a near-tie. struct Vec3f { float x = 0.0f, y = 0.0f, z = 0.0f; }; // A system as the retreat search sees it. Every field is a saved one. struct SystemView { int index = -1; // the system's own index Vec3f pos{}; // system position bool destroyed = false; // system was destroyed bool hiveHost = false; // a self-replicator hive is present bool hiveExhausted = false; // ...and it has been cleared out int ownerPlayer = -1; // -1 when unowned int turnAcquired = -1; // the turn the current owner took it std::uint32_t presenceMask = 0; // bit per player judged "present" here }; // The three per-player diplomacy bitmasks. `self` is excluded from hostility implicitly. struct Diplomacy { std::uint32_t alliance = 0; std::uint32_t nonAggression = 0; std::uint32_t ceaseFire = 0; }; // A player, only as much of one as the retreat rules read. struct PlayerView { int index = -1; // player index, the bit position in every mask above int species = -1; // species id; species 1 retreats by gate Diplomacy diplomacy{}; }; // Species that retreats by opening a gate at the destination instead of flying home. constexpr int kGateSpecies = 1; // The retreat "mode" word that selects the gate path. Any other value flies. constexpr int kGateRetreatMode = 1; // A ship's design, only the fields the eligibility gate reads. struct DesignView { bool hasDriveSectionA = false; // one of two design slots that must be populated bool hasDriveSectionB = false; int mobilityClass = 0; // >= 2 means the design is never grounded this way }; // One ship the battle reported as trying to leave. struct ShipView { int id = -1; int fleetId = -1; // the fleet it is in right now int ownerPlayer = -1; bool departed = false; // already left the system under a previous order int encounterType = -1; // which encounter faction it belongs to, -1 for a normal ship DesignView design{}; float driveHealth = 0.0f; int requestedDestination = -1; // system index the battle asked for, -1 for none int mode = 0; // retreat mode word int variant = 0; // second grouping word, carried through untouched }; // A fleet, only its size (the whole-versus-partial rule needs nothing else). struct FleetView { int id = -1; int shipCount = 0; int ownerPlayer = -1; }; // ------------------------------------------------------------------------------------ // Destination search // ------------------------------------------------------------------------------------ // Systems that can never be retreated to at all: a destroyed system, or one hosting a // self-replicator hive that has not been cleared. CONFIDENCE: high. bool IsUsableRetreatTarget(const SystemView& s); // The mask of players `p` counts as hostile to, evaluated against one system. // // hostile = ~( selfBit | (ceaseFire & ~ownerBit) | nonAggression | alliance ) // // where `ownerBit` is the system owner's bit, but ONLY when that owner took the system on // the current turn -- a system captured this turn loses its ceasefire cover. Unowned // systems, and systems held since an earlier turn, contribute no ownerBit. // // The complement is taken over the full 32 bits, so bits above the player count are set. // That is harmless because it is only ever ANDed with a presence mask, which never has // them, but a reimplementation that narrows the mask must narrow both sides together. // CONFIDENCE: high. std::uint32_t HostileMask(const PlayerView& p, const SystemView& s, int currentTurn); // True when any player hostile to `p` is present at `s`. CONFIDENCE: high. bool HasHostilePresence(const PlayerView& p, const SystemView& s, int currentTurn); // The default retreat destination for one player, given where the battle happened. // // Three independent nearest-system searches run over the whole system list in one pass, // each keeping its own best-so-far, all skipping the battle's own system and any system // `IsUsableRetreatTarget` rejects: // // 1. nearest system the player owns // 2. nearest system with no hostile presence // 3. nearest system, unconditionally // // The answer is the first of those that found anything. Note that a candidate failing a // predicate does NOT spoil that search's best-so-far -- the three are genuinely independent, // which is why (2) can name a system further away than one (1) rejected. // // Distance is the SQUARED euclidean distance, accumulated in float32, and the comparison is // a strict `<`, so on an exact tie the earlier system in the list wins. Returns -1 when the // list yields nothing. CONFIDENCE: high. int ChooseRetreatDestination(const std::vector& systems, int battleSystemIndex, const Vec3f& battlePos, const PlayerView& player, int currentTurn); // ------------------------------------------------------------------------------------ // Per-ship eligibility // ------------------------------------------------------------------------------------ // True when a ship is too badly hurt to run on its own: its design has a drive section, its // mobility class is below 2, and its drive health has fallen to (effectively) zero -- the // comparison is against a single-precision epsilon, so a health of exactly 0 is grounded // and anything at or above the epsilon is not. // // The gate species skips this test entirely; it does not fly out, so a dead drive does not // stop it. CONFIDENCE: high. bool IsGroundedByDamage(const ShipView& ship); // Encounter-faction ids that may never retreat. The original reads a data-driven bitmask // here plus one hard-coded id; `blockedEncounterTypes` is that bitmask, and the hard-coded // exclusion is folded in by the caller supplying it. DB-SOURCED -- do not hard-code the // mask in a reimplementation, load it. constexpr int kNeverRetreatsEncounterType = 0x15; // Whether a reported ship is actually allowed to retreat. `blockedEncounterTypes` is the // data-driven bitmask above; bits are indexed by encounter type and only types 0..23 are // tested. CONFIDENCE: high. bool ShipMayRetreat(const ShipView& ship, const PlayerView& owner, std::uint32_t blockedEncounterTypes, int localPlayerIndex); // ------------------------------------------------------------------------------------ // Grouping and the plan // ------------------------------------------------------------------------------------ // How a group leaves. enum class RetreatMove { None, // no destination was resolved; nothing moves and a warning is logged Gate, // relocated to the destination at once, and an event is posted FlightPlan, // given a move order and detached from the system }; // Ships pulled out of one fleet because only part of that fleet ran. struct FleetSplit { int sourceFleet = -1; std::vector ships{}; }; // One retreat group. Groups are keyed by (owner, destination, mode, variant) -- four words, // all four compared -- so one player retreating to two places in one battle produces two // groups, and so does one player retreating to one place by two different modes. struct RetreatGroup { int ownerPlayer = -1; int destinationSystem = -1; int mode = 0; int variant = 0; std::vector ships{}; // every eligible ship, in report order std::vector wholeFleets{}; // fleets every one of whose ships is in `ships` bool partial = false; // at least one fleet ran only in part // Filled by the planner: bool needsNewFleet = false; // a fleet must be created for the split-off ships std::vector splits{}; RetreatMove move = RetreatMove::None; }; // A fleet that ends up with no ships left and is therefore destroyed. Destroying a fleet // also aborts every intercept order aimed at it, which posts an event to each interceptor's // owner -- that is why this list matters outside this module. struct RetreatPlan { std::vector groups{}; std::vector emptiedFleets{}; // fleets left with zero ships, to be destroyed std::vector exploredGrants{}; // players who learn the battle system by leaving it }; // Assign each eligible ship to its group. `destinationFor` is consulted per ship: a ship // that named a destination gets it, otherwise the owner's default from // `ChooseRetreatDestination`. Groups appear in first-seen order. CONFIDENCE: high. std::vector GroupRetreats(const std::vector& ships, const std::vector& destinationPerShip); // Decide, per group, which source fleets ran whole and which ran only in part. // A fleet runs whole when the number of its ships in the group equals its total ship count. // CONFIDENCE: high. void ClassifyFleets(RetreatGroup& group, const std::vector& ships, const std::vector& fleets); // Whether a group leaves by gate: mode word equal to the gate mode, owner of the gate // species, and a destination actually resolved. CONFIDENCE: high. bool UsesGateRetreat(const RetreatGroup& group, const PlayerView& owner); // The whole thing. `battleSystemIndex` is -1 when the battle was not at a system, which // suppresses both the explore grant and the "put the new fleet here" step. // // The split rule, stated exactly because it is easy to get subtly wrong: a group that has // `partial` set gets ONE new fleet, appended to `wholeFleets` before any ship is moved. A // ship is then moved into it only if its own source fleet is not already in that list -- // which is what keeps ships belonging to a wholly-retreating fleet where they are. A source // fleet that loses all of its group's ships and had none left over is, by construction, // a whole fleet and is never split. // CONFIDENCE: high on the rule; the emptied-fleet list is a consequence of it. RetreatPlan BuildRetreatPlan(const std::vector& systems, const std::vector& fleets, const std::vector& ships, const std::vector& players, int battleSystemIndex, const Vec3f& battlePos, int currentTurn, std::uint32_t blockedEncounterTypes, int localPlayerIndex, const std::vector& exploredMaskPerSystem); } // namespace sots::combat