#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" namespace { GameConfig loadConfig() { return ConfigLoader::loadFromDirectory(CONFIG_DIR); } } // namespace // 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(loadConfig(), 99); Simulation viaDirect(loadConfig(), 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(loadConfig(), 99); Simulation viaDirect(loadConfig(), 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); SimulationTestAccess::buildings(viaDirect).setRecipe(id, "mine_iron_ore"); REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum()); } TEST_CASE("apply(DemolishCommand) matches direct demolish", "[command]") { Simulation viaCommand(loadConfig(), 99); Simulation viaDirect(loadConfig(), 99); const BuildingId idA = SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East); const BuildingId idB = SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-3, 0), Rotation::East); REQUIRE(idA == idB); DemolishCommand command; command.id = idA; viaCommand.apply(command); SimulationTestAccess::demolish(viaDirect, idB); REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum()); } TEST_CASE("CommandManager drains queued commands in FIFO order through apply", "[command]") { Simulation viaManager(loadConfig(), 99); Simulation viaDirect(loadConfig(), 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()); }