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:
2026-07-01 19:20:08 +00:00
committed by mlangkabel
parent cf68ac2862
commit d74ba5bfad
40 changed files with 3385 additions and 129 deletions

View File

@@ -16,6 +16,7 @@
#include "Rotation.h"
#include "ShipLayout.h"
#include "Simulation.h"
#include "SimulationTestAccess.h"
#include "SurfaceMask.h"
#include "Tick.h"
@@ -524,9 +525,9 @@ TEST_CASE("Blueprint placement: buildings land at anchor + offset from cursor",
const QPoint offsetA(-1, 0);
const QPoint offsetB( 1, 0);
const BuildingId idA = sim.tryPlaceBuilding(
const BuildingId idA = SimulationTestAccess::place(sim,
BuildingType::Belt, cursor + offsetA, Rotation::East);
const BuildingId idB = sim.tryPlaceBuilding(
const BuildingId idB = SimulationTestAccess::place(sim,
BuildingType::Belt, cursor + offsetB, Rotation::East);
REQUIRE(idA != kInvalidBuildingId);
@@ -551,10 +552,10 @@ TEST_CASE("Blueprint placement: cost is deducted for each building in sequence",
const int startBlocks = sim.buildingBlocksStock();
REQUIRE(startBlocks >= 2 * beltCost); // test config has enough starting blocks
sim.tryPlaceBuilding(BuildingType::Belt, QPoint(-6, 0), Rotation::East);
SimulationTestAccess::place(sim,BuildingType::Belt, QPoint(-6, 0), Rotation::East);
REQUIRE(sim.buildingBlocksStock() == startBlocks - beltCost);
sim.tryPlaceBuilding(BuildingType::Belt, QPoint(-4, 0), Rotation::East);
SimulationTestAccess::place(sim,BuildingType::Belt, QPoint(-4, 0), Rotation::East);
REQUIRE(sim.buildingBlocksStock() == startBlocks - 2 * beltCost);
}
@@ -576,12 +577,12 @@ TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId a
int col = -2;
while (sim.buildingBlocksStock() >= minerCost)
{
sim.tryPlaceBuilding(BuildingType::Miner, QPoint(col, 0), Rotation::East);
SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(col, 0), Rotation::East);
col -= 2;
}
const int blocksBeforeAttempt = sim.buildingBlocksStock();
const BuildingId id = sim.tryPlaceBuilding(
const BuildingId id = SimulationTestAccess::place(sim,
BuildingType::Miner, QPoint(col - 2, 0), Rotation::East);
// Placement must fail and leave the stock unchanged.
@@ -598,7 +599,7 @@ TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and ch
// A miner is all-asteroid; placing it in space (x >= 0) violates the terrain
// rule, so it must be rejected without consuming building blocks.
const BuildingId id =
sim.tryPlaceBuilding(BuildingType::Miner, QPoint(0, 0), Rotation::East);
SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(0, 0), Rotation::East);
REQUIRE(id == kInvalidBuildingId);
REQUIRE(sim.buildingBlocksStock() == startBlocks);
@@ -621,7 +622,7 @@ TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies
// Miner mask ["AA","A>"] East at (-3,0) → all-asteroid body at
// (-3,0),(-2,0),(-3,1); a valid spot.
const BuildingId id =
sim.tryPlaceBuilding(BuildingType::Miner, QPoint(-3, 0), Rotation::East);
SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-3, 0), Rotation::East);
REQUIRE(id != kInvalidBuildingId);
REQUIRE(sim.buildingBlocksStock() == startBlocks - minerCost);
@@ -664,10 +665,10 @@ TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "
Simulation sim(loadConfig());
// Miner body cells: (0,0),(1,0),(0,1) — all at x < 0, valid for asteroid.
const BuildingId id = sim.tryPlaceBuilding(BuildingType::Miner, QPoint(-2, 0), Rotation::East);
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East);
REQUIRE(id != kInvalidBuildingId);
sim.buildings().setRecipe(id, "mine_iron_ore");
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
const ConstructionSite* site = sim.buildings().findSite(id);
REQUIRE(site != nullptr);
@@ -679,9 +680,9 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction
{
Simulation sim(loadConfig());
const BuildingId id = sim.tryPlaceBuilding(BuildingType::Miner, QPoint(-2, 0), Rotation::East);
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East);
REQUIRE(id != kInvalidBuildingId);
sim.buildings().setRecipe(id, "mine_copper_ore");
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_copper_ore");
// Miner construction_time_seconds = 10 → completesAt = secondsToTicks(10) = 300.
// Run 301 ticks (0..300) to process the completion tick.
@@ -759,7 +760,7 @@ TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout
// Shipyard surface_mask ["AAAS>","AAAS "] with Rotation::East:
// A-tiles at (-3,0),(-2,0),(-1,0),(-3,1),(-2,1),(-1,1) — all x < 0, valid asteroid tiles.
// S-tile at (0,0) and (0,1) — x >= 0, valid space tiles.
const BuildingId id = sim.tryPlaceBuilding(BuildingType::Shipyard, QPoint(-3, 0), Rotation::East);
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Shipyard, QPoint(-3, 0), Rotation::East);
REQUIRE(id != kInvalidBuildingId);
ShipLayoutConfig layout;
@@ -769,7 +770,7 @@ TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout
pm.rotation = Rotation::East;
layout.placedModules.push_back(pm);
sim.buildings().setShipLayout(id, layout);
SimulationTestAccess::buildings(sim).setShipLayout(id, layout);
const ConstructionSite* site = sim.buildings().findSite(id);
REQUIRE(site != nullptr);
@@ -783,7 +784,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc
{
Simulation sim(loadConfig());
const BuildingId id = sim.tryPlaceBuilding(BuildingType::Shipyard, QPoint(-3, 0), Rotation::East);
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Shipyard, QPoint(-3, 0), Rotation::East);
REQUIRE(id != kInvalidBuildingId);
ShipLayoutConfig layout;
@@ -793,7 +794,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc
pm.rotation = Rotation::North;
layout.placedModules.push_back(pm);
sim.buildings().setShipLayout(id, layout);
SimulationTestAccess::buildings(sim).setShipLayout(id, layout);
// Shipyard construction_time_seconds = 30 in the test config.
double constructionTime = 0.0;