#pragma once #include #include #include #include #include #include #include "BeltSystem.h" #include "EntityAdmin.h" #include "entt/entity/entity.hpp" #include "SchematicChoiceOption.h" #include "BuildingType.h" #include "BuildingId.h" #include "EventHandler.h" #include "BeamFiredEvent.h" #include "GameConfig.h" #include "Rotation.h" #include "Tick.h" #include "TracePrintRequestedEvent.h" #include "UnlockState.h" class AiSystem; class BuildingSystem; struct Command; class Hasher; class CombatSystem; class DynamicBodySystem; class MovementIntentSystem; class RepairSystem; class SalvagerSystem; class ShipSystem; class DebrisSystem; class WaveSystem; class Simulation: public CombinedEventHandler { public: explicit Simulation(GameConfig config, unsigned int seed = 0); ~Simulation(); const GameConfig& getConfig() const; // Reinitializes all simulation state as if constructed fresh. void reset(unsigned int seed = 0); // Reloads config then reinitializes all simulation state. void reset(GameConfig newConfig, unsigned int seed = 0); // Advances the simulation by one tick. Tick order per architecture.md §Tick Order. void tick(); // The single command chokepoint: applies one player command by dispatching // to the underlying mutators. Every sim mutation during play must flow // through here so it can be recorded and replayed (see docs/replay_design.md // and CommandManager). Reached via CommandManager::drain. void apply(const Command& command); // Returns all fire events accumulated since the last drain, clearing the // internal queue. Call once per rendered frame (REQ-SHP-FIRING-BEAM). std::vector drainBeamFiredEvents(); // Returns the pending schematic choices (empty if no drop is pending). const std::vector& getPendingSchematicChoices() const; // Returns true if there are pending schematic choices waiting for player input. bool hasSchematicChoicesPending() const; Tick getCurrentTick() const; // The seed this run was (re)initialized with; written to the replay header. unsigned int getSeed() const; int getBuildingBlocksStock() const; // Current asteroid width in tiles = base width + purchased expansions // (REQ-EXP-UNLOCK, REQ-GW-ASTEROID-EXPAND). int getCurrentAsteroidWidth_tiles() const; // Building block cost of the next expansion, floored to an integer // (REQ-EXP-COST); x = number of expansions already purchased. int getCurrentExpansionCost() const; bool isGameOver() const; bool isWon() const; int getArtifactCount() const; double getThreatLevel() const; double getThreatAccumulationRate() const; double getMaxFactoryProductionThreatRate() const; double getCurrentFactoryProductionThreatRate() const; int getBossWaveCounter() const; Tick getBossCountdownTicks() const; Tick getNormalGapRemainingTicks() const; // Ship schematic state query. bool isSchematicUnlocked(const std::string& shipId) const; // Module schematic state query. bool isModuleSchematicUnlocked(const std::string& moduleId) const; // Implicit recipe/item unlock queries (REQ-LOCK-IMPLICIT). bool isRecipeUnlocked(const std::string& recipeId) const; bool isItemUnlocked(const std::string& itemId) const; // Building unlock query (REQ-LOCK-BUILDING). True if the building type is not // gated by any unlock group, or its granting group has been awarded. bool isBuildingUnlocked(BuildingType type) const; // -- Determinism (see docs/replay_design.md) ----------------------------- // 64-bit fingerprint of the RNG stream state. Cheap; written to the replay // file periodically + after each command for desync detection. unsigned long long getRngFingerprint() const; // 64-bit fingerprint of the full simulation state (RNG, scalars, buildings, // belts, and ECS component state). Used by the double-run determinism test; // a superset of getRngFingerprint(). unsigned long long computeStateChecksum() const; // Const subsystem accessors (queries only). The mutable counterparts are // private and reachable only through Simulation::apply (the command // chokepoint) or, in tests, SimulationTestAccess — so production code cannot // mutate the factory outside the recorded command path (docs/replay_design.md). const BuildingSystem& getBuildings() const; const BeltSystem& getBelts() const; ShipSystem& getShips(); const ShipSystem& getShips() const; DebrisSystem& getDebrisSystem(); const DebrisSystem& getDebrisSystem() const; EntityAdmin& getAdmin(); const EntityAdmin& getAdmin() const; private: // Grants tests access to the private player-action mutators below without // opening them to production code (see src/test/SimulationTestAccess.h). friend struct SimulationTestAccess; // -- Player-action mutators (command chokepoint only) -------------------- // Reached during play exclusively via apply(); never called by UI/app code. // Checks affordability, deducts building blocks, and places the building. // Returns the new entity id, or nullopt if blocks are insufficient. std::optional tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation); // Marks the building with the given id for demolition: a construction site is // removed instantly (full refund), a built building is queued for timed // deconstruction (REQ-BLD-DECON-QUEUE), refunded on completion. void deconstruct(BuildingId id); // Takes a queued building back out of the deconstruction queue (REQ-BLD-DECON-QUEUE). void cancelDeconstruction(BuildingId id); // Applies the player's chosen schematic from the pending choices. // choiceIndex must be in [0, pendingChoices.size()). // Clears the pending choices after application. void applySchematicChoice(int choiceIndex); // Unlocks one asteroid expansion if affordable (REQ-EXP-UNLOCK): checks the // current cost against the stock, deducts it, increments the expansion // counter, and widens the buildable asteroid. No-op if blocks are short. void tryExpandAsteroid(); // Mutable subsystem accessors; same chokepoint rule as the mutators above. BuildingSystem& getBuildingsMutable(); BeltSystem& getBeltsMutable(); void handleEvent(std::shared_ptr event) override; BuildingId allocateBuildingId(); // Strictly increasing; never returns kInvalidBuildingId. // (Re-)create every owned subsystem. Shared by the constructor and reset(); // the construction order is load-bearing for determinism, so both paths must // go through here. Only called before the first tick of a run. void initializeSubsystems(); // Populate HQ, player defence stations, and the first enemy station set. void placeInitialStructures(); // Place two enemy defence stations for the given generation level. // Stores their IDs in m_currentEnemyStationIds. void placeEnemyStationSet(int generation); // Tick step 9: remove dead ships and buildings, drop debris, handle push. void tickDeathsAndLoot(); // Generate up to 3 schematic choices (REQ-DEF-SCHEMATIC-DROP) for the player. void generateSchematicChoices(int destroyedStationLevel); GameConfig m_config; std::mt19937 m_rng; unsigned int m_seed; Tick m_currentTick; Tick m_nextDepartureTick; BuildingId m_nextBuildingId; int m_buildingBlocksStock; int m_expansionsPurchased = 0; // REQ-EXP-COST formula variable x bool m_gameOver = false; bool m_isWon = false; int m_artifactCount = 0; // Pre-placed structure IDs. std::optional m_hqBuildingId; // Building id (for belt integration) entt::entity m_hqProxyEntity; // ECS entity (HP, targeting) entt::entity m_playerStation1Entity; entt::entity m_playerStation2Entity; entt::entity m_currentEnemyStationEntities[2]; // Schematic/unlock bookkeeping (REQ-DEF-SCHEMATIC-DROP, REQ-LOCK-EXPLICIT, // REQ-LOCK-IMPLICIT, REQ-LOCK-BUILDING, REQ-LOCK-PREREQ). Constructed before // initializeSubsystems() runs since BuildingSystem's spawn-gating lambda // calls into it (see initializeSubsystems()). UnlockState m_unlockState; EntityAdmin m_admin; BeltSystem m_beltSystem; std::unique_ptr m_buildingSystem; std::unique_ptr m_shipSystem; std::unique_ptr m_aiSystem; std::unique_ptr m_movementIntentSystem; std::unique_ptr m_dynamicBodySystem; std::unique_ptr m_debrisSystem; std::unique_ptr m_salvagerSystem; std::unique_ptr m_repairSystem; std::unique_ptr m_waveSystem; std::unique_ptr m_combatSystem; std::vector m_beamFiredEvents; std::vector m_pendingSchematicChoices; };