938 lines
31 KiB
C++
938 lines
31 KiB
C++
#include "catch.hpp"
|
|
|
|
#include <vector>
|
|
|
|
#include <QPoint>
|
|
#include <QRect>
|
|
|
|
#include "BeltSystem.h"
|
|
#include "Item.h"
|
|
#include "ItemType.h"
|
|
#include "Port.h"
|
|
#include "Rotation.h"
|
|
#include "Tick.h"
|
|
|
|
// Belt speed of 30 t/s means progress/tick = 30/30 = 1.0.
|
|
// One tick() call advances any item exactly one full tile.
|
|
// This makes test assertions on item positions simple and deterministic.
|
|
static constexpr double kFastBeltSpeed = static_cast<double>(kTickRateHz);
|
|
|
|
static Item makeItem(const std::string& id)
|
|
{
|
|
Item item;
|
|
item.type.id = id;
|
|
return item;
|
|
}
|
|
|
|
static Port eastPort(QPoint tile)
|
|
{
|
|
Port p;
|
|
p.tile = tile;
|
|
p.direction = Rotation::East;
|
|
return p;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Placement
|
|
// ---------------------------------------------------------------------------
|
|
|
|
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(tile, makeItem("iron_ore")));
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: tryPutItem fails on unregistered tile", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
REQUIRE_FALSE(bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore")));
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: tryPutItem fails after removeTile", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
const QPoint tile(0, 0);
|
|
bs.placeBelt(tile, Rotation::East);
|
|
bs.removeTile(tile);
|
|
|
|
REQUIRE_FALSE(bs.tryPutItem(tile, makeItem("iron_ore")));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Capacity
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BeltSystem: four items fit in one tile", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
const QPoint tile(0, 0);
|
|
bs.placeBelt(tile, Rotation::East);
|
|
|
|
REQUIRE(bs.tryPutItem(tile, makeItem("a")));
|
|
REQUIRE(bs.tryPutItem(tile, makeItem("b")));
|
|
REQUIRE(bs.tryPutItem(tile, makeItem("c")));
|
|
REQUIRE(bs.tryPutItem(tile, makeItem("d")));
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: fifth tryPutItem on full tile returns false", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
const QPoint tile(0, 0);
|
|
bs.placeBelt(tile, Rotation::East);
|
|
|
|
bs.tryPutItem(tile, makeItem("a"));
|
|
bs.tryPutItem(tile, makeItem("b"));
|
|
bs.tryPutItem(tile, makeItem("c"));
|
|
bs.tryPutItem(tile, makeItem("d"));
|
|
|
|
REQUIRE_FALSE(bs.tryPutItem(tile, makeItem("e")));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// tryTakeItem
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BeltSystem: tryTakeItem returns placed item after reaching output edge", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
const QPoint tile(0, 0);
|
|
bs.placeBelt(tile, Rotation::East);
|
|
bs.tryPutItem(tile, makeItem("iron_ore"));
|
|
bs.tick(); // advance to output edge
|
|
|
|
const std::optional<Item> taken = bs.tryTakeItem(eastPort(tile));
|
|
|
|
REQUIRE(taken.has_value());
|
|
REQUIRE(taken->type.id == "iron_ore");
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: tryTakeItem requires item to reach output edge before yielding", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
const QPoint tile(0, 0);
|
|
bs.placeBelt(tile, Rotation::East);
|
|
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());
|
|
REQUIRE_FALSE(bs.peekItem(eastPort(tile)).has_value());
|
|
|
|
// After one tick the item has reached progress 1.0 and is available.
|
|
bs.tick();
|
|
REQUIRE(bs.tryTakeItem(eastPort(tile)).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: tryTakeItem with two items returns both after each reaches output edge", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
const QPoint tile(0, 0);
|
|
bs.placeBelt(tile, Rotation::East);
|
|
bs.tryPutItem(tile, makeItem("first"));
|
|
bs.tryPutItem(tile, makeItem("second"));
|
|
|
|
// Front item reaches output edge after one tick.
|
|
bs.tick();
|
|
const std::optional<Item> taken1 = bs.tryTakeItem(eastPort(tile));
|
|
REQUIRE(taken1.has_value());
|
|
|
|
// Back item (now promoted to front) needs another tick to reach output edge.
|
|
bs.tick();
|
|
const std::optional<Item> taken2 = bs.tryTakeItem(eastPort(tile));
|
|
REQUIRE(taken2.has_value());
|
|
|
|
REQUIRE_FALSE(bs.tryTakeItem(eastPort(tile)).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: tryTakeItem returns nullopt on empty tile", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
bs.placeBelt(QPoint(0, 0), Rotation::East);
|
|
|
|
REQUIRE_FALSE(bs.tryTakeItem(eastPort(QPoint(0, 0))).has_value());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// tick() — item advancement
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BeltSystem: item transfers from tile A to tile B and becomes available after two ticks", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
const QPoint tileA(0, 0);
|
|
const QPoint tileB(1, 0);
|
|
bs.placeBelt(tileA, Rotation::East);
|
|
bs.placeBelt(tileB, Rotation::East);
|
|
|
|
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
|
|
|
|
REQUIRE_FALSE(bs.tryTakeItem(eastPort(tileA)).has_value());
|
|
const std::optional<Item> inB = bs.tryTakeItem(eastPort(tileB));
|
|
REQUIRE(inB.has_value());
|
|
REQUIRE(inB->type.id == "iron_ore");
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: item stays at progress 1.0 when next tile is absent", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
const QPoint tileA(0, 0);
|
|
bs.placeBelt(tileA, Rotation::East);
|
|
|
|
bs.tryPutItem(tileA, makeItem("iron_ore"));
|
|
bs.tick();
|
|
|
|
// Item should still be on tileA (no registered tile to the east).
|
|
REQUIRE(bs.tryTakeItem(eastPort(tileA)).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: item traverses 3-tile chain in 3 ticks (one per tile)", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
const QPoint tileA(0, 0);
|
|
const QPoint tileB(1, 0);
|
|
const QPoint tileC(2, 0);
|
|
bs.placeBelt(tileA, Rotation::East);
|
|
bs.placeBelt(tileB, Rotation::East);
|
|
bs.placeBelt(tileC, Rotation::East);
|
|
|
|
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
|
|
|
|
REQUIRE_FALSE(bs.tryTakeItem(eastPort(tileA)).has_value());
|
|
REQUIRE_FALSE(bs.tryTakeItem(eastPort(tileB)).has_value());
|
|
REQUIRE(bs.tryTakeItem(eastPort(tileC)).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: item stays blocked when next tile is full", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
const QPoint tileA(0, 0);
|
|
const QPoint tileB(1, 0);
|
|
bs.placeBelt(tileA, Rotation::East);
|
|
bs.placeBelt(tileB, Rotation::East);
|
|
|
|
// Fill tileB to capacity.
|
|
bs.tryPutItem(tileB, makeItem("b1"));
|
|
bs.tryPutItem(tileB, makeItem("b2"));
|
|
bs.tryPutItem(tileB, makeItem("b3"));
|
|
bs.tryPutItem(tileB, makeItem("b4"));
|
|
|
|
// Place item in tileA — should be blocked.
|
|
bs.tryPutItem(tileA, makeItem("a1"));
|
|
bs.tick();
|
|
|
|
// Item in tileA must still be there.
|
|
REQUIRE(bs.tryTakeItem(eastPort(tileA)).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: belt second slot is capped at progress 0.75", "[belt]")
|
|
{
|
|
// Use progress/tick = 0.4 so the cap is observable: without it, slot[1] would
|
|
// advance to 0.8 while slot[0] is stuck at 1.0. With the 0.75 cap it stays
|
|
// at 0.75 and needs exactly 1 more tick after promotion.
|
|
const double medBeltSpeed = 0.4 * static_cast<double>(kTickRateHz);
|
|
BeltSystem bs(medBeltSpeed);
|
|
|
|
const QPoint tile(0, 0);
|
|
bs.placeBelt(tile, Rotation::East);
|
|
|
|
// Advance front item to the output edge; it stays there (no next tile).
|
|
bs.tryPutItem(tile, makeItem("front_item"));
|
|
bs.tick(); // slot[0]: 0.4
|
|
bs.tick(); // slot[0]: 0.8
|
|
bs.tick(); // slot[0]: 1.0 (capped, stuck)
|
|
|
|
// Place second item; slot[0] is at 1.0.
|
|
bs.tryPutItem(tile, makeItem("back_item"));
|
|
bs.tick(); // slot[1]: 0.4
|
|
bs.tick(); // slot[1] would reach 0.8 — capped at 0.75
|
|
|
|
// Remove front; slot[1] (now promoted to slot[0]) must be at 0.75.
|
|
REQUIRE(bs.tryTakeItem(eastPort(tile)).has_value());
|
|
// At 0.4/tick, 0.75 → 1.0 (capped) after one tick — available.
|
|
bs.tick();
|
|
REQUIRE(bs.tryTakeItem(eastPort(tile)).has_value());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// clearTiles
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BeltSystem: clearTiles removes all items from specified tiles", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
const QPoint tile(0, 0);
|
|
bs.placeBelt(tile, Rotation::East);
|
|
bs.tryPutItem(tile, makeItem("iron_ore"));
|
|
bs.tryPutItem(tile, makeItem("copper_ore"));
|
|
|
|
bs.clearTiles({tile});
|
|
|
|
REQUIRE_FALSE(bs.tryTakeItem(eastPort(tile)).has_value());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// forEachVisualItem
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BeltSystem: forEachVisualItem visits items inside viewport", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
const QPoint tile(5, 5);
|
|
bs.placeBelt(tile, Rotation::East);
|
|
bs.tryPutItem(tile, makeItem("iron_ore"));
|
|
|
|
int count = 0;
|
|
bs.forEachVisualItem(QRect(0, 0, 20, 20), [&count](VisualItem) { ++count; });
|
|
|
|
REQUIRE(count == 1);
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: forEachVisualItem skips items outside viewport", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
const QPoint tile(50, 50);
|
|
bs.placeBelt(tile, Rotation::East);
|
|
bs.tryPutItem(tile, makeItem("iron_ore"));
|
|
|
|
int count = 0;
|
|
bs.forEachVisualItem(QRect(0, 0, 20, 20), [&count](VisualItem) { ++count; });
|
|
|
|
REQUIRE(count == 0);
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: forEachVisualItem reports correct ItemType", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
const QPoint tile(0, 0);
|
|
bs.placeBelt(tile, Rotation::East);
|
|
bs.tryPutItem(tile, makeItem("copper_ingot"));
|
|
|
|
std::vector<ItemType> seen;
|
|
bs.forEachVisualItem(QRect(-1, -1, 10, 10), [&seen](VisualItem vi)
|
|
{
|
|
seen.push_back(vi.type);
|
|
});
|
|
|
|
REQUIRE(seen.size() == 1);
|
|
REQUIRE(seen[0].id == "copper_ingot");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Splitter — basic alternation (no filters)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BeltSystem: splitter alternates between outputA and outputB", "[belt]")
|
|
{
|
|
// Layout: tileIn -> splitter -> tileA (North output)
|
|
// -> tileB (South output)
|
|
// Pipeline per item: tileIn(1) -> back(2) -> front(3) -> output belt(4)
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint tileIn(0, 0);
|
|
const QPoint tileSpl(1, 0);
|
|
const QPoint tileA(1, -1); // North of splitter
|
|
const QPoint tileB(1, 1); // South of splitter
|
|
|
|
bs.placeBelt(tileIn, Rotation::East);
|
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
|
bs.placeBelt(tileA, Rotation::North);
|
|
bs.placeBelt(tileB, Rotation::South);
|
|
|
|
bs.tryPutItem(tileIn, makeItem("item1"));
|
|
bs.tick(); // item1: tileIn -> splitter back (progress 0)
|
|
|
|
bs.tryPutItem(tileIn, makeItem("item2"));
|
|
bs.tick(); // item1 back -> 0.5 -> frontA; item2 advances but back is occupied
|
|
bs.tick(); // item1 frontA -> 1.0 -> tileA; item2 enters splitter back
|
|
bs.tick(); // item2 back -> 0.5 -> frontB; item1 at tileA output edge
|
|
bs.tick(); // item2 frontB -> 1.0 -> tileB
|
|
bs.tick(); // item2 at tileB output edge
|
|
|
|
const bool inA = bs.tryTakeItem(Port{tileA, Rotation::North}).has_value();
|
|
const bool inB = bs.tryTakeItem(Port{tileB, Rotation::South}).has_value();
|
|
|
|
// One item in each output — alternation worked.
|
|
REQUIRE(inA);
|
|
REQUIRE(inB);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Splitter — filter routing
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BeltSystem: splitter routes to preferred output when item matches both filters", "[belt]")
|
|
{
|
|
// filterA = iron_ore, filterB = {} (accept all) → iron_ore matches both.
|
|
// With nextOutputIsA=true initially, alternation sends the item to A.
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint tileIn(0, 0);
|
|
const QPoint tileSpl(1, 0);
|
|
const QPoint tileA(1, -1); // North output
|
|
const QPoint tileB(1, 1); // South output
|
|
|
|
bs.placeBelt(tileIn, Rotation::East);
|
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
|
bs.placeBelt(tileA, Rotation::North);
|
|
bs.placeBelt(tileB, Rotation::South);
|
|
|
|
bs.setSplitterFilters(tileSpl, {ItemType{"iron_ore"}}, {});
|
|
|
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
|
bs.tick(); // tileIn -> splitter back
|
|
bs.tick(); // back -> frontA (both match, alternation, preferred A)
|
|
bs.tick(); // frontA -> tileA
|
|
bs.tick(); // item at tileA output edge
|
|
|
|
REQUIRE(bs.tryTakeItem(Port{tileA, Rotation::North}).has_value());
|
|
REQUIRE_FALSE(bs.tryTakeItem(Port{tileB, Rotation::South}).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: splitter routes item to output A when only filter A matches", "[belt]")
|
|
{
|
|
// filterA = {iron_ore}, filterB = {copper_ore}: iron_ore matches A exclusively → goes to A.
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint tileIn(0, 0);
|
|
const QPoint tileSpl(1, 0);
|
|
|
|
bs.placeBelt(tileIn, Rotation::East);
|
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
|
|
|
bs.setSplitterFilters(tileSpl, {ItemType{"iron_ore"}}, {ItemType{"copper_ore"}});
|
|
|
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
|
bs.tick(); // tileIn -> splitter back
|
|
bs.tick(); // back -> frontA (exclusive match to A)
|
|
bs.tick(); // frontA reaches 1.0; no downstream belt, waits for building pickup
|
|
|
|
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value());
|
|
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: splitter routes item to output B when only filter B matches", "[belt]")
|
|
{
|
|
// filterA = {copper_ore}, filterB = {iron_ore}: iron_ore matches B exclusively → goes to B.
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint tileIn(0, 0);
|
|
const QPoint tileSpl(1, 0);
|
|
|
|
bs.placeBelt(tileIn, Rotation::East);
|
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
|
|
|
bs.setSplitterFilters(tileSpl, {ItemType{"copper_ore"}}, {ItemType{"iron_ore"}});
|
|
|
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
|
bs.tick();
|
|
bs.tick();
|
|
bs.tick();
|
|
|
|
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value());
|
|
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: splitter alternates A then B when item matches both explicit filters", "[belt]")
|
|
{
|
|
// filterA = {iron_ore}, filterB = {iron_ore}: both match → strict alternation A, B, A.
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint tileIn(0, 0);
|
|
const QPoint tileSpl(1, 0);
|
|
|
|
bs.placeBelt(tileIn, Rotation::East);
|
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
|
|
|
bs.setSplitterFilters(tileSpl, {ItemType{"iron_ore"}}, {ItemType{"iron_ore"}});
|
|
|
|
// Item 1 → preferred A (nextOutputIsA=true initially).
|
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
|
bs.tick(); bs.tick(); bs.tick();
|
|
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::North}).has_value());
|
|
|
|
// Item 2 → preferred B (nextOutputIsA toggled to false).
|
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
|
bs.tick(); bs.tick(); bs.tick();
|
|
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::South}).has_value());
|
|
|
|
// Item 3 → preferred A again (nextOutputIsA toggled back to true).
|
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
|
bs.tick(); bs.tick(); bs.tick();
|
|
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value());
|
|
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: splitter routes unmatched item to the unfiltered output", "[belt]")
|
|
{
|
|
// filterA = {copper_ore} (non-empty), filterB = {} (accept all).
|
|
// iron_ore: matchesA=false, matchesB=true → goes to B.
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint tileIn(0, 0);
|
|
const QPoint tileSpl(1, 0);
|
|
|
|
bs.placeBelt(tileIn, Rotation::East);
|
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
|
|
|
bs.setSplitterFilters(tileSpl, {ItemType{"copper_ore"}}, {});
|
|
|
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
|
bs.tick(); bs.tick(); bs.tick();
|
|
|
|
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value());
|
|
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: splitter stalls when item matches neither filter", "[belt]")
|
|
{
|
|
// filterA = {copper_ore}, filterB = {iron_ingot}: iron_ore matches neither → stall.
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint tileIn(0, 0);
|
|
const QPoint tileSpl(1, 0);
|
|
|
|
bs.placeBelt(tileIn, Rotation::East);
|
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
|
|
|
bs.setSplitterFilters(tileSpl, {ItemType{"copper_ore"}}, {ItemType{"iron_ingot"}});
|
|
|
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
|
bs.tick(); // tileIn -> splitter back
|
|
bs.tick(); // back reaches 0.5; routing fires but stalls (no filter match)
|
|
bs.tick(); // back stays at 0.5; stall persists
|
|
|
|
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value());
|
|
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: splitter falls back to other output when preferred is blocked", "[belt]")
|
|
{
|
|
// filterA = filterB = {iron_ore}: both match → alternation.
|
|
// When preferred output is occupied, item goes to the other without toggling nextOutputIsA.
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint tileIn(0, 0);
|
|
const QPoint tileSpl(1, 0);
|
|
|
|
bs.placeBelt(tileIn, Rotation::East);
|
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
|
|
|
bs.setSplitterFilters(tileSpl, {ItemType{"iron_ore"}}, {ItemType{"iron_ore"}});
|
|
|
|
// Item 1 → preferred A (nextOutputIsA=true → false after routing).
|
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
|
bs.tick(); bs.tick(); bs.tick(); // frontA = item1 at 1.0
|
|
|
|
// Item 2 → preferred B (nextOutputIsA=false → true after routing). Take item2 to free frontB.
|
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
|
bs.tick(); bs.tick(); bs.tick(); // frontB = item2 at 1.0
|
|
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::South}).has_value());
|
|
|
|
// frontA still holds item1; nextOutputIsA=true (prefer A).
|
|
// Item 3: both match, preferred A is occupied → fallback to B without toggling nextOutputIsA.
|
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
|
bs.tick(); bs.tick(); bs.tick(); // frontB = item3 at 1.0
|
|
|
|
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value()); // item1 still in A
|
|
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value()); // item3 in B via fallback
|
|
|
|
// nextOutputIsA was not toggled by the fallback: next item should still prefer A.
|
|
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::North}).has_value()); // free frontA
|
|
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::South}).has_value()); // free frontB
|
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
|
bs.tick(); bs.tick(); bs.tick();
|
|
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value()); // item4 → A (preferA still true)
|
|
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Splitter — direct building input (no output belts)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BeltSystem: splitter back slot is capped at 0.5 and waits before routing", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint tileIn(0, 0);
|
|
const QPoint tileSpl(1, 0);
|
|
|
|
bs.placeBelt(tileIn, Rotation::East);
|
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
|
|
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
|
bs.tick(); // item enters splitter back at progress 0; routing not yet triggered
|
|
|
|
// Back has not yet reached 0.5 — front slots empty, nothing available.
|
|
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value());
|
|
REQUIRE_FALSE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
|
|
|
|
bs.tick(); // back advances to 0.5, routes to frontA at progress 0
|
|
bs.tick(); // frontA advances to 1.0, available for building pickup
|
|
|
|
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: splitter delivers item directly to building input via tryTakeItem", "[belt]")
|
|
{
|
|
// Bug 1: splitter could not insert into a building input with no belt in between.
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint tileIn(0, 0);
|
|
const QPoint tileSpl(1, 0);
|
|
|
|
bs.placeBelt(tileIn, Rotation::East);
|
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
|
// No output belts — both outputs lead directly to building inputs.
|
|
|
|
bs.tryPutItem(tileIn, makeItem("iron_ore"));
|
|
bs.tick(); // tileIn -> splitter back
|
|
bs.tick(); // back -> frontA at progress 0
|
|
bs.tick(); // frontA reaches 1.0; no downstream belt, item waits for building pickup
|
|
|
|
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value());
|
|
const std::optional<Item> taken = bs.tryTakeItem(Port{tileSpl, Rotation::North});
|
|
REQUIRE(taken.has_value());
|
|
REQUIRE(taken->type.id == "iron_ore");
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: splitter accepts new items after building pulls from front slot", "[belt]")
|
|
{
|
|
// Bug 2: when outputs had no belts, splitter never cleared its held state
|
|
// so no new items could enter.
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint tileIn(0, 0);
|
|
const QPoint tileSpl(1, 0);
|
|
|
|
bs.placeBelt(tileIn, Rotation::East);
|
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
|
|
|
bs.tryPutItem(tileIn, makeItem("item1"));
|
|
bs.tick();
|
|
bs.tick();
|
|
bs.tick(); // item1 now in frontA at 1.0
|
|
|
|
// Building pulls item1 — clears frontA; nextOutputIsA toggled to false.
|
|
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::North}).has_value());
|
|
|
|
// Feed item2; preferred is now South.
|
|
bs.tryPutItem(tileIn, makeItem("item2"));
|
|
bs.tick();
|
|
bs.tick();
|
|
bs.tick(); // item2 now in frontB at 1.0
|
|
|
|
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: tryPutItem succeeds directly on a splitter tile", "[belt]")
|
|
{
|
|
// Regression: buildings outputting onto a splitter tile were silently dropped
|
|
// because tryPutItem had no splitter case and returned false.
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint tileSpl(0, 0);
|
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
|
|
|
REQUIRE(bs.tryPutItem(tileSpl, makeItem("iron_ore"), Rotation::East));
|
|
|
|
// Item should arrive at one of the output fronts after the splitter ticks through.
|
|
bs.tick(); // back advances to 0.5, routes to frontA
|
|
bs.tick(); // frontA reaches 1.0
|
|
|
|
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: splitter alternates between two unregistered outputs (building inputs)", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint tileIn(0, 0);
|
|
const QPoint tileSpl(1, 0);
|
|
|
|
bs.placeBelt(tileIn, Rotation::East);
|
|
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
|
|
|
|
// item1 → frontA (preferred, nextOutputIsA=true)
|
|
bs.tryPutItem(tileIn, makeItem("item1"));
|
|
bs.tick();
|
|
bs.tick();
|
|
bs.tick();
|
|
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::North}).has_value());
|
|
|
|
// item2 → frontB (preferred, nextOutputIsA now false)
|
|
bs.tryPutItem(tileIn, makeItem("item2"));
|
|
bs.tick();
|
|
bs.tick();
|
|
bs.tick();
|
|
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::South}).has_value());
|
|
|
|
// item3 → frontA again (nextOutputIsA toggled back to true)
|
|
bs.tryPutItem(tileIn, makeItem("item3"));
|
|
bs.tick();
|
|
bs.tick();
|
|
bs.tick();
|
|
REQUIRE(bs.tryTakeItem(Port{tileSpl, Rotation::North}).has_value());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tunnel — pairing
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BeltSystem: tunnel pairing — basic pair within max distance", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint entry(0, 0);
|
|
const QPoint exit(3, 0);
|
|
|
|
bs.placeTunnelEntry(entry, Rotation::East, 10);
|
|
bs.placeTunnelExit(exit, Rotation::East);
|
|
|
|
bs.tryPutItem(entry, makeItem("iron_ore"));
|
|
|
|
// With kFastBeltSpeed, items cross one tile per tick.
|
|
// entry tile: 1 tick to reach front progress 1.0
|
|
// transit: 3 tiles distance → 3 ticks
|
|
// exit tile: 1 tick to reach front progress 1.0
|
|
// Total: 1 (entry) + 1 (entry→transit) + 3 (transit) + 1 (transit→exit) + 1 (exit advance) = ~5-7 ticks
|
|
|
|
for (int i = 0; i < 20; ++i)
|
|
{
|
|
bs.tick();
|
|
}
|
|
|
|
REQUIRE(bs.peekItem(Port{exit, Rotation::East}).has_value());
|
|
const std::optional<Item> taken = bs.tryTakeItem(Port{exit, Rotation::East});
|
|
REQUIRE(taken.has_value());
|
|
REQUIRE(taken->type.id == "iron_ore");
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: tunnel pairing — wrong direction prevents pair", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
bs.placeTunnelEntry(QPoint(0, 0), Rotation::East, 10);
|
|
bs.placeTunnelExit(QPoint(3, 0), Rotation::North);
|
|
|
|
bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"));
|
|
|
|
for (int i = 0; i < 20; ++i)
|
|
{
|
|
bs.tick();
|
|
}
|
|
|
|
// Exit faces North, not East — no pair formed, item stuck in entry.
|
|
REQUIRE_FALSE(bs.peekItem(Port{QPoint(3, 0), Rotation::North}).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: tunnel pairing — beyond max distance prevents pair", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
bs.placeTunnelEntry(QPoint(0, 0), Rotation::East, 2);
|
|
bs.placeTunnelExit(QPoint(3, 0), Rotation::East);
|
|
|
|
bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"));
|
|
|
|
for (int i = 0; i < 20; ++i)
|
|
{
|
|
bs.tick();
|
|
}
|
|
|
|
REQUIRE_FALSE(bs.peekItem(Port{QPoint(3, 0), Rotation::East}).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: tunnel pairing — same-dir entry between blocks pairing", "[belt]")
|
|
{
|
|
// Entry1 at (0,0) East, Entry2 at (2,0) East, Exit at (4,0) East.
|
|
// Entry2 is closer to Exit → Entry2 pairs with Exit; Entry1 is blocked by Entry2.
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
bs.placeTunnelEntry(QPoint(0, 0), Rotation::East, 10);
|
|
bs.placeTunnelEntry(QPoint(2, 0), Rotation::East, 10);
|
|
bs.placeTunnelExit(QPoint(4, 0), Rotation::East);
|
|
|
|
// Put item on Entry2 — should reach exit.
|
|
bs.tryPutItem(QPoint(2, 0), makeItem("copper_ore"));
|
|
for (int i = 0; i < 20; ++i)
|
|
{
|
|
bs.tick();
|
|
}
|
|
REQUIRE(bs.peekItem(Port{QPoint(4, 0), Rotation::East}).has_value());
|
|
bs.tryTakeItem(Port{QPoint(4, 0), Rotation::East});
|
|
|
|
// Put item on Entry1 — should NOT reach exit (Entry1 is unpaired).
|
|
bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"));
|
|
for (int i = 0; i < 20; ++i)
|
|
{
|
|
bs.tick();
|
|
}
|
|
REQUIRE_FALSE(bs.peekItem(Port{QPoint(4, 0), Rotation::East}).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: tunnel pairing — cross-dir entry between is ignored", "[belt]")
|
|
{
|
|
// Entry1 at (0,0) East, Entry2 at (2,0) North (different dir), Exit at (4,0) East.
|
|
// Entry2 faces North → ignored → Entry1 pairs with Exit.
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
bs.placeTunnelEntry(QPoint(0, 0), Rotation::East, 10);
|
|
bs.placeTunnelEntry(QPoint(2, 0), Rotation::North, 10);
|
|
bs.placeTunnelExit(QPoint(4, 0), Rotation::East);
|
|
|
|
bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"));
|
|
for (int i = 0; i < 20; ++i)
|
|
{
|
|
bs.tick();
|
|
}
|
|
REQUIRE(bs.peekItem(Port{QPoint(4, 0), Rotation::East}).has_value());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tunnel — item transit
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST_CASE("BeltSystem: unpaired entry blocks items at front", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
bs.placeTunnelEntry(QPoint(0, 0), Rotation::East, 10);
|
|
// No exit placed — entry is unpaired.
|
|
|
|
bs.tryPutItem(QPoint(0, 0), makeItem("iron_ore"));
|
|
for (int i = 0; i < 10; ++i)
|
|
{
|
|
bs.tick();
|
|
}
|
|
|
|
// Item should not vanish — it stays in the entry.
|
|
// We can verify by placing an exit and seeing item eventually arrive.
|
|
bs.placeTunnelExit(QPoint(3, 0), Rotation::East);
|
|
for (int i = 0; i < 20; ++i)
|
|
{
|
|
bs.tick();
|
|
}
|
|
REQUIRE(bs.peekItem(Port{QPoint(3, 0), Rotation::East}).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: demolish entry discards transit items", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint entry(0, 0);
|
|
const QPoint exit(5, 0);
|
|
|
|
bs.placeTunnelEntry(entry, Rotation::East, 10);
|
|
bs.placeTunnelExit(exit, Rotation::East);
|
|
|
|
bs.tryPutItem(entry, makeItem("iron_ore"));
|
|
|
|
// Advance just enough for item to enter transit but not reach exit.
|
|
bs.tick(); // item enters entry front
|
|
bs.tick(); // entry front → transit (progress 0)
|
|
|
|
bs.removeTile(entry);
|
|
|
|
// Even with many more ticks, nothing arrives at exit.
|
|
for (int i = 0; i < 30; ++i)
|
|
{
|
|
bs.tick();
|
|
}
|
|
REQUIRE_FALSE(bs.peekItem(Port{exit, Rotation::East}).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: clearTiles discards tunnel transit items", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint entry(0, 0);
|
|
const QPoint exit(5, 0);
|
|
|
|
bs.placeTunnelEntry(entry, Rotation::East, 10);
|
|
bs.placeTunnelExit(exit, Rotation::East);
|
|
|
|
bs.tryPutItem(entry, makeItem("iron_ore"));
|
|
bs.tick();
|
|
bs.tick();
|
|
|
|
bs.clearTiles({entry});
|
|
|
|
for (int i = 0; i < 30; ++i)
|
|
{
|
|
bs.tick();
|
|
}
|
|
REQUIRE_FALSE(bs.peekItem(Port{exit, Rotation::East}).has_value());
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: belt to entry to transit to exit to belt full chain", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint beltIn(0, 0);
|
|
const QPoint entry(1, 0);
|
|
const QPoint exit(4, 0);
|
|
const QPoint beltOut(5, 0);
|
|
|
|
bs.placeBelt(beltIn, Rotation::East);
|
|
bs.placeTunnelEntry(entry, Rotation::East, 10);
|
|
bs.placeTunnelExit(exit, Rotation::East);
|
|
bs.placeBelt(beltOut, Rotation::East);
|
|
|
|
bs.tryPutItem(beltIn, makeItem("iron_ore"));
|
|
|
|
for (int i = 0; i < 30; ++i)
|
|
{
|
|
bs.tick();
|
|
}
|
|
|
|
// Item should have arrived on beltOut.
|
|
REQUIRE(bs.peekItem(eastPort(beltOut)).has_value());
|
|
const std::optional<Item> taken = bs.tryTakeItem(eastPort(beltOut));
|
|
REQUIRE(taken.has_value());
|
|
REQUIRE(taken->type.id == "iron_ore");
|
|
}
|
|
|
|
TEST_CASE("BeltSystem: multiple items transit tunnel in order", "[belt]")
|
|
{
|
|
BeltSystem bs(kFastBeltSpeed);
|
|
|
|
const QPoint entry(0, 0);
|
|
const QPoint exit(3, 0);
|
|
|
|
bs.placeTunnelEntry(entry, Rotation::East, 10);
|
|
bs.placeTunnelExit(exit, Rotation::East);
|
|
|
|
bs.tryPutItem(entry, makeItem("item1"));
|
|
bs.tick();
|
|
bs.tick(); // item1 enters transit
|
|
|
|
bs.tryPutItem(entry, makeItem("item2"));
|
|
|
|
for (int i = 0; i < 30; ++i)
|
|
{
|
|
bs.tick();
|
|
}
|
|
|
|
// item1 should arrive first.
|
|
const std::optional<Item> taken1 = bs.tryTakeItem(Port{exit, Rotation::East});
|
|
REQUIRE(taken1.has_value());
|
|
REQUIRE(taken1->type.id == "item1");
|
|
|
|
for (int i = 0; i < 10; ++i)
|
|
{
|
|
bs.tick();
|
|
}
|
|
|
|
const std::optional<Item> taken2 = bs.tryTakeItem(Port{exit, Rotation::East});
|
|
REQUIRE(taken2.has_value());
|
|
REQUIRE(taken2->type.id == "item2");
|
|
}
|