#![warn(missing_docs)]
use rand::{prelude::SliceRandom, Rng};
use std::convert::TryInto;
use std::ops::{Add, Index, IndexMut, Mul};
use std::slice::{Iter, IterMut};
use thiserror::Error;
const TILE_FLOOR: u8 = 0;
const TILE_WALL: u8 = 1;
#[derive(Error, Debug, PartialEq, Eq)]
pub enum MazeGenerationError {
#[error("maze dimensions must be odd and >= 5")]
InvalidDimensions,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Direction {
North,
East,
South,
West,
}
const ALL_DIRS: [Direction; 4] = [
Direction::North,
Direction::East,
Direction::South,
Direction::West,
];
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
struct TinyVec {
x: isize,
y: isize,
}
impl Add for TinyVec {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self::Output {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
impl Mul<isize> for TinyVec {
type Output = Self;
fn mul(self, rhs: isize) -> Self::Output {
Self::Output {
x: self.x * rhs,
y: self.y * rhs,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Maze {
pub width: usize,
pub height: usize,
data: Vec<Vec<u8>>
}
impl Index<usize> for Maze {
type Output = Vec<u8>;
fn index(&self, index: usize) -> &Self::Output {
&self.data[index]
}
}
impl IndexMut<usize> for Maze {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.data[index]
}
}
impl IntoIterator for Maze {
type Item = Vec<u8>;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.data.into_iter()
}
}
impl Maze {
pub fn new(width: usize, height: usize) -> Result<Self, MazeGenerationError> {
if width < 5 || width % 2 == 0 || height < 5 || height % 2 == 0 {
return Err(MazeGenerationError::InvalidDimensions);
}
Ok(Self {
width,
height,
data: vec![vec![TILE_WALL; height]; width]
})
}
pub fn iter(&self) -> Iter<Vec<u8>> {
self.data.iter()
}
pub fn iter_mut(&mut self) -> IterMut<Vec<u8>> {
self.data.iter_mut()
}
pub fn generate<R>(mut self, rng: &mut R) -> Self
where
R: Rng + ?Sized,
{
let mut stack = Vec::new();
let start = TinyVec { x: 1, y: 1 };
stack.push((start, start));
while !stack.is_empty() {
let (path, cell) = stack.pop().unwrap();
if self.data[cell.x as usize][cell.y as usize] == TILE_WALL {
self.data[path.x as usize][path.y as usize] = TILE_FLOOR;
self.data[cell.x as usize][cell.y as usize] = TILE_FLOOR;
let mut dirs = ALL_DIRS.clone();
dirs.shuffle(rng);
for dir in dirs {
let step = match dir {
Direction::North => TinyVec { x: 0, y: -1 },
Direction::East => TinyVec { x: 1, y: 0 },
Direction::South => TinyVec { x: 0, y: 1 },
Direction::West => TinyVec { x: -1, y: 0 },
};
let double = cell + step * 2;
if double.x >= 0
&& double.x < self.width.try_into().unwrap()
&& double.y >= 0
&& double.y < self.height.try_into().unwrap()
&& self.data[double.x as usize][double.y as usize] == TILE_WALL
{
stack.push((cell + step, double));
}
}
}
}
self
}
pub fn to_pbm(&self) -> String {
let mut pbm = String::from("P1\n");
pbm.push_str(&format!("{} {}\n", self.width, self.height));
for y in 0..self.height {
for x in 0..self.width {
pbm.push_str(&format!("{} ", self.data[x][y]));
}
pbm.push_str("\n");
}
pbm
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dimensions() {
assert_eq!(Maze::new(3, 7), Err(MazeGenerationError::InvalidDimensions));
assert_eq!(Maze::new(9, 6), Err(MazeGenerationError::InvalidDimensions));
let (w, h) = (7, 5);
let maze = Maze::new(w, h);
assert_eq!(maze.is_ok(), true);
if let Ok(maze) = maze {
assert_eq!(maze.width, w);
assert_eq!(maze.data.len(), w);
assert_eq!(maze.height, h);
assert_eq!(maze.data[0].len(), h);
}
}
#[test]
fn test_generation() {
use rand::thread_rng;
let mut rng = thread_rng();
let maze = Maze::new(13, 13).unwrap().generate(&mut rng);
assert_eq!(maze.iter().flatten().all(|&tile| tile == TILE_WALL), false);
assert_eq!(maze.data.len(), maze.width);
assert_eq!(maze.data[0].len(), maze.height);
for y in 0..maze.height {
for x in 0..maze.width {
if y == 0 || y == maze.height - 1 {
assert_eq!(maze[x][y], TILE_WALL);
} else if x == 0 || x == maze.width - 1 {
assert_eq!(maze[x][y], TILE_WALL);
}
}
}
}
#[test]
fn test_pbm() {
use rand::thread_rng;
let mut rng = thread_rng();
let (w, h) = (11, 13);
let maze = Maze::new(w, h).unwrap().generate(&mut rng);
let pbm = maze.to_pbm();
assert_eq!(pbm.starts_with(&format!("P1\n{} {}\n", w, h)), true);
let data: Vec<&str> = pbm.lines().skip(2).collect();
for y in 0..maze.height {
let tiles: Vec<u8> = data[y]
.split_whitespace()
.map(|t| t.parse::<u8>().unwrap())
.collect();
for x in 0..maze.width {
assert_eq!(tiles[x], maze[x][y]);
}
}
}
}