19 test translation units each defined an identical local loadConfig(). They now include src/test/TestConfig.h, which lives off the lib/ui/app include path like SimulationTestAccess.h. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
155 lines
4.4 KiB
C++
155 lines
4.4 KiB
C++
#include "catch.hpp"
|
|
|
|
#include <cstdint>
|
|
#include <random>
|
|
#include <vector>
|
|
|
|
#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<std::uint64_t> 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<std::uint64_t> 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<std::uint64_t> first = runScriptedSession(424242);
|
|
const std::vector<std::uint64_t> second = runScriptedSession(424242);
|
|
|
|
REQUIRE(first.size() == second.size());
|
|
REQUIRE(first.size() == static_cast<std::size_t>(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<std::uint64_t> a = runScriptedSession(111);
|
|
const std::vector<std::uint64_t> 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);
|
|
}
|