Simulation now owns an UnlockState member (constructed before initializeSubsystems(), since BuildingSystem's spawn-gating lambda calls into it via isSchematicUnlocked instead of poking the old m_schematicLevels map directly). The public isXUnlocked accessors become one-line forwards, applySchematicChoice's group-awarding block becomes a single awardUnlockGroup() call, and generateSchematicChoices (which still owns m_rng and must not change call ordering) now reads group state and builds options through UnlockState. The checksum fold at computeStateChecksum's schematic/unlock section had to move together with the containers it reads; UnlockState:: appendChecksum makes the identical seven appendSchematicMap/ appendStringSet calls in the identical order, so the fold is unaffected. Verified via a temporary golden-checksum test case (added, checked, then removed) that tick1/100/999/1999 checksums for seed 12345 are byte-identical to pre-refactor. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
227 lines
9.3 KiB
C++
227 lines
9.3 KiB
C++
#pragma once
|
|
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <random>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <QPoint>
|
|
|
|
#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<TracePrintRequestedEvent>
|
|
{
|
|
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<BeamFiredEvent> drainBeamFiredEvents();
|
|
|
|
// Returns the pending schematic choices (empty if no drop is pending).
|
|
const std::vector<SchematicChoiceOption>& 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<BuildingId> 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<const TracePrintRequestedEvent> 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<BuildingId> 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<BuildingSystem> m_buildingSystem;
|
|
std::unique_ptr<ShipSystem> m_shipSystem;
|
|
std::unique_ptr<AiSystem> m_aiSystem;
|
|
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;
|
|
std::unique_ptr<DynamicBodySystem> m_dynamicBodySystem;
|
|
std::unique_ptr<DebrisSystem> m_debrisSystem;
|
|
std::unique_ptr<SalvagerSystem> m_salvagerSystem;
|
|
std::unique_ptr<RepairSystem> m_repairSystem;
|
|
std::unique_ptr<WaveSystem> m_waveSystem;
|
|
std::unique_ptr<CombatSystem> m_combatSystem;
|
|
|
|
std::vector<BeamFiredEvent> m_beamFiredEvents;
|
|
std::vector<SchematicChoiceOption> m_pendingSchematicChoices;
|
|
};
|