cover unlock state in the determinism tests
The scripted session only placed buildings, so every unlock container stayed at its initial value for the whole run and the checksum never saw them change. It now destroys the enemy stations twice and takes the offered schematic choice, so awarded groups, per-schematic levels and the derived recipe/item sets are exercised too. Adds a test that pins down that unlock state actually reaches the checksum: two sessions in lockstep, one takes the choice, checksums must diverge. The scripted-session tests cannot show this themselves — they compare runs against each other, so they pass whether or not UnlockState is in the fold. Verified by temporarily removing the fold: the new test fails, the old two do not. The first choice is asserted rather than assumed, so a config change that stops offering a group fails loudly instead of silently dropping the coverage. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
This commit is contained in:
@@ -5,11 +5,15 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
|
#include "FactionComponent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
|
#include "HealthComponent.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
|
#include "SchematicChoiceOption.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "SimulationTestAccess.h"
|
#include "SimulationTestAccess.h"
|
||||||
#include "StateChecksum.h"
|
#include "StateChecksum.h"
|
||||||
|
#include "StationBodyComponent.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "TestConfig.h"
|
#include "TestConfig.h"
|
||||||
|
|
||||||
@@ -17,9 +21,33 @@ namespace
|
|||||||
{
|
{
|
||||||
constexpr int kScriptTicks = 2000;
|
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<StationBodyComponent, FactionComponent, HealthComponent>(
|
||||||
|
[](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
|
// 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
|
// 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.
|
// 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<std::uint64_t> runScriptedSession(unsigned int seed)
|
std::vector<std::uint64_t> runScriptedSession(unsigned int seed)
|
||||||
{
|
{
|
||||||
Simulation sim(loadTestConfig(), seed);
|
Simulation sim(loadTestConfig(), seed);
|
||||||
@@ -40,6 +68,26 @@ std::vector<std::uint64_t> runScriptedSession(unsigned int seed)
|
|||||||
SimulationTestAccess::place(sim, BuildingType::Smelter, QPoint(-3, 3), Rotation::East);
|
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();
|
sim.tick();
|
||||||
checksums.push_back(sim.computeStateChecksum());
|
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).
|
// the RNG-driven divergence; a constant checksum would be a broken hash).
|
||||||
REQUIRE(a != b);
|
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());
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user