move the placement rules and the config-dependent queries off BuildingSystem
isPlacementValid, findRotateInPlaceTarget and the bodyCellsWithinWorldBounds helper become PlacementRules.h — where a building may go and what already sits on those tiles, answered from the factory state and the config. getInputPorts and getSiteSplitterInfo join FactoryQueries.h, whose header comment now says plainly that the last two also take the config because answering them means reading a building definition. computeInputPorts goes to PortGeometry.h alongside outputBodyTile/inputBodyTile: it needs only Port and QPoint, so it belongs in core rather than in sim. BuildingSystem is left with no query that reads the factory — its remaining const methods are the emerging/incoming item walks, the checksum fold, and the buffer initialisers. It changes the factory now; it no longer describes it. Verified with a golden-checksum capture before and after — all four sample ticks identical. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user