replay: closing tests and overlay polish (Phase 4)

- Round-trip: every command verb serialize->parse->re-serialize is identical;
  malformed input is rejected.
- Equivalence: add a long ~2400-tick run through waves/combat that records,
  reads back, and replays to a byte-identical final state with no desync
  (alongside the existing short scenario).
- Desync detection: corrupting one recorded checksum makes ReplayPlayer report
  the exact desync tick.
- Reset boundary: a Reset drained through CommandManager rolls the recorder to
  a new file named by the new seed.
- Polish: the end-of-replay / desync overlay dims the world behind the message.

Record + playback is now functionally complete and covered by headless tests.
Full suite green (354 cases / 3418 assertions).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DUsFgd2Ga6pmLz8giS8WUn
This commit is contained in:
2026-06-30 21:20:22 +02:00
parent 26e108f3e1
commit bd9f550b67
4 changed files with 191 additions and 5 deletions

View File

@@ -184,3 +184,29 @@ TEST_CASE("ReplayRecorder startNewRun rolls to a new file", "[replay]")
REQUIRE(second.find("_2.replay") != std::string::npos);
recorder.close();
}
TEST_CASE("CommandManager rolls the replay file on a Reset command", "[replay]")
{
Simulation sim(ConfigLoader::loadFromDirectory(CONFIG_DIR), 1u);
CommandManager manager(sim);
std::unique_ptr<ReplayRecorder> recorder =
std::make_unique<ReplayRecorder>(CONFIG_DIR, tempOutputDir());
ReplayRecorder* recorderPtr = recorder.get();
manager.setRecorder(std::move(recorder));
const std::string firstPath = recorderPtr->currentFilePath();
std::shared_ptr<ResetCommand> reset = std::make_shared<ResetCommand>();
reset->config = std::make_shared<GameConfig>(ConfigLoader::loadFromDirectory(CONFIG_DIR));
reset->seed = 999u;
manager.enqueue(reset);
manager.drain();
const std::string secondPath = recorderPtr->currentFilePath();
REQUIRE(firstPath != secondPath);
REQUIRE(secondPath.find("_999.replay") != std::string::npos);
manager.setRecorder(nullptr);
QFile::remove(QString::fromStdString(firstPath));
QFile::remove(QString::fromStdString(secondPath));
}