Use std::optional instead of sentinel values for absent data

This commit is contained in:
2026-07-20 20:27:20 +02:00
parent 1cdafb7bcd
commit 9622fa4345
32 changed files with 228 additions and 209 deletions

View File

@@ -119,7 +119,7 @@ TEST_CASE("BuildingSystem: place miner occupies expected body tiles", "[building
[](const std::string&) -> bool { return true; },
rng);
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
const BuildingId id = bs.place(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).
@@ -138,8 +138,8 @@ TEST_CASE("BuildingSystem: place rejects a building above the world (y < 0)", "[
// Miner mask ["AA","A>"] East → body at (0,0),(1,0),(0,1); at y=-1 the top
// row sits above the world.
const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, -1), Rotation::East, 0);
REQUIRE(id == kInvalidBuildingId);
const std::optional<BuildingId> id = f.bs.place(BuildingType::Miner, QPoint(0, -1), Rotation::East, 0);
REQUIRE_FALSE(id.has_value());
REQUIRE(f.bs.getAllSites().empty());
REQUIRE_FALSE(f.bs.isTileOccupied(QPoint(0, 0)));
}
@@ -151,9 +151,9 @@ TEST_CASE("BuildingSystem: place rejects a building below the world (y >= height
// Anchored on the last in-bounds row, the miner's lower body row reaches
// y == heightTiles, which is outside the world.
const BuildingId id = f.bs.place(BuildingType::Miner,
const std::optional<BuildingId> id = f.bs.place(BuildingType::Miner,
QPoint(0, heightTiles - 1), Rotation::East, 0);
REQUIRE(id == kInvalidBuildingId);
REQUIRE_FALSE(id.has_value());
REQUIRE(f.bs.getAllSites().empty());
}
@@ -162,9 +162,9 @@ TEST_CASE("BuildingSystem: place rejects a building left of the asteroid edge",
PlacementFixture f;
const int leftEdgeX = -f.cfg.world.regions.asteroidWidth_tiles;
const BuildingId id = f.bs.place(BuildingType::Miner,
const std::optional<BuildingId> id = f.bs.place(BuildingType::Miner,
QPoint(leftEdgeX - 1, 0), Rotation::East, 0);
REQUIRE(id == kInvalidBuildingId);
REQUIRE_FALSE(id.has_value());
REQUIRE(f.bs.getAllSites().empty());
}
@@ -176,7 +176,7 @@ TEST_CASE("BuildingSystem: place accepts a building flush against the world's le
// Miner body min relative x is 0, so its leftmost cell sits exactly on the edge.
const BuildingId id = f.bs.place(BuildingType::Miner,
QPoint(leftEdgeX, 0), Rotation::East, 0);
QPoint(leftEdgeX, 0), Rotation::East, 0).value();
REQUIRE(id != kInvalidBuildingId);
REQUIRE(f.bs.isTileOccupied(QPoint(leftEdgeX, 0)));
}
@@ -187,7 +187,7 @@ TEST_CASE("BuildingSystem: place imposes no right-side bound (space extends righ
PlacementFixture f;
const BuildingId id = f.bs.place(BuildingType::Miner,
QPoint(1000, 0), Rotation::East, 0);
QPoint(1000, 0), Rotation::East, 0).value();
REQUIRE(id != kInvalidBuildingId);
}
@@ -256,7 +256,7 @@ TEST_CASE("BuildingSystem: placed building enters construction queue", "[buildin
[](const std::string&) -> bool { return true; },
rng);
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
REQUIRE(bs.getAllSites().size() == 1);
REQUIRE(bs.getAllBuildings().empty());
@@ -277,7 +277,7 @@ TEST_CASE("BuildingSystem: demolish frees tiles and returns refund", "[building]
[](const std::string&) -> bool { return true; },
rng);
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
const BuildingId id = bs.place(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).
@@ -351,7 +351,7 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[
[](const std::string&) -> bool { return true; },
rng);
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
const BuildingId id = bs.place(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).
@@ -377,7 +377,7 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil
rng);
bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
const BuildingId id2 = bs.place(BuildingType::Miner, QPoint(5, 5), Rotation::East, 0);
const BuildingId id2 = bs.place(BuildingType::Miner, QPoint(5, 5), Rotation::East, 0).value();
// Process through tick 300 to complete first miner's construction.
Tick tick = 0;
@@ -406,7 +406,7 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui
[](const std::string&) -> bool { return true; },
rng);
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
bs.setRecipe(id, "mine_iron_ore");
Tick tick = 0;
@@ -439,7 +439,7 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]")
[](const std::string&) -> bool { return true; },
rng);
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
bs.setRecipe(id, "mine_iron_ore");
Tick tick = 0;
@@ -479,8 +479,8 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites",
[](const std::string&) -> bool { return true; },
rng);
const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
const BuildingId smelterId = bs.place(BuildingType::Smelter, QPoint(10, 0), Rotation::East, 0);
const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
const BuildingId smelterId = bs.place(BuildingType::Smelter, QPoint(10, 0), Rotation::East, 0).value();
(void)smelterId;
Tick tick = 0;
@@ -519,7 +519,7 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle
[](const std::string&) -> bool { return true; },
rng);
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
bs.setRecipe(id, "mine_iron_ore");
Tick tick = 0;
@@ -563,7 +563,7 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing
// 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(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0);
const BuildingId sid = bs.place(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.
@@ -603,7 +603,7 @@ TEST_CASE("BuildingSystem: accepted input travels inward before entering the buf
[](const std::string&) -> bool { return true; },
rng);
const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0);
const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
@@ -645,7 +645,7 @@ TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at th
rng);
const BuildingId id = bs.place(BuildingType::ReprocessingPlant,
QPoint(0, 0), Rotation::East, 0);
QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(25.0)) + 1, tick);
@@ -686,7 +686,7 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection",
[](const std::string&) -> bool { return true; },
rng);
const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0);
const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
@@ -732,7 +732,7 @@ TEST_CASE("BuildingSystem: smelter runs a satisfiable recipe while an incomplete
[](const std::string&) -> bool { return true; },
rng);
const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0);
const BuildingId sid = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(15.0)) + 1, tick);
@@ -786,7 +786,7 @@ TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[bui
[](const std::string&) -> bool { return true; },
rng);
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
bs.setRecipe(id, "mine_iron_ore");
// Belt at the miner's output port tile (1,1) flowing East.
@@ -826,12 +826,12 @@ TEST_CASE("BuildingSystem: output port couples directly into an adjacent input p
rng);
// Miner at (0,0): body (0,0),(1,0),(0,1); output port tile (1,1) flowing East.
const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
bs.setRecipe(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(BuildingType::Smelter, QPoint(1, 1), Rotation::East, 0);
const BuildingId smelterId = bs.place(BuildingType::Smelter, QPoint(1, 1), Rotation::East, 0).value();
Tick tick = 0;
// Smelter build (15s) + margin for coupling and a smelt cycle.
@@ -866,11 +866,11 @@ TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stu
rng);
// Producing miner at (0,0), output port (1,1) East.
const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
const BuildingId minerId = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
bs.setRecipe(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(BuildingType::Miner, QPoint(1, 1), Rotation::East, 0);
const BuildingId sinkId = bs.place(BuildingType::Miner, QPoint(1, 1), Rotation::East, 0).value();
Tick tick = 0;
// Both miners build sequentially (10s each), then the producer runs and jams.
@@ -904,7 +904,7 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production"
[](const std::string&) -> bool { return true; },
rng);
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0);
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
bs.setRecipe(id, "mine_iron_ore");
Tick tick = 0;
@@ -948,7 +948,7 @@ TEST_CASE("BuildingSystem: reprocessing plant output buffer capacity equals max
rng);
const BuildingId id = bs.place(BuildingType::ReprocessingPlant,
QPoint(0, 0), Rotation::East, 0);
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.
@@ -980,7 +980,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta
rng);
const BuildingId id = bs.place(BuildingType::ReprocessingPlant,
QPoint(0, 0), Rotation::East, 0);
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.
@@ -1056,7 +1056,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a que
[](const std::string&) -> bool { return true; },
rng);
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
const std::optional<BuildingId> result =
bs.findRotateInPlaceTarget(BuildingType::Belt, QPoint(0, 0), Rotation::North);
@@ -1079,7 +1079,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a
[](const std::string&) -> bool { return true; },
rng);
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
@@ -1154,7 +1154,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-t
// 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(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0);
const BuildingId id = bs.place(BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value();
const std::optional<BuildingId> result =
bs.findRotateInPlaceTarget(BuildingType::Smelter, QPoint(0, 0), Rotation::North);
@@ -1181,7 +1181,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates the rotation field of a constru
[](const std::string&) -> bool { return true; },
rng);
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
REQUIRE(bs.findSite(id)->rotation == Rotation::East);
bs.rotateInPlace(id, Rotation::North);
@@ -1204,7 +1204,7 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the construction progress of
[](const std::string&) -> bool { return true; },
rng);
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
const Tick completesAt = bs.findSite(id)->completesAt;
REQUIRE(completesAt > 0);
@@ -1228,7 +1228,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direct
[](const std::string&) -> bool { return true; },
rng);
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
@@ -1259,7 +1259,7 @@ TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSyste
[](const std::string&) -> bool { return true; },
rng);
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0);
const BuildingId id = bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
Tick tick = 0;
runTicks(bs, belts, static_cast<int>(secondsToTicks(1.0)) + 1, tick);
@@ -1276,7 +1276,7 @@ TEST_CASE("BuildingSystem: splitter filters configured on a construction site ca
PlacementFixture f;
const QPoint tile(5, 5);
const BuildingId id = f.bs.place(BuildingType::Splitter, tile, Rotation::East, 0);
const BuildingId id = f.bs.place(BuildingType::Splitter, tile, Rotation::East, 0).value();
REQUIRE(id != kInvalidBuildingId);
REQUIRE(f.bs.findSite(id) != nullptr);