diff --git a/docs/requirements.md b/docs/requirements.md index 44b5224..cd15834 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -82,7 +82,7 @@ Output port indicators are not building tiles themselves. A building may have mo - REQ-MAT-BELT-ONLY: Materials are transported exclusively via belts and splitters. - REQ-MAT-INPUT-PORTS: A building accepts items from any adjacent belt tile on any edge of its footprint (excluding cells occupied by output port(s)) whose direction points toward the building, provided the item is an input required by the currently selected recipe and the matching per-material input buffer has free space. -- REQ-MAT-OUTPUT-PORT: Each building has one or more fixed output port(s) defined by its surface_mask (direction determined by rotation). Produced items are placed onto the belt at the output port. +- REQ-MAT-OUTPUT-PORT: Each building has one or more fixed output port(s) defined by its surface_mask (direction determined by rotation). Produced items are placed onto the belt at the output port tile regardless of that belt's direction. - REQ-MAT-INPUT-BUFFER: Each building has one input buffer per required input material. Each per-material buffer holds up to twice that material's per-cycle requirement. When the player selects a new recipe or blueprint, all items in all input buffers are cleared. - REQ-MAT-OUTPUT-BUFFER: Each building has an output buffer that holds up to twice the quantity produced by one production cycle. If the output buffer is full, production stops until space is available. When the player selects a new recipe or blueprint, all items in the output buffer are cleared (relevant when the adjacent belt is jammed and items have accumulated). - REQ-MAT-OUTPUT-BUFFER-REPROCESSING: Exception to REQ-MAT-OUTPUT-BUFFER — the Reprocessing Plant's output buffer holds at most one cycle's output. This prevents exploits where the player stalls the output belt to force the plant to reroll. diff --git a/src/lib/sim/BeltSystem.cpp b/src/lib/sim/BeltSystem.cpp index 5e74bbe..14e49e2 100644 --- a/src/lib/sim/BeltSystem.cpp +++ b/src/lib/sim/BeltSystem.cpp @@ -92,18 +92,14 @@ void BeltSystem::setSplitterFilters(QPoint tile, // Port interface // --------------------------------------------------------------------------- -bool BeltSystem::tryPutItem(Port port, Item item) +bool BeltSystem::tryPutItem(QPoint tile, Item item) { - const std::map, BeltTile>::iterator it = m_belts.find(key(port.tile)); + const std::map, BeltTile>::iterator it = m_belts.find(key(tile)); if (it == m_belts.end()) { return false; } - if (it->second.direction != port.direction) - { - return false; - } - return tryPlaceOnBelt(port.tile, item); + return tryPlaceOnBelt(tile, item); } std::optional BeltSystem::tryTakeItem(Port port) diff --git a/src/lib/sim/BeltSystem.h b/src/lib/sim/BeltSystem.h index 05d35c5..fba6219 100644 --- a/src/lib/sim/BeltSystem.h +++ b/src/lib/sim/BeltSystem.h @@ -56,9 +56,9 @@ public: // port.tile = the belt tile adjacent to the building // port.direction = direction items flow on that tile // - // tryPutItem: place item onto port.tile entering from the opposite side. - // Returns false if the tile is not a belt, direction mismatches, or tile full. - bool tryPutItem(Port port, Item item); + // tryPutItem: place item onto tile. + // Returns false if the tile is not a belt, or tile full. + bool tryPutItem(QPoint tile, Item item); // tryTakeItem: remove and return the leading item from port.tile. // Returns nullopt if tile is not a belt, direction mismatches, or tile empty. @@ -119,3 +119,4 @@ private: std::map, BeltTile> m_belts; std::map, SplitterTile> m_splitters; }; + diff --git a/src/lib/sim/BuildingSystem.cpp b/src/lib/sim/BuildingSystem.cpp index 721d3b6..d6fd12c 100644 --- a/src/lib/sim/BuildingSystem.cpp +++ b/src/lib/sim/BuildingSystem.cpp @@ -726,7 +726,7 @@ void BuildingSystem::tickBeltPush() break; } const Item item = building.outputBuffer.items.front(); - if (m_belts.tryPutItem(outputPort, item)) + if (m_belts.tryPutItem(outputPort.tile, item)) { building.outputBuffer.items.erase(building.outputBuffer.items.begin()); } diff --git a/src/test/BeltSystemTest.cpp b/src/test/BeltSystemTest.cpp index f52eaa5..9439d95 100644 --- a/src/test/BeltSystemTest.cpp +++ b/src/test/BeltSystemTest.cpp @@ -36,29 +36,20 @@ static Port eastPort(QPoint tile) // Placement // --------------------------------------------------------------------------- -TEST_CASE("BeltSystem: tryPutItem succeeds on registered belt with matching direction", "[belt]") +TEST_CASE("BeltSystem: tryPutItem succeeds on registered belt", "[belt]") { BeltSystem bs(kFastBeltSpeed); const QPoint tile(0, 0); bs.placeBelt(tile, Rotation::East); - REQUIRE(bs.tryPutItem(eastPort(tile), makeItem("iron_ore"))); + REQUIRE(bs.tryPutItem(tile, makeItem("iron_ore"))); } TEST_CASE("BeltSystem: tryPutItem fails on unregistered tile", "[belt]") { BeltSystem bs(kFastBeltSpeed); - REQUIRE_FALSE(bs.tryPutItem(eastPort(QPoint(0, 0)), makeItem("iron_ore"))); -} - -TEST_CASE("BeltSystem: tryPutItem fails on direction mismatch", "[belt]") -{ - BeltSystem bs(kFastBeltSpeed); - const QPoint tile(0, 0); - bs.placeBelt(tile, Rotation::North); - - REQUIRE_FALSE(bs.tryPutItem(eastPort(tile), makeItem("iron_ore"))); + REQUIRE_FALSE(bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"))); } TEST_CASE("BeltSystem: tryPutItem fails after removeTile", "[belt]") @@ -68,7 +59,7 @@ TEST_CASE("BeltSystem: tryPutItem fails after removeTile", "[belt]") bs.placeBelt(tile, Rotation::East); bs.removeTile(tile); - REQUIRE_FALSE(bs.tryPutItem(eastPort(tile), makeItem("iron_ore"))); + REQUIRE_FALSE(bs.tryPutItem(tile, makeItem("iron_ore"))); } // --------------------------------------------------------------------------- @@ -81,8 +72,8 @@ TEST_CASE("BeltSystem: two items fit in one tile", "[belt]") const QPoint tile(0, 0); bs.placeBelt(tile, Rotation::East); - REQUIRE(bs.tryPutItem(eastPort(tile), makeItem("iron_ore"))); - REQUIRE(bs.tryPutItem(eastPort(tile), makeItem("copper_ore"))); + REQUIRE(bs.tryPutItem(tile, makeItem("iron_ore"))); + REQUIRE(bs.tryPutItem(tile, makeItem("copper_ore"))); } TEST_CASE("BeltSystem: third tryPutItem on full tile returns false", "[belt]") @@ -91,10 +82,10 @@ TEST_CASE("BeltSystem: third tryPutItem on full tile returns false", "[belt]") const QPoint tile(0, 0); bs.placeBelt(tile, Rotation::East); - bs.tryPutItem(eastPort(tile), makeItem("a")); - bs.tryPutItem(eastPort(tile), makeItem("b")); + bs.tryPutItem(tile, makeItem("a")); + bs.tryPutItem(tile, makeItem("b")); - REQUIRE_FALSE(bs.tryPutItem(eastPort(tile), makeItem("c"))); + REQUIRE_FALSE(bs.tryPutItem(tile, makeItem("c"))); } // --------------------------------------------------------------------------- @@ -106,7 +97,7 @@ TEST_CASE("BeltSystem: tryTakeItem returns placed item after reaching output edg BeltSystem bs(kFastBeltSpeed); const QPoint tile(0, 0); bs.placeBelt(tile, Rotation::East); - bs.tryPutItem(eastPort(tile), makeItem("iron_ore")); + bs.tryPutItem(tile, makeItem("iron_ore")); bs.tick(); // advance to output edge const std::optional taken = bs.tryTakeItem(eastPort(tile)); @@ -120,7 +111,7 @@ TEST_CASE("BeltSystem: tryTakeItem requires item to reach output edge before yie BeltSystem bs(kFastBeltSpeed); const QPoint tile(0, 0); bs.placeBelt(tile, Rotation::East); - bs.tryPutItem(eastPort(tile), makeItem("iron_ore")); + bs.tryPutItem(tile, makeItem("iron_ore")); // Item placed but not yet at output edge — must not be available. REQUIRE_FALSE(bs.tryTakeItem(eastPort(tile)).has_value()); @@ -136,8 +127,8 @@ TEST_CASE("BeltSystem: tryTakeItem with two items returns both after each reache BeltSystem bs(kFastBeltSpeed); const QPoint tile(0, 0); bs.placeBelt(tile, Rotation::East); - bs.tryPutItem(eastPort(tile), makeItem("first")); - bs.tryPutItem(eastPort(tile), makeItem("second")); + bs.tryPutItem(tile, makeItem("first")); + bs.tryPutItem(tile, makeItem("second")); // Front item reaches output edge after one tick. bs.tick(); @@ -160,16 +151,6 @@ TEST_CASE("BeltSystem: tryTakeItem returns nullopt on empty tile", "[belt]") REQUIRE_FALSE(bs.tryTakeItem(eastPort(QPoint(0, 0))).has_value()); } -TEST_CASE("BeltSystem: tryTakeItem returns nullopt on direction mismatch", "[belt]") -{ - BeltSystem bs(kFastBeltSpeed); - const QPoint tile(0, 0); - bs.placeBelt(tile, Rotation::North); - bs.tryPutItem(Port{tile, Rotation::North}, makeItem("x")); - - REQUIRE_FALSE(bs.tryTakeItem(eastPort(tile)).has_value()); -} - // --------------------------------------------------------------------------- // tick() — item advancement // --------------------------------------------------------------------------- @@ -182,7 +163,7 @@ TEST_CASE("BeltSystem: item transfers from tile A to tile B and becomes availabl bs.placeBelt(tileA, Rotation::East); bs.placeBelt(tileB, Rotation::East); - bs.tryPutItem(eastPort(tileA), makeItem("iron_ore")); + bs.tryPutItem(tileA, makeItem("iron_ore")); bs.tick(); // item reaches output edge of A, moves to B at progress 0 bs.tick(); // item reaches output edge of B @@ -198,7 +179,7 @@ TEST_CASE("BeltSystem: item stays at progress 1.0 when next tile is absent", "[b const QPoint tileA(0, 0); bs.placeBelt(tileA, Rotation::East); - bs.tryPutItem(eastPort(tileA), makeItem("iron_ore")); + bs.tryPutItem(tileA, makeItem("iron_ore")); bs.tick(); // Item should still be on tileA (no registered tile to the east). @@ -215,7 +196,7 @@ TEST_CASE("BeltSystem: item traverses 3-tile chain in 3 ticks (one per tile)", " bs.placeBelt(tileB, Rotation::East); bs.placeBelt(tileC, Rotation::East); - bs.tryPutItem(eastPort(tileA), makeItem("iron_ore")); + bs.tryPutItem(tileA, makeItem("iron_ore")); bs.tick(); // A output edge → moves to B at progress 0 bs.tick(); // B output edge → moves to C at progress 0 bs.tick(); // C output edge → available for pickup @@ -234,11 +215,11 @@ TEST_CASE("BeltSystem: item stays blocked when next tile is full", "[belt]") bs.placeBelt(tileB, Rotation::East); // Fill tileB to capacity. - bs.tryPutItem(eastPort(tileB), makeItem("b1")); - bs.tryPutItem(eastPort(tileB), makeItem("b2")); + bs.tryPutItem(tileB, makeItem("b1")); + bs.tryPutItem(tileB, makeItem("b2")); // Place item in tileA — should be blocked. - bs.tryPutItem(eastPort(tileA), makeItem("a1")); + bs.tryPutItem(tileA, makeItem("a1")); bs.tick(); // Item in tileA must still be there. @@ -254,8 +235,8 @@ TEST_CASE("BeltSystem: clearTiles removes all items from specified tiles", "[bel BeltSystem bs(kFastBeltSpeed); const QPoint tile(0, 0); bs.placeBelt(tile, Rotation::East); - bs.tryPutItem(eastPort(tile), makeItem("iron_ore")); - bs.tryPutItem(eastPort(tile), makeItem("copper_ore")); + bs.tryPutItem(tile, makeItem("iron_ore")); + bs.tryPutItem(tile, makeItem("copper_ore")); bs.clearTiles({tile}); @@ -271,7 +252,7 @@ TEST_CASE("BeltSystem: forEachVisualItem visits items inside viewport", "[belt]" BeltSystem bs(kFastBeltSpeed); const QPoint tile(5, 5); bs.placeBelt(tile, Rotation::East); - bs.tryPutItem(eastPort(tile), makeItem("iron_ore")); + bs.tryPutItem(tile, makeItem("iron_ore")); int count = 0; bs.forEachVisualItem(QRect(0, 0, 20, 20), [&count](VisualItem) { ++count; }); @@ -284,7 +265,7 @@ TEST_CASE("BeltSystem: forEachVisualItem skips items outside viewport", "[belt]" BeltSystem bs(kFastBeltSpeed); const QPoint tile(50, 50); bs.placeBelt(tile, Rotation::East); - bs.tryPutItem(eastPort(tile), makeItem("iron_ore")); + bs.tryPutItem(tile, makeItem("iron_ore")); int count = 0; bs.forEachVisualItem(QRect(0, 0, 20, 20), [&count](VisualItem) { ++count; }); @@ -297,7 +278,7 @@ TEST_CASE("BeltSystem: forEachVisualItem reports correct ItemType", "[belt]") BeltSystem bs(kFastBeltSpeed); const QPoint tile(0, 0); bs.placeBelt(tile, Rotation::East); - bs.tryPutItem(eastPort(tile), makeItem("copper_ingot")); + bs.tryPutItem(tile, makeItem("copper_ingot")); std::vector seen; bs.forEachVisualItem(QRect(-1, -1, 10, 10), [&seen](VisualItem vi) @@ -329,10 +310,10 @@ TEST_CASE("BeltSystem: splitter alternates between outputA and outputB", "[belt] bs.placeBelt(tileA, Rotation::North); bs.placeBelt(tileB, Rotation::South); - bs.tryPutItem(eastPort(tileIn), makeItem("item1")); + bs.tryPutItem(tileIn, makeItem("item1")); bs.tick(); // item moves: tileIn -> splitter held - bs.tryPutItem(eastPort(tileIn), makeItem("item2")); + bs.tryPutItem(tileIn, makeItem("item2")); bs.tick(); // item1 routes to outputA (North=tileA); item2 moves to splitter bs.tick(); // item2 routes to outputB (South=tileB) @@ -366,7 +347,7 @@ TEST_CASE("BeltSystem: splitter routes filtered item to matching output", "[belt // Filter: outputA = iron_ore only; outputB = accept all. bs.setSplitterFilters(tileSpl, {ItemType{"iron_ore"}}, {}); - bs.tryPutItem(eastPort(tileIn), makeItem("iron_ore")); + bs.tryPutItem(tileIn, makeItem("iron_ore")); bs.tick(); // tileIn -> splitter held bs.tick(); // routed to outputA (filter match) diff --git a/src/test/BuildingTest.cpp b/src/test/BuildingTest.cpp index b0b4b39..c554601 100644 --- a/src/test/BuildingTest.cpp +++ b/src/test/BuildingTest.cpp @@ -107,7 +107,7 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem", "[build bs.place(BuildingType::Belt, QPoint(5, 5), Rotation::East, 0); - REQUIRE(belts.tryPutItem(eastPort(QPoint(5, 5)), makeItem("iron_ore"))); + REQUIRE(belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"))); REQUIRE(bs.allBuildings().empty()); // belts do not create Building instances } @@ -145,6 +145,12 @@ TEST_CASE("BuildingSystem: demolish frees tiles and returns refund", "[building] rng); const EntityId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0); + + // 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, belts, static_cast(secondsToTicks(10.0)) + 1, tick); + const int refund = bs.demolish(id); // Miner cost = 15, refund = floor(15 * 75 / 100) = 11. @@ -341,7 +347,7 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing // Place west-flowing belt at (2,0): belt flows West, delivers to smelter. belts.placeBelt(QPoint(2, 0), Rotation::West); - belts.tryPutItem(westPort(QPoint(2, 0)), makeItem("iron_ore")); + belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore")); bs.tickBeltPull(); @@ -493,7 +499,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta belts.placeBelt(QPoint(-1, 0), Rotation::East); for (int i = 0; i < 5; ++i) { - belts.tryPutItem(eastPort(QPoint(-1, 0)), makeItem("scrap")); + belts.tryPutItem(QPoint(-1, 0), makeItem("scrap")); bs.tickBeltPull(); }