The strategic half of a battle: where beaten fleets go, which fleets split,
which are left empty, and who learns the system they were beaten at. Draw-free
end to end -- the whole sub-tree contains no random draw -- so this is a pure
function of its inputs and needs no generator.
What is modelled:
* the destination search: three independent nearest-system passes (owned /
no hostile presence / anything), each with its own best-so-far, over
squared float32 distances with a strict comparison. The independence is
load-bearing: a nearer system rejected by one predicate must not spoil
that pass's best, and a single-loop version gets it wrong.
* the hostility mask, including the rule that a system captured on the
current turn loses its owner's ceasefire cover.
* per-ship eligibility: already-departed, encounter-faction exclusions (one
hard-coded id plus a data-driven bitmask), and the dead-drive gate, which
tests against a single-precision epsilon rather than zero and which the
gate species skips because it does not fly out.
* grouping on all four key words (owner, destination, mode, variant).
* whole-versus-partial: a fleet runs whole only when every one of its ships
is in the group; otherwise the group gets one new fleet and the leftover
ships move into it, while ships of a wholly-retreating fleet stay put.
* the emptied-fleet list, which matters because destroying a fleet aborts
every intercept aimed at it.
Deliberately NOT modelled: applying the plan. Creating a fleet mints an object
id from a monotonic counter and appends to the master fleet list, and both of
those are saved state; that belongs above this layer, where the object store
lives. Keeping the decision separate is what makes it host-testable.
53 hand-computed checks. ctest 42/42, clean-room check OK.
255 lines
12 KiB
C++
255 lines
12 KiB
C++
// 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 <cstdint>
|
|
#include <vector>
|
|
|
|
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<SystemView>& 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<int> 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<int> ships{}; // every eligible ship, in report order
|
|
std::vector<int> 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<FleetSplit> 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<RetreatGroup> groups{};
|
|
std::vector<int> emptiedFleets{}; // fleets left with zero ships, to be destroyed
|
|
std::vector<int> 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<RetreatGroup> GroupRetreats(const std::vector<ShipView>& ships,
|
|
const std::vector<int>& 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<ShipView>& ships,
|
|
const std::vector<FleetView>& 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<SystemView>& systems,
|
|
const std::vector<FleetView>& fleets,
|
|
const std::vector<ShipView>& ships,
|
|
const std::vector<PlayerView>& players,
|
|
int battleSystemIndex,
|
|
const Vec3f& battlePos,
|
|
int currentTurn,
|
|
std::uint32_t blockedEncounterTypes,
|
|
int localPlayerIndex,
|
|
const std::vector<std::uint32_t>& exploredMaskPerSystem);
|
|
|
|
} // namespace sots::combat
|