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;
use std::io::{self, Write};

const MAGIC: [u8; 4] = *b"AMZE";
const VERSION: u8 = 1;
const TYPE_SQUARE: u8 = 0;
const TYPE_HEX: u8 = 1;

#[derive(Debug)]
pub enum BinaryError {
    Io(io::Error),
    InvalidHeader(String),
    InvalidData(String),
}

impl std::fmt::Display for BinaryError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            BinaryError::Io(e) => write!(f, "I/O error: {e}"),
            BinaryError::InvalidHeader(msg) => write!(f, "Invalid header: {msg}"),
            BinaryError::InvalidData(msg) => write!(f, "Invalid data: {msg}"),
        }
    }
}

impl std::error::Error for BinaryError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            BinaryError::Io(e) => Some(e),
            _ => None,
        }
    }
}

impl From<io::Error> for BinaryError {
    fn from(e: io::Error) -> Self {
        BinaryError::Io(e)
    }
}

pub trait ToBinary {
    fn to_binary(&self) -> Result<Vec<u8>, BinaryError>;
}

pub trait FromBinary: Sized {
    fn from_binary(data: &[u8]) -> Result<Self, BinaryError>;
}

impl ToBinary for Wall4Grid {
    fn to_binary(&self) -> Result<Vec<u8>, BinaryError> {
        let mut buf = Vec::with_capacity(8 + self.width() * self.height());
        buf.write_all(&MAGIC)?;
        buf.write_all(&[VERSION, TYPE_SQUARE])?;
        buf.write_all(&(self.width() as u16).to_le_bytes())?;
        buf.write_all(&(self.height() as u16).to_le_bytes())?;
        for cell in self.coords() {
            buf.push(*self[cell]);
        }
        Ok(buf)
    }
}

impl FromBinary for Wall4Grid {
    fn from_binary(data: &[u8]) -> Result<Self, BinaryError> {
        if data.len() < 8 {
            return Err(BinaryError::InvalidHeader("data too short".into()));
        }
        if data[0..4] != MAGIC {
            return Err(BinaryError::InvalidHeader("invalid magic bytes".into()));
        }
        if data[4] != VERSION {
            return Err(BinaryError::InvalidHeader(format!(
                "unsupported version {}",
                data[4]
            )));
        }
        if data[5] != TYPE_SQUARE {
            return Err(BinaryError::InvalidHeader("not a square maze".into()));
        }

        let width = u16::from_le_bytes([data[6], data[7]]) as usize;
        let height = u16::from_le_bytes([data[8], data[9]]) as usize;
        let expected = 10 + width * height;
        if data.len() < expected {
            return Err(BinaryError::InvalidData(format!(
                "expected {expected} bytes, got {}",
                data.len()
            )));
        }

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

impl ToBinary for Wall6Grid {
    fn to_binary(&self) -> Result<Vec<u8>, BinaryError> {
        let mut buf = Vec::with_capacity(8 + self.width() * self.height());
        buf.write_all(&MAGIC)?;
        buf.write_all(&[VERSION, TYPE_HEX])?;
        buf.write_all(&(self.width() as u16).to_le_bytes())?;
        buf.write_all(&(self.height() as u16).to_le_bytes())?;
        for cell in self.coords() {
            buf.push(*self[cell]);
        }
        Ok(buf)
    }
}

impl FromBinary for Wall6Grid {
    fn from_binary(data: &[u8]) -> Result<Self, BinaryError> {
        if data.len() < 8 {
            return Err(BinaryError::InvalidHeader("data too short".into()));
        }
        if data[0..4] != MAGIC {
            return Err(BinaryError::InvalidHeader("invalid magic bytes".into()));
        }
        if data[4] != VERSION {
            return Err(BinaryError::InvalidHeader(format!(
                "unsupported version {}",
                data[4]
            )));
        }
        if data[5] != TYPE_HEX {
            return Err(BinaryError::InvalidHeader("not a hex maze".into()));
        }

        let width = u16::from_le_bytes([data[6], data[7]]) as usize;
        let height = u16::from_le_bytes([data[8], data[9]]) as usize;
        let expected = 10 + width * height;
        if data.len() < expected {
            return Err(BinaryError::InvalidData(format!(
                "expected {expected} bytes, got {}",
                data.len()
            )));
        }

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

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

    #[test]
    fn roundtrip_square_maze() {
        let maze = RecursiveBacktracker4::new_from_seed(42).generate(10, 10);
        let bytes = maze.to_binary().unwrap();
        let restored = Wall4Grid::from_binary(&bytes).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 invalid_magic_returns_error() {
        let data = [0, 0, 0, 0, 1, 0, 0, 0, 0, 0];
        assert!(Wall4Grid::from_binary(&data).is_err());
    }

    #[test]
    fn too_short_returns_error() {
        assert!(Wall4Grid::from_binary(b"AMZE").is_err());
    }

    #[test]
    #[cfg(feature = "generator-hex-recursive-backtracker")]
    fn hex_roundtrip() {
        use crate::generators::RecursiveBacktracker6;
        let maze = RecursiveBacktracker6::new_from_seed(42).generate(5, 5);
        let bytes = maze.to_binary().unwrap();
        let restored = Wall6Grid::from_binary(&bytes).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());
        }
    }
}