// The per-player turn record -- the last phase of the post-combat tail. // // Each player carries a small "what my empire looked like this turn" summary that the final // tail phase fills and then copies into a history archive keyed by the turn number. The // archive IS on the wire: it is the per-player turn-statistics history, one element per turn, // and every save carries an element for its own frame. That makes this phase testable without // a running game: build the record from a save's own state and compare it with the element the // save already holds for that turn. // // Seven fields are recoverable from the wire and are reproduced here. Six more -- the // per-hull-class ship census -- are recoverable only with the game's section catalog, because // neither the hull size nor the defence-platform flag is anywhere on the wire; they are filled // when the operator supplies a data root and are reported as unmodelled when they are not. // The rest are not recoverable at all, and each is named with the reason -- they belong to // phases or inputs the standalone does not hold. #pragma once #include #include #include #include #include "game/data/catalog.h" #include "game/design/hull.h" #include "mars/stream/shapes.h" namespace sots::app { // The per-player ship census, built once for the whole simulation. // // The counts cannot come from a save alone: a ship names a design, and a design's hull size // and defence-platform flag are recomputed from the section catalog whenever it changes and // are never written down. So this needs a data root, and says so when it does not have one. // // Built once rather than per player because design ids are unique across the whole game and // the fleet list is global: classifying every design once and then walking the fleets is the // same shape the original uses, and it keeps the catalog lookups off the per-player path. class ShipCensusIndex { public: // Without a catalog: an index that reports itself unmodelled and counts nothing. ShipCensusIndex() = default; ShipCensusIndex(const mars::stream::shapes::SaveGame& game, const game::data::Catalog& cat); bool modelled() const { return modelled_; } // Null when the census is unmodelled or the index is out of range. const game::design::ShipCensus* ForPlayer(std::size_t vectorIndex) const; int designsSeen = 0; // A design none of whose sections resolve against the catalog. NOT counted as class 0: // an unclassifiable design is a gap, and a silent class-0 count would hide it. int designsUnclassified = 0; // A ship naming a design id no player's design list carries, or an unclassifiable one. int shipsWithoutDesign = 0; // Fleets whose owner id names no player in the player vector -- the NPC pools. Skipped, // and counted so "skipped" is a measurement rather than an omission. int fleetsWithoutOwner = 0; private: bool modelled_ = false; std::vector byPlayer_; }; // The part of a turn record this model can produce. struct TurnRecord { std::int32_t turn = 0; // the frame counter std::int64_t population = 0; // summed over owned systems std::int32_t colonies = 0; // owned-system count std::int32_t savings = 0; std::int32_t income = 0; // savings minus previous-turn savings std::int32_t completedTech = 0; // tech-tree entries in the completed state std::int32_t allianceMask = 0; // the shared-vision mask the spine's phase 4 rebuilds // The census, by hull size 0/1/2. Filled only when `censusModelled`; six zeros otherwise, // which is exactly the value a wrong model would produce, hence the flag rather than a // sentinel. bool censusModelled = false; std::array ships{}; std::array platforms{}; // Fields the archive element also carries that this model does NOT fill, kept as a // published list rather than as silence. Each is blocked on something named. static const char* const* Unmodelled(std::size_t& count); }; // The completed state's value in the tech-tree state word. Named rather than spelled inline // because it is the one magic number in this file. constexpr std::int32_t kTechStateCompleted = 4; // Build one player's record from the simulation state. `systemsById` is the save's system // table indexed the way the player's owned-system ids index it. // // `vectorIndex` is the player's position in the player vector -- the alliance mask's bit // index, and NOT the player's own index field. See app/alliance.h. // // `spineRan` says whether the turn whose record this is actually ran the strategic spine. // The archiving phase runs in two places -- at the end of a turn, and on load -- and the // alliance mask is written only by the spine. So a record archived by the load path carries // a zero mask, and that is a positive prediction of this model, not an exclusion: the // earliest turn every save carries has `almem == 0` on every player, in all eleven saves. // // `census` may be null, and is null whenever no data root was supplied: the six census // fields then stay zero and `censusModelled` stays false. TurnRecord BuildTurnRecord(const mars::stream::shapes::Player& p, const std::vector& systems, std::int32_t frame, std::size_t vectorIndex, bool spineRan, // set when an owned-system id is not present in the table, which // would silently drop a term from the population sum int* danglingOwnedSystems = nullptr, const ShipCensusIndex* census = nullptr); // The earliest turn the archive carries for a player. The record for that turn was written // by the new-game / load path, not by a turn. std::int32_t EarliestArchivedTurn(const mars::stream::shapes::PlayerTurnHistory& hist); // What the archive element for `turn` holds, if the save carries one for that turn. const mars::stream::shapes::PlayerTurnStats* FindArchivedRecord( const mars::stream::shapes::PlayerTurnHistory& hist, std::int32_t turn); // A comparison of a built record against a stored one, over the six modelled fields only. struct TurnRecordDiff { int compared = 0; std::vector mismatches; // "field: built != stored" bool ok() const { return mismatches.empty(); } }; TurnRecordDiff CompareTurnRecord(const TurnRecord& built, const mars::stream::shapes::PlayerTurnStats& stored); } // namespace sots::app