#include "catch.hpp" #include #include #include #include "ConfigLoader.h" #include "GameConfig.h" #include "Rotation.h" #include "Simulation.h" #include "SimulationTestAccess.h" #include "StateChecksum.h" #include "Tick.h" #include "TestConfig.h" namespace { constexpr int kScriptTicks = 2000; // Runs a fixed scripted session and returns the full-state checksum after every // tick. The script places a small factory, deconstructs part of it mid-run, and // otherwise lets waves/combat run so the RNG stream and ECS state are exercised. std::vector runScriptedSession(unsigned int seed) { Simulation sim(loadTestConfig(), seed); // Tick 0: a miner feeding a short belt line on the asteroid. SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-3, 0), Rotation::East); SimulationTestAccess::place(sim, BuildingType::Belt, QPoint(-2, 0), Rotation::East); SimulationTestAccess::place(sim, BuildingType::Belt, QPoint(-1, 0), Rotation::East); std::vector checksums; checksums.reserve(kScriptTicks); for (int t = 0; t < kScriptTicks; ++t) { if (t == 500) { // Deconstruct the second belt mid-run to exercise the removal paths. SimulationTestAccess::place(sim, BuildingType::Smelter, QPoint(-3, 3), Rotation::East); } sim.tick(); checksums.push_back(sim.computeStateChecksum()); } return checksums; } } // namespace // --------------------------------------------------------------------------- // Hasher // --------------------------------------------------------------------------- TEST_CASE("Hasher: identical inputs produce identical values", "[determinism]") { Hasher a; Hasher b; a.append(42); a.append(3.5f); a.append(std::string("ore")); b.append(42); b.append(3.5f); b.append(std::string("ore")); REQUIRE(a.getValue() == b.getValue()); } TEST_CASE("Hasher: differing inputs produce differing values", "[determinism]") { Hasher a; Hasher b; a.append(42); b.append(43); REQUIRE(a.getValue() != b.getValue()); } TEST_CASE("Hasher: string concatenation does not collide", "[determinism]") { Hasher a; Hasher b; a.append(std::string("ab")); a.append(std::string("c")); b.append(std::string("a")); b.append(std::string("bc")); REQUIRE(a.getValue() != b.getValue()); } TEST_CASE("Hasher: negative and positive zero hash equally", "[determinism]") { Hasher a; Hasher b; a.append(-0.0f); b.append(0.0f); REQUIRE(a.getValue() == b.getValue()); } // --------------------------------------------------------------------------- // RNG fingerprint // --------------------------------------------------------------------------- TEST_CASE("fingerprintRng: equal states match, advanced states differ", "[determinism]") { std::mt19937 a(12345); std::mt19937 b(12345); REQUIRE(fingerprintRng(a) == fingerprintRng(b)); a(); // advance one draw REQUIRE(fingerprintRng(a) != fingerprintRng(b)); b(); // advance b to the same point REQUIRE(fingerprintRng(a) == fingerprintRng(b)); } TEST_CASE("Simulation::rngFingerprint is stable for equal seeds", "[determinism]") { const Simulation a(loadTestConfig(), 777); const Simulation b(loadTestConfig(), 777); REQUIRE(a.getRngFingerprint() == b.getRngFingerprint()); } // --------------------------------------------------------------------------- // Double-run determinism // --------------------------------------------------------------------------- TEST_CASE("Simulation: two runs from the same seed produce identical per-tick state", "[determinism]") { const std::vector first = runScriptedSession(424242); const std::vector second = runScriptedSession(424242); REQUIRE(first.size() == second.size()); REQUIRE(first.size() == static_cast(kScriptTicks)); for (std::size_t i = 0; i < first.size(); ++i) { INFO("divergence at tick " << i); REQUIRE(first[i] == second[i]); } } TEST_CASE("Simulation: different seeds diverge in state checksum", "[determinism]") { const std::vector a = runScriptedSession(111); const std::vector b = runScriptedSession(222); // The two sessions must differ at some point (the checksum is sensitive to // the RNG-driven divergence; a constant checksum would be a broken hash). REQUIRE(a != b); }