#include "catch.hpp" #include #include #include #include #include "Blueprint.h" #include "Building.h" #include "BuildingsConfig.h" #include "BuildingSystem.h" #include "BuildingType.h" #include "ConfigLoader.h" #include "BuildingId.h" #include "Rotation.h" #include "Simulation.h" #include "SurfaceMask.h" #include "Tick.h" // --------------------------------------------------------------------------- // Helpers that mirror the production implementations under test. // --------------------------------------------------------------------------- // Mirror of the CW / CCW offset transforms in GameWorldView::keyPressEvent. static QPoint rotateCW(QPoint p) { return QPoint(-p.y(), p.x()); } static QPoint rotateCCW(QPoint p) { return QPoint( p.y(), -p.x()); } // Mirror of the Rotation cycling in GameWorldView (anonymous-namespace helpers). static Rotation rotCW(Rotation r) { switch (r) { case Rotation::North: return Rotation::East; case Rotation::East: return Rotation::South; case Rotation::South: return Rotation::West; case Rotation::West: return Rotation::North; } return Rotation::East; } static Rotation rotCCW(Rotation r) { switch (r) { case Rotation::North: return Rotation::West; case Rotation::East: return Rotation::North; case Rotation::South: return Rotation::East; case Rotation::West: return Rotation::South; } return Rotation::East; } // Mirror of BlueprintPanel::createBlueprintFromSelection: given per-building // (anchor, bodyCells, type, rotation), compute Blueprint with floor-division // bounding-box center and per-building tile offsets. struct BuildingSpec { QPoint anchor; std::vector bodyCells; BuildingType type; Rotation rotation; std::string recipeId; // empty = none }; static Blueprint buildBlueprint(const std::vector& specs) { int minX = INT_MAX, maxX = INT_MIN; int minY = INT_MAX, maxY = INT_MIN; for (const BuildingSpec& s : specs) { for (const QPoint& cell : s.bodyCells) { minX = std::min(minX, cell.x()); maxX = std::max(maxX, cell.x()); minY = std::min(minY, cell.y()); maxY = std::max(maxY, cell.y()); } } const QPoint center((minX + maxX) / 2, (minY + maxY) / 2); Blueprint bp; for (const BuildingSpec& s : specs) { BlueprintBuilding bb; bb.type = s.type; bb.rotation = s.rotation; bb.offset = s.anchor - center; bb.recipeId = s.recipeId; bp.buildings.push_back(bb); } return bp; } // Apply one CW/CCW rotation to every building in the constellation, mirroring // the corrected Q / E handling in GameWorldView::keyPressEvent. // Each building's anchor is recomputed from the rotated body cells so that // multi-tile footprints stay correctly aligned after rotation. static void applyRotationCW(Blueprint& bp, const GameConfig& cfg) { for (BlueprintBuilding& bb : bp.buildings) { const BuildingDef* def = nullptr; for (const BuildingDef& d : cfg.buildings.buildings) { if (d.type == bb.type) { def = &d; break; } } if (!def) { continue; } const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, bb.rotation); int minX = INT_MAX, minY = INT_MAX; for (const QPoint& cell : mask.bodyCells) { const QPoint abs = bb.offset + cell; minX = std::min(minX, -abs.y()); minY = std::min(minY, abs.x()); } bb.offset = QPoint(minX, minY); bb.rotation = rotCW(bb.rotation); } } static void applyRotationCCW(Blueprint& bp, const GameConfig& cfg) { for (BlueprintBuilding& bb : bp.buildings) { const BuildingDef* def = nullptr; for (const BuildingDef& d : cfg.buildings.buildings) { if (d.type == bb.type) { def = &d; break; } } if (!def) { continue; } const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, bb.rotation); int minX = INT_MAX, minY = INT_MAX; for (const QPoint& cell : mask.bodyCells) { const QPoint abs = bb.offset + cell; minX = std::min(minX, abs.y()); minY = std::min(minY, -abs.x()); } bb.offset = QPoint(minX, minY); bb.rotation = rotCCW(bb.rotation); } } static GameConfig loadConfig() { return ConfigLoader::loadFromDirectory(CONFIG_DIR); } // Mirrors BlueprintPanel::createBlueprintFromSelection's player-placeable filter: // building types absent from buildings.toml (HQ, stations) or with playerPlaceable=false // are silently excluded before the bounding-box center and offsets are computed. static Blueprint buildBlueprintFiltered(const std::vector& specs, const GameConfig& cfg) { std::vector filtered; for (const BuildingSpec& s : specs) { for (const BuildingDef& def : cfg.buildings.buildings) { if (def.type == s.type) { if (def.playerPlaceable) { filtered.push_back(s); } break; } } // If the type is not in buildings (e.g. Hq, defence stations), it is skipped. } return buildBlueprint(filtered); } // --------------------------------------------------------------------------- // Offset computation // --------------------------------------------------------------------------- TEST_CASE("Blueprint: single 1x1 building gets zero offset", "[blueprint]") { // Body = one tile at (-5, 3). Center = (-5, 3). Offset = (0, 0). const BuildingSpec spec{ QPoint(-5, 3), {QPoint(-5, 3)}, BuildingType::Belt, Rotation::East }; const Blueprint bp = buildBlueprint({ spec }); REQUIRE(bp.buildings.size() == 1); REQUIRE(bp.buildings[0].offset == QPoint(0, 0)); } TEST_CASE("Blueprint: two 1x1 buildings with odd span get symmetric offsets", "[blueprint]") { // Anchors at (-6, 0) and (-4, 0). bboxX = [-6, -4], center.x = -5. // Offsets: -6 - (-5) = -1, -4 - (-5) = +1. const BuildingSpec left { QPoint(-6, 0), {QPoint(-6, 0)}, BuildingType::Belt, Rotation::East }; const BuildingSpec right{ QPoint(-4, 0), {QPoint(-4, 0)}, BuildingType::Belt, Rotation::East }; const Blueprint bp = buildBlueprint({ left, right }); REQUIRE(bp.buildings[0].offset == QPoint(-1, 0)); REQUIRE(bp.buildings[1].offset == QPoint( 1, 0)); } TEST_CASE("Blueprint: even span uses C++ integer truncation for center", "[blueprint]") { // Anchors at (-5, 0) and (-4, 0). bboxX = [-5, -4], sum = -9. // center.x = -9 / 2 = -4 (C++ truncates toward zero, i.e. rounds up for negatives). // Offsets: -5 - (-4) = -1, -4 - (-4) = 0. const BuildingSpec left { QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Belt, Rotation::East }; const BuildingSpec right{ QPoint(-4, 0), {QPoint(-4, 0)}, BuildingType::Belt, Rotation::East }; const Blueprint bp = buildBlueprint({ left, right }); REQUIRE(bp.buildings[0].offset == QPoint(-1, 0)); REQUIRE(bp.buildings[1].offset == QPoint( 0, 0)); } TEST_CASE("Blueprint: bounding box is computed from body cells, not anchors alone", "[blueprint]") { // A 2x1 building: anchor (-6, 0), body cells (-6, 0) and (-5, 0). // A 1x1 building: anchor (-3, 0), body cell (-3, 0). // All body cells: {(-6,0),(-5,0),(-3,0)}. bboxX = [-6, -3], sum = -9. // center.x = -9 / 2 = -4 (C++ truncation). // Wide building anchor offset: -6 - (-4) = -2. // Small building anchor offset: -3 - (-4) = +1. const BuildingSpec wide{ QPoint(-6, 0), { QPoint(-6, 0), QPoint(-5, 0) }, BuildingType::Belt, Rotation::East }; const BuildingSpec small{ QPoint(-3, 0), { QPoint(-3, 0) }, BuildingType::Belt, Rotation::East }; const Blueprint bp = buildBlueprint({ wide, small }); REQUIRE(bp.buildings[0].offset == QPoint(-2, 0)); REQUIRE(bp.buildings[1].offset == QPoint( 1, 0)); } TEST_CASE("Blueprint: 2-D bounding box with buildings on both axes", "[blueprint]") { // Four 1x1 buildings at corners of a 4x2 rectangle (-7,0),(-5,0),(-7,2),(-5,2). // bboxX = [-7, -5], center.x = -6. bboxY = [0, 2], center.y = 1. // Offsets: (-1,-1), (1,-1), (-1,1), (1,1). const auto belt = BuildingType::Belt; const auto east = Rotation::East; std::vector specs = { { QPoint(-7, 0), {QPoint(-7, 0)}, belt, east }, { QPoint(-5, 0), {QPoint(-5, 0)}, belt, east }, { QPoint(-7, 2), {QPoint(-7, 2)}, belt, east }, { QPoint(-5, 2), {QPoint(-5, 2)}, belt, east }, }; const Blueprint bp = buildBlueprint(specs); REQUIRE(bp.buildings[0].offset == QPoint(-1, -1)); REQUIRE(bp.buildings[1].offset == QPoint( 1, -1)); REQUIRE(bp.buildings[2].offset == QPoint(-1, 1)); REQUIRE(bp.buildings[3].offset == QPoint( 1, 1)); } // --------------------------------------------------------------------------- // Offset rotation math // --------------------------------------------------------------------------- TEST_CASE("Blueprint: CW rotation in screen space maps right→down, down→left", "[blueprint]") { // Screen space has Y growing downward. CW means: right→down, down→left, left→up, up→right. REQUIRE(rotateCW(QPoint( 1, 0)) == QPoint( 0, 1)); REQUIRE(rotateCW(QPoint( 0, 1)) == QPoint(-1, 0)); REQUIRE(rotateCW(QPoint(-1, 0)) == QPoint( 0, -1)); REQUIRE(rotateCW(QPoint( 0, -1)) == QPoint( 1, 0)); } TEST_CASE("Blueprint: CCW rotation is the inverse of CW rotation", "[blueprint]") { REQUIRE(rotateCCW(QPoint( 1, 0)) == QPoint( 0, -1)); REQUIRE(rotateCCW(QPoint( 0, -1)) == QPoint(-1, 0)); REQUIRE(rotateCCW(QPoint(-1, 0)) == QPoint( 0, 1)); REQUIRE(rotateCCW(QPoint( 0, 1)) == QPoint( 1, 0)); } TEST_CASE("Blueprint: four CW rotations restore any offset to its original", "[blueprint]") { const QPoint original(-3, 5); QPoint p = original; for (int i = 0; i < 4; ++i) { p = rotateCW(p); } REQUIRE(p == original); } TEST_CASE("Blueprint: CW followed by CCW is identity", "[blueprint]") { const QPoint original(2, -7); REQUIRE(rotateCCW(rotateCW(original)) == original); REQUIRE(rotateCW(rotateCCW(original)) == original); } TEST_CASE("Blueprint: non-axis-aligned offset rotates correctly", "[blueprint]") { // (2, 3) → CW → (-3, 2) → CW → (-2, -3) → CW → (3, -2) → CW → (2, 3) QPoint p(2, 3); p = rotateCW(p); REQUIRE(p == QPoint(-3, 2)); p = rotateCW(p); REQUIRE(p == QPoint(-2, -3)); p = rotateCW(p); REQUIRE(p == QPoint( 3, -2)); p = rotateCW(p); REQUIRE(p == QPoint( 2, 3)); } // --------------------------------------------------------------------------- // Constellation rotation: offsets AND building rotations both update // --------------------------------------------------------------------------- TEST_CASE("Blueprint: CW constellation rotation updates offset and building rotation", "[blueprint]") { const GameConfig cfg = loadConfig(); // Building one tile to the right, facing East. Blueprint bp; bp.name = "test"; BlueprintBuilding bb; bb.type = BuildingType::Belt; bb.rotation = Rotation::East; bb.offset = QPoint(1, 0); bp.buildings.push_back(bb); applyRotationCW(bp, cfg); // Offset: right → down. REQUIRE(bp.buildings[0].offset == QPoint(0, 1)); // Building rotation: East → South. REQUIRE(bp.buildings[0].rotation == Rotation::South); } TEST_CASE("Blueprint: CCW constellation rotation updates offset and building rotation", "[blueprint]") { const GameConfig cfg = loadConfig(); Blueprint bp; bp.name = "test"; BlueprintBuilding bb; bb.type = BuildingType::Belt; bb.rotation = Rotation::East; bb.offset = QPoint(1, 0); bp.buildings.push_back(bb); applyRotationCCW(bp, cfg); // Offset: right → up. REQUIRE(bp.buildings[0].offset == QPoint(0, -1)); // Building rotation: East → North. REQUIRE(bp.buildings[0].rotation == Rotation::North); } TEST_CASE("Blueprint: four CW rotations restore offset and building rotation", "[blueprint]") { const GameConfig cfg = loadConfig(); Blueprint bp; bp.name = "test"; BlueprintBuilding bb; bb.type = BuildingType::Belt; bb.rotation = Rotation::East; bb.offset = QPoint(2, -1); bp.buildings.push_back(bb); const QPoint originalOffset = bb.offset; const Rotation originalRotation = bb.rotation; for (int i = 0; i < 4; ++i) { applyRotationCW(bp, cfg); } REQUIRE(bp.buildings[0].offset == originalOffset); REQUIRE(bp.buildings[0].rotation == originalRotation); } TEST_CASE("Blueprint: multi-building constellation rotates symmetrically CW", "[blueprint]") { const GameConfig cfg = loadConfig(); // Two buildings left and right of center; after CW they should be above and below. Blueprint bp; bp.name = "test"; BlueprintBuilding left, right; left.type = right.type = BuildingType::Belt; left.rotation = right.rotation = Rotation::East; left.offset = QPoint(-1, 0); right.offset = QPoint( 1, 0); bp.buildings = { left, right }; applyRotationCW(bp, cfg); // left (-1, 0) → CW → (0, -1) (above center) // right ( 1, 0) → CW → (0, 1) (below center) REQUIRE(bp.buildings[0].offset == QPoint(0, -1)); REQUIRE(bp.buildings[1].offset == QPoint(0, 1)); REQUIRE(bp.buildings[0].rotation == Rotation::South); REQUIRE(bp.buildings[1].rotation == Rotation::South); } // --------------------------------------------------------------------------- // Regression: multi-tile building rotation (the anchor-offset-only bug) // --------------------------------------------------------------------------- TEST_CASE("Blueprint: CW rotation keeps belt adjacent to miner output port", "[blueprint]") { const GameConfig cfg = loadConfig(); // 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. // Belt placed at (1,1) — the port exit tile. // Constellation body cells: {(0,0),(1,0),(0,1),(1,1)}. // Integer center: ((0+1)/2, (0+1)/2) = (0,0). // Miner offset (0,0), belt offset (1,1). Blueprint bp; bp.name = "test"; BlueprintBuilding miner; miner.type = BuildingType::Miner; miner.rotation = Rotation::East; miner.offset = QPoint(0, 0); BlueprintBuilding belt; belt.type = BuildingType::Belt; belt.rotation = Rotation::East; belt.offset = QPoint(1, 1); // port exit tile of East miner relative to its anchor bp.buildings = { miner, belt }; applyRotationCW(bp, cfg); // Miner body cells (0,0),(1,0),(0,1) rotated CW: (0,0),(0,1),(-1,0). // New miner anchor = min(-1..0, 0..1) = (-1, 0). REQUIRE(bp.buildings[0].offset == QPoint(-1, 0)); REQUIRE(bp.buildings[0].rotation == Rotation::South); // Belt body cell (1,1) rotated CW: (-1, 1). New belt anchor = (-1, 1). REQUIRE(bp.buildings[1].offset == QPoint(-1, 1)); REQUIRE(bp.buildings[1].rotation == Rotation::South); // South miner port tile relative to its anchor is (0,1) (the 'v' indicator in ["AA","vA"]). // Miner anchor in constellation = (-1,0). Port exit = (-1,0)+(0,1) = (-1,1) = belt anchor. ✓ REQUIRE(bp.buildings[1].offset == bp.buildings[0].offset + QPoint(0, 1)); } TEST_CASE("Blueprint: CCW rotation keeps belt adjacent to miner output port", "[blueprint]") { const GameConfig cfg = loadConfig(); Blueprint bp; bp.name = "test"; BlueprintBuilding miner; miner.type = BuildingType::Miner; miner.rotation = Rotation::East; miner.offset = QPoint(0, 0); BlueprintBuilding belt; belt.type = BuildingType::Belt; belt.rotation = Rotation::East; belt.offset = QPoint(1, 1); bp.buildings = { miner, belt }; applyRotationCCW(bp, cfg); // Miner body cells (0,0),(1,0),(0,1) rotated CCW: (0,0),(0,-1),(1,0). // New miner anchor = min(0..1, -1..0) = (0, -1). REQUIRE(bp.buildings[0].offset == QPoint(0, -1)); REQUIRE(bp.buildings[0].rotation == Rotation::North); // Belt body cell (1,1) rotated CCW: (1,-1). New belt anchor = (1,-1). REQUIRE(bp.buildings[1].offset == QPoint(1, -1)); REQUIRE(bp.buildings[1].rotation == Rotation::North); // North miner port tile relative to its anchor is (1,0) (the '^' indicator in ["A^","AA"]). // Miner anchor in constellation = (0,-1). Port exit = (0,-1)+(1,0) = (1,-1) = belt anchor. ✓ REQUIRE(bp.buildings[1].offset == bp.buildings[0].offset + QPoint(1, 0)); } // --------------------------------------------------------------------------- // Player-placeable filter in blueprint creation // --------------------------------------------------------------------------- TEST_CASE("Blueprint creation: non-player-placeable building alone yields empty blueprint", "[blueprint]") { const GameConfig cfg = loadConfig(); // 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 Blueprint bp = buildBlueprintFiltered({ hq }, cfg); REQUIRE(bp.buildings.empty()); } TEST_CASE("Blueprint creation: mixed selection keeps only player-placeable buildings", "[blueprint]") { const GameConfig cfg = loadConfig(); 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 Blueprint bp = buildBlueprintFiltered({ belt, hq }, cfg); REQUIRE(bp.buildings.size() == 1); REQUIRE(bp.buildings[0].type == BuildingType::Belt); } TEST_CASE("Blueprint creation: bounding box ignores non-player-placeable buildings", "[blueprint]") { const GameConfig cfg = loadConfig(); // 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. // With HQ excluded: bboxX = [-5, -5], center.x = -5, belt offset = 0. 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 Blueprint bp = buildBlueprintFiltered({ belt, hq }, cfg); REQUIRE(bp.buildings.size() == 1); REQUIRE(bp.buildings[0].offset == QPoint(0, 0)); } // --------------------------------------------------------------------------- // Simulation-level: blueprint placement places buildings at correct tiles // --------------------------------------------------------------------------- TEST_CASE("Blueprint placement: buildings land at anchor + offset from cursor", "[blueprint]") { // 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). // (Belt surface_mask ["A>"] — body at relative (0,0), port at (1,0).) Simulation sim(loadConfig()); const QPoint cursor(-5, 0); const QPoint offsetA(-1, 0); const QPoint offsetB( 1, 0); const BuildingId idA = sim.tryPlaceBuilding( BuildingType::Belt, cursor + offsetA, Rotation::East); const BuildingId idB = sim.tryPlaceBuilding( BuildingType::Belt, cursor + offsetB, Rotation::East); REQUIRE(idA != kInvalidBuildingId); REQUIRE(idB != kInvalidBuildingId); REQUIRE(sim.buildings().isTileOccupied(cursor + offsetA)); // (-6, 0) REQUIRE(sim.buildings().isTileOccupied(cursor + offsetB)); // (-4, 0) REQUIRE_FALSE(sim.buildings().isTileOccupied(cursor)); // center not occupied } TEST_CASE("Blueprint placement: cost is deducted for each building in sequence", "[blueprint]") { Simulation sim(loadConfig()); // Find belt cost from config (belt cost = 2 in test config). int beltCost = 0; for (const BuildingDef& def : sim.config().buildings.buildings) { if (def.type == BuildingType::Belt) { beltCost = def.cost; break; } } REQUIRE(beltCost > 0); const int startBlocks = sim.buildingBlocksStock(); REQUIRE(startBlocks >= 2 * beltCost); // test config has enough starting blocks sim.tryPlaceBuilding(BuildingType::Belt, QPoint(-6, 0), Rotation::East); REQUIRE(sim.buildingBlocksStock() == startBlocks - beltCost); sim.tryPlaceBuilding(BuildingType::Belt, QPoint(-4, 0), Rotation::East); REQUIRE(sim.buildingBlocksStock() == startBlocks - 2 * beltCost); } TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId and deducts nothing", "[blueprint]") { Simulation sim(loadConfig()); // Find miner cost (15 in test config) — expensive enough to exhaust a small stock. int minerCost = 0; for (const BuildingDef& def : sim.config().buildings.buildings) { if (def.type == BuildingType::Miner) { minerCost = def.cost; break; } } REQUIRE(minerCost > 0); // Drain the stock by placing miners until we no longer have enough. // Non-overlapping columns: miner body is 2 wide, so step by 2. int col = -2; while (sim.buildingBlocksStock() >= minerCost) { sim.tryPlaceBuilding(BuildingType::Miner, QPoint(col, 0), Rotation::East); col -= 2; } const int blocksBeforeAttempt = sim.buildingBlocksStock(); const BuildingId id = sim.tryPlaceBuilding( BuildingType::Miner, QPoint(col - 2, 0), Rotation::East); // Placement must fail and leave the stock unchanged. REQUIRE(id == kInvalidBuildingId); REQUIRE(sim.buildingBlocksStock() == blocksBeforeAttempt); } // --------------------------------------------------------------------------- // Recipe / schematic capture and re-application // --------------------------------------------------------------------------- TEST_CASE("Blueprint: recipeId is stored in BlueprintBuilding", "[blueprint]") { const BuildingSpec spec{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Miner, Rotation::East, "mine_iron_ore" }; const Blueprint bp = buildBlueprint({ spec }); REQUIRE(bp.buildings.size() == 1); REQUIRE(bp.buildings[0].recipeId == "mine_iron_ore"); } TEST_CASE("Blueprint: building with no recipe has empty recipeId", "[blueprint]") { const BuildingSpec spec{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Belt, Rotation::East // recipeId defaults to "" }; const Blueprint bp = buildBlueprint({ spec }); REQUIRE(bp.buildings[0].recipeId.empty()); } TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "[blueprint]") { Simulation sim(loadConfig()); // Miner body cells: (0,0),(1,0),(0,1) — all at x < 0, valid for asteroid. const BuildingId id = sim.tryPlaceBuilding(BuildingType::Miner, QPoint(-2, 0), Rotation::East); REQUIRE(id != kInvalidBuildingId); sim.buildings().setRecipe(id, "mine_iron_ore"); const ConstructionSite* site = sim.buildings().findSite(id); REQUIRE(site != nullptr); REQUIRE(site->recipeId == "mine_iron_ore"); } TEST_CASE("Blueprint placement: recipe transfers to building after construction completes", "[blueprint]") { Simulation sim(loadConfig()); const BuildingId id = sim.tryPlaceBuilding(BuildingType::Miner, QPoint(-2, 0), Rotation::East); REQUIRE(id != kInvalidBuildingId); sim.buildings().setRecipe(id, "mine_copper_ore"); // Miner construction_time_seconds = 10 → completesAt = secondsToTicks(10) = 300. // Run 301 ticks (0..300) to process the completion tick. for (int i = 0; i <= static_cast(secondsToTicks(10.0)); ++i) { sim.tick(); } const Building* b = sim.buildings().findBuilding(id); REQUIRE(b != nullptr); REQUIRE(b->recipeId == "mine_copper_ore"); } TEST_CASE("Blueprint placement: interceptor schematic is unlocked at game start", "[blueprint]") { // "interceptor" has available_from_start = true in the test config. // This confirms the guard in placeBlueprintAtTile passes for start-unlocked schematics. Simulation sim(loadConfig()); REQUIRE(sim.isSchematicUnlocked("interceptor")); } TEST_CASE("Blueprint placement: repair_ship schematic is locked at game start", "[blueprint]") { // "repair_ship" has available_from_start = false in the test config. // This confirms the guard in placeBlueprintAtTile blocks locked schematics, // leaving the shipyard's schematic unset. Simulation sim(loadConfig()); REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship")); }