Skip to content

Add ReplayIO: save/load replay data to/from replays/ directory#5

Closed
ScottWang05 with Copilot wants to merge 3 commits into
mainfrom
copilot/save-load-replay-data-files
Closed

Add ReplayIO: save/load replay data to/from replays/ directory#5
ScottWang05 with Copilot wants to merge 3 commits into
mainfrom
copilot/save-load-replay-data-files

Conversation

Copilot AI commented Apr 12, 2026

Copy link
Copy Markdown

Implements file-based replay persistence via a new ReplayIO class, enabling match history to be written to timestamped files in replays/ and fully reconstructed from them.

New files

  • include/core/replay_io.hppReplayIO class with ReplayData struct (rows, cols, initialState, history) and saveReplay/loadReplay static methods
  • src/core/replay_io.cpp — Full implementation with format:
HEADER
rows=7
cols=7
p1_row=0
p1_col=0
p2_row=6
p2_col=6
END_HEADER
TURNS
0 1 2 3 4 120 85
...

Key correctness/robustness decisions

  • END_HEADER explicit delimiter — avoids blank-line truncation bugs
  • Validates headerFound && endHeaderFound, rows/cols > 0, actor ∈ {0,1}, and player positions within board bounds before constructing GameState
  • loadReplay fully restores GameState: player positions, sideToMove, phase, status
  • history cleared before loading to prevent stale-data accumulation
  • std::stoi wrapped in try/catch; localtime_r used over localtime
  • long for thinkTicks fields, consistent with TurnRecord

Modified files

  • Makefilesrc/core/replay_io.cpp added to both manual_curses_match and test_main_menu targets
  • .gitignore — added to exclude *.o artifacts and compiled binaries
Original prompt

Save and load replay data to/from files in replays/ directory.

New Files

include/core/replay_io.hpp

#pragma once
#include <string>
#include <vector>
#include "game_state.hpp"
#include "turn_record.hpp"

class ReplayIO
{
public:
    struct ReplayData
    {
        int rows;
        int cols;
        GameState initialState;
        std::vector<TurnRecord> history;
    };

    static bool saveReplay(int rows,
                          int cols,
                          const GameState& initialState,
                          const std::vector<TurnRecord>& history,
                          std::string& outFilename);

    static bool loadReplay(const std::string& filepath,
                          ReplayData& outData);

private:
    static const std::string REPLAY_DIR;
    static bool ensureDirectoryExists();
    static std::string generateFilename();
};

src/core/replay_io.cpp

#include "core/replay_io.hpp"
#include <fstream>
#include <sstream>
#include <ctime>
#include <filesystem>

const std::string ReplayIO::REPLAY_DIR = "replays";

bool ReplayIO::ensureDirectoryExists()
{
    try
    {
        if (!std::filesystem::exists(REPLAY_DIR))
        {
            std::filesystem::create_directory(REPLAY_DIR);
        }
        return true;
    }
    catch (const std::exception& e)
    {
        return false;
    }
}

std::string ReplayIO::generateFilename()
{
    time_t now = time(nullptr);
    struct tm timeinfo = *localtime(&now);

    char buffer[64];
    strftime(buffer, sizeof(buffer), "replay_%Y%m%d_%H%M%S.txt", &timeinfo);

    return std::string(REPLAY_DIR) + "/" + std::string(buffer);
}

bool ReplayIO::saveReplay(int rows,
                         int cols,
                         const GameState& initialState,
                         const std::vector<TurnRecord>& history,
                         std::string& outFilename)
{
    if (!ensureDirectoryExists())
    {
        return false;
    }

    outFilename = generateFilename();

    std::ofstream file(outFilename);
    if (!file.is_open())
    {
        return false;
    }

    file << "HEADER\n";
    file << "rows=" << rows << "\n";
    file << "cols=" << cols << "\n";

    Coord p1Pos = initialState.playerPos(Side::Player1);
    file << "p1_row=" << p1Pos.row << "\n";
    file << "p1_col=" << p1Pos.col << "\n";

    Coord p2Pos = initialState.playerPos(Side::Player2);
    file << "p2_row=" << p2Pos.row << "\n";
    file << "p2_col=" << p2Pos.col << "\n";

    file << "END_HEADER\n";

    file << "TURNS\n";
    for (const auto& turn : history)
    {
        int actor;
        if (turn.actor == Side::Player1)
        {
            actor = 0;
        }
        else
        {
            actor = 1;
        }

        file << actor << " "
             << turn.moveCoord.row << " " << turn.moveCoord.col << " "
             << turn.breakCoord.row << " " << turn.breakCoord.col << " "
             << turn.thinkTicksBeforeMove << " " << turn.thinkTicksBeforeBreak << "\n";
    }

    file.close();
    return true;
}

bool ReplayIO::loadReplay(const std::string& filepath,
                         ReplayData& outData)
{
    std::ifstream file(filepath);
    if (!file.is_open())
    {
        return false;
    }

    outData.history.clear();

    std::string line;
    int p1Row = 0;
    int p1Col = 0;
    int p2Row = 0;
    int p2Col = 0;

    bool headerFound = false;
    bool endHeaderFound = false;

    while (std::getline(file, line))
    {
        if (line == "HEADER")
        {
            headerFound = true;
            continue;
        }

        if (line == "END_HEADER")
        {
            endHeaderFound = true;
            break;
        }

        if (!headerFound)
        {
            continue;
        }

        if (line.find("rows=") == 0)
        {
            outData.rows = std::stoi(line.substr(5));
        }
        else if (line.find("cols=") == 0)
        {
            outData.cols = std::stoi(line.substr(5));
        }
        else if (line.find("p1_row=") == 0)
        {
            p1Row = std::stoi(line.substr(7));
        }
        else if (line.find("p1_col=") == 0)
        {
            p1Col = std::stoi(line.substr(7));
        }
        else if (line.find("p2_row=") == 0)
        {
            p2Row = std::stoi(line.substr(7));
        }
        else if (line.find("p2_col=") == 0)
        {
            p2Col = std::stoi(line.substr(7));
        }
    }

    if (!headerFound || !endHeaderFound)
    {
        file.close();
        return false;
    }

    if (outData.rows <= 0 || outData.cols <= 0)
    {
        file.close();
        return false;
    }

    outData.initialState = GameState(outData.rows, outData.cols);
    outData.initialState.setPlayerPos(Side::Player1, Coord{p1Row, p1Col});
    outData.initialState.setPlayerPos(Side::Player2, Coord{p2Row, p2Col});
    outData.initialState.setSideToMove(Side::Player1);
    outData.initialState.setPhase(TurnPhase::NewTurn);
    outData.initialSta...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

Copilot AI changed the title [WIP] Add functionality to save and load replay data from files Add ReplayIO: save/load replay data to/from replays/ directory Apr 12, 2026
Copilot AI requested a review from ScottWang05 April 12, 2026 15:31
@ScottWang05
ScottWang05 deleted the copilot/save-load-replay-data-files branch April 12, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants