Snap belt-drag end tile to a building's input edge

This commit is contained in:
2026-07-21 21:09:07 +02:00
parent 0c4eb480be
commit 4ca5b332cd
6 changed files with 210 additions and 5 deletions

View File

@@ -2,7 +2,9 @@
#include <map>
#include <random>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include <QPoint>
@@ -1421,3 +1423,106 @@ TEST_CASE("BuildingSystem: getProductionStatus classifies production state", "[b
REQUIRE(statusOf(bay) == ProductionStatus::Producing); // holding scrap -> green
}
}
// ---------------------------------------------------------------------------
// getInputPorts (REQ-BLD-BELT-DRAG snapping, REQ-MAT-INPUT-PORTS)
// ---------------------------------------------------------------------------
namespace
{
QPoint directionDelta(Rotation direction)
{
switch (direction)
{
case Rotation::North: return QPoint(0, -1);
case Rotation::East: return QPoint(1, 0);
case Rotation::South: return QPoint(0, 1);
case Rotation::West: return QPoint(-1, 0);
}
return QPoint(0, 0);
}
bool hasInputPort(const std::vector<Port>& ports, QPoint tile, Rotation direction)
{
for (const Port& port : ports)
{
if (port.tile == tile && port.direction == direction) { return true; }
}
return false;
}
// Advances the sim until the given site becomes an operational building, or a
// safety cap is reached.
void buildToCompletion(BuildingSystem& bs, BeltSystem& belts, BuildingId id,
Tick& tick)
{
for (int i = 0; i < 20000 && bs.findBuilding(id) == nullptr; ++i)
{
runTicks(bs, belts, 1, tick);
}
}
}
TEST_CASE("BuildingSystem: getInputPorts on a miner site lists every input edge", "[building]")
{
PlacementFixture f;
// 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);
// Every perimeter edge except the output-port edge at (1,1), each pointing in.
REQUIRE(ports.size() == 6);
REQUIRE(hasInputPort(ports, QPoint(-1, 0), Rotation::East));
REQUIRE(hasInputPort(ports, QPoint(0, -1), Rotation::South));
REQUIRE(hasInputPort(ports, QPoint(2, 0), Rotation::West));
REQUIRE(hasInputPort(ports, QPoint(1, -1), Rotation::South));
REQUIRE(hasInputPort(ports, QPoint(-1, 1), Rotation::East));
REQUIRE(hasInputPort(ports, QPoint(0, 2), Rotation::North));
// The output-port tile is never an input port.
REQUIRE_FALSE(hasInputPort(ports, QPoint(1, 1), Rotation::North));
REQUIRE_FALSE(hasInputPort(ports, QPoint(1, 1), Rotation::West));
}
TEST_CASE("BuildingSystem: getInputPorts matches between a site and the built building",
"[building]")
{
PlacementFixture f;
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);
buildToCompletion(f.bs, f.belts, id, tick);
REQUIRE(f.bs.findBuilding(id) != nullptr);
const std::vector<Port> builtPorts = f.bs.getInputPorts(id);
// The operational path (stored inputPorts) agrees with the site path (mask-derived).
REQUIRE(builtPorts.size() == sitePorts.size());
for (const Port& port : sitePorts)
{
REQUIRE(hasInputPort(builtPorts, port.tile, port.direction));
}
}
TEST_CASE("BuildingSystem: getInputPorts invariants hold for a rotated site", "[building]")
{
PlacementFixture f;
const BuildingId id = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::South, 0).value();
const ConstructionSite* site = f.bs.findSite(id);
REQUIRE(site != nullptr);
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);
REQUIRE_FALSE(ports.empty());
for (const Port& port : ports)
{
// Each port tile is outside the footprint...
REQUIRE(bodySet.count({port.tile.x(), port.tile.y()}) == 0);
// ...and its direction points into an adjacent body cell.
const QPoint into = port.tile + directionDelta(port.direction);
REQUIRE(bodySet.count({into.x(), into.y()}) == 1);
}
}