share one loadTestConfig() helper across the tests

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
This commit is contained in:
2026-08-02 20:53:21 +02:00
parent dc58f6ea32
commit 3c549a160c
22 changed files with 247 additions and 308 deletions

View File

@@ -11,11 +11,7 @@
#include "Simulation.h" #include "Simulation.h"
#include "SimulationTestAccess.h" #include "SimulationTestAccess.h"
#include "StationBodyComponent.h" #include "StationBodyComponent.h"
#include "TestConfig.h"
static GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
static void killEnemyStations(Simulation& sim) static void killEnemyStations(Simulation& sim)
{ {
@@ -51,7 +47,7 @@ static int findArtifactChoiceIndex(const Simulation& sim)
TEST_CASE("ArtifactWinCondition: artifact_chance_formula and artifact_win_count are loaded", TEST_CASE("ArtifactWinCondition: artifact_chance_formula and artifact_win_count are loaded",
"[artifact_win]") "[artifact_win]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
CHECK(cfg.world.artifacts.artifactWinCount == 3); CHECK(cfg.world.artifacts.artifactWinCount == 3);
// 0.05 * x at x=2 should be 0.1 // 0.05 * x at x=2 should be 0.1
CHECK(cfg.world.artifacts.artifactChanceFormula.evaluate(2.0) == Approx(0.1)); CHECK(cfg.world.artifacts.artifactChanceFormula.evaluate(2.0) == Approx(0.1));
@@ -64,7 +60,7 @@ TEST_CASE("ArtifactWinCondition: artifact_chance_formula and artifact_win_count
TEST_CASE("ArtifactWinCondition: artifact count is 0 and isWon is false at game start", TEST_CASE("ArtifactWinCondition: artifact count is 0 and isWon is false at game start",
"[artifact_win]") "[artifact_win]")
{ {
const Simulation sim(loadConfig()); const Simulation sim(loadTestConfig());
CHECK(sim.getArtifactCount() == 0); CHECK(sim.getArtifactCount() == 0);
CHECK_FALSE(sim.isWon()); CHECK_FALSE(sim.isWon());
} }
@@ -76,7 +72,7 @@ TEST_CASE("ArtifactWinCondition: artifact count is 0 and isWon is false at game
TEST_CASE("ArtifactWinCondition: artifact option appears when chance formula returns 1", TEST_CASE("ArtifactWinCondition: artifact option appears when chance formula returns 1",
"[artifact_win]") "[artifact_win]")
{ {
GameConfig cfg = loadConfig(); GameConfig cfg = loadTestConfig();
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1"); cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
Simulation sim(std::move(cfg)); Simulation sim(std::move(cfg));
@@ -95,7 +91,7 @@ TEST_CASE("ArtifactWinCondition: artifact option appears when chance formula ret
TEST_CASE("ArtifactWinCondition: at most 2 schematic options accompany the artifact", TEST_CASE("ArtifactWinCondition: at most 2 schematic options accompany the artifact",
"[artifact_win]") "[artifact_win]")
{ {
GameConfig cfg = loadConfig(); GameConfig cfg = loadTestConfig();
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1"); cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
Simulation sim(std::move(cfg)); Simulation sim(std::move(cfg));
@@ -112,7 +108,7 @@ TEST_CASE("ArtifactWinCondition: at most 2 schematic options accompany the artif
TEST_CASE("ArtifactWinCondition: no artifact option when chance formula returns 0", TEST_CASE("ArtifactWinCondition: no artifact option when chance formula returns 0",
"[artifact_win]") "[artifact_win]")
{ {
GameConfig cfg = loadConfig(); GameConfig cfg = loadTestConfig();
cfg.world.artifacts.artifactChanceFormula = Formula::compile("0"); cfg.world.artifacts.artifactChanceFormula = Formula::compile("0");
Simulation sim(std::move(cfg)); Simulation sim(std::move(cfg));
@@ -133,7 +129,7 @@ TEST_CASE("ArtifactWinCondition: no artifact option when chance formula returns
TEST_CASE("ArtifactWinCondition: selecting artifact increments artifact count", TEST_CASE("ArtifactWinCondition: selecting artifact increments artifact count",
"[artifact_win]") "[artifact_win]")
{ {
GameConfig cfg = loadConfig(); GameConfig cfg = loadTestConfig();
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1"); cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
Simulation sim(std::move(cfg)); Simulation sim(std::move(cfg));
@@ -151,7 +147,7 @@ TEST_CASE("ArtifactWinCondition: selecting artifact increments artifact count",
TEST_CASE("ArtifactWinCondition: selecting a non-artifact option does not increment artifact count", TEST_CASE("ArtifactWinCondition: selecting a non-artifact option does not increment artifact count",
"[artifact_win]") "[artifact_win]")
{ {
GameConfig cfg = loadConfig(); GameConfig cfg = loadTestConfig();
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1"); cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
Simulation sim(std::move(cfg)); Simulation sim(std::move(cfg));
@@ -176,7 +172,7 @@ TEST_CASE("ArtifactWinCondition: selecting a non-artifact option does not increm
TEST_CASE("ArtifactWinCondition: isWon becomes true when artifact count reaches win count", TEST_CASE("ArtifactWinCondition: isWon becomes true when artifact count reaches win count",
"[artifact_win]") "[artifact_win]")
{ {
GameConfig cfg = loadConfig(); GameConfig cfg = loadTestConfig();
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1"); cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
cfg.world.artifacts.artifactWinCount = 1; cfg.world.artifacts.artifactWinCount = 1;
Simulation sim(std::move(cfg)); Simulation sim(std::move(cfg));
@@ -196,7 +192,7 @@ TEST_CASE("ArtifactWinCondition: isWon becomes true when artifact count reaches
TEST_CASE("ArtifactWinCondition: isWon stays false when artifact count is below win count", TEST_CASE("ArtifactWinCondition: isWon stays false when artifact count is below win count",
"[artifact_win]") "[artifact_win]")
{ {
GameConfig cfg = loadConfig(); GameConfig cfg = loadTestConfig();
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1"); cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
cfg.world.artifacts.artifactWinCount = 2; cfg.world.artifacts.artifactWinCount = 2;
Simulation sim(std::move(cfg)); Simulation sim(std::move(cfg));
@@ -212,7 +208,7 @@ TEST_CASE("ArtifactWinCondition: isWon stays false when artifact count is below
TEST_CASE("ArtifactWinCondition: isWon becomes true after collecting required number of artifacts", TEST_CASE("ArtifactWinCondition: isWon becomes true after collecting required number of artifacts",
"[artifact_win]") "[artifact_win]")
{ {
GameConfig cfg = loadConfig(); GameConfig cfg = loadTestConfig();
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1"); cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
cfg.world.artifacts.artifactWinCount = 2; cfg.world.artifacts.artifactWinCount = 2;
Simulation sim(std::move(cfg)); Simulation sim(std::move(cfg));
@@ -237,7 +233,7 @@ TEST_CASE("ArtifactWinCondition: isWon becomes true after collecting required nu
TEST_CASE("ArtifactWinCondition: reset clears artifact count and win state", TEST_CASE("ArtifactWinCondition: reset clears artifact count and win state",
"[artifact_win]") "[artifact_win]")
{ {
GameConfig cfg = loadConfig(); GameConfig cfg = loadTestConfig();
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1"); cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
cfg.world.artifacts.artifactWinCount = 1; cfg.world.artifacts.artifactWinCount = 1;
Simulation sim(std::move(cfg)); Simulation sim(std::move(cfg));

View File

@@ -45,16 +45,12 @@
#include "ShipLayout.h" #include "ShipLayout.h"
#include "ShipSystem.h" #include "ShipSystem.h"
#include "Tick.h" #include "Tick.h"
#include "TestConfig.h"
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Fixture // Fixture
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
static GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
struct Fixture struct Fixture
{ {
GameConfig cfg; GameConfig cfg;
@@ -75,7 +71,7 @@ struct Fixture
std::vector<BeamFiredEvent> beamEvents; std::vector<BeamFiredEvent> beamEvents;
explicit Fixture() explicit Fixture()
: cfg(loadConfig()) : cfg(loadTestConfig())
, belts(cfg.world.beltSpeed_tps) , belts(cfg.world.beltSpeed_tps)
, nextBuildingId(1) , nextBuildingId(1)
, stock(0) , stock(0)

View File

@@ -20,6 +20,7 @@
#include "SimulationTestAccess.h" #include "SimulationTestAccess.h"
#include "SurfaceMask.h" #include "SurfaceMask.h"
#include "Tick.h" #include "Tick.h"
#include "TestConfig.h"
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers that mirror the production implementations under test. // Helpers that mirror the production implementations under test.
@@ -147,11 +148,6 @@ static void applyRotationCCW(Blueprint& bp, const GameConfig& cfg)
} }
} }
static GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
// Mirrors BlueprintPanel::createBlueprintFromSelection's player-placeable filter: // Mirrors BlueprintPanel::createBlueprintFromSelection's player-placeable filter:
// building types absent from buildings.toml (HQ, stations) or with playerPlaceable=false // building types absent from buildings.toml (HQ, stations) or with playerPlaceable=false
// are silently excluded before the bounding-box center and offsets are computed. // are silently excluded before the bounding-box center and offsets are computed.
@@ -312,7 +308,7 @@ TEST_CASE("Blueprint: non-axis-aligned offset rotates correctly", "[blueprint]")
TEST_CASE("Blueprint: CW constellation rotation updates offset and building rotation", "[blueprint]") TEST_CASE("Blueprint: CW constellation rotation updates offset and building rotation", "[blueprint]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
// Building one tile to the right, facing East. // Building one tile to the right, facing East.
Blueprint bp; Blueprint bp;
bp.name = "test"; bp.name = "test";
@@ -332,7 +328,7 @@ TEST_CASE("Blueprint: CW constellation rotation updates offset and building rota
TEST_CASE("Blueprint: CCW constellation rotation updates offset and building rotation", "[blueprint]") TEST_CASE("Blueprint: CCW constellation rotation updates offset and building rotation", "[blueprint]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
Blueprint bp; Blueprint bp;
bp.name = "test"; bp.name = "test";
BlueprintBuilding bb; BlueprintBuilding bb;
@@ -351,7 +347,7 @@ TEST_CASE("Blueprint: CCW constellation rotation updates offset and building rot
TEST_CASE("Blueprint: four CW rotations restore offset and building rotation", "[blueprint]") TEST_CASE("Blueprint: four CW rotations restore offset and building rotation", "[blueprint]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
Blueprint bp; Blueprint bp;
bp.name = "test"; bp.name = "test";
BlueprintBuilding bb; BlueprintBuilding bb;
@@ -371,7 +367,7 @@ TEST_CASE("Blueprint: four CW rotations restore offset and building rotation", "
TEST_CASE("Blueprint: multi-building constellation rotates symmetrically CW", "[blueprint]") TEST_CASE("Blueprint: multi-building constellation rotates symmetrically CW", "[blueprint]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
// Two buildings left and right of center; after CW they should be above and below. // Two buildings left and right of center; after CW they should be above and below.
Blueprint bp; Blueprint bp;
bp.name = "test"; bp.name = "test";
@@ -398,7 +394,7 @@ TEST_CASE("Blueprint: multi-building constellation rotates symmetrically CW", "[
TEST_CASE("Blueprint: CW rotation keeps belt adjacent to miner output port", "[blueprint]") TEST_CASE("Blueprint: CW rotation keeps belt adjacent to miner output port", "[blueprint]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
// East miner: anchor (0,0), body cells (0,0),(1,0),(0,1). // East miner: anchor (0,0), body cells (0,0),(1,0),(0,1).
// Output port indicator '>' at (1,1) → port tile (1,1), direction East. // Output port indicator '>' at (1,1) → port tile (1,1), direction East.
@@ -436,7 +432,7 @@ TEST_CASE("Blueprint: CW rotation keeps belt adjacent to miner output port", "[b
TEST_CASE("Blueprint: CCW rotation keeps belt adjacent to miner output port", "[blueprint]") TEST_CASE("Blueprint: CCW rotation keeps belt adjacent to miner output port", "[blueprint]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
Blueprint bp; Blueprint bp;
bp.name = "test"; bp.name = "test";
@@ -473,7 +469,7 @@ TEST_CASE("Blueprint: CCW rotation keeps belt adjacent to miner output port", "[
TEST_CASE("Blueprint creation: non-player-placeable building alone yields empty blueprint", TEST_CASE("Blueprint creation: non-player-placeable building alone yields empty blueprint",
"[blueprint]") "[blueprint]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
// Hq has no entry in buildings.toml, so it is treated as non-player-placeable. // Hq has no entry in buildings.toml, so it is treated as non-player-placeable.
const BuildingSpec hq{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Hq, Rotation::East }; const BuildingSpec hq{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Hq, Rotation::East };
@@ -485,7 +481,7 @@ TEST_CASE("Blueprint creation: non-player-placeable building alone yields empty
TEST_CASE("Blueprint creation: mixed selection keeps only player-placeable buildings", TEST_CASE("Blueprint creation: mixed selection keeps only player-placeable buildings",
"[blueprint]") "[blueprint]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const BuildingSpec belt{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Belt, Rotation::East }; const BuildingSpec belt{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Belt, Rotation::East };
const BuildingSpec hq { QPoint(-3, 0), {QPoint(-3, 0)}, BuildingType::Hq, Rotation::East }; const BuildingSpec hq { QPoint(-3, 0), {QPoint(-3, 0)}, BuildingType::Hq, Rotation::East };
@@ -498,7 +494,7 @@ TEST_CASE("Blueprint creation: mixed selection keeps only player-placeable build
TEST_CASE("Blueprint creation: bounding box ignores non-player-placeable buildings", TEST_CASE("Blueprint creation: bounding box ignores non-player-placeable buildings",
"[blueprint]") "[blueprint]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
// Belt at (-5, 0). HQ at (-3, 0) — excluded from the blueprint. // Belt at (-5, 0). HQ at (-3, 0) — excluded from the blueprint.
// If HQ were included: bboxX = [-5, -3], center.x = -4, belt offset = -1. // If HQ were included: bboxX = [-5, -3], center.x = -4, belt offset = -1.
@@ -520,7 +516,7 @@ TEST_CASE("Blueprint placement: buildings land at anchor + offset from cursor",
// Simulate placing a two-belt blueprint with offsets (-1, 0) and (+1, 0) // Simulate placing a two-belt blueprint with offsets (-1, 0) and (+1, 0)
// at cursor tile (-5, 0). Expected anchors: (-6, 0) and (-4, 0). // at cursor tile (-5, 0). Expected anchors: (-6, 0) and (-4, 0).
// (Belt surface_mask ["A>"] — body at relative (0,0), port at (1,0).) // (Belt surface_mask ["A>"] — body at relative (0,0), port at (1,0).)
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
const QPoint cursor(-5, 0); const QPoint cursor(-5, 0);
const QPoint offsetA(-1, 0); const QPoint offsetA(-1, 0);
@@ -540,7 +536,7 @@ TEST_CASE("Blueprint placement: buildings land at anchor + offset from cursor",
TEST_CASE("Blueprint placement: cost is deducted for each building in sequence", "[blueprint]") TEST_CASE("Blueprint placement: cost is deducted for each building in sequence", "[blueprint]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
// Find belt cost from config (belt cost = 2 in test config). // Find belt cost from config (belt cost = 2 in test config).
int beltCost = 0; int beltCost = 0;
@@ -563,7 +559,7 @@ TEST_CASE("Blueprint placement: cost is deducted for each building in sequence",
TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId and deducts nothing", TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId and deducts nothing",
"[blueprint]") "[blueprint]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
// Find miner cost (15 in test config) — expensive enough to exhaust a small stock. // Find miner cost (15 in test config) — expensive enough to exhaust a small stock.
int minerCost = 0; int minerCost = 0;
@@ -594,7 +590,7 @@ TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId a
TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and charges nothing", TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and charges nothing",
"[blueprint]") "[blueprint]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
const int startBlocks = sim.getBuildingBlocksStock(); const int startBlocks = sim.getBuildingBlocksStock();
// A miner is all-asteroid; placing it in space (x >= 0) violates the terrain // A miner is all-asteroid; placing it in space (x >= 0) violates the terrain
@@ -610,7 +606,7 @@ TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and ch
TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies tiles, charges cost", TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies tiles, charges cost",
"[blueprint]") "[blueprint]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
int minerCost = 0; int minerCost = 0;
for (const BuildingDef& def : sim.getConfig().buildings.buildings) for (const BuildingDef& def : sim.getConfig().buildings.buildings)
@@ -663,7 +659,7 @@ TEST_CASE("Blueprint: building with no recipe has empty recipeId", "[blueprint]"
TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "[blueprint]") TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "[blueprint]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
// Miner body cells: (0,0),(1,0),(0,1) — all at x < 0, valid for asteroid. // Miner body cells: (0,0),(1,0),(0,1) — all at x < 0, valid for asteroid.
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
@@ -679,7 +675,7 @@ TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "
TEST_CASE("Blueprint placement: recipe transfers to building after construction completes", TEST_CASE("Blueprint placement: recipe transfers to building after construction completes",
"[blueprint]") "[blueprint]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
REQUIRE(id != kInvalidBuildingId); REQUIRE(id != kInvalidBuildingId);
@@ -705,7 +701,7 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction
TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]") TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
// Freshly placed → a ConstructionSite (not ticked to completion). A 1x1 belt keeps // Freshly placed → a ConstructionSite (not ticked to completion). A 1x1 belt keeps
// the body-cell bounding-box centered on the anchor, so a single site → zero offset. // the body-cell bounding-box centered on the anchor, so a single site → zero offset.
@@ -724,7 +720,7 @@ TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]")
TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blueprint]") TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blueprint]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
const BuildingId id = const BuildingId id =
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
@@ -740,7 +736,7 @@ TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blue
TEST_CASE("Blueprint creation: mixed operational building and construction site are both captured", TEST_CASE("Blueprint creation: mixed operational building and construction site are both captured",
"[blueprint]") "[blueprint]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
// Building A: place, configure, and tick to completion so it is operational. // Building A: place, configure, and tick to completion so it is operational.
const BuildingId idA = const BuildingId idA =
@@ -772,7 +768,7 @@ TEST_CASE("Blueprint creation: mixed operational building and construction site
TEST_CASE("Blueprint creation: selectionHasPlaceableBuilding sees a construction site", "[blueprint]") TEST_CASE("Blueprint creation: selectionHasPlaceableBuilding sees a construction site", "[blueprint]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
REQUIRE_FALSE(selectionHasPlaceableBuilding(sim, {})); REQUIRE_FALSE(selectionHasPlaceableBuilding(sim, {}));
@@ -787,7 +783,7 @@ TEST_CASE("Blueprint placement: interceptor schematic is unlocked at game start"
{ {
// "interceptor" has unlock_at_station_level = -1 in the test config. // "interceptor" has unlock_at_station_level = -1 in the test config.
// This confirms the guard in placeBlueprintAtTile passes for start-unlocked schematics. // This confirms the guard in placeBlueprintAtTile passes for start-unlocked schematics.
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
REQUIRE(sim.isSchematicUnlocked("interceptor")); REQUIRE(sim.isSchematicUnlocked("interceptor"));
} }
@@ -796,7 +792,7 @@ TEST_CASE("Blueprint placement: repair_ship schematic is locked at game start",
// "repair_ship" has unlock_at_station_level = 0 in the test config. // "repair_ship" has unlock_at_station_level = 0 in the test config.
// This confirms the guard in placeBlueprintAtTile blocks locked schematics, // This confirms the guard in placeBlueprintAtTile blocks locked schematics,
// leaving the shipyard's schematic unset. // leaving the shipyard's schematic unset.
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship")); REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship"));
} }
@@ -842,7 +838,7 @@ TEST_CASE("Blueprint: building without layout has nullopt shipLayout", "[bluepri
TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout", "[blueprint]") TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout", "[blueprint]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
// Shipyard surface_mask ["AAAS>","AAAS "] with Rotation::East: // Shipyard surface_mask ["AAAS>","AAAS "] with Rotation::East:
// A-tiles at (-3,0),(-2,0),(-1,0),(-3,1),(-2,1),(-1,1) — all x < 0, valid asteroid tiles. // A-tiles at (-3,0),(-2,0),(-1,0),(-3,1),(-2,1),(-1,1) — all x < 0, valid asteroid tiles.
@@ -869,7 +865,7 @@ TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout
TEST_CASE("Blueprint placement: ship layout transfers to building after construction completes", TEST_CASE("Blueprint placement: ship layout transfers to building after construction completes",
"[blueprint]") "[blueprint]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Shipyard, QPoint(-3, 0), Rotation::East).value(); const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Shipyard, QPoint(-3, 0), Rotation::East).value();
REQUIRE(id != kInvalidBuildingId); REQUIRE(id != kInvalidBuildingId);

View File

@@ -13,6 +13,7 @@
#include "ShipsConfig.h" #include "ShipsConfig.h"
#include "Simulation.h" #include "Simulation.h"
#include "SimulationTestAccess.h" #include "SimulationTestAccess.h"
#include "TestConfig.h"
// readBuildingConfig underpins the copy-settings gesture (REQ-BLD-COPY-CONFIG): // readBuildingConfig underpins the copy-settings gesture (REQ-BLD-COPY-CONFIG):
// it extracts a building's recipe / schematic / layout / splitter filters so they // it extracts a building's recipe / schematic / layout / splitter filters so they
@@ -21,11 +22,6 @@
namespace namespace
{ {
GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
const BuildingDef* findDef(const GameConfig& cfg, BuildingType type) const BuildingDef* findDef(const GameConfig& cfg, BuildingType type)
{ {
for (const BuildingDef& def : cfg.buildings.buildings) for (const BuildingDef& def : cfg.buildings.buildings)
@@ -66,8 +62,8 @@ const ShipDef* findAvailableSchematic(const GameConfig& cfg)
TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]") TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
Simulation sim(loadConfig(), 7); Simulation sim(loadTestConfig(), 7);
const BuildingId id = placeOperational(sim, cfg, BuildingType::Miner, QPoint(0, 0)); const BuildingId id = placeOperational(sim, cfg, BuildingType::Miner, QPoint(0, 0));
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore"); SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
@@ -84,8 +80,8 @@ TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]"
TEST_CASE("readBuildingConfig leaves recipe unset when nothing is selected", TEST_CASE("readBuildingConfig leaves recipe unset when nothing is selected",
"[copyconfig]") "[copyconfig]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
Simulation sim(loadConfig(), 7); Simulation sim(loadTestConfig(), 7);
const BuildingId id = placeOperational(sim, cfg, BuildingType::Assembler, QPoint(0, 0)); const BuildingId id = placeOperational(sim, cfg, BuildingType::Assembler, QPoint(0, 0));
@@ -99,8 +95,8 @@ TEST_CASE("readBuildingConfig leaves recipe unset when nothing is selected",
TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout", TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
"[copyconfig]") "[copyconfig]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
Simulation sim(loadConfig(), 7); Simulation sim(loadTestConfig(), 7);
const ShipDef* schematic = findAvailableSchematic(cfg); const ShipDef* schematic = findAvailableSchematic(cfg);
REQUIRE(schematic != nullptr); REQUIRE(schematic != nullptr);
@@ -119,8 +115,8 @@ TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]") TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
Simulation sim(loadConfig(), 7); Simulation sim(loadTestConfig(), 7);
// A placed miner enters the construction queue as a site (not yet operational). // A placed miner enters the construction queue as a site (not yet operational).
const BuildingId id = const BuildingId id =
@@ -140,6 +136,6 @@ TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
TEST_CASE("readBuildingConfig returns nullopt for an unknown id", "[copyconfig]") TEST_CASE("readBuildingConfig returns nullopt for an unknown id", "[copyconfig]")
{ {
Simulation sim(loadConfig(), 7); Simulation sim(loadTestConfig(), 7);
CHECK_FALSE(readBuildingConfig(sim, kInvalidBuildingId).has_value()); CHECK_FALSE(readBuildingConfig(sim, kInvalidBuildingId).has_value());
} }

View File

@@ -19,16 +19,12 @@
#include "Port.h" #include "Port.h"
#include "Rotation.h" #include "Rotation.h"
#include "Tick.h" #include "Tick.h"
#include "TestConfig.h"
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Fixture helpers // Fixture helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
static GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
static Item makeItem(const std::string& id) static Item makeItem(const std::string& id)
{ {
Item item; Item item;
@@ -86,7 +82,7 @@ static std::vector<Item> outputSideItems(const Building& b)
// Owns a BuildingSystem and its dependencies for placement-bounds tests. // Owns a BuildingSystem and its dependencies for placement-bounds tests.
struct PlacementFixture struct PlacementFixture
{ {
GameConfig cfg = loadConfig(); GameConfig cfg = loadTestConfig();
BeltSystem belts{cfg.world.beltSpeed_tps}; BeltSystem belts{cfg.world.beltSpeed_tps};
int stock = 0; int stock = 0;
std::mt19937 rng{0}; std::mt19937 rng{0};
@@ -110,7 +106,7 @@ struct PlacementFixture
TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building]") TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -218,7 +214,7 @@ TEST_CASE("BuildingSystem: isPlacementValid enforces terrain and world bounds",
TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after construction", TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after construction",
"[building]") "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -247,7 +243,7 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con
TEST_CASE("BuildingSystem: placed building enters construction queue", "[building]") TEST_CASE("BuildingSystem: placed building enters construction queue", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -289,7 +285,7 @@ TEST_CASE("BuildingSystem: deconstructing a construction site removes it instant
TEST_CASE("BuildingSystem: first queued building starts construction immediately", TEST_CASE("BuildingSystem: first queued building starts construction immediately",
"[building]") "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -307,7 +303,7 @@ TEST_CASE("BuildingSystem: first queued building starts construction immediately
TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[building]") TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -329,7 +325,7 @@ TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[b
TEST_CASE("BuildingSystem: construction completes after configured duration", "[building]") TEST_CASE("BuildingSystem: construction completes after configured duration", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -495,7 +491,7 @@ TEST_CASE("BuildingSystem: splitter filters survive a queue/un-queue round-trip"
TEST_CASE("BuildingSystem: second building starts after first completes", "[building]") TEST_CASE("BuildingSystem: second building starts after first completes", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -525,7 +521,7 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil
TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[building]") TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -558,7 +554,7 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui
TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]") TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -598,7 +594,7 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites", "[building]") TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -638,7 +634,7 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites",
TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle state", TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle state",
"[building]") "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -679,7 +675,7 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle
TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing belt", TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing belt",
"[building]") "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
// Fast belt so items are immediately available for peek/take. // Fast belt so items are immediately available for peek/take.
BeltSystem belts(static_cast<double>(kTickRateHz)); BeltSystem belts(static_cast<double>(kTickRateHz));
int stock = 0; int stock = 0;
@@ -722,7 +718,7 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
TEST_CASE("BuildingSystem: accepted input travels inward before entering the buffer", TEST_CASE("BuildingSystem: accepted input travels inward before entering the buffer",
"[building]") "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(static_cast<double>(kTickRateHz)); // fast belt: 1 tile/tick BeltSystem belts(static_cast<double>(kTickRateHz)); // fast belt: 1 tile/tick
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -763,7 +759,7 @@ TEST_CASE("BuildingSystem: accepted input travels inward before entering the buf
TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at the cap", TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at the cap",
"[building]") "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(static_cast<double>(kTickRateHz)); // fast belt BeltSystem belts(static_cast<double>(kTickRateHz)); // fast belt
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -805,7 +801,7 @@ TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at th
TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection", TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection",
"[building]") "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(static_cast<double>(kTickRateHz)); BeltSystem belts(static_cast<double>(kTickRateHz));
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -851,7 +847,7 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection",
TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete batch waits", TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete batch waits",
"[building]") "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(static_cast<double>(kTickRateHz)); BeltSystem belts(static_cast<double>(kTickRateHz));
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -905,7 +901,7 @@ TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete
TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[building]") TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(static_cast<double>(kTickRateHz)); BeltSystem belts(static_cast<double>(kTickRateHz));
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -944,7 +940,7 @@ TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[bui
TEST_CASE("BuildingSystem: output port couples directly into an adjacent input port", TEST_CASE("BuildingSystem: output port couples directly into an adjacent input port",
"[building]") "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -984,7 +980,7 @@ TEST_CASE("BuildingSystem: output port couples directly into an adjacent input p
TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stuck", TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stuck",
"[building]") "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -1023,7 +1019,7 @@ TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stu
TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production", TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production",
"[building]") "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(static_cast<double>(kTickRateHz)); BeltSystem belts(static_cast<double>(kTickRateHz));
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -1066,7 +1062,7 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production"
TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max output per roll", TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max output per roll",
"[building]") "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -1097,7 +1093,7 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max
TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then stalls", TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then stalls",
"[building]") "[building]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(static_cast<double>(kTickRateHz)); BeltSystem belts(static_cast<double>(kTickRateHz));
int stock = 0; int stock = 0;
// Seed chosen so first roll produces 2-item output (iron_ingot), filling buffer. // Seed chosen so first roll produces 2-item output (iron_ingot), filling buffer.
@@ -1156,7 +1152,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when tile is empty", TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when tile is empty",
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -1175,7 +1171,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when tile is
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a queued belt (same type, different rotation)", TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a queued belt (same type, different rotation)",
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -1198,7 +1194,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a que
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a completed operational belt", TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a completed operational belt",
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -1225,7 +1221,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when building type differs", TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when building type differs",
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -1247,7 +1243,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when building
TEST_CASE("BuildingSystem: findRotateInPlaceTarget never rotates a tunnel in place", TEST_CASE("BuildingSystem: findRotateInPlaceTarget never rotates a tunnel in place",
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -1273,7 +1269,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget never rotates a tunnel in pla
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprints only partially overlap", TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprints only partially overlap",
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -1297,7 +1293,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprin
TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-tile building with rotated ghost", TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-tile building with rotated ghost",
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -1326,7 +1322,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-t
TEST_CASE("BuildingSystem: rotateInPlace updates the rotation field of a construction site", TEST_CASE("BuildingSystem: rotateInPlace updates the rotation field of a construction site",
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -1349,7 +1345,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates the rotation field of a constru
TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of a queued site", TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of a queued site",
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -1373,7 +1369,7 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of
TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direction on an operational building", TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direction on an operational building",
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);
@@ -1404,7 +1400,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direct
TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSystem so it still accepts items", TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSystem so it still accepts items",
"[building][rotate-in-place]") "[building][rotate-in-place]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
BeltSystem belts(cfg.world.beltSpeed_tps); BeltSystem belts(cfg.world.beltSpeed_tps);
int stock = 0; int stock = 0;
std::mt19937 rng(0); std::mt19937 rng(0);

View File

@@ -2,6 +2,8 @@
add_files( add_files(
TEST_FILES TEST_FILES
TestConfig.h
test.cpp test.cpp
FormulaTest.cpp FormulaTest.cpp
ConfigLoaderTest.cpp ConfigLoaderTest.cpp

View File

@@ -22,11 +22,7 @@
#include "StationBodyComponent.h" #include "StationBodyComponent.h"
#include "Tick.h" #include "Tick.h"
#include "WeaponComponent.h" #include "WeaponComponent.h"
#include "TestConfig.h"
static GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
static const ShipDef* findCombatShip(const GameConfig& cfg) static const ShipDef* findCombatShip(const GameConfig& cfg)
{ {
@@ -64,7 +60,7 @@ struct CombatFixture
CombatSystem combat; CombatSystem combat;
explicit CombatFixture() explicit CombatFixture()
: cfg(loadConfig()) : cfg(loadTestConfig())
, rng(42) , rng(42)
, nextBuildingId(1) , nextBuildingId(1)
, belts(cfg.world.beltSpeed_tps) , belts(cfg.world.beltSpeed_tps)
@@ -178,7 +174,7 @@ TEST_CASE("CombatSystem: no fire when target is out of range", "[combat]")
TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]") TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
// Find the player station entity via ECS. // Find the player station entity via ECS.
entt::entity stationEntity = entt::null; entt::entity stationEntity = entt::null;
@@ -217,7 +213,7 @@ TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]
TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]") TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
entt::entity stationEntity = entt::null; entt::entity stationEntity = entt::null;
QVector2D stationCenter; QVector2D stationCenter;
@@ -255,7 +251,7 @@ TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]
TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]") TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
entt::entity stationEntity = entt::null; entt::entity stationEntity = entt::null;
QVector2D stationCenter; QVector2D stationCenter;
@@ -388,7 +384,7 @@ TEST_CASE("CombatSystem: damage still applied if shooter already dead", "[combat
TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]") TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const ShipDef* combatDef = findCombatShip(sim.getConfig()); const ShipDef* combatDef = findCombatShip(sim.getConfig());
REQUIRE(combatDef != nullptr); REQUIRE(combatDef != nullptr);
@@ -405,7 +401,7 @@ TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]") TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
// Scrap dropped on death is derived from the ship's as-built threat cost // Scrap dropped on death is derived from the ship's as-built threat cost
// (REQ-RES-DEBRIS-DROP): round(threat * scrap_per_threat). The interceptor's // (REQ-RES-DEBRIS-DROP): round(threat * scrap_per_threat). The interceptor's
@@ -424,7 +420,7 @@ TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
TEST_CASE("CombatSystem: HQ death sets game over", "[combat]") TEST_CASE("CombatSystem: HQ death sets game over", "[combat]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
// Damage the HQ proxy entity (has HqProxy + Health). // Damage the HQ proxy entity (has HqProxy + Health).
sim.getAdmin().forEach<HqProxyComponent, HealthComponent>( sim.getAdmin().forEach<HqProxyComponent, HealthComponent>(

View File

@@ -11,14 +11,7 @@
#include "Simulation.h" #include "Simulation.h"
#include "SimulationTestAccess.h" #include "SimulationTestAccess.h"
#include "Tick.h" #include "Tick.h"
#include "TestConfig.h"
namespace
{
GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
} // namespace
// The command chokepoint (Simulation::apply) must produce exactly the same state // The command chokepoint (Simulation::apply) must produce exactly the same state
// as driving the underlying mutators directly — that equivalence is what lets a // as driving the underlying mutators directly — that equivalence is what lets a
@@ -26,8 +19,8 @@ GameConfig loadConfig()
TEST_CASE("apply(PlaceBuildingCommand) matches direct placement", "[command]") TEST_CASE("apply(PlaceBuildingCommand) matches direct placement", "[command]")
{ {
Simulation viaCommand(loadConfig(), 99); Simulation viaCommand(loadTestConfig(), 99);
Simulation viaDirect(loadConfig(), 99); Simulation viaDirect(loadTestConfig(), 99);
PlaceBuildingCommand command; PlaceBuildingCommand command;
command.type = BuildingType::Miner; command.type = BuildingType::Miner;
@@ -42,8 +35,8 @@ TEST_CASE("apply(PlaceBuildingCommand) matches direct placement", "[command]")
TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe", "[command]") TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe", "[command]")
{ {
Simulation viaCommand(loadConfig(), 99); Simulation viaCommand(loadTestConfig(), 99);
Simulation viaDirect(loadConfig(), 99); Simulation viaDirect(loadTestConfig(), 99);
PlaceBuildingCommand command; PlaceBuildingCommand command;
command.type = BuildingType::Miner; command.type = BuildingType::Miner;
@@ -61,8 +54,8 @@ TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe"
TEST_CASE("apply(DeconstructCommand) matches direct deconstruct", "[command]") TEST_CASE("apply(DeconstructCommand) matches direct deconstruct", "[command]")
{ {
Simulation viaCommand(loadConfig(), 99); Simulation viaCommand(loadTestConfig(), 99);
Simulation viaDirect(loadConfig(), 99); Simulation viaDirect(loadTestConfig(), 99);
const BuildingId idA = const BuildingId idA =
SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value(); SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value();
@@ -81,8 +74,8 @@ TEST_CASE("apply(DeconstructCommand) matches direct deconstruct", "[command]")
TEST_CASE("apply(CancelDeconstructionCommand) matches direct cancelDeconstruction", "[command]") TEST_CASE("apply(CancelDeconstructionCommand) matches direct cancelDeconstruction", "[command]")
{ {
Simulation viaCommand(loadConfig(), 99); Simulation viaCommand(loadTestConfig(), 99);
Simulation viaDirect(loadConfig(), 99); Simulation viaDirect(loadTestConfig(), 99);
const BuildingId idA = const BuildingId idA =
SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value(); SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value();
@@ -110,8 +103,8 @@ TEST_CASE("apply(CancelDeconstructionCommand) matches direct cancelDeconstructio
TEST_CASE("CommandManager drains queued commands in FIFO order through apply", "[command]") TEST_CASE("CommandManager drains queued commands in FIFO order through apply", "[command]")
{ {
Simulation viaManager(loadConfig(), 99); Simulation viaManager(loadTestConfig(), 99);
Simulation viaDirect(loadConfig(), 99); Simulation viaDirect(loadTestConfig(), 99);
CommandManager manager(viaManager); CommandManager manager(viaManager);

View File

@@ -11,14 +11,10 @@
#include "SimulationTestAccess.h" #include "SimulationTestAccess.h"
#include "StateChecksum.h" #include "StateChecksum.h"
#include "Tick.h" #include "Tick.h"
#include "TestConfig.h"
namespace namespace
{ {
GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
constexpr int kScriptTicks = 2000; constexpr int kScriptTicks = 2000;
// 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
@@ -26,7 +22,7 @@ constexpr int kScriptTicks = 2000;
// 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.
std::vector<std::uint64_t> runScriptedSession(unsigned int seed) std::vector<std::uint64_t> runScriptedSession(unsigned int seed)
{ {
Simulation sim(loadConfig(), seed); Simulation sim(loadTestConfig(), seed);
// Tick 0: a miner feeding a short belt line on the asteroid. // 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::Miner, QPoint(-3, 0), Rotation::East);
@@ -121,8 +117,8 @@ TEST_CASE("fingerprintRng: equal states match, advanced states differ", "[determ
TEST_CASE("Simulation::rngFingerprint is stable for equal seeds", "[determinism]") TEST_CASE("Simulation::rngFingerprint is stable for equal seeds", "[determinism]")
{ {
const Simulation a(loadConfig(), 777); const Simulation a(loadTestConfig(), 777);
const Simulation b(loadConfig(), 777); const Simulation b(loadTestConfig(), 777);
REQUIRE(a.getRngFingerprint() == b.getRngFingerprint()); REQUIRE(a.getRngFingerprint() == b.getRngFingerprint());
} }

View File

@@ -2,11 +2,7 @@
#include "ConfigLoader.h" #include "ConfigLoader.h"
#include "ModulesConfig.h" #include "ModulesConfig.h"
#include "TestConfig.h"
static GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
static const ModuleDef* findModule(const GameConfig& cfg, const std::string& id) static const ModuleDef* findModule(const GameConfig& cfg, const std::string& id)
{ {
@@ -28,7 +24,7 @@ static const ModuleStatModifier* findModifier(const ModuleDef& def, const std::s
TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]") TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
REQUIRE(cfg.modules.modules.size() >= 2); REQUIRE(cfg.modules.modules.size() >= 2);
const ModuleDef& armor = cfg.modules.modules[0]; const ModuleDef& armor = cfg.modules.modules[0];
@@ -49,7 +45,7 @@ TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]")
TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modules]") TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modules]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
REQUIRE(cfg.modules.modules.size() >= 2); REQUIRE(cfg.modules.modules.size() >= 2);
const ModuleDef& sensor = cfg.modules.modules[1]; const ModuleDef& sensor = cfg.modules.modules[1];
@@ -62,7 +58,7 @@ TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modul
TEST_CASE("ConfigLoader: multiplicative modifier with unit suffix is parsed (weapon_primer)", "[config][modules]") TEST_CASE("ConfigLoader: multiplicative modifier with unit suffix is parsed (weapon_primer)", "[config][modules]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ModuleDef* primer = findModule(cfg, "weapon_primer"); const ModuleDef* primer = findModule(cfg, "weapon_primer");
REQUIRE(primer != nullptr); REQUIRE(primer != nullptr);
REQUIRE(primer->statModifiers.size() == 1); REQUIRE(primer->statModifiers.size() == 1);
@@ -74,7 +70,7 @@ TEST_CASE("ConfigLoader: multiplicative modifier with unit suffix is parsed (wea
TEST_CASE("ConfigLoader: weapon_stabilizer parses two multiplicative weapon modifiers", "[config][modules]") TEST_CASE("ConfigLoader: weapon_stabilizer parses two multiplicative weapon modifiers", "[config][modules]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ModuleDef* stab = findModule(cfg, "weapon_stabilizer"); const ModuleDef* stab = findModule(cfg, "weapon_stabilizer");
REQUIRE(stab != nullptr); REQUIRE(stab != nullptr);
REQUIRE(stab->statModifiers.size() == 2); REQUIRE(stab->statModifiers.size() == 2);
@@ -92,7 +88,7 @@ TEST_CASE("ConfigLoader: weapon_stabilizer parses two multiplicative weapon modi
TEST_CASE("ConfigLoader: afterburner parses multiplicative speed and additive main_acceleration", "[config][modules]") TEST_CASE("ConfigLoader: afterburner parses multiplicative speed and additive main_acceleration", "[config][modules]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ModuleDef* ab = findModule(cfg, "afterburner"); const ModuleDef* ab = findModule(cfg, "afterburner");
REQUIRE(ab != nullptr); REQUIRE(ab != nullptr);
REQUIRE(ab->statModifiers.size() == 2); REQUIRE(ab->statModifiers.size() == 2);
@@ -110,7 +106,7 @@ TEST_CASE("ConfigLoader: afterburner parses multiplicative speed and additive ma
TEST_CASE("ConfigLoader: maneuvering_thrusters parses multiplicative speed and additive maneuvering_acceleration", "[config][modules]") TEST_CASE("ConfigLoader: maneuvering_thrusters parses multiplicative speed and additive maneuvering_acceleration", "[config][modules]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ModuleDef* mt = findModule(cfg, "maneuvering_thrusters"); const ModuleDef* mt = findModule(cfg, "maneuvering_thrusters");
REQUIRE(mt != nullptr); REQUIRE(mt != nullptr);
REQUIRE(mt->statModifiers.size() == 2); REQUIRE(mt->statModifiers.size() == 2);
@@ -128,7 +124,7 @@ TEST_CASE("ConfigLoader: maneuvering_thrusters parses multiplicative speed and a
TEST_CASE("ConfigLoader: loadShips parses layout field", "[config][ships]") TEST_CASE("ConfigLoader: loadShips parses layout field", "[config][ships]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
REQUIRE(!cfg.ships.ships.empty()); REQUIRE(!cfg.ships.ships.empty());
const ShipDef& ship = cfg.ships.ships[0]; const ShipDef& ship = cfg.ships.ships[0];

View File

@@ -12,11 +12,7 @@
#include "Simulation.h" #include "Simulation.h"
#include "SimulationTestAccess.h" #include "SimulationTestAccess.h"
#include "StationBodyComponent.h" #include "StationBodyComponent.h"
#include "TestConfig.h"
static GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
// Zeros the HP of both enemy defence stations and advances one tick so that // Zeros the HP of both enemy defence stations and advances one tick so that
// tickDeathsAndLoot fires, triggering the push and schematic choices. // tickDeathsAndLoot fires, triggering the push and schematic choices.
@@ -62,7 +58,7 @@ static bool awaitRecipeUnlock(Simulation& sim, const std::string& recipeId,
TEST_CASE("RecipeSchematic: unlocked_at_start = true parsed correctly", "[recipe_schematic]") TEST_CASE("RecipeSchematic: unlocked_at_start = true parsed correctly", "[recipe_schematic]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const auto it = std::find_if(cfg.recipes.recipes.begin(), cfg.recipes.recipes.end(), const auto it = std::find_if(cfg.recipes.recipes.begin(), cfg.recipes.recipes.end(),
[](const RecipeDef& r) { return r.id == "premium_circuit"; }); [](const RecipeDef& r) { return r.id == "premium_circuit"; });
REQUIRE(it != cfg.recipes.recipes.end()); REQUIRE(it != cfg.recipes.recipes.end());
@@ -71,7 +67,7 @@ TEST_CASE("RecipeSchematic: unlocked_at_start = true parsed correctly", "[recipe
TEST_CASE("RecipeSchematic: a gated recipe is granted by an unlock group", "[recipe_schematic]") TEST_CASE("RecipeSchematic: a gated recipe is granted by an unlock group", "[recipe_schematic]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const bool granted = std::any_of(cfg.unlocks.groups.begin(), cfg.unlocks.groups.end(), const bool granted = std::any_of(cfg.unlocks.groups.begin(), cfg.unlocks.groups.end(),
[](const UnlockGroupDef& g) [](const UnlockGroupDef& g)
{ {
@@ -82,7 +78,7 @@ TEST_CASE("RecipeSchematic: a gated recipe is granted by an unlock group", "[rec
TEST_CASE("RecipeSchematic: an untagged assembler recipe has unlocked_at_start = false", "[recipe_schematic]") TEST_CASE("RecipeSchematic: an untagged assembler recipe has unlocked_at_start = false", "[recipe_schematic]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const auto it = std::find_if(cfg.recipes.recipes.begin(), cfg.recipes.recipes.end(), const auto it = std::find_if(cfg.recipes.recipes.begin(), cfg.recipes.recipes.end(),
[](const RecipeDef& r) { return r.id == "circuit_board"; }); [](const RecipeDef& r) { return r.id == "circuit_board"; });
REQUIRE(it != cfg.recipes.recipes.end()); REQUIRE(it != cfg.recipes.recipes.end());
@@ -96,21 +92,21 @@ TEST_CASE("RecipeSchematic: an untagged assembler recipe has unlocked_at_start =
TEST_CASE("RecipeSchematic: recipe with unlock_at_station_level = -1 is unlocked at game start", TEST_CASE("RecipeSchematic: recipe with unlock_at_station_level = -1 is unlocked at game start",
"[recipe_schematic]") "[recipe_schematic]")
{ {
const Simulation sim(loadConfig()); const Simulation sim(loadTestConfig());
REQUIRE(sim.isRecipeUnlocked("premium_circuit")); REQUIRE(sim.isRecipeUnlocked("premium_circuit"));
} }
TEST_CASE("RecipeSchematic: recipe with unlock_at_station_level = 0 is locked at game start", TEST_CASE("RecipeSchematic: recipe with unlock_at_station_level = 0 is locked at game start",
"[recipe_schematic]") "[recipe_schematic]")
{ {
const Simulation sim(loadConfig()); const Simulation sim(loadTestConfig());
REQUIRE_FALSE(sim.isRecipeUnlocked("quick_circuit")); REQUIRE_FALSE(sim.isRecipeUnlocked("quick_circuit"));
} }
TEST_CASE("RecipeSchematic: recipe with unlock_at_station_level = 1 is locked at game start", TEST_CASE("RecipeSchematic: recipe with unlock_at_station_level = 1 is locked at game start",
"[recipe_schematic]") "[recipe_schematic]")
{ {
const Simulation sim(loadConfig()); const Simulation sim(loadTestConfig());
REQUIRE_FALSE(sim.isRecipeUnlocked("advanced_circuit")); REQUIRE_FALSE(sim.isRecipeUnlocked("advanced_circuit"));
} }
@@ -123,7 +119,7 @@ TEST_CASE("RecipeSchematic: -1 recipe seeds its output item into the implicit un
{ {
// premium_circuit is not needed by any ship or module schematic, so it can // premium_circuit is not needed by any ship or module schematic, so it can
// only reach the implicit set via the -1 recipe seed in Phase 1. // only reach the implicit set via the -1 recipe seed in Phase 1.
const Simulation sim(loadConfig()); const Simulation sim(loadTestConfig());
REQUIRE(sim.isItemUnlocked("premium_circuit")); REQUIRE(sim.isItemUnlocked("premium_circuit"));
} }
@@ -132,7 +128,7 @@ TEST_CASE("RecipeSchematic: -1 recipe's inputs are in the implicit unlock set",
{ {
// premium_circuit takes circuit_board as input; that item was already // premium_circuit takes circuit_board as input; that item was already
// implicitly unlocked by ship schematics, so it must remain unlocked. // implicitly unlocked by ship schematics, so it must remain unlocked.
const Simulation sim(loadConfig()); const Simulation sim(loadTestConfig());
REQUIRE(sim.isItemUnlocked("circuit_board")); REQUIRE(sim.isItemUnlocked("circuit_board"));
} }
@@ -142,7 +138,7 @@ TEST_CASE("RecipeSchematic: locked recipe's unique input is not in the implicit
// exotic_alloy has unlock_at_station_level = 0 (locked at start) and takes // exotic_alloy has unlock_at_station_level = 0 (locked at start) and takes
// exotic_ore as input. exotic_ore is only reachable through this locked // exotic_ore as input. exotic_ore is only reachable through this locked
// recipe, so it must not appear in the implicit set. // recipe, so it must not appear in the implicit set.
const Simulation sim(loadConfig()); const Simulation sim(loadTestConfig());
REQUIRE_FALSE(sim.isItemUnlocked("exotic_ore")); REQUIRE_FALSE(sim.isItemUnlocked("exotic_ore"));
} }
@@ -152,7 +148,7 @@ TEST_CASE("RecipeSchematic: locked recipe's output item is not in the implicit u
// exotic_alloy is produced only by the locked recipe of the same name and // exotic_alloy is produced only by the locked recipe of the same name and
// is not needed by any schematic, so neither the item nor the recipe should // is not needed by any schematic, so neither the item nor the recipe should
// be unlocked at game start. // be unlocked at game start.
const Simulation sim(loadConfig()); const Simulation sim(loadTestConfig());
REQUIRE_FALSE(sim.isItemUnlocked("exotic_alloy")); REQUIRE_FALSE(sim.isItemUnlocked("exotic_alloy"));
REQUIRE_FALSE(sim.isRecipeUnlocked("exotic_alloy")); REQUIRE_FALSE(sim.isRecipeUnlocked("exotic_alloy"));
} }
@@ -162,7 +158,7 @@ TEST_CASE("RecipeSchematic: normal implicit unlock is unaffected for untagged as
{ {
// circuit_board carries no unlock_at_station_level and is needed by ships // circuit_board carries no unlock_at_station_level and is needed by ships
// that start unlocked, so it must still be implicitly unlocked. // that start unlocked, so it must still be implicitly unlocked.
const Simulation sim(loadConfig()); const Simulation sim(loadTestConfig());
REQUIRE(sim.isRecipeUnlocked("circuit_board")); REQUIRE(sim.isRecipeUnlocked("circuit_board"));
} }
@@ -176,7 +172,7 @@ TEST_CASE("RecipeSchematic: eligible recipe schematic is eventually awarded on s
// quick_circuit has unlock_at_station_level = 0 and produces circuit_board // quick_circuit has unlock_at_station_level = 0 and produces circuit_board
// (already implicitly unlocked), so it is eligible from the first station // (already implicitly unlocked), so it is eligible from the first station
// destruction. With up to 150 trials it must be awarded at least once. // destruction. With up to 150 trials it must be awarded at least once.
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
REQUIRE(awaitRecipeUnlock(sim, "quick_circuit")); REQUIRE(awaitRecipeUnlock(sim, "quick_circuit"));
} }
@@ -185,7 +181,7 @@ TEST_CASE("RecipeSchematic: an implicitly-gated recipe with no unlock group is n
{ {
// exotic_alloy is in no unlock group and its output/inputs are unreachable // exotic_alloy is in no unlock group and its output/inputs are unreachable
// via the item graph, so it can never be dropped or implicitly unlocked. // via the item graph, so it can never be dropped or implicitly unlocked.
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
for (int i = 0; i < 50; ++i) for (int i = 0; i < 50; ++i)
{ {
killEnemyStationsAndApply(sim); killEnemyStationsAndApply(sim);
@@ -198,7 +194,7 @@ TEST_CASE("RecipeSchematic: recipe with level > destroyed station level is not a
{ {
// advanced_circuit has unlock_at_station_level = 1. Destroying a single // advanced_circuit has unlock_at_station_level = 1. Destroying a single
// level-0 station set must not award it regardless of the RNG outcome. // level-0 station set must not award it regardless of the RNG outcome.
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
killEnemyStationsAndApply(sim); killEnemyStationsAndApply(sim);
REQUIRE_FALSE(sim.isRecipeUnlocked("advanced_circuit")); REQUIRE_FALSE(sim.isRecipeUnlocked("advanced_circuit"));
} }
@@ -208,14 +204,14 @@ TEST_CASE("RecipeSchematic: recipe with higher level is awarded once eligible st
{ {
// After enough destructions to pass station level 1, advanced_circuit must // After enough destructions to pass station level 1, advanced_circuit must
// eventually be awarded. // eventually be awarded.
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
REQUIRE(awaitRecipeUnlock(sim, "advanced_circuit", 300)); REQUIRE(awaitRecipeUnlock(sim, "advanced_circuit", 300));
} }
TEST_CASE("RecipeSchematic: awarded recipe schematic stays unlocked and is not awarded again", TEST_CASE("RecipeSchematic: awarded recipe schematic stays unlocked and is not awarded again",
"[recipe_schematic]") "[recipe_schematic]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
awaitRecipeUnlock(sim, "quick_circuit"); awaitRecipeUnlock(sim, "quick_circuit");
REQUIRE(sim.isRecipeUnlocked("quick_circuit")); REQUIRE(sim.isRecipeUnlocked("quick_circuit"));
@@ -231,7 +227,7 @@ TEST_CASE("RecipeSchematic: awarded recipe schematic stays unlocked and is not a
TEST_CASE("RecipeSchematic: recipe schematic can appear in pending choices", TEST_CASE("RecipeSchematic: recipe schematic can appear in pending choices",
"[recipe_schematic]") "[recipe_schematic]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
bool foundRecipeChoice = false; bool foundRecipeChoice = false;
for (int i = 0; i < 150 && !foundRecipeChoice; ++i) for (int i = 0; i < 150 && !foundRecipeChoice; ++i)
@@ -260,7 +256,7 @@ TEST_CASE("RecipeSchematic: recipe schematic can appear in pending choices",
TEST_CASE("RecipeSchematic: reset re-locks a previously awarded recipe schematic", TEST_CASE("RecipeSchematic: reset re-locks a previously awarded recipe schematic",
"[recipe_schematic]") "[recipe_schematic]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
awaitRecipeUnlock(sim, "quick_circuit"); awaitRecipeUnlock(sim, "quick_circuit");
REQUIRE(sim.isRecipeUnlocked("quick_circuit")); REQUIRE(sim.isRecipeUnlocked("quick_circuit"));
@@ -272,7 +268,7 @@ TEST_CASE("RecipeSchematic: reset re-locks a previously awarded recipe schematic
TEST_CASE("RecipeSchematic: reset keeps -1 recipes unlocked and their seed items accessible", TEST_CASE("RecipeSchematic: reset keeps -1 recipes unlocked and their seed items accessible",
"[recipe_schematic]") "[recipe_schematic]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
sim.reset(); sim.reset();
REQUIRE(sim.isRecipeUnlocked("premium_circuit")); REQUIRE(sim.isRecipeUnlocked("premium_circuit"));
@@ -286,7 +282,7 @@ TEST_CASE("RecipeSchematic: reset keeps -1 recipes unlocked and their seed items
TEST_CASE("RecipeSchematic: newlyUnlockedRecipeIds is sorted, deduplicated, and empty for level-ups", TEST_CASE("RecipeSchematic: newlyUnlockedRecipeIds is sorted, deduplicated, and empty for level-ups",
"[recipe_schematic]") "[recipe_schematic]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
for (int i = 0; i < 100; ++i) for (int i = 0; i < 100; ++i)
{ {
@@ -310,8 +306,8 @@ TEST_CASE("RecipeSchematic: newlyUnlockedRecipeIds is sorted, deduplicated, and
TEST_CASE("RecipeSchematic: newlyUnlockedRecipeIds matches recipes that actually become unlocked", TEST_CASE("RecipeSchematic: newlyUnlockedRecipeIds matches recipes that actually become unlocked",
"[recipe_schematic]") "[recipe_schematic]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
auto unlockedTrackedRecipeIds = [&]() auto unlockedTrackedRecipeIds = [&]()
{ {
@@ -365,7 +361,7 @@ TEST_CASE("SchematicDrop: an owned ship schematic is never offered again",
{ {
// repair_ship has unlock_at_station_level = 0 in the test config, so it is // repair_ship has unlock_at_station_level = 0 in the test config, so it is
// locked at start and becomes eligible once a station set is destroyed. // locked at start and becomes eligible once a station set is destroyed.
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship")); REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship"));
bool wasUnlocked = false; bool wasUnlocked = false;

View File

@@ -16,14 +16,10 @@
#include "ReplayRecorder.h" #include "ReplayRecorder.h"
#include "Rotation.h" #include "Rotation.h"
#include "Simulation.h" #include "Simulation.h"
#include "TestConfig.h"
namespace namespace
{ {
GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
std::string tempOutputDir() std::string tempOutputDir()
{ {
return (QDir::tempPath() + "/dota_factory_replay_playback_test").toStdString(); return (QDir::tempPath() + "/dota_factory_replay_playback_test").toStdString();
@@ -148,7 +144,7 @@ TEST_CASE("a recorded run replays to byte-identical state with no desync", "[rep
std::string replayPath; std::string replayPath;
std::uint64_t recordedFinalChecksum = 0; std::uint64_t recordedFinalChecksum = 0;
{ {
Simulation rec(loadConfig(), seed); Simulation rec(loadTestConfig(), seed);
CommandManager manager(rec); CommandManager manager(rec);
std::unique_ptr<ReplayRecorder> recorder = std::unique_ptr<ReplayRecorder> recorder =
std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir()); std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir());
@@ -179,7 +175,7 @@ TEST_CASE("a recorded run replays to byte-identical state with no desync", "[rep
REQUIRE_FALSE(parsed->entries.empty()); REQUIRE_FALSE(parsed->entries.empty());
// --- Replay it. --- // --- Replay it. ---
Simulation play(loadConfig(), parsed->header.seed); Simulation play(loadTestConfig(), parsed->header.seed);
ReplayPlayer player(play, parsed->entries); ReplayPlayer player(play, parsed->entries);
player.start(); player.start();
while (!player.isFinished()) while (!player.isFinished())
@@ -201,7 +197,7 @@ TEST_CASE("ReplayPlayer reports a desync when a checksum is corrupted", "[replay
std::string replayPath; std::string replayPath;
{ {
Simulation rec(loadConfig(), seed); Simulation rec(loadTestConfig(), seed);
CommandManager manager(rec); CommandManager manager(rec);
std::unique_ptr<ReplayRecorder> recorder = std::unique_ptr<ReplayRecorder> recorder =
std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir()); std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir());
@@ -228,7 +224,7 @@ TEST_CASE("ReplayPlayer reports a desync when a checksum is corrupted", "[replay
} }
REQUIRE(corrupted); REQUIRE(corrupted);
Simulation play(loadConfig(), parsed->header.seed); Simulation play(loadTestConfig(), parsed->header.seed);
ReplayPlayer player(play, parsed->entries); ReplayPlayer player(play, parsed->entries);
player.start(); player.start();
while (!player.isFinished()) while (!player.isFinished())
@@ -250,7 +246,7 @@ TEST_CASE("a long recorded run (through waves and combat) replays with no desync
std::string replayPath; std::string replayPath;
std::uint64_t recordedFinalChecksum = 0; std::uint64_t recordedFinalChecksum = 0;
{ {
Simulation rec(loadConfig(), seed); Simulation rec(loadTestConfig(), seed);
CommandManager manager(rec); CommandManager manager(rec);
std::unique_ptr<ReplayRecorder> recorder = std::unique_ptr<ReplayRecorder> recorder =
std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir()); std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir());
@@ -275,7 +271,7 @@ TEST_CASE("a long recorded run (through waves and combat) replays with no desync
const std::optional<ParsedReplay> parsed = readReplayFile(replayPath); const std::optional<ParsedReplay> parsed = readReplayFile(replayPath);
REQUIRE(parsed.has_value()); REQUIRE(parsed.has_value());
Simulation play(loadConfig(), parsed->header.seed); Simulation play(loadTestConfig(), parsed->header.seed);
ReplayPlayer player(play, parsed->entries); ReplayPlayer player(play, parsed->entries);
player.start(); player.start();
while (!player.isFinished()) while (!player.isFinished())

View File

@@ -16,6 +16,7 @@
#include "GameConfig.h" #include "GameConfig.h"
#include "ReplayRecorder.h" #include "ReplayRecorder.h"
#include "Simulation.h" #include "Simulation.h"
#include "TestConfig.h"
namespace namespace
{ {
@@ -144,7 +145,7 @@ TEST_CASE("ReplayRecorder writes a well-formed file", "[replay]")
TEST_CASE("CommandManager records commands and an initial checksum on drain", "[replay]") TEST_CASE("CommandManager records commands and an initial checksum on drain", "[replay]")
{ {
Simulation sim(ConfigLoader::loadFromDirectory(CONFIG_DIR), 7u); Simulation sim(loadTestConfig(), 7u);
CommandManager manager(sim); CommandManager manager(sim);
std::unique_ptr<ReplayRecorder> recorder = std::unique_ptr<ReplayRecorder> recorder =
@@ -187,7 +188,7 @@ TEST_CASE("ReplayRecorder startNewRun rolls to a new file", "[replay]")
TEST_CASE("CommandManager rolls the replay file on a Reset command", "[replay]") TEST_CASE("CommandManager rolls the replay file on a Reset command", "[replay]")
{ {
Simulation sim(ConfigLoader::loadFromDirectory(CONFIG_DIR), 1u); Simulation sim(loadTestConfig(), 1u);
CommandManager manager(sim); CommandManager manager(sim);
std::unique_ptr<ReplayRecorder> recorder = std::unique_ptr<ReplayRecorder> recorder =
@@ -197,7 +198,7 @@ TEST_CASE("CommandManager rolls the replay file on a Reset command", "[replay]")
const std::string firstPath = recorderPtr->getCurrentFilePath(); const std::string firstPath = recorderPtr->getCurrentFilePath();
std::shared_ptr<ResetCommand> reset = std::make_shared<ResetCommand>(); std::shared_ptr<ResetCommand> reset = std::make_shared<ResetCommand>();
reset->config = std::make_shared<GameConfig>(ConfigLoader::loadFromDirectory(CONFIG_DIR)); reset->config = std::make_shared<GameConfig>(loadTestConfig());
reset->seed = 999u; reset->seed = 999u;
manager.enqueue(reset); manager.enqueue(reset);
manager.drain(); manager.drain();

View File

@@ -22,11 +22,7 @@
#include "SimulationTestAccess.h" #include "SimulationTestAccess.h"
#include "Tick.h" #include "Tick.h"
#include "WeaponComponent.h" #include "WeaponComponent.h"
#include "TestConfig.h"
static GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
static const ShipDef* findSchematic(const GameConfig& cfg, const std::string& id) static const ShipDef* findSchematic(const GameConfig& cfg, const std::string& id)
{ {
@@ -108,7 +104,7 @@ static void fillMaterials(Simulation& sim, BuildingId yardId,
TEST_CASE("Ship spawn: no modules leaves base stats unchanged", "[modules]") TEST_CASE("Ship spawn: no modules leaves base stats unchanged", "[modules]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
@@ -123,7 +119,7 @@ TEST_CASE("Ship spawn: no modules leaves base stats unchanged", "[modules]")
TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]") TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
@@ -148,7 +144,7 @@ TEST_CASE("Ship spawn: multiplicative HP module applies correctly", "[modules]")
TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]") TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
@@ -173,7 +169,7 @@ TEST_CASE("Ship spawn: additive sensor module applies correctly", "[modules]")
TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]") TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
@@ -206,7 +202,7 @@ TEST_CASE("Ship spawn: multiple modules stack correctly", "[modules]")
TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials", TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
"[modules][shipyard]") "[modules][shipyard]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const BuildingDef* yardDef = findShipyardDef(sim.getConfig()); const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
@@ -233,7 +229,7 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
TEST_CASE("Shipyard: setShipLayout cancels in-progress production", TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
"[modules][shipyard]") "[modules][shipyard]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
const BuildingDef* yardDef = findShipyardDef(sim.getConfig()); const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
@@ -269,7 +265,7 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
TEST_CASE("Shipyard: builds a bare hull when no layout is configured", TEST_CASE("Shipyard: builds a bare hull when no layout is configured",
"[modules][shipyard]") "[modules][shipyard]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
// The schematic carries a weapon in its (wave-only) default loadout. This // The schematic carries a weapon in its (wave-only) default loadout. This
@@ -312,7 +308,7 @@ TEST_CASE("Shipyard: builds a bare hull when no layout is configured",
TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]") TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const BuildingDef* yardDef = findShipyardDef(sim.getConfig()); const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
@@ -341,7 +337,7 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout", TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
"[modules][shipyard]") "[modules][shipyard]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const BuildingDef* yardDef = findShipyardDef(sim.getConfig()); const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
@@ -376,7 +372,7 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
TEST_CASE("Ship spawn: weapon_primer multiplies attack rate in simulation", "[modules]") TEST_CASE("Ship spawn: weapon_primer multiplies attack rate in simulation", "[modules]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
@@ -401,7 +397,7 @@ TEST_CASE("Ship spawn: weapon_primer multiplies attack rate in simulation", "[mo
TEST_CASE("Ship spawn: weapon_stabilizer multiplies attack range in simulation", "[modules]") TEST_CASE("Ship spawn: weapon_stabilizer multiplies attack range in simulation", "[modules]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
@@ -428,7 +424,7 @@ TEST_CASE("Ship spawn: weapon_stabilizer multiplies attack range in simulation",
TEST_CASE("Ship spawn: afterburner additive main_acceleration is converted m/s² to tiles/tick", "[modules]") TEST_CASE("Ship spawn: afterburner additive main_acceleration is converted m/s² to tiles/tick", "[modules]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
@@ -453,7 +449,7 @@ TEST_CASE("Ship spawn: afterburner additive main_acceleration is converted m/s²
TEST_CASE("Ship spawn: maneuvering_thrusters additive maneuvering_acceleration is converted m/s² to tiles/tick", "[modules]") TEST_CASE("Ship spawn: maneuvering_thrusters additive maneuvering_acceleration is converted m/s² to tiles/tick", "[modules]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const ShipDef* def = findSchematic(sim.getConfig(), "interceptor"); const ShipDef* def = findSchematic(sim.getConfig(), "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
@@ -482,7 +478,7 @@ TEST_CASE("Ship spawn: maneuvering_thrusters additive maneuvering_acceleration i
TEST_CASE("calculateShipStats: weapon_primer multiplies attack rate in stats view", "[modules]") TEST_CASE("calculateShipStats: weapon_primer multiplies attack rate in stats view", "[modules]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ShipDef* def = findSchematic(cfg, "interceptor"); const ShipDef* def = findSchematic(cfg, "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
@@ -506,7 +502,7 @@ TEST_CASE("calculateShipStats: weapon_primer multiplies attack rate in stats vie
TEST_CASE("calculateShipStats: weapon_stabilizer multiplies attack range in stats view", "[modules]") TEST_CASE("calculateShipStats: weapon_stabilizer multiplies attack range in stats view", "[modules]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ShipDef* def = findSchematic(cfg, "interceptor"); const ShipDef* def = findSchematic(cfg, "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
@@ -532,7 +528,7 @@ TEST_CASE("calculateShipStats: weapon_stabilizer multiplies attack range in stat
TEST_CASE("calculateShipStats: afterburner additive main_acceleration is converted m/s² to tiles/s²", "[modules]") TEST_CASE("calculateShipStats: afterburner additive main_acceleration is converted m/s² to tiles/s²", "[modules]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ShipDef* def = findSchematic(cfg, "interceptor"); const ShipDef* def = findSchematic(cfg, "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
@@ -556,7 +552,7 @@ TEST_CASE("calculateShipStats: afterburner additive main_acceleration is convert
TEST_CASE("calculateShipStats: maneuvering_thrusters additive maneuvering_acceleration is converted m/s² to tiles/s²", "[modules]") TEST_CASE("calculateShipStats: maneuvering_thrusters additive maneuvering_acceleration is converted m/s² to tiles/s²", "[modules]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ShipDef* def = findSchematic(cfg, "interceptor"); const ShipDef* def = findSchematic(cfg, "interceptor");
REQUIRE(def != nullptr); REQUIRE(def != nullptr);

View File

@@ -29,11 +29,7 @@
#include "ShipSystem.h" #include "ShipSystem.h"
#include "Tick.h" #include "Tick.h"
#include "WeaponComponent.h" #include "WeaponComponent.h"
#include "TestConfig.h"
static GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers // Helpers
@@ -91,7 +87,7 @@ TEST_CASE("ShipSystem: interceptor spawn has weapon child and attack behavior, n
"[ship]") "[ship]")
{ {
EntityAdmin admin; EntityAdmin admin;
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
ShipSystem ss(cfg, admin); ShipSystem ss(cfg, admin);
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f)); const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
@@ -115,7 +111,7 @@ TEST_CASE("ShipSystem: interceptor spawn has weapon child and attack behavior, n
TEST_CASE("ShipSystem: enemy combat ship has no rally or retreat behavior", "[ship]") TEST_CASE("ShipSystem: enemy combat ship has no rally or retreat behavior", "[ship]")
{ {
EntityAdmin admin; EntityAdmin admin;
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
ShipSystem ss(cfg, admin); ShipSystem ss(cfg, admin);
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f), /*isEnemy=*/true); const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f), /*isEnemy=*/true);
@@ -129,7 +125,7 @@ TEST_CASE("ShipSystem: enemy combat ship has no rally or retreat behavior", "[sh
TEST_CASE("ShipSystem: setRetreatEnabled(false) suppresses player retreat behavior", "[ship]") TEST_CASE("ShipSystem: setRetreatEnabled(false) suppresses player retreat behavior", "[ship]")
{ {
EntityAdmin admin; EntityAdmin admin;
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
ShipSystem ss(cfg, admin); ShipSystem ss(cfg, admin);
ss.setRetreatEnabled(false); ss.setRetreatEnabled(false);
@@ -144,7 +140,7 @@ TEST_CASE("ShipSystem: setRetreatEnabled(false) suppresses player retreat behavi
TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]") TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]")
{ {
EntityAdmin admin; EntityAdmin admin;
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
ShipSystem ss(cfg, admin); ShipSystem ss(cfg, admin);
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f)); const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
@@ -166,7 +162,7 @@ TEST_CASE("ShipSystem: interceptor level 1 stats match config formulas", "[ship]
TEST_CASE("ShipSystem: interceptor hp matches config value", "[ship]") TEST_CASE("ShipSystem: interceptor hp matches config value", "[ship]")
{ {
EntityAdmin admin; EntityAdmin admin;
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
ShipSystem ss(cfg, admin); ShipSystem ss(cfg, admin);
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f)); const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
@@ -178,7 +174,7 @@ TEST_CASE("ShipSystem: interceptor hp matches config value", "[ship]")
TEST_CASE("ShipSystem: interceptor maxSpeed_tpt matches config value / tileSize / kTickRateHz", "[ship]") TEST_CASE("ShipSystem: interceptor maxSpeed_tpt matches config value / tileSize / kTickRateHz", "[ship]")
{ {
EntityAdmin admin; EntityAdmin admin;
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
ShipSystem ss(cfg, admin); ShipSystem ss(cfg, admin);
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f)); const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
@@ -196,7 +192,7 @@ TEST_CASE("ShipSystem: salvage_ship spawn with salvage module has cargo child an
"[ship]") "[ship]")
{ {
EntityAdmin admin; EntityAdmin admin;
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
ShipSystem ss(cfg, admin); ShipSystem ss(cfg, admin);
const ShipLayoutConfig layout = makeSingleModuleLayout("salvager"); const ShipLayoutConfig layout = makeSingleModuleLayout("salvager");
@@ -212,7 +208,7 @@ TEST_CASE("ShipSystem: salvage_ship spawn with salvage module has cargo child an
TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]") TEST_CASE("ShipSystem: salvage_ship cargo capacity matches config", "[ship]")
{ {
EntityAdmin admin; EntityAdmin admin;
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
ShipSystem ss(cfg, admin); ShipSystem ss(cfg, admin);
const ShipLayoutConfig layout = makeSingleModuleLayout("salvager"); const ShipLayoutConfig layout = makeSingleModuleLayout("salvager");
@@ -237,7 +233,7 @@ TEST_CASE("ShipSystem: repair_ship spawn with repair module has repair child and
"[ship]") "[ship]")
{ {
EntityAdmin admin; EntityAdmin admin;
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
ShipSystem ss(cfg, admin); ShipSystem ss(cfg, admin);
const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool"); const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool");
@@ -252,7 +248,7 @@ TEST_CASE("ShipSystem: repair_ship spawn with repair module has repair child and
TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas", "[ship]") TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas", "[ship]")
{ {
EntityAdmin admin; EntityAdmin admin;
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
ShipSystem ss(cfg, admin); ShipSystem ss(cfg, admin);
const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool"); const ShipLayoutConfig layout = makeSingleModuleLayout("repair_tool");
@@ -277,7 +273,7 @@ TEST_CASE("ShipSystem: repair_ship level 1 repair stats match config formulas",
TEST_CASE("ShipSystem: spawned ships are valid entities", "[ship]") TEST_CASE("ShipSystem: spawned ships are valid entities", "[ship]")
{ {
EntityAdmin admin; EntityAdmin admin;
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
ShipSystem ss(cfg, admin); ShipSystem ss(cfg, admin);
const entt::entity e1 = ss.spawn("interceptor", QVector2D(0.0f, 0.0f)); const entt::entity e1 = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));
@@ -292,7 +288,7 @@ TEST_CASE("ShipSystem: spawned ships are valid entities", "[ship]")
TEST_CASE("ShipSystem: despawn removes the ship and its weapon children", "[ship]") TEST_CASE("ShipSystem: despawn removes the ship and its weapon children", "[ship]")
{ {
EntityAdmin admin; EntityAdmin admin;
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
ShipSystem ss(cfg, admin); ShipSystem ss(cfg, admin);
const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f)); const entt::entity e = ss.spawn("interceptor", QVector2D(0.0f, 0.0f));

View File

@@ -16,11 +16,7 @@
#include "Simulation.h" #include "Simulation.h"
#include "SimulationTestAccess.h" #include "SimulationTestAccess.h"
#include "Tick.h" #include "Tick.h"
#include "TestConfig.h"
static GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
// A ship starts unlocked iff no unlock group grants it (REQ-LOCK-EXPLICIT). // A ship starts unlocked iff no unlock group grants it (REQ-LOCK-EXPLICIT).
static bool startsUnlocked(const GameConfig& cfg, const std::string& shipId) static bool startsUnlocked(const GameConfig& cfg, const std::string& shipId)
@@ -98,7 +94,7 @@ static void fillMaterials(Simulation& sim, BuildingId yardId, const ShipDef& def
TEST_CASE("Shipyard: spawns a player ship after production cycle completes", TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
"[shipyard]") "[shipyard]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const ShipDef* def = findAvailableSchematic(sim.getConfig()); const ShipDef* def = findAvailableSchematic(sim.getConfig());
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
@@ -143,7 +139,7 @@ TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
TEST_CASE("Shipyard: does not spawn without a schematic set", "[shipyard]") TEST_CASE("Shipyard: does not spawn without a schematic set", "[shipyard]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const BuildingDef* yardDef = findShipyardDef(sim.getConfig()); const BuildingDef* yardDef = findShipyardDef(sim.getConfig());
REQUIRE(yardDef != nullptr); REQUIRE(yardDef != nullptr);
@@ -159,7 +155,7 @@ TEST_CASE("Shipyard: does not spawn without a schematic set", "[shipyard]")
TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]") TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const ShipDef* def = findAvailableSchematic(sim.getConfig()); const ShipDef* def = findAvailableSchematic(sim.getConfig());
REQUIRE(def != nullptr); REQUIRE(def != nullptr);
@@ -183,7 +179,7 @@ TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]")
TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipyard]") TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipyard]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
const ShipDef* def = findAvailableSchematic(sim.getConfig()); const ShipDef* def = findAvailableSchematic(sim.getConfig());
REQUIRE(def != nullptr); REQUIRE(def != nullptr);

View File

@@ -5,11 +5,7 @@
#include "Simulation.h" #include "Simulation.h"
#include "Tick.h" #include "Tick.h"
#include "TickDriver.h" #include "TickDriver.h"
#include "TestConfig.h"
static GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Simulation // Simulation
@@ -17,14 +13,14 @@ static GameConfig loadConfig()
TEST_CASE("Simulation::currentTick starts at 0", "[simulation]") TEST_CASE("Simulation::currentTick starts at 0", "[simulation]")
{ {
const Simulation sim(loadConfig()); const Simulation sim(loadTestConfig());
REQUIRE(sim.getCurrentTick() == 0); REQUIRE(sim.getCurrentTick() == 0);
} }
TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]") TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
sim.tick(); sim.tick();
@@ -33,7 +29,7 @@ TEST_CASE("Simulation::tick increments currentTick by 1", "[simulation]")
TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]") TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
for (int i = 0; i < 10; ++i) for (int i = 0; i < 10; ++i)
{ {
@@ -45,14 +41,14 @@ TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]")
TEST_CASE("Simulation::drainBeamFiredEvents returns empty initially", "[simulation]") TEST_CASE("Simulation::drainBeamFiredEvents returns empty initially", "[simulation]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
REQUIRE(sim.drainBeamFiredEvents().empty()); REQUIRE(sim.drainBeamFiredEvents().empty());
} }
TEST_CASE("Simulation::drainBeamFiredEvents clears queue on drain", "[simulation]") TEST_CASE("Simulation::drainBeamFiredEvents clears queue on drain", "[simulation]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
// First drain: empty. // First drain: empty.
sim.drainBeamFiredEvents(); sim.drainBeamFiredEvents();
@@ -63,7 +59,7 @@ TEST_CASE("Simulation::drainBeamFiredEvents clears queue on drain", "[simulation
TEST_CASE("Simulation::hasSchematicChoicesPending returns false initially", "[simulation]") TEST_CASE("Simulation::hasSchematicChoicesPending returns false initially", "[simulation]")
{ {
Simulation sim(loadConfig()); Simulation sim(loadTestConfig());
REQUIRE_FALSE(sim.hasSchematicChoicesPending()); REQUIRE_FALSE(sim.hasSchematicChoicesPending());
} }

15
src/test/TestConfig.h Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include "ConfigLoader.h"
#include "GameConfig.h"
// Loads the test fixture config set (CONFIG_DIR points at bin/test/data/config
// for the test target). Shared by every test that needs a full GameConfig, so
// the one-liner is not repeated per translation unit.
//
// Like SimulationTestAccess.h this header lives under src/test and is
// deliberately off the lib/ui/app include path.
inline GameConfig loadTestConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}

View File

@@ -2,15 +2,11 @@
#include "ConfigLoader.h" #include "ConfigLoader.h"
#include "ThreatCostCalculator.h" #include "ThreatCostCalculator.h"
#include "TestConfig.h"
static GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
TEST_CASE("ThreatCostCalculator: miner item threat equals duration", "[threat]") TEST_CASE("ThreatCostCalculator: miner item threat equals duration", "[threat]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ThreatCostTable& table = cfg.threatCosts; const ThreatCostTable& table = cfg.threatCosts;
CHECK(table.itemThreat.at("iron_ore") == Approx(1.0)); CHECK(table.itemThreat.at("iron_ore") == Approx(1.0));
@@ -19,7 +15,7 @@ TEST_CASE("ThreatCostCalculator: miner item threat equals duration", "[threat]")
TEST_CASE("ThreatCostCalculator: smelter item threat includes input costs", "[threat]") TEST_CASE("ThreatCostCalculator: smelter item threat includes input costs", "[threat]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ThreatCostTable& table = cfg.threatCosts; const ThreatCostTable& table = cfg.threatCosts;
// iron_ingot: duration 2.0 + iron_ore(1.0) * 2 = 4.0 // iron_ingot: duration 2.0 + iron_ore(1.0) * 2 = 4.0
@@ -30,7 +26,7 @@ TEST_CASE("ThreatCostCalculator: smelter item threat includes input costs", "[th
TEST_CASE("ThreatCostCalculator: assembler takes max across recipes", "[threat]") TEST_CASE("ThreatCostCalculator: assembler takes max across recipes", "[threat]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ThreatCostTable& table = cfg.threatCosts; const ThreatCostTable& table = cfg.threatCosts;
// circuit_board has three non-reprocessing recipes: // circuit_board has three non-reprocessing recipes:
@@ -43,7 +39,7 @@ TEST_CASE("ThreatCostCalculator: assembler takes max across recipes", "[threat]"
TEST_CASE("ThreatCostCalculator: scrap threat is 1 / scrap_per_threat", "[threat]") TEST_CASE("ThreatCostCalculator: scrap threat is 1 / scrap_per_threat", "[threat]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ThreatCostTable& table = cfg.threatCosts; const ThreatCostTable& table = cfg.threatCosts;
// REQ-THREAT-SCRAP: scrap threat is the constant 1 / world.scrap_per_threat. // REQ-THREAT-SCRAP: scrap threat is the constant 1 / world.scrap_per_threat.
@@ -54,7 +50,7 @@ TEST_CASE("ThreatCostCalculator: scrap threat is 1 / scrap_per_threat", "[threat
TEST_CASE("ThreatCostCalculator: reprocessing-only item threat", "[threat]") TEST_CASE("ThreatCostCalculator: reprocessing-only item threat", "[threat]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ThreatCostTable& table = cfg.threatCosts; const ThreatCostTable& table = cfg.threatCosts;
// advanced_alloy: reprocessing recipe with scrap*5, duration 3.0, probability 0.1 // advanced_alloy: reprocessing recipe with scrap*5, duration 3.0, probability 0.1
@@ -64,7 +60,7 @@ TEST_CASE("ThreatCostCalculator: reprocessing-only item threat", "[threat]")
TEST_CASE("ThreatCostCalculator: ship threat with default modules", "[threat]") TEST_CASE("ThreatCostCalculator: ship threat with default modules", "[threat]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ThreatCostTable& table = cfg.threatCosts; const ThreatCostTable& table = cfg.threatCosts;
// interceptor: 10 + iron_ingot(4)*3 + circuit_board(28)*1 + laser_cannon(5 + 4*1) = 59.0 // interceptor: 10 + iron_ingot(4)*3 + circuit_board(28)*1 + laser_cannon(5 + 4*1) = 59.0
@@ -80,7 +76,7 @@ TEST_CASE("ThreatCostCalculator: ship threat with default modules", "[threat]")
TEST_CASE("ThreatCostCalculator: ship threat with custom modules", "[threat]") TEST_CASE("ThreatCostCalculator: ship threat with custom modules", "[threat]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ThreatCostTable& table = cfg.threatCosts; const ThreatCostTable& table = cfg.threatCosts;
// interceptor base: 10 + iron_ingot(4)*3 + circuit_board(28)*1 = 50.0 // interceptor base: 10 + iron_ingot(4)*3 + circuit_board(28)*1 = 50.0
@@ -101,7 +97,7 @@ TEST_CASE("ThreatCostCalculator: ship threat with custom modules", "[threat]")
TEST_CASE("ThreatCostCalculator: unknown ship returns zero", "[threat]") TEST_CASE("ThreatCostCalculator: unknown ship returns zero", "[threat]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ThreatCostTable& table = cfg.threatCosts; const ThreatCostTable& table = cfg.threatCosts;
double threat = calculateShipThreatCost(table, cfg, "nonexistent_ship", {}); double threat = calculateShipThreatCost(table, cfg, "nonexistent_ship", {});
@@ -113,7 +109,7 @@ TEST_CASE("ThreatCostCalculator: unknown ship returns zero", "[threat]")
// be excluded. iron_ingot threat must not be inflated by the scrap path. // be excluded. iron_ingot threat must not be inflated by the scrap path.
TEST_CASE("ThreatCostCalculator: scrap-consuming recipe excluded when scrap-free recipe exists", "[threat]") TEST_CASE("ThreatCostCalculator: scrap-consuming recipe excluded when scrap-free recipe exists", "[threat]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ThreatCostTable& table = cfg.threatCosts; const ThreatCostTable& table = cfg.threatCosts;
// scrap_iron recipe: duration=1.0, 1 scrap (threat=1.0) -> 1 iron_ingot. // scrap_iron recipe: duration=1.0, 1 scrap (threat=1.0) -> 1 iron_ingot.
@@ -132,7 +128,7 @@ TEST_CASE("ThreatCostCalculator: scrap-consuming recipe excluded when scrap-free
// Per-unit threat = (3.0 + iron_ore(1.0)*1) / 2 = 4.0 / 2 = 2.0. // Per-unit threat = (3.0 + iron_ore(1.0)*1) / 2 = 4.0 / 2 = 2.0.
TEST_CASE("ThreatCostCalculator: per-unit division by output amount", "[threat]") TEST_CASE("ThreatCostCalculator: per-unit division by output amount", "[threat]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ThreatCostTable& table = cfg.threatCosts; const ThreatCostTable& table = cfg.threatCosts;
CHECK(table.itemThreat.at("dual_wire") == Approx(2.0)); CHECK(table.itemThreat.at("dual_wire") == Approx(2.0));
@@ -146,7 +142,7 @@ TEST_CASE("ThreatCostCalculator: per-unit division by output amount", "[threat]"
// downstream_product = 2.0 + 80.0*1 = 82.0. // downstream_product = 2.0 + 80.0*1 = 82.0.
TEST_CASE("ThreatCostCalculator: downstream-of-reprocessing item resolves via fixpoint", "[threat]") TEST_CASE("ThreatCostCalculator: downstream-of-reprocessing item resolves via fixpoint", "[threat]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ThreatCostTable& table = cfg.threatCosts; const ThreatCostTable& table = cfg.threatCosts;
CHECK(table.itemThreat.at("advanced_alloy") == Approx(80.0)); CHECK(table.itemThreat.at("advanced_alloy") == Approx(80.0));
@@ -161,7 +157,7 @@ TEST_CASE("ThreatCostCalculator: downstream-of-reprocessing item resolves via fi
// expected threat = max(2.0, 29.0) = 29.0, not 2.0. // expected threat = max(2.0, 29.0) = 29.0, not 2.0.
TEST_CASE("ThreatCostCalculator: staggered recipes committed only when all computable", "[threat]") TEST_CASE("ThreatCostCalculator: staggered recipes committed only when all computable", "[threat]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
const ThreatCostTable& table = cfg.threatCosts; const ThreatCostTable& table = cfg.threatCosts;
CHECK(table.itemThreat.at("staggered_item") == Approx(29.0)); CHECK(table.itemThreat.at("staggered_item") == Approx(29.0));

View File

@@ -19,15 +19,11 @@
#include "SimulationTestAccess.h" #include "SimulationTestAccess.h"
#include "StationBodyComponent.h" #include "StationBodyComponent.h"
#include "UnlocksConfig.h" #include "UnlocksConfig.h"
#include "TestConfig.h"
namespace namespace
{ {
GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
void killEnemyStations(Simulation& sim) void killEnemyStations(Simulation& sim)
{ {
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
@@ -42,7 +38,7 @@ void killEnemyStations(Simulation& sim)
// bay together, gated to the given station level. // bay together, gated to the given station level.
GameConfig configWithSalvageGroup(int stationLevel) GameConfig configWithSalvageGroup(int stationLevel)
{ {
GameConfig cfg = loadConfig(); GameConfig cfg = loadTestConfig();
cfg.unlocks.groups.clear(); cfg.unlocks.groups.clear();
cfg.unlocks.groups.push_back( cfg.unlocks.groups.push_back(
UnlockGroupDef{"salvage_operations", stationLevel, {}, {}, {"salvager"}, {"salvage_bay"}, {}}); UnlockGroupDef{"salvage_operations", stationLevel, {}, {}, {"salvager"}, {"salvage_bay"}, {}});

View File

@@ -22,15 +22,11 @@
#include "SimulationTestAccess.h" #include "SimulationTestAccess.h"
#include "StationBodyComponent.h" #include "StationBodyComponent.h"
#include "UnlocksConfig.h" #include "UnlocksConfig.h"
#include "TestConfig.h"
namespace namespace
{ {
GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
UnlockGroupDef& findGroup(GameConfig& cfg, const std::string& id) UnlockGroupDef& findGroup(GameConfig& cfg, const std::string& id)
{ {
for (UnlockGroupDef& group : cfg.unlocks.groups) for (UnlockGroupDef& group : cfg.unlocks.groups)
@@ -85,7 +81,7 @@ TEST_CASE("UnlockPrereq: a group gated behind a locked ship is withheld from the
// quick_circuit (recipe group, level 0) would normally be eligible at the // quick_circuit (recipe group, level 0) would normally be eligible at the
// first station destruction. Gate it behind the repair_ship group (level 0, // first station destruction. Gate it behind the repair_ship group (level 0,
// locked at game start). // locked at game start).
GameConfig cfg = loadConfig(); GameConfig cfg = loadTestConfig();
findGroup(cfg, "quick_circuit").requiredGroupIds = {"repair_ship"}; findGroup(cfg, "quick_circuit").requiredGroupIds = {"repair_ship"};
Simulation sim(std::move(cfg), 123); Simulation sim(std::move(cfg), 123);
@@ -103,7 +99,7 @@ TEST_CASE("UnlockPrereq: a group gated behind a locked ship is withheld from the
TEST_CASE("UnlockPrereq: the gated group becomes eligible once its prerequisite is awarded", TEST_CASE("UnlockPrereq: the gated group becomes eligible once its prerequisite is awarded",
"[unlock_prereq]") "[unlock_prereq]")
{ {
GameConfig cfg = loadConfig(); GameConfig cfg = loadTestConfig();
findGroup(cfg, "quick_circuit").requiredGroupIds = {"repair_ship"}; findGroup(cfg, "quick_circuit").requiredGroupIds = {"repair_ship"};
Simulation sim(std::move(cfg), 123); Simulation sim(std::move(cfg), 123);
@@ -126,7 +122,7 @@ TEST_CASE("UnlockPrereq: a module group gated behind another module is withheld
{ {
// Make laser_cannon and armor_plate (both start-unlocked in the test config) // Make laser_cannon and armor_plate (both start-unlocked in the test config)
// lockable via new unlock groups, with armor_plate gated behind laser_cannon. // lockable via new unlock groups, with armor_plate gated behind laser_cannon.
GameConfig cfg = loadConfig(); GameConfig cfg = loadTestConfig();
cfg.unlocks.groups.push_back( cfg.unlocks.groups.push_back(
UnlockGroupDef{"laser_cannon", 0, {}, {}, {"laser_cannon"}, {}, {}}); UnlockGroupDef{"laser_cannon", 0, {}, {}, {"laser_cannon"}, {}, {}});
cfg.unlocks.groups.push_back( cfg.unlocks.groups.push_back(

View File

@@ -25,11 +25,7 @@
#include "Tick.h" #include "Tick.h"
#include "ThreatCostCalculator.h" #include "ThreatCostCalculator.h"
#include "WaveSystem.h" #include "WaveSystem.h"
#include "TestConfig.h"
static GameConfig loadConfig()
{
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Threat accumulation // Threat accumulation
@@ -37,7 +33,7 @@ static GameConfig loadConfig()
TEST_CASE("WaveSystem: threat accumulates at boss wave counter rate", "[wave]") TEST_CASE("WaveSystem: threat accumulates at boss wave counter rate", "[wave]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
std::mt19937 rng(42); std::mt19937 rng(42);
WaveSystem ws(cfg, rng); WaveSystem ws(cfg, rng);
@@ -55,7 +51,7 @@ TEST_CASE("WaveSystem: threat accumulates at boss wave counter rate", "[wave]")
TEST_CASE("WaveSystem: threatAccumulationRate matches the rate formula outside quiet windows", TEST_CASE("WaveSystem: threatAccumulationRate matches the rate formula outside quiet windows",
"[wave]") "[wave]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
std::mt19937 rng(42); std::mt19937 rng(42);
WaveSystem ws(cfg, rng); WaveSystem ws(cfg, rng);
@@ -65,7 +61,7 @@ TEST_CASE("WaveSystem: threatAccumulationRate matches the rate formula outside q
TEST_CASE("WaveSystem: threatAccumulationRate is 0 during a quiet window", "[wave]") TEST_CASE("WaveSystem: threatAccumulationRate is 0 during a quiet window", "[wave]")
{ {
GameConfig cfg = loadConfig(); GameConfig cfg = loadTestConfig();
// Start with the boss countdown already at the pre-boss quiet threshold. // Start with the boss countdown already at the pre-boss quiet threshold.
cfg.world.waves.bossCountdownSeconds = cfg.world.waves.bossQuietBeforeSeconds; cfg.world.waves.bossCountdownSeconds = cfg.world.waves.bossQuietBeforeSeconds;
std::mt19937 rng(42); std::mt19937 rng(42);
@@ -83,7 +79,7 @@ TEST_CASE("WaveSystem: threatAccumulationRate is 0 during a quiet window", "[wav
TEST_CASE("WaveSystem: generation starts at 0 and increments on station destruction", "[wave]") TEST_CASE("WaveSystem: generation starts at 0 and increments on station destruction", "[wave]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
std::mt19937 rng(42); std::mt19937 rng(42);
WaveSystem ws(cfg, rng); WaveSystem ws(cfg, rng);
@@ -100,7 +96,7 @@ TEST_CASE("WaveSystem: generation starts at 0 and increments on station destruct
TEST_CASE("WaveSystem: Simulation pre-places HQ + 2 player + 2 enemy stations", "[wave]") TEST_CASE("WaveSystem: Simulation pre-places HQ + 2 player + 2 enemy stations", "[wave]")
{ {
const Simulation sim(loadConfig(), 42); const Simulation sim(loadTestConfig(), 42);
// HQ is still a Building (for belt integration). // HQ is still a Building (for belt integration).
int hqCount = 0; int hqCount = 0;
@@ -126,7 +122,7 @@ TEST_CASE("WaveSystem: Simulation pre-places HQ + 2 player + 2 enemy stations",
TEST_CASE("WaveSystem: HQ has correct initial HP from config", "[wave]") TEST_CASE("WaveSystem: HQ has correct initial HP from config", "[wave]")
{ {
const Simulation sim(loadConfig(), 42); const Simulation sim(loadTestConfig(), 42);
const float expectedHp = const float expectedHp =
static_cast<float>(sim.getConfig().stations.hq.hpFormula.evaluate(0.0)); static_cast<float>(sim.getConfig().stations.hq.hpFormula.evaluate(0.0));
@@ -145,7 +141,7 @@ TEST_CASE("WaveSystem: HQ has correct initial HP from config", "[wave]")
TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]") TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]")
{ {
const Simulation sim(loadConfig(), 42); const Simulation sim(loadTestConfig(), 42);
for (const Building& b : sim.getBuildings().getAllBuildings()) for (const Building& b : sim.getBuildings().getAllBuildings())
{ {
@@ -162,7 +158,7 @@ TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]")
TEST_CASE("WaveSystem: player stations have weapon set", "[wave]") TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
int armedPlayerStations = 0; int armedPlayerStations = 0;
sim.getAdmin().forEach<WeaponComponent, ModuleOwnerComponent>( sim.getAdmin().forEach<WeaponComponent, ModuleOwnerComponent>(
@@ -183,7 +179,7 @@ TEST_CASE("WaveSystem: player stations have weapon set", "[wave]")
TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]") TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
int armedEnemyStations = 0; int armedEnemyStations = 0;
sim.getAdmin().forEach<WeaponComponent, ModuleOwnerComponent>( sim.getAdmin().forEach<WeaponComponent, ModuleOwnerComponent>(
@@ -208,7 +204,7 @@ TEST_CASE("WaveSystem: enemy stations have weapon set", "[wave]")
TEST_CASE("WaveSystem: enemy ships spawn after the initial gap elapses", "[wave]") TEST_CASE("WaveSystem: enemy ships spawn after the initial gap elapses", "[wave]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
// The maximum gap is gapMaxSeconds = 45s → 1350 ticks. // The maximum gap is gapMaxSeconds = 45s → 1350 ticks.
// Run 1500 ticks to guarantee at least one wave has triggered. // Run 1500 ticks to guarantee at least one wave has triggered.
@@ -234,7 +230,7 @@ TEST_CASE("WaveSystem: enemy ships spawn after the initial gap elapses", "[wave]
TEST_CASE("WaveSystem: all ships have positive dynamic threat cost", "[wave]") TEST_CASE("WaveSystem: all ships have positive dynamic threat cost", "[wave]")
{ {
const GameConfig cfg = loadConfig(); const GameConfig cfg = loadTestConfig();
for (const ShipDef& def : cfg.ships.ships) for (const ShipDef& def : cfg.ships.ships)
{ {
@@ -250,7 +246,7 @@ TEST_CASE("WaveSystem: all ships have positive dynamic threat cost", "[wave]")
TEST_CASE("WaveSystem: destroying both enemy stations triggers a push", "[wave]") TEST_CASE("WaveSystem: destroying both enemy stations triggers a push", "[wave]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
// Damage both enemy stations to 0. // Damage both enemy stations to 0.
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
@@ -273,7 +269,7 @@ TEST_CASE("WaveSystem: destroying both enemy stations triggers a push", "[wave]"
TEST_CASE("WaveSystem: push generates pending schematic choices", "[wave]") TEST_CASE("WaveSystem: push generates pending schematic choices", "[wave]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h) [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
@@ -291,7 +287,7 @@ TEST_CASE("WaveSystem: push generates pending schematic choices", "[wave]")
TEST_CASE("WaveSystem: push schematic choices have valid ids", "[wave]") TEST_CASE("WaveSystem: push schematic choices have valid ids", "[wave]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h) [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
@@ -339,7 +335,7 @@ TEST_CASE("WaveSystem: push schematic choices have valid ids", "[wave]")
TEST_CASE("WaveSystem: schematic choices have no duplicates", "[wave]") TEST_CASE("WaveSystem: schematic choices have no duplicates", "[wave]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h) [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
@@ -359,7 +355,7 @@ TEST_CASE("WaveSystem: schematic choices have no duplicates", "[wave]")
TEST_CASE("WaveSystem: applySchematicChoice clears pending and applies", "[wave]") TEST_CASE("WaveSystem: applySchematicChoice clears pending and applies", "[wave]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>( sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h) [](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
@@ -375,7 +371,7 @@ TEST_CASE("WaveSystem: applySchematicChoice clears pending and applies", "[wave]
TEST_CASE("WaveSystem: push places new enemy stations further right", "[wave]") TEST_CASE("WaveSystem: push places new enemy stations further right", "[wave]")
{ {
Simulation sim(loadConfig(), 42); Simulation sim(loadTestConfig(), 42);
// Record the X position of the initial enemy stations. // Record the X position of the initial enemy stations.
int initialX = std::numeric_limits<int>::min(); int initialX = std::numeric_limits<int>::min();