#include "catch.hpp" #include #include "BuildingSystem.h" #include "Command.h" #include "CommandManager.h" #include "ConfigLoader.h" #include "GameConfig.h" #include "Rotation.h" #include "Simulation.h" #include "SimulationTestAccess.h" #include "Tick.h" #include "TestConfig.h" // The command chokepoint (Simulation::apply) must produce exactly the same state // as driving the underlying mutators directly — that equivalence is what lets a // recorded command stream reproduce a live run (see docs/replay_design.md). TEST_CASE("apply(PlaceBuildingCommand) matches direct placement", "[command]") { Simulation viaCommand(loadTestConfig(), 99); Simulation viaDirect(loadTestConfig(), 99); PlaceBuildingCommand command; command.type = BuildingType::Miner; command.anchor = QPoint(-3, 0); command.rotation = Rotation::East; viaCommand.apply(command); SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-3, 0), Rotation::East); REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum()); } TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe", "[command]") { Simulation viaCommand(loadTestConfig(), 99); Simulation viaDirect(loadTestConfig(), 99); PlaceBuildingCommand command; command.type = BuildingType::Miner; command.anchor = QPoint(-2, 0); command.rotation = Rotation::East; command.recipeId = "mine_iron_ore"; viaCommand.apply(command); const BuildingId id = SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value(); SimulationTestAccess::buildings(viaDirect).setRecipe(SimulationTestAccess::state(viaDirect), id, "mine_iron_ore"); REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum()); } TEST_CASE("apply(DeconstructCommand) matches direct deconstruct", "[command]") { Simulation viaCommand(loadTestConfig(), 99); Simulation viaDirect(loadTestConfig(), 99); const BuildingId idA = SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value(); const BuildingId idB = SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value(); REQUIRE(idA == idB); DeconstructCommand command; command.id = idA; viaCommand.apply(command); SimulationTestAccess::deconstruct(viaDirect, idB); REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum()); } TEST_CASE("apply(CancelDeconstructionCommand) matches direct cancelDeconstruction", "[command]") { Simulation viaCommand(loadTestConfig(), 99); Simulation viaDirect(loadTestConfig(), 99); const BuildingId idA = SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value(); const BuildingId idB = SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value(); REQUIRE(idA == idB); // Build the miners, then queue both for deconstruction identically. for (int i = 0; i < static_cast(secondsToTicks(10.0)) + 1; ++i) { viaCommand.tick(); viaDirect.tick(); } SimulationTestAccess::deconstruct(viaCommand, idA); SimulationTestAccess::deconstruct(viaDirect, idB); CancelDeconstructionCommand command; command.id = idA; viaCommand.apply(command); SimulationTestAccess::cancelDeconstruction(viaDirect, idB); REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum()); } TEST_CASE("CommandManager drains queued commands in FIFO order through apply", "[command]") { Simulation viaManager(loadTestConfig(), 99); Simulation viaDirect(loadTestConfig(), 99); CommandManager manager(viaManager); std::shared_ptr first = std::make_shared(); first->type = BuildingType::Miner; first->anchor = QPoint(-3, 0); std::shared_ptr second = std::make_shared(); second->type = BuildingType::Belt; second->anchor = QPoint(-2, 0); manager.enqueue(first); manager.enqueue(second); REQUIRE(manager.hasPending()); manager.drain(); REQUIRE_FALSE(manager.hasPending()); SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-3, 0), Rotation::East); SimulationTestAccess::place(viaDirect, BuildingType::Belt, QPoint(-2, 0), Rotation::East); REQUIRE(viaManager.computeStateChecksum() == viaDirect.computeStateChecksum()); }