Add deconstruction queue
This commit is contained in:
@@ -58,6 +58,7 @@ static void runTicks(BuildingSystem& bs, BeltSystem& belts, int n, Tick& tick)
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
bs.tickConstruction(tick);
|
||||
bs.tickDeconstruction(tick);
|
||||
bs.tickBeltPull();
|
||||
bs.tickProduction(tick);
|
||||
bs.tickOutputBelts();
|
||||
@@ -265,33 +266,20 @@ TEST_CASE("BuildingSystem: placed building enters construction queue", "[buildin
|
||||
REQUIRE(bs.findSite(id) != nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: demolish frees tiles and returns refund", "[building]")
|
||||
TEST_CASE("BuildingSystem: demolishing a construction site removes it instantly with full refund",
|
||||
"[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
BeltSystem belts(cfg.world.beltSpeed_tps);
|
||||
int stock = 0;
|
||||
std::mt19937 rng(0);
|
||||
BuildingId nextBuildingId = 1;
|
||||
BuildingSystem bs(cfg, belts,
|
||||
[&nextBuildingId]() { return nextBuildingId++; },
|
||||
[&stock](int n) { stock += n; },
|
||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||
[](const std::string&) -> bool { return true; },
|
||||
rng);
|
||||
PlacementFixture f;
|
||||
const BuildingId id =
|
||||
f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
||||
|
||||
const BuildingId id = bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
||||
// Still queued for construction (not yet built): instant removal, full cost
|
||||
// refunded immediately, never entering the deconstruction queue (REQ-BLD-DEMOLISH).
|
||||
const int refund = f.bs.demolish(id, 0);
|
||||
|
||||
// Miner construction_time_seconds = 10. completesAt = secondsToTicks(10) = 300.
|
||||
// We need to process tick 300 itself, so run 301 ticks (ticks 0..300).
|
||||
Tick tick = 0;
|
||||
runTicks(bs, belts, static_cast<int>(secondsToTicks(10.0)) + 1, tick);
|
||||
|
||||
const int refund = bs.demolish(id);
|
||||
|
||||
// Miner cost = 15, refund = floor(15 * 75 / 100) = 11.
|
||||
REQUIRE(refund == 15 * cfg.world.refundPercentage / 100);
|
||||
REQUIRE_FALSE(bs.isTileOccupied(QPoint(0, 0)));
|
||||
REQUIRE(bs.getAllSites().empty());
|
||||
REQUIRE(refund == 15); // Miner cost = 15
|
||||
REQUIRE_FALSE(f.bs.isTileOccupied(QPoint(0, 0)));
|
||||
REQUIRE(f.bs.getAllSites().empty());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -364,6 +352,147 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[
|
||||
REQUIRE(bs.findBuilding(id) != nullptr);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Deconstruction queue (REQ-BLD-DECON-QUEUE)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Runs ticks until the building with the given id is operational, or fails.
|
||||
static void runUntilBuilt(PlacementFixture& f, BuildingId id, Tick& tick)
|
||||
{
|
||||
for (int i = 0; i < 100000 && f.bs.findBuilding(id) == nullptr; ++i)
|
||||
{
|
||||
runTicks(f.bs, f.belts, 1, tick);
|
||||
}
|
||||
REQUIRE(f.bs.findBuilding(id) != nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: demolishing a built building queues it; refund credited on completion",
|
||||
"[building][decon]")
|
||||
{
|
||||
PlacementFixture f;
|
||||
const BuildingId id =
|
||||
f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
||||
|
||||
Tick tick = 0;
|
||||
runUntilBuilt(f, id, tick);
|
||||
|
||||
// Demolishing a built building returns nothing immediately and queues it,
|
||||
// stopping it operating while its tiles stay occupied (REQ-BLD-DECON-QUEUE).
|
||||
const int refund = f.bs.demolish(id, tick);
|
||||
REQUIRE(refund == 0);
|
||||
REQUIRE(f.bs.isQueuedForDeconstruction(id));
|
||||
REQUIRE(f.bs.isTileOccupied(QPoint(0, 0)));
|
||||
REQUIRE(f.stock == 0);
|
||||
|
||||
// After the deconstruction time (0.1s = 3 ticks) it is removed and the partial
|
||||
// refund (15 * 75 / 100 = 11) is credited exactly once.
|
||||
runTicks(f.bs, f.belts, static_cast<int>(secondsToTicks(0.1)) + 1, tick);
|
||||
REQUIRE(f.bs.findBuilding(id) == nullptr);
|
||||
REQUIRE_FALSE(f.bs.isTileOccupied(QPoint(0, 0)));
|
||||
REQUIRE(f.stock == 15 * f.cfg.world.refundPercentage / 100);
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: deconstruction queue removes one building at a time", "[building][decon]")
|
||||
{
|
||||
PlacementFixture f;
|
||||
const BuildingId a = f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
||||
const BuildingId b = f.bs.place(BuildingType::Miner, QPoint(5, 5), Rotation::East, 0).value();
|
||||
|
||||
Tick tick = 0;
|
||||
runUntilBuilt(f, a, tick);
|
||||
runUntilBuilt(f, b, tick);
|
||||
|
||||
// Queue both in one tick; 'a' is at the front of the deconstruction queue.
|
||||
f.bs.demolish(a, tick);
|
||||
f.bs.demolish(b, tick);
|
||||
REQUIRE(f.bs.isQueuedForDeconstruction(a));
|
||||
REQUIRE(f.bs.isQueuedForDeconstruction(b));
|
||||
|
||||
// After one deconstruction interval only the front building is gone; the
|
||||
// second is still queued and its refund not yet credited.
|
||||
runTicks(f.bs, f.belts, static_cast<int>(secondsToTicks(0.1)) + 1, tick);
|
||||
REQUIRE(f.bs.findBuilding(a) == nullptr);
|
||||
REQUIRE(f.bs.findBuilding(b) != nullptr);
|
||||
REQUIRE(f.bs.isQueuedForDeconstruction(b));
|
||||
REQUIRE(f.stock == 15 * f.cfg.world.refundPercentage / 100);
|
||||
|
||||
// The second drains next.
|
||||
runTicks(f.bs, f.belts, static_cast<int>(secondsToTicks(0.1)) + 2, tick);
|
||||
REQUIRE(f.bs.findBuilding(b) == nullptr);
|
||||
REQUIRE(f.stock == 2 * (15 * f.cfg.world.refundPercentage / 100));
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: cancelling deconstruction resumes the building with no refund",
|
||||
"[building][decon]")
|
||||
{
|
||||
PlacementFixture f;
|
||||
const BuildingId id =
|
||||
f.bs.place(BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value();
|
||||
|
||||
Tick tick = 0;
|
||||
runUntilBuilt(f, id, tick);
|
||||
|
||||
f.bs.demolish(id, tick);
|
||||
REQUIRE(f.bs.isQueuedForDeconstruction(id));
|
||||
|
||||
// Un-queue before it drains: it operates again, no refund, tiles still occupied.
|
||||
f.bs.cancelDeconstruction(id);
|
||||
REQUIRE_FALSE(f.bs.isQueuedForDeconstruction(id));
|
||||
REQUIRE(f.bs.findBuilding(id) != nullptr);
|
||||
REQUIRE(f.bs.isTileOccupied(QPoint(0, 0)));
|
||||
REQUIRE(f.stock == 0);
|
||||
|
||||
// It is never removed even after more than a deconstruction interval passes.
|
||||
runTicks(f.bs, f.belts, static_cast<int>(secondsToTicks(0.1)) + 5, tick);
|
||||
REQUIRE(f.bs.findBuilding(id) != nullptr);
|
||||
REQUIRE(f.stock == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: queued belt stops transporting; cancel restores it", "[building][decon]")
|
||||
{
|
||||
PlacementFixture f;
|
||||
const BuildingId id =
|
||||
f.bs.place(BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value();
|
||||
|
||||
Tick tick = 0;
|
||||
runUntilBuilt(f, id, tick);
|
||||
REQUIRE(f.belts.tryPutItem(QPoint(0, 0), makeItem("iron_ore"), Rotation::East));
|
||||
|
||||
// Queuing a belt unregisters its tile from the belt subsystem, so it no longer
|
||||
// accepts or transports items, though the tile stays occupied (REQ-BLD-DECON-QUEUE).
|
||||
f.bs.demolish(id, tick);
|
||||
REQUIRE_FALSE(f.belts.tryPutItem(QPoint(0, 0), makeItem("iron_ore"), Rotation::East));
|
||||
REQUIRE(f.bs.isTileOccupied(QPoint(0, 0)));
|
||||
|
||||
// Un-queuing re-registers the belt tile so it transports again.
|
||||
f.bs.cancelDeconstruction(id);
|
||||
REQUIRE(f.belts.tryPutItem(QPoint(0, 0), makeItem("iron_ore"), Rotation::East));
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: splitter filters survive a queue/un-queue round-trip", "[building][decon]")
|
||||
{
|
||||
PlacementFixture f;
|
||||
const BuildingId id =
|
||||
f.bs.place(BuildingType::Splitter, QPoint(0, 0), Rotation::East, 0).value();
|
||||
|
||||
Tick tick = 0;
|
||||
runUntilBuilt(f, id, tick);
|
||||
|
||||
f.belts.setSplitterFilters(QPoint(0, 0), {ItemType{"iron_ore"}}, {});
|
||||
|
||||
// Queue: the belt subsystem tile (and its filters) are unregistered, but the
|
||||
// filters are captured so an un-queue can restore them.
|
||||
f.bs.demolish(id, tick);
|
||||
REQUIRE_FALSE(f.belts.getSplitterInfo(QPoint(0, 0)).has_value());
|
||||
|
||||
f.bs.cancelDeconstruction(id);
|
||||
const std::optional<BeltSystem::SplitterInfo> info = f.belts.getSplitterInfo(QPoint(0, 0));
|
||||
REQUIRE(info.has_value());
|
||||
REQUIRE(info->filterA.size() == 1);
|
||||
REQUIRE(info->filterA[0].id == "iron_ore");
|
||||
REQUIRE(info->filterB.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("BuildingSystem: second building starts after first completes", "[building]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
|
||||
Reference in New Issue
Block a user