move the placement rules and the config-dependent queries off BuildingSystem

This commit is contained in:
2026-08-05 06:49:49 +02:00
parent 537597c854
commit d87d063b10
14 changed files with 340 additions and 274 deletions

View File

@@ -1,4 +1,5 @@
#include "catch.hpp"
#include "PlacementRules.h"
#include "FactoryQueries.h"
#include "ProductionRules.h"
@@ -201,19 +202,18 @@ TEST_CASE("BuildingSystem: isPlacementValid enforces terrain and world bounds",
const int leftEdgeX = -f.cfg.world.regions.asteroidWidth_tiles;
// Miner is all-asteroid (A): valid only fully on the asteroid (x < 0).
REQUIRE(f.bs.isPlacementValid(BuildingType::Miner, QPoint(-3, 0), Rotation::East));
REQUIRE_FALSE(f.bs.isPlacementValid(BuildingType::Miner, QPoint(0, 0), Rotation::East)); // A cells in space
REQUIRE_FALSE(f.bs.isPlacementValid(BuildingType::Miner, QPoint(0, -1), Rotation::East)); // above world
REQUIRE(f.bs.isPlacementValid(BuildingType::Miner, QPoint(leftEdgeX, 0), Rotation::East));
REQUIRE_FALSE(f.bs.isPlacementValid(BuildingType::Miner,
QPoint(leftEdgeX - 1, 0), Rotation::East)); // past left edge
REQUIRE(isPlacementValid(f.state, f.cfg, BuildingType::Miner, QPoint(-3, 0), Rotation::East));
REQUIRE_FALSE(isPlacementValid(f.state, f.cfg, BuildingType::Miner, QPoint(0, 0), Rotation::East)); // A cells in space
REQUIRE_FALSE(isPlacementValid(f.state, f.cfg, BuildingType::Miner, QPoint(0, -1), Rotation::East)); // above world
REQUIRE(isPlacementValid(f.state, f.cfg, BuildingType::Miner, QPoint(leftEdgeX, 0), Rotation::East));
REQUIRE_FALSE(isPlacementValid(f.state, f.cfg, BuildingType::Miner, QPoint(leftEdgeX - 1, 0), Rotation::East)); // past left edge
// Shipyard mask ["AAAS>","AAAS "] straddles the boundary: A cells on the
// asteroid, the S (dock) cell in space. At anchor (-3,0) the A cells land at
// x=-3..-1 and the dock at x=0.
REQUIRE(f.bs.isPlacementValid(BuildingType::Shipyard, QPoint(-3, 0), Rotation::East));
REQUIRE_FALSE(f.bs.isPlacementValid(BuildingType::Shipyard, QPoint(0, 0), Rotation::East)); // A cells in space
REQUIRE_FALSE(f.bs.isPlacementValid(BuildingType::Shipyard, QPoint(-4, 0), Rotation::East)); // dock on asteroid
REQUIRE(isPlacementValid(f.state, f.cfg, BuildingType::Shipyard, QPoint(-3, 0), Rotation::East));
REQUIRE_FALSE(isPlacementValid(f.state, f.cfg, BuildingType::Shipyard, QPoint(0, 0), Rotation::East)); // A cells in space
REQUIRE_FALSE(isPlacementValid(f.state, f.cfg, BuildingType::Shipyard, QPoint(-4, 0), Rotation::East)); // dock on asteroid
}
TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after construction",
@@ -1192,7 +1192,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when tile is
rng);
REQUIRE_FALSE(
bs.findRotateInPlaceTarget(BuildingType::Belt, QPoint(0, 0), Rotation::East).has_value());
findRotateInPlaceTarget(state_bs, 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)",
@@ -1214,7 +1214,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the site id for a que
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);
findRotateInPlaceTarget(state_bs, cfg, BuildingType::Belt, QPoint(0, 0), Rotation::North);
REQUIRE(result.has_value());
REQUIRE(*result == id);
}
@@ -1242,7 +1242,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a
REQUIRE(getAllSites(state_bs).empty());
const std::optional<BuildingId> result =
bs.findRotateInPlaceTarget(BuildingType::Belt, QPoint(0, 0), Rotation::South);
findRotateInPlaceTarget(state_bs, cfg, BuildingType::Belt, QPoint(0, 0), Rotation::South);
REQUIRE(result.has_value());
REQUIRE(*result == id);
}
@@ -1267,7 +1267,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when building
// Querying with Splitter at the same tile — type mismatch → nullopt.
REQUIRE_FALSE(
bs.findRotateInPlaceTarget(BuildingType::Splitter, QPoint(0, 0), Rotation::East).has_value());
findRotateInPlaceTarget(state_bs, cfg, BuildingType::Splitter, QPoint(0, 0), Rotation::East).has_value());
}
TEST_CASE("BuildingSystem: findRotateInPlaceTarget never rotates a tunnel in place",
@@ -1292,9 +1292,9 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget never rotates a tunnel in pla
bs.place(BuildingType::TunnelExit, QPoint(-2, 0), Rotation::East, 0);
REQUIRE_FALSE(
bs.findRotateInPlaceTarget(BuildingType::TunnelEntry, QPoint(-1, 0), Rotation::North).has_value());
findRotateInPlaceTarget(state_bs, cfg, BuildingType::TunnelEntry, QPoint(-1, 0), Rotation::North).has_value());
REQUIRE_FALSE(
bs.findRotateInPlaceTarget(BuildingType::TunnelExit, QPoint(-2, 0), Rotation::North).has_value());
findRotateInPlaceTarget(state_bs, cfg, BuildingType::TunnelExit, QPoint(-2, 0), Rotation::North).has_value());
}
TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprints only partially overlap",
@@ -1319,7 +1319,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns nullopt when footprin
// 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(
bs.findRotateInPlaceTarget(BuildingType::Smelter, QPoint(1, 0), Rotation::East).has_value());
findRotateInPlaceTarget(state_bs, cfg, BuildingType::Smelter, QPoint(1, 0), Rotation::East).has_value());
}
TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-tile building with rotated ghost",
@@ -1343,7 +1343,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget works for a symmetric multi-t
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);
findRotateInPlaceTarget(state_bs, cfg, BuildingType::Smelter, QPoint(0, 0), Rotation::North);
REQUIRE(result.has_value());
REQUIRE(*result == id);
}
@@ -1507,7 +1507,7 @@ TEST_CASE("BuildingSystem: splitter filters configured on a construction site ca
// The site reports its two output directions and the stored filters before
// it is built; it is not yet registered with BeltSystem.
const std::optional<BeltSystem::SplitterInfo> siteInfo = f.bs.getSiteSplitterInfo(id);
const std::optional<BeltSystem::SplitterInfo> siteInfo = getSiteSplitterInfo(f.state, f.cfg, id);
REQUIRE(siteInfo.has_value());
REQUIRE(siteInfo->filterA == filterA);
REQUIRE(siteInfo->filterB.empty());
@@ -1687,7 +1687,7 @@ TEST_CASE("BuildingSystem: getInputPorts on a miner site lists every input edge"
// Miner mask ["AA","A>"] East → body (0,0),(1,0),(0,1); output tile (1,1) East.
const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
const std::vector<Port> ports = f.bs.getInputPorts(id);
const std::vector<Port> ports = getInputPorts(f.state, f.cfg, id);
// Every perimeter edge except the output-port edge at (1,1), each pointing in.
REQUIRE(ports.size() == 6);
@@ -1710,10 +1710,10 @@ TEST_CASE("BuildingSystem: getInputPorts matches between a site and the built bu
Tick tick = 0;
const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
const std::vector<Port> sitePorts = f.bs.getInputPorts(id);
const std::vector<Port> sitePorts = getInputPorts(f.state, f.cfg, id);
buildToCompletion(f.bs, f.state, f.belts, id, tick);
REQUIRE(findBuilding(f.state, id) != nullptr);
const std::vector<Port> builtPorts = f.bs.getInputPorts(id);
const std::vector<Port> builtPorts = getInputPorts(f.state, f.cfg, id);
// The operational path (stored inputPorts) agrees with the site path (mask-derived).
REQUIRE(builtPorts.size() == sitePorts.size());
@@ -1733,7 +1733,7 @@ TEST_CASE("BuildingSystem: getInputPorts invariants hold for a rotated site", "[
std::set<std::pair<int, int>> bodySet;
for (const QPoint& cell : site->bodyCells) { bodySet.insert({cell.x(), cell.y()}); }
const std::vector<Port> ports = f.bs.getInputPorts(id);
const std::vector<Port> ports = getInputPorts(f.state, f.cfg, id);
REQUIRE_FALSE(ports.empty());
for (const Port& port : ports)
{