cover unlock state in the determinism tests
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