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
153 lines
4.3 KiB
C++
153 lines
4.3 KiB
C++
#include "catch.hpp"
|
|
|
|
#include "ConfigLoader.h"
|
|
#include "GameConfig.h"
|
|
#include "Simulation.h"
|
|
#include "Tick.h"
|
|
#include "TickDriver.h"
|
|
#include "TestConfig.h"
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Simulation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("Simulation::currentTick starts at 0", "[simulation]")
|
|
{
|
|
const Simulation sim(loadTestConfig());
|
|
|
|
REQUIRE(sim.getCurrentTick() == 0);
|
|
}
|
|
|
|
TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]")
|
|
{
|
|
Simulation sim(loadTestConfig());
|
|
|
|
sim.tick();
|
|
|
|
REQUIRE(sim.getCurrentTick() == 1);
|
|
}
|
|
|
|
TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]")
|
|
{
|
|
Simulation sim(loadTestConfig());
|
|
|
|
for (int i = 0; i < 10; ++i)
|
|
{
|
|
sim.tick();
|
|
}
|
|
|
|
REQUIRE(sim.getCurrentTick() == 10);
|
|
}
|
|
|
|
TEST_CASE("Simulation::drainBeamFiredEvents returns empty initially", "[simulation]")
|
|
{
|
|
Simulation sim(loadTestConfig());
|
|
|
|
REQUIRE(sim.drainBeamFiredEvents().empty());
|
|
}
|
|
|
|
TEST_CASE("Simulation::drainBeamFiredEvents clears queue on drain", "[simulation]")
|
|
{
|
|
Simulation sim(loadTestConfig());
|
|
|
|
// First drain: empty.
|
|
sim.drainBeamFiredEvents();
|
|
|
|
// Second drain must also be empty (not a double-return).
|
|
REQUIRE(sim.drainBeamFiredEvents().empty());
|
|
}
|
|
|
|
TEST_CASE("Simulation::hasSchematicChoicesPending returns false initially", "[simulation]")
|
|
{
|
|
Simulation sim(loadTestConfig());
|
|
|
|
REQUIRE_FALSE(sim.hasSchematicChoicesPending());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TickDriver
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("TickDriver: 0x speed never produces ticks", "[simulation]")
|
|
{
|
|
TickDriver driver;
|
|
|
|
REQUIRE(driver.advance(100.0, 0.0) == 0);
|
|
REQUIRE(driver.advance(1000.0, 0.0) == 0);
|
|
}
|
|
|
|
TEST_CASE("TickDriver: exactly one tick duration at 1x produces 1 tick", "[simulation]")
|
|
{
|
|
TickDriver driver;
|
|
|
|
REQUIRE(driver.advance(kTickDurationMs, 1.0) == 1);
|
|
}
|
|
|
|
TEST_CASE("TickDriver: two tick durations at 1x produces 2 ticks", "[simulation]")
|
|
{
|
|
TickDriver driver;
|
|
|
|
REQUIRE(driver.advance(2.0 * kTickDurationMs, 1.0) == 2);
|
|
}
|
|
|
|
TEST_CASE("TickDriver: 4x speed with quarter tick duration produces 1 tick", "[simulation]")
|
|
{
|
|
TickDriver driver;
|
|
|
|
REQUIRE(driver.advance(kTickDurationMs / 4.0, 4.0) == 1);
|
|
}
|
|
|
|
TEST_CASE("TickDriver: 0.5x speed with double tick duration produces 1 tick", "[simulation]")
|
|
{
|
|
TickDriver driver;
|
|
|
|
REQUIRE(driver.advance(kTickDurationMs / 0.5, 0.5) == 1);
|
|
}
|
|
|
|
TEST_CASE("TickDriver: accumulator carries partial progress across frames", "[simulation]")
|
|
{
|
|
TickDriver driver;
|
|
|
|
// 60% of a tick — not enough to fire yet.
|
|
REQUIRE(driver.advance(kTickDurationMs * 0.6, 1.0) == 0);
|
|
|
|
// Another 60% — cumulative 120%, so exactly 1 tick fires.
|
|
REQUIRE(driver.advance(kTickDurationMs * 0.6, 1.0) == 1);
|
|
}
|
|
|
|
TEST_CASE("TickDriver::reset clears the accumulator", "[simulation]")
|
|
{
|
|
TickDriver driver;
|
|
|
|
// Advance to just below one tick.
|
|
driver.advance(kTickDurationMs * 0.9, 1.0);
|
|
|
|
driver.reset();
|
|
|
|
// Nothing in the accumulator: zero elapsed time should not fire.
|
|
REQUIRE(driver.advance(0.0, 1.0) == 0);
|
|
}
|
|
|
|
TEST_CASE("TickDriver::reset discards a large pending delta so a restart does not "
|
|
"fast-forward", "[simulation]")
|
|
{
|
|
// Regression guard for the "restart fast-forwards by the time spent in the
|
|
// Game Over / Win / escape dialog" bug: on restart the frame timer holds the
|
|
// whole wall-clock duration the modal was open. GameWorldView::resetForNewGame()
|
|
// now calls TickDriver::reset() to discard that pending delta; without it the
|
|
// fresh run would burst forward by the dialog duration on its first frame.
|
|
TickDriver driver;
|
|
|
|
// 30 seconds spent in the dialog would otherwise be ~900 ticks at 30 Hz.
|
|
const double dialogOpenMs = 30000.0;
|
|
driver.reset();
|
|
|
|
// A brand-new run's first normal frame (~16 ms) advances only its own ticks;
|
|
// the discarded dialog time contributes nothing.
|
|
REQUIRE(driver.advance(16.0, 1.0) == 0);
|
|
|
|
// Sanity: had the dialog delta not been discarded, it would have fired ~900 ticks.
|
|
TickDriver leaked;
|
|
REQUIRE(leaked.advance(dialogOpenMs, 1.0) > 800);
|
|
}
|