"""File handling for saving and loading mazes.""" import json import os from pathlib import Path from typing import Optional from ..core.maze import Maze class FileHandler: """Handles saving and loading mazes to/from files.""" DEFAULT_SAVE_DIR = "saved_mazes" @staticmethod def ensure_save_directory(directory: Optional[str] = None) -> Path: """Ensure the save directory exists. Args: directory: Directory path (uses default if None) Returns: Path object for the directory """ save_dir = Path(directory) if directory else Path(FileHandler.DEFAULT_SAVE_DIR) save_dir.mkdir(parents=True, exist_ok=True) return save_dir @staticmethod def save_maze(maze: Maze, filename: str, directory: Optional[str] = None) -> str: """Save a maze to a JSON file. Args: maze: Maze to save filename: Name of the file (without extension) directory: Directory to save to (uses default if None) Returns: Full path to the saved file Raises: IOError: If file cannot be written """ save_dir = FileHandler.ensure_save_directory(directory) # Ensure .json extension if not filename.endswith('.json'): filename += '.json' file_path = save_dir / filename try: with open(file_path, 'w', encoding='utf-8') as f: f.write(maze.to_json()) return str(file_path) except Exception as e: raise IOError(f"Failed to save maze to {file_path}: {e}") @staticmethod def load_maze(filename: str, directory: Optional[str] = None) -> Maze: """Load a maze from a JSON file. Args: filename: Name of the file to load directory: Directory to load from (uses default if None) Returns: Loaded Maze instance Raises: FileNotFoundError: If file doesn't exist ValueError: If file contains invalid maze data IOError: If file cannot be read """ save_dir = Path(directory) if directory else Path(FileHandler.DEFAULT_SAVE_DIR) # Ensure .json extension if not filename.endswith('.json'): filename += '.json' file_path = save_dir / filename if not file_path.exists(): raise FileNotFoundError(f"Maze file not found: {file_path}") try: with open(file_path, 'r', encoding='utf-8') as f: json_str = f.read() # Validate JSON structure data = json.loads(json_str) FileHandler._validate_maze_data(data) return Maze.from_json(json_str) except json.JSONDecodeError as e: raise ValueError(f"Invalid JSON in maze file: {e}") except Exception as e: raise IOError(f"Failed to load maze from {file_path}: {e}") @staticmethod def _validate_maze_data(data: dict) -> None: """Validate maze data structure. Args: data: Dictionary with maze data Raises: ValueError: If data is invalid """ required_fields = ["rows", "cols", "seed", "grid"] for field in required_fields: if field not in data: raise ValueError(f"Missing required field: {field}") # Validate dimensions rows = data["rows"] cols = data["cols"] if not (5 <= rows <= 50) or not (5 <= cols <= 50): raise ValueError("Invalid maze dimensions") # Validate grid structure grid = data["grid"] if len(grid) != rows: raise ValueError("Grid row count doesn't match maze rows") for row_idx, row in enumerate(grid): if len(row) != cols: raise ValueError(f"Grid column count doesn't match at row {row_idx}") @staticmethod def list_saved_mazes(directory: Optional[str] = None) -> list: """List all saved maze files. Args: directory: Directory to list from (uses default if None) Returns: List of maze filenames """ save_dir = Path(directory) if directory else Path(FileHandler.DEFAULT_SAVE_DIR) if not save_dir.exists(): return [] return [f.name for f in save_dir.glob("*.json")] @staticmethod def delete_maze(filename: str, directory: Optional[str] = None) -> bool: """Delete a saved maze file. Args: filename: Name of the file to delete directory: Directory containing the file (uses default if None) Returns: True if deleted successfully, False if file didn't exist """ save_dir = Path(directory) if directory else Path(FileHandler.DEFAULT_SAVE_DIR) if not filename.endswith('.json'): filename += '.json' file_path = save_dir / filename if file_path.exists(): file_path.unlink() return True return False