From 949937d2c2a7a523b51285157e046069928bf20d Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Wed, 5 Aug 2026 07:53:08 +0200 Subject: [PATCH] re-use PlacementFixture in BuildingTests --- src/test/BuildingTest.cpp | 784 +++++++++++--------------------------- 1 file changed, 214 insertions(+), 570 deletions(-) diff --git a/src/test/BuildingTest.cpp b/src/test/BuildingTest.cpp index 9e72f9a..5af4252 100644 --- a/src/test/BuildingTest.cpp +++ b/src/test/BuildingTest.cpp @@ -87,18 +87,25 @@ static std::vector outputSideItems(const Building& b) } // Owns a BuildingSystem and its dependencies for placement-bounds tests. +// Belt speed for the tests that need an item to cross a tile in a single tick, so +// it is available to peek or take on the next one. +constexpr double kFastBeltSpeed_tps = static_cast(kTickRateHz); + struct PlacementFixture { GameConfig cfg = loadTestConfig(); - FactoryState state = makeFactoryState(cfg); - BeltSystem belts{cfg.world.beltSpeed_tps}; + FactoryState state = makeFactoryState(cfg); + BeltSystem belts; int stock = 0; std::mt19937 rng{0}; BuildingId nextBuildingId = 1; BuildingSystem bs; - PlacementFixture() - : bs(cfg, belts, + // Defaults to the configured belt speed; pass kFastBeltSpeed_tps where the test + // needs items to arrive immediately. + explicit PlacementFixture(std::optional beltSpeed_tps = std::nullopt) + : belts(beltSpeed_tps.value_or(cfg.world.beltSpeed_tps)) + , bs(cfg, belts, [this]() { return nextBuildingId++; }, [this](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, @@ -114,28 +121,17 @@ struct PlacementFixture TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - const BuildingId id = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); REQUIRE(id != kInvalidBuildingId); // Miner mask ["AA","A>"] with East rotation → body at (0,0),(1,0),(0,1). - REQUIRE(isTileOccupied(state_bs, QPoint(0, 0))); - REQUIRE(isTileOccupied(state_bs, QPoint(1, 0))); - REQUIRE(isTileOccupied(state_bs, QPoint(0, 1))); + REQUIRE(isTileOccupied(f.state, QPoint(0, 0))); + REQUIRE(isTileOccupied(f.state, QPoint(1, 0))); + REQUIRE(isTileOccupied(f.state, QPoint(0, 1))); // (1,1) is the output-port tile, NOT a body cell. - REQUIRE_FALSE(isTileOccupied(state_bs, QPoint(1, 1))); + REQUIRE_FALSE(isTileOccupied(f.state, QPoint(1, 1))); } // -- World-bounds rejection (REQ-BLD-PLACE-VALID) --------------------------- @@ -222,54 +218,32 @@ TEST_CASE("BuildingSystem: isPlacementValid enforces terrain and world bounds", TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after construction", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - bs.place(state_bs, BuildingType::Belt, QPoint(5, 5), Rotation::East, 0); + f.bs.place(f.state, BuildingType::Belt, QPoint(5, 5), Rotation::East, 0); // Belt is queued — not yet in BeltSystem. - REQUIRE_FALSE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East)); + REQUIRE_FALSE(f.belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East)); // Complete construction (1 s). Tick tick = 0; - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(1.0)) + 1, tick); - REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East)); - REQUIRE(getAllBuildings(state_bs).size() == 1); - REQUIRE(getAllBuildings(state_bs)[0].type == BuildingType::Belt); - REQUIRE(getAllBuildings(state_bs)[0].anchor == QPoint(5, 5)); + REQUIRE(f.belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East)); + REQUIRE(getAllBuildings(f.state).size() == 1); + REQUIRE(getAllBuildings(f.state)[0].type == BuildingType::Belt); + REQUIRE(getAllBuildings(f.state)[0].anchor == QPoint(5, 5)); } TEST_CASE("BuildingSystem: placed building enters construction queue", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - const BuildingId id = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - REQUIRE(getAllSites(state_bs).size() == 1); - REQUIRE(getAllBuildings(state_bs).empty()); - REQUIRE(findSite(state_bs, id) != nullptr); + REQUIRE(getAllSites(f.state).size() == 1); + REQUIRE(getAllBuildings(f.state).empty()); + REQUIRE(findSite(f.state, id) != nullptr); } TEST_CASE("BuildingSystem: deconstructing a construction site removes it instantly with full refund", @@ -295,70 +269,37 @@ TEST_CASE("BuildingSystem: deconstructing a construction site removes it instant TEST_CASE("BuildingSystem: first queued building starts construction immediately", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); - REQUIRE(getAllSites(state_bs).front().completesAt > 0); + f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + REQUIRE(getAllSites(f.state).front().completesAt > 0); } TEST_CASE("BuildingSystem: second queued building waits (completesAt == 0)", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); - bs.place(state_bs, BuildingType::Miner, QPoint(5, 5), Rotation::East, 0); + f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + f.bs.place(f.state, BuildingType::Miner, QPoint(5, 5), Rotation::East, 0); - REQUIRE(getAllSites(state_bs).size() == 2); - REQUIRE(getAllSites(state_bs)[0].completesAt > 0); - REQUIRE(getAllSites(state_bs)[1].completesAt == 0); + REQUIRE(getAllSites(f.state).size() == 2); + REQUIRE(getAllSites(f.state)[0].completesAt > 0); + REQUIRE(getAllSites(f.state)[1].completesAt == 0); } TEST_CASE("BuildingSystem: construction completes after configured duration", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - const BuildingId id = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); // Miner construction_time_seconds = 10. completesAt = secondsToTicks(10) = 300. // We need to process tick 300 itself, so run 301 ticks (ticks 0..300). Tick tick = 0; - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(10.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(10.0)) + 1, tick); - REQUIRE(getAllSites(state_bs).empty()); - REQUIRE(findBuilding(state_bs, id) != nullptr); + REQUIRE(getAllSites(f.state).empty()); + REQUIRE(findBuilding(f.state, id) != nullptr); } // --------------------------------------------------------------------------- @@ -504,29 +445,18 @@ TEST_CASE("BuildingSystem: splitter filters survive a queue/un-queue round-trip" TEST_CASE("BuildingSystem: second building starts after first completes", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); - const BuildingId id2 = bs.place(state_bs, BuildingType::Miner, QPoint(5, 5), Rotation::East, 0).value(); + f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + const BuildingId id2 = f.bs.place(f.state, BuildingType::Miner, QPoint(5, 5), Rotation::East, 0).value(); // Process through tick 300 to complete first miner's construction. Tick tick = 0; - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(10.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(10.0)) + 1, tick); - REQUIRE(getAllSites(state_bs).size() == 1); - REQUIRE(getAllSites(state_bs).front().id == id2); - REQUIRE(getAllSites(state_bs).front().completesAt > 0); + REQUIRE(getAllSites(f.state).size() == 1); + REQUIRE(getAllSites(f.state).front().id == id2); + REQUIRE(getAllSites(f.state).front().completesAt > 0); } // --------------------------------------------------------------------------- @@ -535,30 +465,19 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - const BuildingId id = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - bs.setRecipe(state_bs, id, "mine_iron_ore"); + const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + f.bs.setRecipe(f.state, id, "mine_iron_ore"); Tick tick = 0; // Construction completes on tick 300; production cycle starts tick 300, // completes on tick 330. Process through tick 330: 331 ticks total. - runTicks(bs, cfg, state_bs, belts, stock, + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(10.0)) + static_cast(secondsToTicks(1.0)) + 1, tick); - const Building* b = findBuilding(state_bs, id); + const Building* b = findBuilding(f.state, id); REQUIRE(b != nullptr); // No belt at the output port, so the produced item emerges and stays on the // building's virtual output belt (REQ-MAT-OUTPUT-EMERGE). @@ -569,21 +488,10 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - const BuildingId id = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - bs.setRecipe(state_bs, id, "mine_iron_ore"); + const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + f.bs.setRecipe(f.state, id, "mine_iron_ore"); Tick tick = 0; // Construction (10s) then cycle 1 starts at tick 300 (completesAt=330). @@ -591,12 +499,12 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]") // Cycle 2 starts at tick 331 (completesAt=361). // Cycle 2 completes at tick 361: deposit item → buffer=2, cycle 3 stalls. // Need to process through tick 361: 362 ticks total. - runTicks(bs, cfg, state_bs, belts, stock, + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(10.0)) + 2 * static_cast(secondsToTicks(1.0)) + 2, tick); - const Building* b = findBuilding(state_bs, id); + const Building* b = findBuilding(f.state, id); REQUIRE(b != nullptr); // Both produced items are held on the output side (buffer + emerging lane), // which is what the capacity rule counts (REQ-MAT-OUTPUT-EMERGE). @@ -610,80 +518,58 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]") TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - const BuildingId minerId = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - const BuildingId smelterId = bs.place(state_bs, BuildingType::Smelter, QPoint(10, 0), Rotation::East, 0).value(); + const BuildingId minerId = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId smelterId = f.bs.place(f.state, BuildingType::Smelter, QPoint(10, 0), Rotation::East, 0).value(); (void)smelterId; Tick tick = 0; // Both still under construction. - REQUIRE(getProductionBuildingCount(state_bs) == 0); + REQUIRE(getProductionBuildingCount(f.state) == 0); // The queue builds one at a time: miner (10s) completes at tick 300, then // the smelter (15s) starts and completes at tick 300 + 450 = 750. - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(10.0)) + 1, tick); - REQUIRE(getProductionBuildingCount(state_bs) == 1); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(10.0)) + 1, tick); + REQUIRE(getProductionBuildingCount(f.state) == 1); - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(15.0)), tick); - REQUIRE(getProductionBuildingCount(state_bs) == 2); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(15.0)), tick); + REQUIRE(getProductionBuildingCount(f.state) == 2); // Neither is producing yet: the miner has no recipe selected, and the // smelter (auto-recipe, REQ-BLD-SMELTER) has no input feeding it. - REQUIRE(getActiveProductionBuildingCount(state_bs) == 0); + REQUIRE(getActiveProductionBuildingCount(f.state) == 0); - bs.setRecipe(state_bs, minerId, "mine_iron_ore"); - runTicks(bs, cfg, state_bs, belts, stock, 1, tick); - REQUIRE(getActiveProductionBuildingCount(state_bs) == 1); + f.bs.setRecipe(f.state, minerId, "mine_iron_ore"); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, 1, tick); + REQUIRE(getActiveProductionBuildingCount(f.state) == 1); } TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle state", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - const BuildingId id = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - bs.setRecipe(state_bs, id, "mine_iron_ore"); + const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + f.bs.setRecipe(f.state, id, "mine_iron_ore"); Tick tick = 0; // Not yet operational while under construction. - REQUIRE(getActiveProductionBuildingCount(state_bs) == 0); + REQUIRE(getActiveProductionBuildingCount(f.state) == 0); // Construction completes at tick 300; cycle 1 starts the same tick (completesAt=330). - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(10.0)) + 1, tick); - REQUIRE(getActiveProductionBuildingCount(state_bs) == 1); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(10.0)) + 1, tick); + REQUIRE(getActiveProductionBuildingCount(f.state) == 1); // Run cycles 1 and 2 to completion (1s each); cycle 3 stalls once the // output buffer (capacity 2) is full (REQ-MAT-OUTPUT-BUFFER). - runTicks(bs, cfg, state_bs, belts, stock, 2 * static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, 2 * static_cast(secondsToTicks(1.0)) + 1, tick); - const Building* b = findBuilding(state_bs, id); + const Building* b = findBuilding(f.state, id); REQUIRE(b != nullptr); REQUIRE(b->getOutputItemCount() == 2); REQUIRE_FALSE(b->production.has_value()); - REQUIRE(getActiveProductionBuildingCount(state_bs) == 0); + REQUIRE(getActiveProductionBuildingCount(f.state) == 0); } // --------------------------------------------------------------------------- @@ -693,38 +579,27 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing belt", "[building]") { - const GameConfig cfg = loadTestConfig(); // Fast belt so items are immediately available for peek/take. - BeltSystem belts(static_cast(kTickRateHz)); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f(kFastBeltSpeed_tps); // Smelter mask ["AA ","AA>"] → body (0,0),(1,0),(0,1),(1,1). // Output port (2,1) East. Input port example: (2,0) West. - const BuildingId sid = bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); // Smelters have no recipe selection (REQ-BLD-SMELTER); they auto-accept any // ore/scrap that is an input to a smelter recipe. // Complete construction (15s → tick 450+1 = 451 ticks). Tick tick = 0; - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(15.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(15.0)) + 1, tick); // Place west-flowing belt at (2,0): belt flows West, delivers to smelter. - belts.placeBelt(QPoint(2, 0), Rotation::West); - belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore")); - belts.tick(); + f.belts.placeBelt(QPoint(2, 0), Rotation::West); + f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore")); + f.belts.tick(); - bs.tickBeltPull(state_bs); + f.bs.tickBeltPull(f.state); - const Building* b = findBuilding(state_bs, sid); + const Building* b = findBuilding(f.state, sid); REQUIRE(b != nullptr); // The item was accepted; it may still be travelling inward on the input belt, // so count buffered + in-transit (REQ-MAT-INPUT-INTAKE). @@ -732,34 +607,23 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing } // An accepted input item travels inward on its input belt before it becomes usable -// stock: it is reserved (counts against the cap) on entry and only enters the +// f.stock: it is reserved (counts against the cap) on entry and only enters the // buffer on reaching the tile centre (REQ-MAT-INPUT-INTAKE). TEST_CASE("BuildingSystem: accepted input travels inward before entering the buffer", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(static_cast(kTickRateHz)); // fast belt: 1 tile/tick - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f(kFastBeltSpeed_tps); - const BuildingId sid = bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(15.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(15.0)) + 1, tick); - belts.placeBelt(QPoint(2, 0), Rotation::West); - belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore")); - belts.tick(); - bs.tickBeltPull(state_bs); // accepts the item onto the input belt at progress 0.0 + f.belts.placeBelt(QPoint(2, 0), Rotation::West); + f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore")); + f.belts.tick(); + f.bs.tickBeltPull(f.state); // accepts the item onto the input belt at progress 0.0 - const Building* b = findBuilding(state_bs, sid); + const Building* b = findBuilding(f.state, sid); REQUIRE(b != nullptr); // Reserved but not yet consumable: nothing in the buffer, but it counts against // the cap via pendingInputCount. @@ -769,7 +633,7 @@ TEST_CASE("BuildingSystem: accepted input travels inward before entering the buf REQUIRE(b->pendingInputCount(ItemType{"iron_ore"}) == 1); // One more pull tick advances the input belt to the centre; the item arrives. - bs.tickBeltPull(state_bs); + f.bs.tickBeltPull(f.state); REQUIRE(b->inputBuffer.counts.at(ItemType{"iron_ore"}) == 1); REQUIRE(b->pendingInputCount(ItemType{"iron_ore"}) == 1); } @@ -779,42 +643,31 @@ TEST_CASE("BuildingSystem: accepted input travels inward before entering the buf TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at the cap", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(static_cast(kTickRateHz)); // fast belt - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f(kFastBeltSpeed_tps); - const BuildingId id = bs.place(state_bs, BuildingType::ReprocessingPlant, + const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(25.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(25.0)) + 1, tick); // Feed scrap via an input belt without ever running production (only pull), so // the buffer fills and stays full. Try to over-fill it well past the cap. - belts.placeBelt(QPoint(-1, 0), Rotation::East); + f.belts.placeBelt(QPoint(-1, 0), Rotation::East); for (int i = 0; i < 20; ++i) { - belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East); - belts.tick(); - bs.tickBeltPull(state_bs); + f.belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East); + f.belts.tick(); + f.bs.tickBeltPull(f.state); } - const Building* b = findBuilding(state_bs, id); + const Building* b = findBuilding(f.state, id); REQUIRE(b != nullptr); const int cap = b->inputBuffer.caps.at(ItemType{"scrap"}); REQUIRE(cap > 0); // buffered + in-transit is capped; the plant never over-pulls. REQUIRE(b->pendingInputCount(ItemType{"scrap"}) == cap); // Excess scrap is left stuck on the feeding belt rather than silently dropped. - REQUIRE(belts.peekItem(eastPort(QPoint(-1, 0))).has_value()); + REQUIRE(f.belts.peekItem(eastPort(QPoint(-1, 0))).has_value()); } // A smelter auto-selects the matching recipe for whatever it is fed, with no @@ -822,38 +675,27 @@ TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at th TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(static_cast(kTickRateHz)); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f(kFastBeltSpeed_tps); - const BuildingId sid = bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(15.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(15.0)) + 1, tick); // Feed 2 iron_ore (the test-config iron_ingot recipe needs 2) via a // west-flowing belt at input port (2,0). - belts.placeBelt(QPoint(2, 0), Rotation::West); + f.belts.placeBelt(QPoint(2, 0), Rotation::West); for (int i = 0; i < 2; ++i) { - belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore")); - belts.tick(); - bs.tickBeltPull(state_bs); + f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore")); + f.belts.tick(); + f.bs.tickBeltPull(f.state); } // iron_ingot recipe cycle is 2s; run to completion. - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(2.0)) + 2, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(2.0)) + 2, tick); - const Building* b = findBuilding(state_bs, sid); + const Building* b = findBuilding(f.state, sid); REQUIRE(b != nullptr); bool hasIronIngot = false; for (const Item& item : outputSideItems(*b)) @@ -869,39 +711,28 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection", TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete batch waits", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(static_cast(kTickRateHz)); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f(kFastBeltSpeed_tps); - const BuildingId sid = bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(15.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(15.0)) + 1, tick); // Feed 1 iron_ore (iron_ingot needs 2 — incomplete) then 2 copper_ore // (copper_ingot needs 2 — satisfiable) via the west-flowing input belt. - belts.placeBelt(QPoint(2, 0), Rotation::West); + f.belts.placeBelt(QPoint(2, 0), Rotation::West); const char* fed[] = { "iron_ore", "copper_ore", "copper_ore" }; for (const char* id : fed) { - belts.tryPutItem(QPoint(2, 0), makeItem(id)); - belts.tick(); - bs.tickBeltPull(state_bs); + f.belts.tryPutItem(QPoint(2, 0), makeItem(id)); + f.belts.tick(); + f.bs.tickBeltPull(f.state); } // copper_ingot cycle is 2.5s; run to completion. - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(2.5)) + 2, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(2.5)) + 2, tick); - const Building* b = findBuilding(state_bs, sid); + const Building* b = findBuilding(f.state, sid); REQUIRE(b != nullptr); // Copper was smelted; the lone iron_ore still waits for a second unit. @@ -924,36 +755,25 @@ TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(static_cast(kTickRateHz)); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f(kFastBeltSpeed_tps); - const BuildingId id = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - bs.setRecipe(state_bs, id, "mine_iron_ore"); + const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + f.bs.setRecipe(f.state, id, "mine_iron_ore"); // Belt at the miner's output port tile (1,1) flowing East. - belts.placeBelt(QPoint(1, 1), Rotation::East); + f.belts.placeBelt(QPoint(1, 1), Rotation::East); Tick tick = 0; // Construction (10s) + 1 production cycle (1s) + 1 extra tick. - runTicks(bs, cfg, state_bs, belts, stock, + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(10.0)) + static_cast(secondsToTicks(1.0)) + 1, tick); // Item should have been pushed onto the belt this tick or a subsequent one. // Run one more tick to ensure tickBeltPush fires after the deposit tick. - runTicks(bs, cfg, state_bs, belts, stock, 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, 1, tick); - const std::optional item = belts.tryTakeItem(eastPort(QPoint(1, 1))); + const std::optional item = f.belts.tryTakeItem(eastPort(QPoint(1, 1))); REQUIRE(item.has_value()); REQUIRE(item->type.id == "iron_ore"); } @@ -964,32 +784,21 @@ TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[bui TEST_CASE("BuildingSystem: output port couples directly into an adjacent input port", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; // Miner at (0,0): body (0,0),(1,0),(0,1); output port tile (1,1) flowing East. - const BuildingId minerId = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - bs.setRecipe(state_bs, minerId, "mine_iron_ore"); + const BuildingId minerId = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + f.bs.setRecipe(f.state, minerId, "mine_iron_ore"); // Smelter anchored at (1,1): body (1,1),(2,1),(1,2),(2,2). Its body cell (1,1) is // the miner's output-port tile, and its west input edge there faces East, so the // two ports meet — no belt placed anywhere. - const BuildingId smelterId = bs.place(state_bs, BuildingType::Smelter, QPoint(1, 1), Rotation::East, 0).value(); + const BuildingId smelterId = f.bs.place(f.state, BuildingType::Smelter, QPoint(1, 1), Rotation::East, 0).value(); Tick tick = 0; // Smelter build (15s) + margin for coupling and a smelt cycle. - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(30.0)), tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(30.0)), tick); - const Building* smelter = findBuilding(state_bs, smelterId); + const Building* smelter = findBuilding(f.state, smelterId); REQUIRE(smelter != nullptr); // iron_ore reached the smelter over the direct coupling and was smelted. bool hasIronIngot = false; @@ -1005,32 +814,21 @@ 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", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; // Producing miner at (0,0), output port (1,1) East. - const BuildingId minerId = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - bs.setRecipe(state_bs, minerId, "mine_iron_ore"); + const BuildingId minerId = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + f.bs.setRecipe(f.state, minerId, "mine_iron_ore"); // A second, idle miner anchored at (1,1) occupies the output-port tile but takes // no inputs, so it cannot accept the iron_ore. - const BuildingId sinkId = bs.place(state_bs, BuildingType::Miner, QPoint(1, 1), Rotation::East, 0).value(); + const BuildingId sinkId = f.bs.place(f.state, BuildingType::Miner, QPoint(1, 1), Rotation::East, 0).value(); Tick tick = 0; // Both miners build sequentially (10s each), then the producer runs and jams. - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(25.0)), tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(25.0)), tick); - const Building* miner = findBuilding(state_bs, minerId); - const Building* sink = findBuilding(state_bs, sinkId); + const Building* miner = findBuilding(f.state, minerId); + const Building* sink = findBuilding(f.state, sinkId); REQUIRE(miner != nullptr); REQUIRE(sink != nullptr); // Nothing was delivered, and the producer's output side has backed up to its cap. @@ -1045,37 +843,26 @@ TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stu TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(static_cast(kTickRateHz)); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f(kFastBeltSpeed_tps); - const BuildingId id = bs.place(state_bs, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); - bs.setRecipe(state_bs, id, "mine_iron_ore"); + const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); + f.bs.setRecipe(f.state, id, "mine_iron_ore"); Tick tick = 0; // Run until first item is in output buffer. - runTicks(bs, cfg, state_bs, belts, stock, + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(10.0)) + static_cast(secondsToTicks(1.0)) + 1, tick); { - const Building* b = findBuilding(state_bs, id); + const Building* b = findBuilding(f.state, id); REQUIRE(b != nullptr); REQUIRE(b->getOutputItemCount() > 0); } - bs.setRecipe(state_bs, id, "mine_copper_ore"); + f.bs.setRecipe(f.state, id, "mine_copper_ore"); - const Building* b = findBuilding(state_bs, id); + const Building* b = findBuilding(f.state, id); // Clearing the output buffer on a recipe change also discards emerging items // (REQ-MAT-OUTPUT-EMERGE). REQUIRE(b->getOutputItemCount() == 0); @@ -1089,29 +876,18 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production" TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max output per roll", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - const BuildingId id = bs.place(state_bs, BuildingType::ReprocessingPlant, + const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant, QPoint(0, 0), Rotation::East, 0).value(); // Reprocessing plants have no recipe selection (REQ-BLD-REPROCESSING); the // single reprocessing recipe is applied automatically on completion. // Complete construction (25s). Tick tick = 0; - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(25.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(25.0)) + 1, tick); - const Building* b = findBuilding(state_bs, id); + const Building* b = findBuilding(f.state, id); REQUIRE(b != nullptr); // reprocessing_cycle outputs: 2 iron_ingot (60%), 1 circuit_board (30%), // 1 advanced_alloy (10%). Max per roll = 2. Capacity = 2 (1× max). @@ -1121,52 +897,41 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then stalls", "[building]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(static_cast(kTickRateHz)); - int stock = 0; // Seed chosen so first roll produces 2-item output (iron_ingot), filling buffer. - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f(kFastBeltSpeed_tps); - const BuildingId id = bs.place(state_bs, BuildingType::ReprocessingPlant, + const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant, QPoint(0, 0), Rotation::East, 0).value(); // Reprocessing plants have no recipe selection (REQ-BLD-REPROCESSING); the // single reprocessing recipe is applied automatically on completion. // Complete construction (25s). Tick tick = 0; - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(25.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(25.0)) + 1, tick); // Feed 5 scrap into the building via a belt at an input port. // Reprocessing plant body (East rotation) = 3×3 at (0,0). // Valid input port: tile (-1,0) flowing East. - belts.placeBelt(QPoint(-1, 0), Rotation::East); + f.belts.placeBelt(QPoint(-1, 0), Rotation::East); for (int i = 0; i < 5; ++i) { - belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East); - belts.tick(); - bs.tickBeltPull(state_bs); + f.belts.tryPutItem(QPoint(-1, 0), makeItem("scrap"), Rotation::East); + f.belts.tick(); + f.bs.tickBeltPull(f.state); } // Verify all five scrap were accepted; some may still be travelling inward on // the input belt (REQ-MAT-INPUT-INTAKE), so count buffered + in-transit. { - const Building* b = findBuilding(state_bs, id); + const Building* b = findBuilding(f.state, id); REQUIRE(b != nullptr); REQUIRE(b->pendingInputCount(ItemType{"scrap"}) == 5); } // Run production cycle (3s = 90 ticks + 1 for the completion tick). - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(3.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(3.0)) + 1, tick); - const Building* b = findBuilding(state_bs, id); + const Building* b = findBuilding(f.state, id); REQUIRE(b != nullptr); // Cycle should have completed and output deposited. REQUIRE_FALSE(b->outputBuffer.items.empty()); @@ -1181,43 +946,21 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when tile is empty", "[building][rotate-in-place]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; REQUIRE_FALSE( - findRotateInPlaceTarget(state_bs, cfg, BuildingType::Belt, QPoint(0, 0), Rotation::East).has_value()); + findRotateInPlaceTarget(f.state, f.cfg, BuildingType::Belt, QPoint(0, 0), Rotation::East).has_value()); } TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a queued belt (same type, different rotation)", "[building][rotate-in-place]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); const std::optional result = - findRotateInPlaceTarget(state_bs, cfg, BuildingType::Belt, QPoint(0, 0), Rotation::North); + findRotateInPlaceTarget(f.state, f.cfg, BuildingType::Belt, QPoint(0, 0), Rotation::North); REQUIRE(result.has_value()); REQUIRE(*result == id); } @@ -1225,27 +968,16 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a que TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a completed operational belt", "[building][rotate-in-place]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(1.0)) + 1, tick); - REQUIRE(getAllSites(state_bs).empty()); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(1.0)) + 1, tick); + REQUIRE(getAllSites(f.state).empty()); const std::optional result = - findRotateInPlaceTarget(state_bs, cfg, BuildingType::Belt, QPoint(0, 0), Rotation::South); + findRotateInPlaceTarget(f.state, f.cfg, BuildingType::Belt, QPoint(0, 0), Rotation::South); REQUIRE(result.has_value()); REQUIRE(*result == id); } @@ -1253,100 +985,56 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when building type differs", "[building][rotate-in-place]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0); + f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0); // Querying with Splitter at the same tile — type mismatch → nullopt. REQUIRE_FALSE( - findRotateInPlaceTarget(state_bs, cfg, BuildingType::Splitter, QPoint(0, 0), Rotation::East).has_value()); + findRotateInPlaceTarget(f.state, f.cfg, BuildingType::Splitter, QPoint(0, 0), Rotation::East).has_value()); } TEST_CASE("BuildingSystem: findRotateInPlaceTarget never rotates a tunnel in place", "[building][rotate-in-place]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; // Even with a coincident same-type tunnel under the ghost, rotate-in-place is // never offered for tunnels (REQ-BLD-ROTATE-IN-PLACE exception). - bs.place(state_bs, BuildingType::TunnelEntry, QPoint(-1, 0), Rotation::East, 0); - bs.place(state_bs, BuildingType::TunnelExit, QPoint(-2, 0), Rotation::East, 0); + f.bs.place(f.state, BuildingType::TunnelEntry, QPoint(-1, 0), Rotation::East, 0); + f.bs.place(f.state, BuildingType::TunnelExit, QPoint(-2, 0), Rotation::East, 0); REQUIRE_FALSE( - findRotateInPlaceTarget(state_bs, cfg, BuildingType::TunnelEntry, QPoint(-1, 0), Rotation::North).has_value()); + findRotateInPlaceTarget(f.state, f.cfg, BuildingType::TunnelEntry, QPoint(-1, 0), Rotation::North).has_value()); REQUIRE_FALSE( - findRotateInPlaceTarget(state_bs, cfg, BuildingType::TunnelExit, QPoint(-2, 0), Rotation::North).has_value()); + findRotateInPlaceTarget(f.state, f.cfg, BuildingType::TunnelExit, QPoint(-2, 0), Rotation::North).has_value()); } TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprints only partially overlap", "[building][rotate-in-place]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; // Smelter at (0,0) occupies body tiles (0,0),(1,0),(0,1),(1,1). - bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0); + f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0); // Ghost anchored at (1,0) would cover (1,0),(2,0),(1,1),(2,1): // only (1,0) and (1,1) are occupied — not a full coincidence. REQUIRE_FALSE( - findRotateInPlaceTarget(state_bs, cfg, BuildingType::Smelter, QPoint(1, 0), Rotation::East).has_value()); + findRotateInPlaceTarget(f.state, f.cfg, BuildingType::Smelter, QPoint(1, 0), Rotation::East).has_value()); } TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-tile building with rotated ghost", "[building][rotate-in-place]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; // Smelter is a fully filled 2×2 footprint — rotating the ghost produces the // same four body tiles, so findRotateInPlaceTarget must still return the id. - const BuildingId id = bs.place(state_bs, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); const std::optional result = - findRotateInPlaceTarget(state_bs, cfg, BuildingType::Smelter, QPoint(0, 0), Rotation::North); + findRotateInPlaceTarget(f.state, f.cfg, BuildingType::Smelter, QPoint(0, 0), Rotation::North); REQUIRE(result.has_value()); REQUIRE(*result == id); } @@ -1358,80 +1046,47 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-t TEST_CASE("BuildingSystem: rotateInPlace updates the rotation field of a construction site", "[building][rotate-in-place]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); - REQUIRE(findSite(state_bs, id)->rotation == Rotation::East); + const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); + REQUIRE(findSite(f.state, id)->rotation == Rotation::East); - bs.rotateInPlace(state_bs, id, Rotation::North); + f.bs.rotateInPlace(f.state, id, Rotation::North); - REQUIRE(findSite(state_bs, id)->rotation == Rotation::North); + REQUIRE(findSite(f.state, id)->rotation == Rotation::North); } TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of a queued site", "[building][rotate-in-place]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); - const Tick completesAt = findSite(state_bs, id)->completesAt; + const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); + const Tick completesAt = findSite(f.state, id)->completesAt; REQUIRE(completesAt > 0); - bs.rotateInPlace(state_bs, id, Rotation::South); + f.bs.rotateInPlace(f.state, id, Rotation::South); - REQUIRE(findSite(state_bs, id)->completesAt == completesAt); + REQUIRE(findSite(f.state, id)->completesAt == completesAt); } TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direction on an operational building", "[building][rotate-in-place]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(1.0)) + 1, tick); - REQUIRE(findBuilding(state_bs, id) != nullptr); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(1.0)) + 1, tick); + REQUIRE(findBuilding(f.state, id) != nullptr); - const Building& before = *findBuilding(state_bs, id); + const Building& before = *findBuilding(f.state, id); REQUIRE(before.outputPorts[0].direction == Rotation::East); - bs.rotateInPlace(state_bs, id, Rotation::North); + f.bs.rotateInPlace(f.state, id, Rotation::North); - const Building& after = *findBuilding(state_bs, id); + const Building& after = *findBuilding(f.state, id); REQUIRE(after.rotation == Rotation::North); REQUIRE(after.outputPorts[0].direction == Rotation::North); } @@ -1439,28 +1094,17 @@ 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", "[building][rotate-in-place]") { - const GameConfig cfg = loadTestConfig(); - BeltSystem belts(cfg.world.beltSpeed_tps); - int stock = 0; - std::mt19937 rng(0); - BuildingId nextBuildingId = 1; - FactoryState state_bs = makeFactoryState(cfg); - BuildingSystem bs(cfg, belts, - [&nextBuildingId]() { return nextBuildingId++; }, - [&stock](int n) { stock += n; }, - [](const std::string&, QVector2D, const std::optional&) {}, - [](const std::string&) -> bool { return true; }, - rng); + PlacementFixture f; - const BuildingId id = bs.place(state_bs, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); + const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(bs, cfg, state_bs, belts, stock, static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(1.0)) + 1, tick); - bs.rotateInPlace(state_bs, id, Rotation::North); + f.bs.rotateInPlace(f.state, id, Rotation::North); // Belt tile must still be registered after rotation — items can be placed on it. - REQUIRE(belts.tryPutItem(QPoint(0, 0), makeItem("iron_ore"))); + REQUIRE(f.belts.tryPutItem(QPoint(0, 0), makeItem("iron_ore"))); } TEST_CASE("BuildingSystem: rotateInPlace preserves the output filters of a splitter "