amaze 0.4.1

A maze generator
Documentation
use crate::direction4::Direction4;
use crate::direction6::Direction6;
use crate::wall4_grid::Wall4Grid;
use crate::wall6_grid::Wall6Grid;

#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct MazeJson {
    version: u8,
    #[serde(rename = "type")]
    maze_type: String,
    width: usize,
    height: usize,
    cells: Vec<u8>,
}

#[derive(Debug)]
pub enum JsonError {
    Serialize(serde_json::Error),
    Deserialize(serde_json::Error),
    InvalidType(String),
    InvalidVersion(u8),
    InvalidCellCount { expected: usize, got: usize },
}

impl std::fmt::Display for JsonError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            JsonError::Serialize(e) => write!(f, "Serialization error: {e}"),
            JsonError::Deserialize(e) => write!(f, "Deserialization error: {e}"),
            JsonError::InvalidType(msg) => write!(f, "Invalid maze type: {msg}"),
            JsonError::InvalidVersion(v) => write!(f, "Unsupported version: {v}"),
            JsonError::InvalidCellCount { expected, got } => {
                write!(f, "Invalid cell count: expected {expected}, got {got}")
            }
        }
    }
}

impl std::error::Error for JsonError {}

impl From<serde_json::Error> for JsonError {
    fn from(e: serde_json::Error) -> Self {
        JsonError::Deserialize(e)
    }
}

pub trait ToJson {
    fn to_json(&self) -> Result<String, JsonError>;
}

pub trait FromJson: Sized {
    fn from_json(json: &str) -> Result<Self, JsonError>;
}

impl ToJson for Wall4Grid {
    fn to_json(&self) -> Result<String, JsonError> {
        let cells: Vec<u8> = self.coords().map(|c| *self[c]).collect();
        let maze = MazeJson {
            version: 1,
            maze_type: "square".into(),
            width: self.width(),
            height: self.height(),
            cells,
        };
        serde_json::to_string(&maze).map_err(JsonError::Serialize)
    }
}

impl FromJson for Wall4Grid {
    fn from_json(json: &str) -> Result<Self, JsonError> {
        let maze: MazeJson = serde_json::from_str(json).map_err(JsonError::Deserialize)?;

        if maze.version != 1 {
            return Err(JsonError::InvalidVersion(maze.version));
        }
        if maze.maze_type != "square" {
            return Err(JsonError::InvalidType(maze.maze_type));
        }

        let expected = maze.width * maze.height;
        if maze.cells.len() != expected {
            return Err(JsonError::InvalidCellCount {
                expected,
                got: maze.cells.len(),
            });
        }

        let mut grid = Wall4Grid::new(maze.width, maze.height);
        for (i, coord) in grid.coords().collect::<Vec<_>>().into_iter().enumerate() {
            let byte = maze.cells[i];
            if byte & !0b00001111 != 0 {
                return Err(JsonError::InvalidType(format!(
                    "invalid wall byte 0b{byte:08b}"
                )));
            }
            grid[coord] = Direction4::from_bits(byte);
        }
        Ok(grid)
    }
}

impl ToJson for Wall6Grid {
    fn to_json(&self) -> Result<String, JsonError> {
        let cells: Vec<u8> = self.coords().map(|c| *self[c]).collect();
        let maze = MazeJson {
            version: 1,
            maze_type: "hex".into(),
            width: self.width(),
            height: self.height(),
            cells,
        };
        serde_json::to_string(&maze).map_err(JsonError::Serialize)
    }
}

impl FromJson for Wall6Grid {
    fn from_json(json: &str) -> Result<Self, JsonError> {
        let maze: MazeJson = serde_json::from_str(json).map_err(JsonError::Deserialize)?;

        if maze.version != 1 {
            return Err(JsonError::InvalidVersion(maze.version));
        }
        if maze.maze_type != "hex" {
            return Err(JsonError::InvalidType(maze.maze_type));
        }

        let expected = maze.width * maze.height;
        if maze.cells.len() != expected {
            return Err(JsonError::InvalidCellCount {
                expected,
                got: maze.cells.len(),
            });
        }

        let mut grid = Wall6Grid::new(maze.width, maze.height);
        for (i, coord) in grid.coords().collect::<Vec<_>>().into_iter().enumerate() {
            let byte = maze.cells[i];
            if byte & !0b00111111 != 0 {
                return Err(JsonError::InvalidType(format!(
                    "invalid wall byte 0b{byte:08b}"
                )));
            }
            grid[coord] = Direction6::from_bits(byte);
        }
        Ok(grid)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::generators::RecursiveBacktracker4;

    #[test]
    fn roundtrip_square_json() {
        let maze = RecursiveBacktracker4::new_from_seed(42).generate(10, 10);
        let json = maze.to_json().unwrap();
        let restored = Wall4Grid::from_json(&json).unwrap();

        assert_eq!(maze.width(), restored.width());
        assert_eq!(maze.height(), restored.height());
        for coord in maze.coords() {
            assert_eq!(maze[coord], restored[coord]);
        }
    }

    #[test]
    fn json_contains_expected_fields() {
        let maze = RecursiveBacktracker4::new_from_seed(42).generate(5, 5);
        let json = maze.to_json().unwrap();
        let parsed: MazeJson = serde_json::from_str(&json).unwrap();

        assert_eq!(parsed.version, 1);
        assert_eq!(parsed.maze_type, "square");
        assert_eq!(parsed.width, 5);
        assert_eq!(parsed.height, 5);
        assert_eq!(parsed.cells.len(), 25);
    }

    #[test]
    #[cfg(feature = "generator-hex-recursive-backtracker")]
    fn hex_json_roundtrip() {
        use crate::generators::RecursiveBacktracker6;
        let maze = RecursiveBacktracker6::new_from_seed(42).generate(5, 5);
        let json = maze.to_json().unwrap();
        let restored = Wall6Grid::from_json(&json).unwrap();

        assert_eq!(maze.width(), restored.width());
        assert_eq!(maze.height(), restored.height());
        for coord in maze.coords() {
            assert_eq!(*maze.get(coord).unwrap(), *restored.get(coord).unwrap());
        }
    }
}