From 5f7211dbe4dea36d4ac9a928651017514f075e2e Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Sat, 25 Apr 2026 21:33:59 +0200 Subject: [PATCH] add more splitter tests --- src/test/BeltSystemTest.cpp | 165 +++++++++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 4 deletions(-) diff --git a/src/test/BeltSystemTest.cpp b/src/test/BeltSystemTest.cpp index d854372..cf16203 100644 --- a/src/test/BeltSystemTest.cpp +++ b/src/test/BeltSystemTest.cpp @@ -365,8 +365,10 @@ TEST_CASE("BeltSystem: splitter alternates between outputA and outputB", "[belt] // Splitter — filter routing // --------------------------------------------------------------------------- -TEST_CASE("BeltSystem: splitter routes filtered item to matching output", "[belt]") +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); @@ -379,13 +381,11 @@ TEST_CASE("BeltSystem: splitter routes filtered item to matching output", "[belt bs.placeBelt(tileA, Rotation::North); bs.placeBelt(tileB, Rotation::South); - // Filter: outputA = iron_ore only; outputB = accept all. - // iron_ore matches both filters → alternation; preferred = outputA (nextOutputIsA=true). bs.setSplitterFilters(tileSpl, {ItemType{"iron_ore"}}, {}); bs.tryPutItem(tileIn, makeItem("iron_ore")); bs.tick(); // tileIn -> splitter back - bs.tick(); // back -> frontA (filter + alternation → preferred outputA) + bs.tick(); // back -> frontA (both match, alternation, preferred A) bs.tick(); // frontA -> tileA bs.tick(); // item at tileA output edge @@ -393,6 +393,163 @@ TEST_CASE("BeltSystem: splitter routes filtered item to matching output", "[belt 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) // ---------------------------------------------------------------------------