From 5d4a97538465f50332fe93f923c7cfed2b4894c0 Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Tue, 4 Aug 2026 18:11:03 +0200 Subject: [PATCH] cover unlock state in the determinism tests --- src/test/DeterminismTest.cpp | 89 ++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/src/test/DeterminismTest.cpp b/src/test/DeterminismTest.cpp index 80d2858..aea05ed 100644 --- a/src/test/DeterminismTest.cpp +++ b/src/test/DeterminismTest.cpp @@ -5,11 +5,15 @@ #include #include "ConfigLoader.h" +#include "FactionComponent.h" #include "GameConfig.h" +#include "HealthComponent.h" #include "Rotation.h" +#include "SchematicChoiceOption.h" #include "Simulation.h" #include "SimulationTestAccess.h" #include "StateChecksum.h" +#include "StationBodyComponent.h" #include "Tick.h" #include "TestConfig.h" @@ -17,9 +21,33 @@ namespace { constexpr int kScriptTicks = 2000; +// Ticks at which the scripted session destroys the enemy stations, and the ticks +// on which the resulting schematic choice is taken. A station dying triggers the +// choice generation (REQ-DEF-SCHEMATIC-DROP), which lands during that same tick, +// so the choice is applied on the tick after. +constexpr int kFirstStationKillTick = 800; +constexpr int kFirstChoiceTick = kFirstStationKillTick + 1; +constexpr int kSecondStationKillTick = 1400; +constexpr int kSecondChoiceTick = kSecondStationKillTick + 1; + +// Zeroes the HP of every enemy station, so the next tick processes their death. +void killEnemyStations(Simulation& sim) +{ + sim.getAdmin().forEach( + [](entt::entity, StationBodyComponent&, FactionComponent& faction, + HealthComponent& health) + { + if (faction.isEnemy) { health.hp = 0.0f; } + }); +} + // 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. +// It also destroys the enemy stations twice and takes the offered schematic +// choice, so that unlock state (awarded groups, per-schematic levels, and the +// implicit recipe/item sets derived from them) is exercised as well and reaches +// the checksum via UnlockState::appendChecksum. std::vector runScriptedSession(unsigned int seed) { Simulation sim(loadTestConfig(), seed); @@ -40,6 +68,26 @@ std::vector runScriptedSession(unsigned int seed) SimulationTestAccess::place(sim, BuildingType::Smelter, QPoint(-3, 3), Rotation::East); } + if (t == kFirstStationKillTick || t == kSecondStationKillTick) + { + killEnemyStations(sim); + } + + if (t == kFirstChoiceTick) + { + // Guarded rather than assumed: if the test config ever stops offering + // a group here, this script would silently stop covering unlock state. + REQUIRE(sim.hasSchematicChoicesPending()); + SimulationTestAccess::applySchematicChoice(sim, 0); + } + + // The second award is opportunistic — whether a group is still eligible + // depends on what the first one granted and on the prerequisite gating. + if (t == kSecondChoiceTick && sim.hasSchematicChoicesPending()) + { + SimulationTestAccess::applySchematicChoice(sim, 0); + } + sim.tick(); checksums.push_back(sim.computeStateChecksum()); } @@ -152,3 +200,44 @@ TEST_CASE("Simulation: different seeds diverge in state checksum", "[determinism // the RNG-driven divergence; a constant checksum would be a broken hash). REQUIRE(a != b); } + +// --------------------------------------------------------------------------- +// Unlock state coverage +// --------------------------------------------------------------------------- + +TEST_CASE("Simulation: unlock state contributes to the state checksum", + "[determinism][unlock]") +{ + // Two sessions with identical history up to the schematic choice; only one + // takes the choice. This pins down that awarding an unlock group actually + // reaches the checksum, which the scripted-session tests above rely on but + // cannot show on their own: they would still pass if UnlockState were left + // out of the fold entirely. + Simulation taken(loadTestConfig(), 12345u); + Simulation skipped(loadTestConfig(), 12345u); + + for (int t = 0; t < kFirstStationKillTick; ++t) + { + taken.tick(); + skipped.tick(); + } + killEnemyStations(taken); + killEnemyStations(skipped); + taken.tick(); + skipped.tick(); + + // In lockstep before the choice, so the divergence below has one cause. + REQUIRE(taken.computeStateChecksum() == skipped.computeStateChecksum()); + + REQUIRE(taken.hasSchematicChoicesPending()); + + // An artifact choice bumps m_artifactCount, which is folded separately; the + // divergence would then not be attributable to unlock state. + REQUIRE_FALSE(taken.getPendingSchematicChoices()[0].isArtifact); + + SimulationTestAccess::applySchematicChoice(taken, 0); + + // The pending-choice list is not itself folded into the checksum, so the + // only state that changed is the unlock bookkeeping. + REQUIRE(taken.computeStateChecksum() != skipped.computeStateChecksum()); +}