replay: record every run to a replay file (Phase 2)

Hooks a recorder into the command chokepoint so each run is written to disk as
it plays, ready for playback in Phase 3.

- ReplayRecorder (lib): line-oriented append-friendly file — keyed header
  (version, build, seed, config_hash, timestamp), then '---', then tick-tagged
  command lines interleaved with "# checksum <tick> <hex>" RNG fingerprints.
  Each line is flushed so a crash leaves a valid partial file. Config hash is a
  64-bit FNV over the config dir's *.toml files; build tag is __DATE__/__TIME__.
- CommandSerializer (lib): per-command text (length-prefixed variable parts;
  ship layouts and splitter filters serialized inline). Reset is a file
  boundary, never a stream entry.
- CommandManager owns an optional ReplayRecorder: drain() records each applied
  command + a post-apply checksum; a drained Reset rolls to a new file;
  recordTickCheckpoint() (called per tick from onFrame) writes a checksum every
  30 ticks.
- Random seed generated in main and on restart (std::random_device); Simulation
  retains it via getSeed() for the header.
- GameWorldView attaches the recorder at construction (first file + tick-0
  checksum); replays land in <data>/replays named <timestamp>_<seed>.replay.

ReplayRecorderTest covers serialization, file well-formedness, file rolling,
and the CommandManager drain->record integration. Full suite green
(346 cases / 3377 assertions); app, tests, and balancing all build.

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 20:08:04 +02:00
parent 82ca9080a5
commit 97b6f0d8fd
16 changed files with 650 additions and 20 deletions

View File

@@ -0,0 +1,53 @@
#pragma once
#include <cstdint>
#include <fstream>
#include <string>
#include "Tick.h"
struct Command;
// Writes a replay file as the game runs (see docs/replay_design.md). The format
// is line-oriented append-friendly text: a small keyed header, then one line per
// command (tick-tagged) interleaved with RNG-state checksum lines for desync
// detection. Each line is flushed so a crash mid-run still leaves a valid partial
// file.
//
// One file = one run between Reset boundaries; startNewRun() closes the current
// file and opens a fresh one.
class ReplayRecorder
{
public:
// configDir: hashed (its *.toml files) into the header for config-mismatch
// detection on playback. outputDir: where .replay files are written.
ReplayRecorder(std::string configDir, std::string outputDir);
~ReplayRecorder();
ReplayRecorder(const ReplayRecorder&) = delete;
ReplayRecorder& operator=(const ReplayRecorder&) = delete;
// Close any current file, then open a fresh one (named <timestamp>_<seed>),
// write the header, and record an initial tick-0 checksum. A run boundary.
void startNewRun(unsigned int seed, std::uint64_t initialRngFingerprint);
// Append one command line tagged with the tick it was applied at, followed by
// the post-apply RNG checksum.
void recordCommand(Tick tick, const Command& command, std::uint64_t rngFingerprint);
// Append a periodic RNG checksum line.
void recordChecksum(Tick tick, std::uint64_t rngFingerprint);
void close();
bool isOpen() const;
const std::string& currentFilePath() const;
private:
// 64-bit hash over the *.toml files in m_configDir, as a 16-char hex string.
std::string computeConfigHash() const;
std::string m_configDir;
std::string m_outputDir;
std::string m_filePath;
std::ofstream m_stream;
};