From 780d5e505243678f6efc3284555d1b2e88d44679 Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Wed, 19 Aug 2026 17:23:46 +0200 Subject: [PATCH] put the block stock where the blocks are The global building block stock lived on Simulation while being factory data through and through: placement spends it, deconstruction refunds it, and blocks delivered to the HQ by belt add to it. Every system that credited it therefore held a std::function back into Simulation to do so -- BuildingSystem and DeconstructionSystem each carried one, and the arena and three test fixtures had to pass a stub. It moves into FactoryState, seeded by makeFactoryState from world.starting_building_blocks, and both callbacks are gone: the HQ's belt intake and the deconstruction refund now credit the state they are already holding. BuildingSystem::deconstruct stops returning a refund for its caller to remember to credit. It had grown asymmetric -- the queued path credits itself through DeconstructionSystem while the instant path handed a number back -- so it now credits the site's full cost directly and returns void. Checksum order is untouched: Simulation still folds the stock at exactly the point it always did, reading it from the state. The arena no longer discards refunds into a no-op sink; nothing there reads the stock either way. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x --- src/balancing/ArenaSimulation.cpp | 1 - src/lib/sim/BuildingSystem.cpp | 16 ++- src/lib/sim/BuildingSystem.h | 14 ++- src/lib/sim/DeconstructionSystem.cpp | 3 +- src/lib/sim/DeconstructionSystem.h | 12 +-- src/lib/sim/FactoryState.h | 21 +++- src/lib/sim/Simulation.cpp | 20 ++-- src/lib/sim/Simulation.h | 1 - src/test/BehaviorSystemTest.cpp | 3 - src/test/BuildingTest.cpp | 141 ++++++++++++++------------- src/test/CombatSystemTest.cpp | 1 - 11 files changed, 114 insertions(+), 119 deletions(-) diff --git a/src/balancing/ArenaSimulation.cpp b/src/balancing/ArenaSimulation.cpp index d31ffba..aefa85a 100644 --- a/src/balancing/ArenaSimulation.cpp +++ b/src/balancing/ArenaSimulation.cpp @@ -52,7 +52,6 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig, m_gameConfig, m_beltSystem, [this]() { return allocateBuildingId(); }, - [](int) {}, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, m_rng); diff --git a/src/lib/sim/BuildingSystem.cpp b/src/lib/sim/BuildingSystem.cpp index 571e719..67b2bdb 100644 --- a/src/lib/sim/BuildingSystem.cpp +++ b/src/lib/sim/BuildingSystem.cpp @@ -28,7 +28,6 @@ bool inputLaneEntryFree(const std::vector& lane) BuildingSystem::BuildingSystem(const GameConfig& config, BeltSystem& belts, std::function allocateBuildingId, - std::function addBuildingBlocks, std::function&)> spawnShip, std::function isItemUnlocked, @@ -36,7 +35,6 @@ BuildingSystem::BuildingSystem(const GameConfig& config, : m_config(config) , m_belts(belts) , m_allocateBuildingId(std::move(allocateBuildingId)) - , m_addBuildingBlocks(std::move(addBuildingBlocks)) , m_spawnShip(std::move(spawnShip)) , m_isItemUnlocked(std::move(isItemUnlocked)) , m_rng(rng) @@ -158,7 +156,7 @@ std::optional BuildingSystem::place(FactoryState& state, BuildingTyp // Deconstruct // --------------------------------------------------------------------------- -int BuildingSystem::deconstruct(FactoryState& state, BuildingId id, Tick currentTick) +void BuildingSystem::deconstruct(FactoryState& state, BuildingId id, Tick currentTick) { // Construction site? Removed instantly with the full refund; never queued // for deconstruction (REQ-BLD-DECONSTRUCT). @@ -173,9 +171,9 @@ int BuildingSystem::deconstruct(FactoryState& state, BuildingId id, Tick current state.constructionQueue.erase(it); if (def) { - return def->cost; + state.buildingBlocksStock += def->cost; } - return 0; + return; } } @@ -185,7 +183,7 @@ int BuildingSystem::deconstruct(FactoryState& state, BuildingId id, Tick current for (Building& building : state.buildings) { if (building.id != id) { continue; } - if (building.queuedForDeconstruction) { return 0; } // already queued + if (building.queuedForDeconstruction) { return; } // already queued building.queuedForDeconstruction = true; @@ -216,10 +214,8 @@ int BuildingSystem::deconstruct(FactoryState& state, BuildingId id, Tick current { startFrontDeconstruction(state, m_config, currentTick); } - return 0; + return; } - - return 0; } // --------------------------------------------------------------------------- @@ -411,7 +407,7 @@ void BuildingSystem::tickBeltPull(FactoryState& state) lane.erase(lane.begin()); if (isHq) { - m_addBuildingBlocks(1); + state.buildingBlocksStock += 1; } else { diff --git a/src/lib/sim/BuildingSystem.h b/src/lib/sim/BuildingSystem.h index 4e851ae..d387ea0 100644 --- a/src/lib/sim/BuildingSystem.h +++ b/src/lib/sim/BuildingSystem.h @@ -41,7 +41,6 @@ public: BuildingSystem(const GameConfig& config, BeltSystem& belts, std::function allocateBuildingId, - std::function addBuildingBlocks, std::function&)> spawnShip, std::function isItemUnlocked, @@ -64,12 +63,12 @@ public: { state.asteroidWidth_tiles = widthTiles; } // Mark a building or construction site for demolition (REQ-BLD-DECONSTRUCT). - // A construction site is removed instantly and the full cost is returned. - // A fully-built building is instead appended to the deconstruction queue - // (REQ-BLD-DECON-QUEUE) and stops operating at once; its (partial) refund is - // credited later, on completion in tickDeconstruction, so this returns 0 for - // it. Returns 0 for unknown ids and for a building already queued. - int deconstruct(FactoryState& state, BuildingId id, Tick currentTick); + // A construction site is removed instantly and its full cost credited back to the + // block stock. A fully-built building is instead appended to the deconstruction + // queue (REQ-BLD-DECON-QUEUE) and stops operating at once; its (partial) refund is + // credited later, on completion, by DeconstructionSystem. No-op for unknown ids and + // for a building already queued. + void deconstruct(FactoryState& state, BuildingId id, Tick currentTick); // Take a building back out of the deconstruction queue before it is removed // (REQ-BLD-DECON-QUEUE). Clears its queued flag and resumes operation @@ -180,7 +179,6 @@ private: const GameConfig& m_config; BeltSystem& m_belts; std::function m_allocateBuildingId; - std::function m_addBuildingBlocks; std::function&)> m_spawnShip; std::function m_isItemUnlocked; diff --git a/src/lib/sim/DeconstructionSystem.cpp b/src/lib/sim/DeconstructionSystem.cpp index 6906a5d..03b06ad 100644 --- a/src/lib/sim/DeconstructionSystem.cpp +++ b/src/lib/sim/DeconstructionSystem.cpp @@ -54,7 +54,8 @@ void DeconstructionSystem::tick(FactoryState& state, Tick currentTick) state.buildings.erase(it); if (def) { - m_addBuildingBlocks(def->cost * m_config.world.refundPercentage / 100); + state.buildingBlocksStock += + def->cost * m_config.world.refundPercentage / 100; } break; } diff --git a/src/lib/sim/DeconstructionSystem.h b/src/lib/sim/DeconstructionSystem.h index 5b2bd87..6df907e 100644 --- a/src/lib/sim/DeconstructionSystem.h +++ b/src/lib/sim/DeconstructionSystem.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "FactoryState.h" #include "GameConfig.h" #include "Tick.h" @@ -14,19 +12,17 @@ // It needs no BeltSystem: a belt, splitter or tunnel end is unregistered the moment // it is queued (see BuildingSystem::deconstruct), not when the timer completes. // -// Holds the config and the refund sink; the world arrives per tick. +// Holds the config alone; the world arrives per tick, the refund going straight into +// the block stock it carries (FactoryState.h). class DeconstructionSystem { public: - DeconstructionSystem(const GameConfig& config, - std::function addBuildingBlocks) - : m_config(config), m_addBuildingBlocks(std::move(addBuildingBlocks)) {} + explicit DeconstructionSystem(const GameConfig& config) : m_config(config) {} void tick(FactoryState& state, Tick currentTick); private: - const GameConfig& m_config; - std::function m_addBuildingBlocks; + const GameConfig& m_config; }; // Starts the timer on the front entry of the deconstruction queue, if it has one and diff --git a/src/lib/sim/FactoryState.h b/src/lib/sim/FactoryState.h index f71d78e..6cac93b 100644 --- a/src/lib/sim/FactoryState.h +++ b/src/lib/sim/FactoryState.h @@ -57,15 +57,26 @@ struct FactoryState // derived from config and Simulation's expansion count, which is folded already. // Seeded from config by BuildingSystem's constructor. int asteroidWidth_tiles = 0; + + // The global building block stock (REQ-HQ-STARTING-BLOCKS, REQ-HQ-BELT-INPUT): + // what placement spends, what deconstruction refunds, and what blocks delivered to + // the HQ add to. Factory data, so it lives with the factory rather than being + // reached through a callback by every system that credits it. + // + // Folded into the checksum by Simulation, in the place it has always occupied + // (docs/replay_design.md). + int buildingBlocksStock = 0; }; -// A fresh factory for a new run: nothing built, and the asteroid bound seeded from -// config. Every owner of a FactoryState creates it this way — the bound has no -// sensible default without the config, so a default-constructed state would refuse -// every placement on the asteroid. +// A fresh factory for a new run: nothing built, the asteroid bound and the starting +// block stock seeded from config. Every owner of a FactoryState creates it this way — +// the bound has no sensible default without the config, so a default-constructed state +// would refuse every placement on the asteroid, and the player would start with nothing +// to build from. inline FactoryState makeFactoryState(const GameConfig& config) { FactoryState state; - state.asteroidWidth_tiles = config.world.regions.asteroidWidth_tiles; + state.asteroidWidth_tiles = config.world.regions.asteroidWidth_tiles; + state.buildingBlocksStock = config.world.startingBuildingBlocks; return state; } diff --git a/src/lib/sim/Simulation.cpp b/src/lib/sim/Simulation.cpp index 7dab1b3..4f7f87f 100644 --- a/src/lib/sim/Simulation.cpp +++ b/src/lib/sim/Simulation.cpp @@ -42,7 +42,6 @@ Simulation::Simulation(GameConfig config, unsigned int seed) , m_currentTick(0) , m_nextDepartureTick(secondsToTicks(m_config.world.departureIntervalSeconds)) , m_nextBuildingId(1) - , m_buildingBlocksStock(m_config.world.startingBuildingBlocks) , m_gameOver(false) , m_hqProxyEntity(entt::null) , m_playerStation1Entity(entt::null) @@ -85,7 +84,6 @@ void Simulation::reset(unsigned int seed) m_currentTick = 0; m_nextDepartureTick = secondsToTicks(m_config.world.departureIntervalSeconds); m_nextBuildingId = 1; - m_buildingBlocksStock = m_config.world.startingBuildingBlocks; m_expansionsPurchased = 0; m_gameOver = false; m_isWon = false; @@ -114,7 +112,6 @@ void Simulation::initializeSubsystems() m_config, m_beltSystem, [this]() { return allocateBuildingId(); }, - [this](int amount) { m_buildingBlocksStock += amount; }, [this](const std::string& id, QVector2D pos, const std::optional& layout) { if (!isSchematicUnlocked(id)) @@ -126,8 +123,7 @@ void Simulation::initializeSubsystems() [this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); }, m_rng); m_constructionSystem = std::make_unique(m_config); - m_deconstructionSystem = std::make_unique( - m_config, [this](int amount) { m_buildingBlocksStock += amount; }); + m_deconstructionSystem = std::make_unique(m_config); m_shipSystem = std::make_unique(m_config, m_admin); m_aiSystem = std::make_unique(m_config); m_movementIntentSystem = std::make_unique(); @@ -658,7 +654,7 @@ unsigned long long Simulation::computeStateChecksum() const hasher.append(m_currentTick); hasher.append(m_nextDepartureTick); hasher.append(m_nextBuildingId); - hasher.append(m_buildingBlocksStock); + hasher.append(m_factoryState.buildingBlocksStock); hasher.append(m_gameOver); hasher.append(m_isWon); hasher.append(m_artifactCount); @@ -763,7 +759,7 @@ unsigned int Simulation::getSeed() const int Simulation::getBuildingBlocksStock() const { - return m_buildingBlocksStock; + return m_factoryState.buildingBlocksStock; } int Simulation::getCurrentAsteroidWidth_tiles() const @@ -782,11 +778,11 @@ int Simulation::getCurrentExpansionCost() const void Simulation::tryExpandAsteroid() { const int cost = getCurrentExpansionCost(); - if (m_buildingBlocksStock < cost) + if (m_factoryState.buildingBlocksStock < cost) { return; } - m_buildingBlocksStock -= cost; + m_factoryState.buildingBlocksStock -= cost; ++m_expansionsPurchased; m_buildingSystem->setAsteroidWidth_tiles(m_factoryState, getCurrentAsteroidWidth_tiles()); } @@ -879,17 +875,17 @@ std::optional Simulation::tryPlaceBuilding(BuildingType type, QPoint break; } } - if (m_buildingBlocksStock < cost) + if (m_factoryState.buildingBlocksStock < cost) { return std::nullopt; } - m_buildingBlocksStock -= cost; + m_factoryState.buildingBlocksStock -= cost; return m_buildingSystem->place(m_factoryState, type, anchor, rotation, m_currentTick); } void Simulation::deconstruct(BuildingId id) { - m_buildingBlocksStock += m_buildingSystem->deconstruct(m_factoryState, id, m_currentTick); + m_buildingSystem->deconstruct(m_factoryState, id, m_currentTick); } void Simulation::cancelDeconstruction(BuildingId id) diff --git a/src/lib/sim/Simulation.h b/src/lib/sim/Simulation.h index 218d379..81ea6ee 100644 --- a/src/lib/sim/Simulation.h +++ b/src/lib/sim/Simulation.h @@ -196,7 +196,6 @@ private: Tick m_currentTick; Tick m_nextDepartureTick; BuildingId m_nextBuildingId; - int m_buildingBlocksStock; int m_expansionsPurchased = 0; // REQ-EXP-COST formula variable x bool m_gameOver = false; bool m_isWon = false; diff --git a/src/test/BehaviorSystemTest.cpp b/src/test/BehaviorSystemTest.cpp index 373f202..9291e03 100644 --- a/src/test/BehaviorSystemTest.cpp +++ b/src/test/BehaviorSystemTest.cpp @@ -60,7 +60,6 @@ struct Fixture FactoryState state = makeFactoryState(cfg); BeltSystem belts; BuildingId nextBuildingId; - int stock; std::mt19937 rng; EntityAdmin admin; BuildingSystem buildings; @@ -79,11 +78,9 @@ struct Fixture : cfg(loadTestConfig()) , belts(cfg.world.beltSpeed_tps) , nextBuildingId(1) - , stock(0) , rng(42) , buildings(cfg, belts, [this]() { return nextBuildingId++; }, - [this](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng) diff --git a/src/test/BuildingTest.cpp b/src/test/BuildingTest.cpp index a642b70..60820f6 100644 --- a/src/test/BuildingTest.cpp +++ b/src/test/BuildingTest.cpp @@ -58,12 +58,12 @@ static Port westPort(QPoint tile) // Run N full sim ticks: construction, belt-pull, production, belt-push, belt tick. static void runTicks(BuildingSystem& bs, const GameConfig& cfg, FactoryState& state_bs, - BeltSystem& belts, int& stock, int n, Tick& tick) + BeltSystem& belts, int n, Tick& tick) { for (int i = 0; i < n; ++i) { ConstructionSystem(cfg).tick(state_bs, belts, tick); - DeconstructionSystem(cfg, [&stock](int n) { stock += n; }).tick(state_bs, tick); + DeconstructionSystem(cfg).tick(state_bs, tick); bs.tickBeltPull(state_bs); bs.tickProduction(state_bs, tick); bs.tickOutputBelts(state_bs); @@ -98,11 +98,17 @@ struct PlacementFixture GameConfig cfg = loadTestConfig(); FactoryState state = makeFactoryState(cfg); BeltSystem belts; - int stock = 0; std::mt19937 rng{0}; BuildingId nextBuildingId = 1; BuildingSystem bs; + // Blocks credited back since the run began. The state is seeded with the configured + // starting stock (FactoryState.h), so a refund reads as a delta rather than a total. + int getRefundedBlocks() const + { + return state.buildingBlocksStock - cfg.world.startingBuildingBlocks; + } + // Defaults to the configured belt speed; pass kFastBeltSpeed_tps where the test // needs items to arrive immediately. Everything counts as unlocked unless the test // says otherwise, which is what the output-group eligibility rule turns on @@ -113,7 +119,6 @@ struct PlacementFixture : belts(beltSpeed_tps.value_or(cfg.world.beltSpeed_tps)) , bs(cfg, belts, [this]() { return nextBuildingId++; }, - [this](int n) { stock += n; }, [](const std::string&, QVector2D, const std::optional&) {}, isItemUnlocked ? std::move(isItemUnlocked) : std::function( @@ -255,7 +260,7 @@ TEST_CASE("BuildingSystem: placing a belt registers it with BeltSystem after con // Complete construction (1 s). Tick tick = 0; - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(1.0)) + 1, tick); REQUIRE(f.belts.tryPutItem(QPoint(5, 5), makeItem("iron_ore"), Rotation::East)); REQUIRE(getAllBuildings(f.state).size() == 1); @@ -283,9 +288,9 @@ TEST_CASE("BuildingSystem: deconstructing a construction site removes it instant // Still queued for construction (not yet built): instant removal, full cost // refunded immediately, never entering the deconstruction queue (REQ-BLD-DECONSTRUCT). - const int refund = f.bs.deconstruct(f.state, id, 0); + f.bs.deconstruct(f.state, id, 0); - REQUIRE(refund == 15); // Miner cost = 15 + REQUIRE(f.getRefundedBlocks() == 15); // Miner cost = 15 REQUIRE_FALSE(isTileOccupied(f.state, QPoint(0, 0))); REQUIRE(getAllSites(f.state).empty()); } @@ -324,7 +329,7 @@ TEST_CASE("BuildingSystem: construction completes after configured duration", "[ // 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(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(10.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(10.0)) + 1, tick); REQUIRE(getAllSites(f.state).empty()); REQUIRE(findBuilding(f.state, id) != nullptr); @@ -339,7 +344,7 @@ static void runUntilBuilt(PlacementFixture& f, BuildingId id, Tick& tick) { for (int i = 0; i < 100000 && findBuilding(f.state, id) == nullptr; ++i) { - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick); } REQUIRE(findBuilding(f.state, id) != nullptr); } @@ -356,18 +361,17 @@ TEST_CASE("BuildingSystem: deconstructing a built building queues it; refund cre // Deconstructing 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.deconstruct(f.state, id, tick); - REQUIRE(refund == 0); + f.bs.deconstruct(f.state, id, tick); REQUIRE(isQueuedForDeconstruction(f.state, id)); REQUIRE(isTileOccupied(f.state, QPoint(0, 0))); - REQUIRE(f.stock == 0); + REQUIRE(f.getRefundedBlocks() == 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.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(0.1)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(0.1)) + 1, tick); REQUIRE(findBuilding(f.state, id) == nullptr); REQUIRE_FALSE(isTileOccupied(f.state, QPoint(0, 0))); - REQUIRE(f.stock == 15 * f.cfg.world.refundPercentage / 100); + REQUIRE(f.getRefundedBlocks() == 15 * f.cfg.world.refundPercentage / 100); } TEST_CASE("BuildingSystem: deconstruction queue removes one building at a time", "[building][decon]") @@ -388,16 +392,16 @@ TEST_CASE("BuildingSystem: deconstruction queue removes one building at a time", // 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.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(0.1)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(0.1)) + 1, tick); REQUIRE(findBuilding(f.state, a) == nullptr); REQUIRE(findBuilding(f.state, b) != nullptr); REQUIRE(isQueuedForDeconstruction(f.state, b)); - REQUIRE(f.stock == 15 * f.cfg.world.refundPercentage / 100); + REQUIRE(f.getRefundedBlocks() == 15 * f.cfg.world.refundPercentage / 100); // The second drains next. - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(0.1)) + 2, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(0.1)) + 2, tick); REQUIRE(findBuilding(f.state, b) == nullptr); - REQUIRE(f.stock == 2 * (15 * f.cfg.world.refundPercentage / 100)); + REQUIRE(f.getRefundedBlocks() == 2 * (15 * f.cfg.world.refundPercentage / 100)); } TEST_CASE("BuildingSystem: cancelling deconstruction resumes the building with no refund", @@ -418,12 +422,12 @@ TEST_CASE("BuildingSystem: cancelling deconstruction resumes the building with n REQUIRE_FALSE(isQueuedForDeconstruction(f.state, id)); REQUIRE(findBuilding(f.state, id) != nullptr); REQUIRE(isTileOccupied(f.state, QPoint(0, 0))); - REQUIRE(f.stock == 0); + REQUIRE(f.getRefundedBlocks() == 0); // It is never removed even after more than a deconstruction interval passes. - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(0.1)) + 5, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(0.1)) + 5, tick); REQUIRE(findBuilding(f.state, id) != nullptr); - REQUIRE(f.stock == 0); + REQUIRE(f.getRefundedBlocks() == 0); } TEST_CASE("BuildingSystem: queued belt stops transporting; cancel restores it", "[building][decon]") @@ -480,7 +484,7 @@ TEST_CASE("BuildingSystem: second building starts after first completes", "[buil // Process through tick 300 to complete first miner's construction. Tick tick = 0; - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(10.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(10.0)) + 1, tick); REQUIRE(getAllSites(f.state).size() == 1); REQUIRE(getAllSites(f.state).front().id == id2); @@ -501,7 +505,7 @@ TEST_CASE("BuildingSystem: miner produces iron_ore after recipe duration", "[bui Tick tick = 0; // Construction completes on tick 300; production cycle starts tick 300, // completes on tick 330. Process through tick 330: 331 ticks total. - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(10.0)) + static_cast(secondsToTicks(1.0)) + 1, tick); @@ -527,7 +531,7 @@ TEST_CASE("BuildingSystem: miner output buffer stalls when full", "[building]") // (completesAt=360). Cycle 2 completes at tick 360: deposit item -> 2 items held, // which fills the buffer (capacity 2), so cycle 3 cannot start. // Need to process through tick 360: 361 ticks total. - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(10.0)) + 2 * static_cast(secondsToTicks(1.0)) + 1, tick); @@ -556,7 +560,7 @@ TEST_CASE("BuildingSystem: the next cycle starts on the tick the last one comple Tick tick = 0; // Construction completes at tick 300 and cycle 1 starts in that same tick. - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(10.0)) + 1, tick); const Building* b = findBuilding(f.state, id); REQUIRE(b != nullptr); @@ -565,7 +569,7 @@ TEST_CASE("BuildingSystem: the next cycle starts on the tick the last one comple // Process up to and including that completion tick: the next cycle is already // running, due exactly one duration later rather than one duration plus a tick. - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(cycleTicks), tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(cycleTicks), tick); b = findBuilding(f.state, id); REQUIRE(b->getOutputItemCount() == 1); REQUIRE(b->production.has_value()); @@ -577,7 +581,7 @@ TEST_CASE("BuildingSystem: the next cycle starts on the tick the last one comple building.outputBuffer.items.clear(); for (std::vector& lane : building.emergingItems) { lane.clear(); } }); - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(cycleTicks), tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(cycleTicks), tick); b = findBuilding(f.state, id); REQUIRE(b->production.has_value()); REQUIRE(b->production->completesAt == firstCompletesAt + 2 * cycleTicks); @@ -601,10 +605,10 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites", // The queue builds one at a time: miner (10s) completes at tick 300, then // the smelter (15s) starts and completes at tick 300 + 450 = 750. - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(10.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(10.0)) + 1, tick); REQUIRE(getProductionBuildingCount(f.state) == 1); - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(15.0)), tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(15.0)), tick); REQUIRE(getProductionBuildingCount(f.state) == 2); // Neither is producing yet: the miner has no recipe selected, and the @@ -612,7 +616,7 @@ TEST_CASE("BuildingSystem: productionBuildingCount excludes construction sites", REQUIRE(getActiveProductionBuildingCount(f.state) == 0); f.bs.setRecipe(f.state, minerId, "mine_iron_ore"); - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick); REQUIRE(getActiveProductionBuildingCount(f.state) == 1); } @@ -629,12 +633,12 @@ TEST_CASE("BuildingSystem: activeProductionBuildingCount tracks production cycle REQUIRE(getActiveProductionBuildingCount(f.state) == 0); // Construction completes at tick 300; cycle 1 starts the same tick (completesAt=330). - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(10.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(10.0)) + 1, tick); REQUIRE(getActiveProductionBuildingCount(f.state) == 1); // Run cycles 1 and 2 to completion (1s each); cycle 3 stalls once the // output buffer (capacity 2) is full (REQ-MAT-OUTPUT-BUFFER). - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, 2 * static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, 2 * static_cast(secondsToTicks(1.0)) + 1, tick); const Building* b = findBuilding(f.state, id); REQUIRE(b != nullptr); @@ -661,7 +665,7 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing // Complete construction (15s → tick 450+1 = 451 ticks). Tick tick = 0; - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(15.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(15.0)) + 1, tick); REQUIRE(findBuilding(f.state, sid)->recipeId.empty()); // Place west-flowing belt at (2,0): belt flows West, delivers to smelter. @@ -685,9 +689,9 @@ TEST_CASE("BuildingSystem: smelter input buffer fills from adjacent west-flowing REQUIRE(b->pendingInputCount(ItemType{"iron_ore"}) >= 1); } -// An accepted input item travels inward on its input belt before it becomes usable -// f.stock: it is reserved (counts against the cap) on entry and only enters the -// buffer on reaching the tile centre (REQ-MAT-INPUT-INTAKE). +// An accepted input item travels inward on its input belt before it becomes usable: +// it is reserved (counts against the cap) on entry and only enters the buffer on +// reaching the tile centre (REQ-MAT-INPUT-INTAKE). TEST_CASE("BuildingSystem: accepted input travels inward before entering the buffer", "[building]") { @@ -695,7 +699,7 @@ TEST_CASE("BuildingSystem: accepted input travels inward before entering the buf const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(15.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(15.0)) + 1, tick); f.belts.placeBelt(QPoint(2, 0), Rotation::West); f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore")); @@ -727,7 +731,7 @@ TEST_CASE("BuildingSystem: input reservation caps buffered plus in-transit at th const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(25.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(25.0)) + 1, tick); // Feed scrap via an input belt without ever running production (only pull), so // the buffer fills and stays full. Try to over-fill it well past the cap. @@ -759,7 +763,7 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection", const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(15.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(15.0)) + 1, tick); // Feed 2 iron_ore (the test-config iron_ingot recipe needs 2) via a // west-flowing belt at input port (2,0). @@ -772,7 +776,7 @@ TEST_CASE("BuildingSystem: smelter auto-smelts ore without a recipe selection", } // iron_ingot recipe cycle is 2s; run to completion. - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(2.0)) + 2, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(2.0)) + 2, tick); const Building* b = findBuilding(f.state, sid); REQUIRE(b != nullptr); @@ -795,7 +799,7 @@ TEST_CASE("BuildingSystem: mixed ore on one belt leaves the smelter on the first const BuildingId sid = f.bs.place(f.state, BuildingType::Smelter, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(15.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(15.0)) + 1, tick); // Feed 1 iron_ore, then 2 copper_ore, via the west-flowing input belt. f.belts.placeBelt(QPoint(2, 0), Rotation::West); @@ -807,7 +811,7 @@ TEST_CASE("BuildingSystem: mixed ore on one belt leaves the smelter on the first f.bs.tickBeltPull(f.state); } - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(2.5)) + 2, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(2.5)) + 2, tick); const Building* b = findBuilding(f.state, sid); REQUIRE(b != nullptr); @@ -845,13 +849,13 @@ TEST_CASE("BuildingSystem: miner output buffer drains onto adjacent belt", "[bui Tick tick = 0; // Construction (10s) + 1 production cycle (1s) + 1 extra tick. - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(10.0)) + static_cast(secondsToTicks(1.0)) + 1, tick); // Item should have been pushed onto the belt this tick or a subsequent one. // Run one more tick to ensure tickBeltPush fires after the deposit tick. - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick); const std::optional item = f.belts.tryTakeItem(eastPort(QPoint(1, 1))); REQUIRE(item.has_value()); @@ -876,7 +880,7 @@ TEST_CASE("BuildingSystem: output port couples directly into an adjacent input p Tick tick = 0; // Smelter build (15s) + margin for coupling and a smelt cycle. - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(30.0)), tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(30.0)), tick); const Building* smelter = findBuilding(f.state, smelterId); REQUIRE(smelter != nullptr); @@ -905,7 +909,7 @@ TEST_CASE("BuildingSystem: direct coupling to a non-consumer leaves the item stu Tick tick = 0; // Both miners build sequentially (10s each), then the producer runs and jams. - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(25.0)), tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(25.0)), tick); const Building* miner = findBuilding(f.state, minerId); const Building* sink = findBuilding(f.state, sinkId); @@ -931,7 +935,7 @@ TEST_CASE("BuildingSystem: setRecipe clears output buffer and active production" Tick tick = 0; // Run until first item is in output buffer. - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(10.0)) + static_cast(secondsToTicks(1.0)) + 1, tick); @@ -996,9 +1000,8 @@ TEST_CASE("BuildingSystem: a single-group recipe consumes no randomness", "[buil advanced.bs.setRecipe(advanced.state, b, "mine_iron_ore"); const int ticks = static_cast(secondsToTicks(10.0)) + 40; - runTicks(quiet.bs, quiet.cfg, quiet.state, quiet.belts, quiet.stock, ticks, tickA); - runTicks(advanced.bs, advanced.cfg, advanced.state, advanced.belts, advanced.stock, - ticks, tickB); + runTicks(quiet.bs, quiet.cfg, quiet.state, quiet.belts, ticks, tickA); + runTicks(advanced.bs, advanced.cfg, advanced.state, advanced.belts, ticks, tickB); const Building* minerA = findBuilding(quiet.state, a); const Building* minerB = findBuilding(advanced.state, b); @@ -1055,7 +1058,7 @@ TEST_CASE("BuildingSystem: a group with a locked item is never picked", "[buildi Tick tick = 0; const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant, QPoint(0, 0), Rotation::East, 0).value(); - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(25.0)) + 1, tick); f.bs.setRecipe(f.state, id, "reprocessing_cycle"); @@ -1071,7 +1074,7 @@ TEST_CASE("BuildingSystem: a group with a locked item is never picked", "[buildi building.outputBuffer.items.clear(); for (std::vector& lane : building.emergingItems) { lane.clear(); } }); - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(3.0)) + 1, tick); for (const Item& item : outputSideItems(*findBuilding(f.state, id))) @@ -1093,7 +1096,7 @@ TEST_CASE("BuildingSystem: reprocessing plant sizes one output buffer per possib // Complete construction (25s). Tick tick = 0; - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(25.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(25.0)) + 1, tick); // A plant holds no buffers until it has a recipe (REQ-BLD-AUTO-RECIPE); selecting // one sizes them, exactly as the first scrap offered to it would. @@ -1123,7 +1126,7 @@ TEST_CASE("BuildingSystem: one full output buffer stops the plant even when the const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(25.0)) + 1, tick); // Feed a full cycle's scrap (5) so only the output side can hold it back. @@ -1145,7 +1148,7 @@ TEST_CASE("BuildingSystem: one full output buffer stops the plant even when the } }); - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, 5, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, 5, tick); const Building* b = findBuilding(f.state, id); REQUIRE(b != nullptr); @@ -1170,7 +1173,7 @@ TEST_CASE("BuildingSystem: reprocessing plant runs a second cycle while holding const BuildingId id = f.bs.place(f.state, BuildingType::ReprocessingPlant, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(25.0)) + 1, tick); // Two cycles' worth of scrap (5 each), which is exactly the input cap. @@ -1185,7 +1188,7 @@ TEST_CASE("BuildingSystem: reprocessing plant runs a second cycle while holding // No belt carries the output away, so the first cycle's result is still held. // reprocessing_cycle runs 3s; run through the completion tick. - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(3.0)) + 1, tick); const Building* b = findBuilding(f.state, id); @@ -1206,7 +1209,7 @@ static BuildingId buildSmelter(PlacementFixture& f, QPoint anchor, Tick& tick) { const BuildingId id = f.bs.place(f.state, BuildingType::Smelter, anchor, Rotation::East, 0).value(); - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(15.0)) + 1, tick); return id; } @@ -1291,7 +1294,7 @@ TEST_CASE("BuildingSystem: selecting a different recipe frees a stuck auto-recip f.belts.tryPutItem(QPoint(2, 0), makeItem("iron_ore")); f.belts.tick(); f.bs.tickBeltPull(f.state); - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, 30, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, 30, tick); const Building* stuck = findBuilding(f.state, id); REQUIRE(stuck->recipeId == "iron_ingot"); @@ -1339,7 +1342,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta // Complete construction (25s). Tick tick = 0; - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(25.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(25.0)) + 1, tick); // Feed 5 scrap into the building via a belt at an input port. // Reprocessing plant body (East rotation) = 3×3 at (0,0). @@ -1361,7 +1364,7 @@ TEST_CASE("BuildingSystem: reprocessing plant produces one cycle output then sta } // Run production cycle (3s = 90 ticks + 1 for the completion tick). - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(3.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(3.0)) + 1, tick); const Building* b = findBuilding(f.state, id); REQUIRE(b != nullptr); @@ -1405,7 +1408,7 @@ TEST_CASE("BuildingSystem: findRotateInPlaceTarget returns the building id for a const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(1.0)) + 1, tick); REQUIRE(getAllSites(f.state).empty()); const std::optional result = @@ -1805,7 +1808,7 @@ TEST_CASE("BuildingSystem: rotateInPlace updates rotation and output port direct const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(1.0)) + 1, tick); REQUIRE(findBuilding(f.state, id) != nullptr); const Building& before = *findBuilding(f.state, id); @@ -1826,7 +1829,7 @@ TEST_CASE("BuildingSystem: rotateInPlace re-registers a belt tile with BeltSyste const BuildingId id = f.bs.place(f.state, BuildingType::Belt, QPoint(0, 0), Rotation::East, 0).value(); Tick tick = 0; - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, static_cast(secondsToTicks(1.0)) + 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, static_cast(secondsToTicks(1.0)) + 1, tick); f.bs.rotateInPlace(f.state, id, Rotation::North); @@ -1846,7 +1849,7 @@ TEST_CASE("BuildingSystem: rotateInPlace preserves the output filters of a split Tick tick = 0; while (getAllBuildings(f.state).empty() && tick < 100000) { - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick); } REQUIRE(getAllBuildings(f.state).size() == 1); @@ -1891,7 +1894,7 @@ TEST_CASE("BuildingSystem: splitter filters configured on a construction site ca Tick tick = 0; while (getAllBuildings(f.state).empty() && tick < 100000) { - runTicks(f.bs, f.cfg, f.state, f.belts, f.stock, 1, tick); + runTicks(f.bs, f.cfg, f.state, f.belts, 1, tick); } REQUIRE(getAllBuildings(f.state).size() == 1); REQUIRE(getAllBuildings(f.state)[0].type == BuildingType::Splitter); @@ -2199,11 +2202,11 @@ namespace // Advances the sim until the given site becomes an operational building, or a // safety cap is reached. void buildToCompletion(BuildingSystem& bs, const GameConfig& cfg, FactoryState& state, - BeltSystem& belts, int& stock, BuildingId id, Tick& tick) + BeltSystem& belts, BuildingId id, Tick& tick) { for (int i = 0; i < 20000 && findBuilding(state, id) == nullptr; ++i) { - runTicks(bs, cfg, state, belts, stock, 1, tick); + runTicks(bs, cfg, state, belts, 1, tick); } } } @@ -2238,7 +2241,7 @@ TEST_CASE("BuildingSystem: getInputPorts matches between a site and the built bu const BuildingId id = f.bs.place(f.state, BuildingType::Miner, QPoint(0, 0), Rotation::East, 0).value(); const std::vector sitePorts = getInputPorts(f.state, f.cfg, id); - buildToCompletion(f.bs, f.cfg, f.state, f.belts, f.stock, id, tick); + buildToCompletion(f.bs, f.cfg, f.state, f.belts, id, tick); REQUIRE(findBuilding(f.state, id) != nullptr); const std::vector builtPorts = getInputPorts(f.state, f.cfg, id); diff --git a/src/test/CombatSystemTest.cpp b/src/test/CombatSystemTest.cpp index 1c13192..2d4c293 100644 --- a/src/test/CombatSystemTest.cpp +++ b/src/test/CombatSystemTest.cpp @@ -69,7 +69,6 @@ struct CombatFixture , ships(cfg, admin) , buildings(cfg, belts, [this]() { return nextBuildingId++; }, - [](int){}, [](const std::string&, QVector2D, const std::optional&) {}, [](const std::string&) -> bool { return true; }, rng)