from __future__ import annotations import json import os import uuid from datetime import datetime from typing import Any class RunStore: """File-based storage for game replays as JSON files. Each saved run is a self-contained JSON file with game configuration, move history, and computed statistics. """ def __init__(self, runs_dir: str | None = None): if runs_dir is None: runs_dir = os.path.join( os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "runs" ) self.runs_dir = os.path.abspath(runs_dir) os.makedirs(self.runs_dir, exist_ok=True) def save(self, session, name: str | None = None) -> str: """Save a GameSession as a replay file. Returns the run_id.""" data = session.to_dict() if name is None: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") bot_type = data["config"]["bot_type"] name = f"{bot_type}_{timestamp}" run_id = f"{name}_{uuid.uuid4().hex[:8]}" filepath = os.path.join(self.runs_dir, f"{run_id}.json") run_data = { "run_id": run_id, "name": name, "created_at": datetime.now().isoformat(), "data": data, } with open(filepath, "w") as f: json.dump(run_data, f, indent=2) return run_id def list_runs(self) -> list[dict[str, Any]]: """List all saved runs with metadata, sorted by date (newest first).""" runs = [] for filename in os.listdir(self.runs_dir): if not filename.endswith(".json"): continue filepath = os.path.join(self.runs_dir, filename) try: with open(filepath) as f: run_data = json.load(f) config = run_data["data"]["config"] stats = run_data["data"].get("statistics", {}) scores = run_data["data"].get("scores", []) runs.append( { "run_id": run_data["run_id"], "name": run_data["name"], "created_at": run_data["created_at"], "board_size": config["board_size"], "num_players": config["num_players"], "human_player": config["human_player"], "bot_type": config["bot_type"], "game_over": run_data["data"]["game_over"], "winner": run_data["data"]["winner"], "scores": scores, "total_moves": stats.get("total_moves", 0), "coverage": stats.get("coverage", 0), } ) except (json.JSONDecodeError, KeyError): continue runs.sort(key=lambda r: r["created_at"], reverse=True) return runs def load(self, run_id: str) -> dict[str, Any] | None: """Load a run by ID. Returns the full run data dict, or None.""" filepath = os.path.join(self.runs_dir, f"{run_id}.json") if not os.path.exists(filepath): return None with open(filepath) as f: return json.load(f) def delete(self, run_id: str) -> bool: """Delete a run by ID. Returns True if deleted, False if not found.""" filepath = os.path.join(self.runs_dir, f"{run_id}.json") if os.path.exists(filepath): os.remove(filepath) return True return False