Replay: deterministic record & playback (#4)
Add deterministic record/playback for a run. Recording captures `(seed, config hash, ordered tick-tagged commands)` and re-simulates on playback — no state snapshots. `DotaFactory.exe --replay <file>` re-plays a recorded run view-only with manual speed/pause. Reviewed-on: #4 Co-authored-by: Malte Langkabel <malte.langkabel@gmail.com> Co-committed-by: Malte Langkabel <malte.langkabel@gmail.com>
This commit was merged in pull request #4.
This commit is contained in:
106
src/test/CommandTest.cpp
Normal file
106
src/test/CommandTest.cpp
Normal file
@@ -0,0 +1,106 @@
|
||||
#include "catch.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#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<PlaceBuildingCommand> first = std::make_shared<PlaceBuildingCommand>();
|
||||
first->type = BuildingType::Miner;
|
||||
first->anchor = QPoint(-3, 0);
|
||||
std::shared_ptr<PlaceBuildingCommand> second = std::make_shared<PlaceBuildingCommand>();
|
||||
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());
|
||||
}
|
||||
Reference in New Issue
Block a user