├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE ├── README.md ├── maker ├── Cargo.toml └── src │ ├── main.rs │ └── types │ ├── background_renderer.rs │ ├── break_post_processor.rs │ ├── heatmap_renderer.rs │ ├── mask_initializer.rs │ ├── mod.rs │ ├── solve_renderer.rs │ └── text_renderer.rs ├── maze ├── Cargo.toml ├── benches │ ├── initialize.rs │ └── walk.rs └── src │ ├── initialize │ ├── braid.rs │ ├── branching.rs │ ├── clear.rs │ ├── dividing.rs │ ├── mod.rs │ ├── spelunker.rs │ └── winding.rs │ ├── lib.rs │ ├── macros.rs │ ├── matrix.rs │ ├── physical.rs │ ├── render │ ├── mod.rs │ └── svg.rs │ ├── room.rs │ ├── shape │ ├── hex.rs │ ├── mod.rs │ ├── quad.rs │ └── tri.rs │ ├── test_utils.rs │ ├── walk.rs │ └── wall.rs ├── test ├── Cargo.toml └── src │ └── lib.rs ├── tools ├── Cargo.toml └── src │ ├── alphabet │ ├── default.rs │ ├── macros.rs │ └── mod.rs │ ├── cell │ └── mod.rs │ ├── image │ └── mod.rs │ ├── lib.rs │ └── voronoi │ ├── initialize.rs │ └── mod.rs └── web ├── Cargo.toml └── src ├── main.rs └── types ├── dimensions.rs ├── maze_type.rs ├── mod.rs └── seed.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | 3 | # This is a library 4 | Cargo.lock 5 | 6 | # Vim swap files 7 | *.swo 8 | *.swp 9 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace.package] 2 | version = "3.1.1" 3 | authors = ["Moses Palmér "] 4 | edition = "2021" 5 | 6 | [workspace.dependencies] 7 | actix-web = "4.9" 8 | bit-set = "0.8" 9 | clap = { version = "4.5", features = [ "cargo", "derive" ] } 10 | futures-util = "0.3" 11 | image = "0.25" 12 | lazy_static = "1.5" 13 | rand = "0.8" 14 | rayon = "1.10" 15 | serde = { version = "1", features = ["derive"] } 16 | serde_json = "1" 17 | serde_urlencoded = "0.7" 18 | svg = "0.17" 19 | 20 | [workspace] 21 | resolver = "2" 22 | members = [ 23 | "maze", 24 | "maker", 25 | "test", 26 | "tools", 27 | "web", 28 | ] 29 | 30 | [profile.bench] 31 | debug = true 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # labyru 2 | 3 | *labyru* generates mazes. 4 | 5 | ## Usage 6 | 7 | ```bash 8 | ./maze-maker --help 9 | Usage: maze-maker [OPTIONS] --method 10 | 11 | Arguments: 12 | 13 | The output SVG 14 | 15 | Options: 16 | --walls 17 | The number of walls per room: 3, 4 or 6 18 | 19 | [default: 4] 20 | 21 | --width 22 | The width of the maze, in rooms 23 | 24 | --height 25 | The height of the maze, in rooms 26 | 27 | --method 28 | The initialisation methods to use. 29 | 30 | This is a comma separated list of the following values: 31 | 32 | braid: A maze containing loops. 33 | 34 | branching: A maze the frequently branches. 35 | 36 | winding: A maze with long corridors. 37 | 38 | clear: A clear area. 39 | 40 | --scale 41 | A relative size for the maze, applied to rooms 42 | 43 | [default: 10] 44 | 45 | --seed 46 | A seed for the random number generator 47 | 48 | --margin 49 | The margin around the maze 50 | 51 | [default: 10] 52 | 53 | --mask 54 | A mask image to determine which rooms are part of the mask and thenshold luminosity value between 0 and 1 on the form "path,0.5" 55 | 56 | --heat-map 57 | Whether to create a heat map 58 | 59 | --background 60 | A background image to colour rooms 61 | 62 | --ratio 63 | A ratio for pixels per room when using a background 64 | 65 | --text 66 | A text to draw on the maze 67 | 68 | --solve 69 | Whether to solve the maze, and the solution colour. If not specified, the colour defaults to "black" 70 | 71 | --break 72 | Whether to break the maze 73 | 74 | -h, --help 75 | Print help (see a summary with '-h') 76 | 77 | -V, --version 78 | Print version 79 | ``` 80 | -------------------------------------------------------------------------------- /maker/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maze-maker" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | maze = { path = "../maze" } 9 | maze-tools = { path = "../tools" } 10 | 11 | clap = { workspace = true } 12 | image = { workspace = true } 13 | rand = { workspace = true } 14 | rayon = { workspace = true } 15 | svg = { workspace = true } 16 | -------------------------------------------------------------------------------- /maker/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use clap::{arg, Parser}; 4 | use svg::Node; 5 | 6 | use maze::render::svg::ToPath; 7 | 8 | mod types; 9 | use self::types::*; 10 | 11 | /// Generates mazes. 12 | #[derive(Parser)] 13 | #[command(author, version, about)] 14 | struct Arguments { 15 | /// The number of walls per room: 3, 4 or 6. 16 | #[arg( 17 | id = "SHAPE", 18 | long = "walls", 19 | default_value = "4", 20 | value_parser = |s: &str| -> Result { 21 | s.parse::() 22 | .map_err(|_| format!("invalid number: {}", s)) 23 | .and_then(|n| n.try_into() 24 | .map_err(|e| format!("invalid number of walls: {}", e))) 25 | }, 26 | )] 27 | shape: maze::Shape, 28 | 29 | /// The width of the maze, in rooms. 30 | #[arg( 31 | id = "WIDTH", 32 | long = "width", 33 | required_unless_present_all(["BACKGROUND", "RATIO"]), 34 | )] 35 | width: Option, 36 | 37 | /// The height of the maze, in rooms. 38 | #[arg( 39 | id = "HEIGHT", 40 | long = "height", 41 | required_unless_present_all(["BACKGROUND", "RATIO"]), 42 | )] 43 | height: Option, 44 | 45 | /// The initialisation methods to use. 46 | /// 47 | /// This is a comma separated list of the following values: 48 | /// 49 | /// braid: A maze containing loops. 50 | /// 51 | /// branching: A maze the frequently branches. 52 | /// 53 | /// winding: A maze with long corridors. 54 | /// 55 | /// clear: A clear area. 56 | #[arg(id = "METHOD", long = "method", required(true))] 57 | methods: Methods, 58 | 59 | /// A relative size for the maze, applied to rooms. 60 | #[arg(id = "SCALE", long = "scale", default_value_t = 10.0)] 61 | scale: f32, 62 | 63 | /// A seed for the random number generator. 64 | #[arg(id = "SEED", long = "seed")] 65 | seed: Option, 66 | 67 | /// The margin around the maze. 68 | #[arg(id = "MARGIN", long = "margin", default_value_t = 10.0)] 69 | margin: f32, 70 | 71 | /// A mask image to determine which rooms are part of the mask and 72 | /// thenshold luminosity value between 0 and 1 on the form "path,0.5". 73 | #[arg(id = "INITIALIZE", long = "mask")] 74 | initialize_mask: Option>, 75 | 76 | /// Whether to create a heat map. 77 | #[arg(id = "HEATMAP", long = "heat-map")] 78 | render_heatmap: Option, 79 | 80 | /// A background image to colour rooms. 81 | #[arg(id = "BACKGROUND", long = "background")] 82 | render_background: Option, 83 | 84 | /// A ratio for pixels per room when using a background. 85 | #[arg( 86 | id = "RATIO", 87 | long = "ratio", 88 | conflicts_with_all(["WIDTH", "HEIGHT"]), 89 | requires("BACKGROUND"), 90 | )] 91 | render_background_ratio: Option, 92 | 93 | /// A text to draw on the maze. 94 | #[arg(id = "TEXT", long = "text")] 95 | render_text: Option, 96 | 97 | /// Whether to solve the maze, and the solution colour. If not specified, 98 | /// the colour defaults to "black". 99 | #[arg( 100 | id = "SOLVE", 101 | long = "solve", 102 | default_missing_value = "black", 103 | conflicts_with_all(["INITIALIZE"]), 104 | )] 105 | render_solve: Option, 106 | 107 | /// Whether to break the maze. 108 | #[arg(long = "break")] 109 | post_break: Option, 110 | 111 | /// The output SVG. 112 | #[arg(id = "PATH", required(true))] 113 | output: PathBuf, 114 | } 115 | 116 | #[allow(unused_variables, clippy::too_many_arguments)] 117 | fn run

( 118 | maze: Maze, 119 | scale: f32, 120 | margin: f32, 121 | renderers: &[&dyn Renderer], 122 | output: P, 123 | ) where 124 | P: AsRef, 125 | { 126 | let document = svg::Document::new() 127 | .set("viewBox", maze_to_viewbox(&maze, scale, margin)); 128 | let mut container = svg::node::element::Group::new() 129 | .set("transform", format!("scale({})", scale)); 130 | 131 | for renderer in renderers { 132 | renderer.render(&maze, &mut container); 133 | } 134 | 135 | // Draw the maze 136 | container.append( 137 | svg::node::element::Path::new() 138 | .set("fill", "none") 139 | .set("stroke", "black") 140 | .set("stroke-linecap", "round") 141 | .set("stroke-linejoin", "round") 142 | .set("stroke-width", 0.4) 143 | .set("vector-effect", "non-scaling-stroke") 144 | .set("d", maze.to_path_d()), 145 | ); 146 | 147 | svg::save(output, &document.add(container)).expect("failed to write SVG"); 148 | } 149 | 150 | /// Calculates the view box for a maze with a margin. 151 | /// 152 | /// # Arguments 153 | /// * `maze` - The maze for which to generate a view box. 154 | /// * `scale` - A scale multiplier. 155 | /// * `margin` - The margin to apply to all sides. 156 | fn maze_to_viewbox( 157 | maze: &Maze, 158 | scale: f32, 159 | margin: f32, 160 | ) -> (f32, f32, f32, f32) { 161 | (maze.viewbox() * scale).expand(margin).tuple() 162 | } 163 | 164 | #[allow(unused_mut)] 165 | fn main() { 166 | let args = Arguments::parse(); 167 | 168 | // Parse maze information 169 | let (width, height) = args 170 | .render_background_ratio 171 | .and_then(|render_background_ratio| { 172 | println!("RENDER BACKGROUND RATIO {}", render_background_ratio); 173 | args.render_background.as_ref().map(|render_background| { 174 | args.shape.minimal_dimensions( 175 | render_background.image.width() as f32 176 | / render_background_ratio, 177 | render_background.image.height() as f32 178 | / render_background_ratio, 179 | ) 180 | }) 181 | }) 182 | .unwrap_or_else(|| (args.width.unwrap(), args.height.unwrap())); 183 | 184 | let mut rng = args 185 | .seed 186 | .map(Random::from_seed) 187 | .unwrap_or_else(Random::from_os); 188 | 189 | // Make sure the maze is initialised 190 | let maze = { 191 | let mut maze = args.initialize_mask.initialize( 192 | args.shape.create(width, height), 193 | &mut rng, 194 | args.methods, 195 | ); 196 | 197 | [&args.post_break as &dyn PostProcessor<_>] 198 | .iter() 199 | .fold(maze, |maze, a| a.post_process(maze, &mut rng)) 200 | }; 201 | 202 | run( 203 | maze, 204 | args.scale, 205 | args.margin, 206 | &[ 207 | &args.render_background, 208 | &args.render_text, 209 | &args.render_heatmap, 210 | &args.render_solve, 211 | ], 212 | &args.output, 213 | ); 214 | } 215 | -------------------------------------------------------------------------------- /maker/src/types/background_renderer.rs: -------------------------------------------------------------------------------- 1 | use std::ops; 2 | use std::str::FromStr; 3 | 4 | use svg::Node; 5 | 6 | use maze::physical; 7 | use maze_tools::cell::*; 8 | use maze_tools::image::Color; 9 | 10 | use crate::types::*; 11 | 12 | /// A background image. 13 | #[derive(Clone)] 14 | pub struct BackgroundRenderer { 15 | /// The background image. 16 | pub image: image::RgbImage, 17 | } 18 | 19 | impl FromStr for BackgroundRenderer { 20 | type Err = String; 21 | 22 | /// Converts a string to a background description. 23 | /// 24 | /// The string must be a path. 25 | fn from_str(s: &str) -> Result { 26 | Ok(Self { 27 | image: image::open(s) 28 | .map_err(|_| format!("failed to open {}", s))? 29 | .to_rgb8(), 30 | }) 31 | } 32 | } 33 | 34 | impl Renderer for BackgroundRenderer { 35 | /// Applies the background action. 36 | /// 37 | /// This action will use an image to sample the background colour of rooms. 38 | /// 39 | /// # Arguments 40 | /// * `maze` - The maze. 41 | /// * `group` - The group to which to add the rooms. 42 | fn render(&self, maze: &Maze, group: &mut svg::node::element::Group) { 43 | let physical::ViewBox { width, height, .. } = maze.viewbox(); 44 | let (cols, rows) = self.image.dimensions(); 45 | let data = self 46 | .image 47 | .enumerate_pixels() 48 | .map(|(x, y, pixel)| { 49 | ( 50 | physical::Pos { 51 | x: width * (x as f32 / cols as f32), 52 | y: height * (y as f32 / rows as f32), 53 | }, 54 | Intermediate::from(pixel), 55 | ) 56 | }) 57 | .split_by(&maze.shape(), maze.width(), maze.height()); 58 | 59 | group.append(draw_rooms(maze, |pos| data[pos])); 60 | } 61 | } 62 | 63 | #[derive(Clone, Copy, Default)] 64 | struct Intermediate(u32, u32, u32); 65 | 66 | impl<'a, P> From<&'a P> for Intermediate 67 | where 68 | P: image::Pixel, 69 | { 70 | fn from(source: &'a P) -> Self { 71 | let channels = source.channels(); 72 | Intermediate( 73 | u32::from(channels[0]), 74 | u32::from(channels[1]), 75 | u32::from(channels[2]), 76 | ) 77 | } 78 | } 79 | 80 | impl ops::Add for Intermediate { 81 | type Output = Self; 82 | 83 | fn add(self, other: Self) -> Self { 84 | Intermediate(self.0 + other.0, self.1 + other.1, self.2 + other.2) 85 | } 86 | } 87 | 88 | impl ops::Div for Intermediate { 89 | type Output = Color; 90 | 91 | fn div(self, divisor: usize) -> Self::Output { 92 | Color { 93 | red: (self.0 / divisor as u32) as u8, 94 | green: (self.1 / divisor as u32) as u8, 95 | blue: (self.2 / divisor as u32) as u8, 96 | alpha: 255, 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /maker/src/types/break_post_processor.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use maze::initialize; 4 | 5 | use crate::types::*; 6 | 7 | /// A full description of the break action. 8 | #[derive(Clone)] 9 | pub struct BreakPostProcessor { 10 | /// The heat map type. 11 | pub map_type: HeatMapType, 12 | 13 | /// The number of times to apply the operation. 14 | pub count: usize, 15 | } 16 | 17 | impl FromStr for BreakPostProcessor { 18 | type Err = String; 19 | 20 | /// Converts a string to a break description. 21 | /// 22 | /// The string can be on two forms: 23 | /// 1. `map_type`: If only a value that can be made into a 24 | /// [`HeatMapType`](HeatMapType) is passed, the `count` will be `1`. 25 | /// 2. `map_type,count`: If a count is passed, it will be used as `count`. 26 | fn from_str(s: &str) -> Result { 27 | let mut parts = s.split(',').map(str::trim); 28 | let map_type = parts.next().map(HeatMapType::from_str).unwrap()?; 29 | 30 | if let Some(part1) = parts.next() { 31 | if let Ok(count) = part1.parse() { 32 | Ok(Self { map_type, count }) 33 | } else { 34 | Err(format!("invalid count: {}", part1)) 35 | } 36 | } else { 37 | Ok(Self { map_type, count: 1 }) 38 | } 39 | } 40 | } 41 | 42 | impl PostProcessor for BreakPostProcessor 43 | where 44 | R: initialize::Randomizer + Sized + Send + Sync, 45 | { 46 | /// Applies the break action. 47 | /// 48 | /// This action will repeatedly calculate a heat map, and then open walls in 49 | /// rooms with higher probability in hot rooms. 50 | /// 51 | /// # Arguments 52 | /// * `maze` - The maze. 53 | /// * `rng` - A random number generator. 54 | fn post_process(&self, mut maze: Maze, rng: &mut R) -> Maze { 55 | for _ in 0..self.count { 56 | let heat_map = self.map_type.generate(&maze); 57 | for pos in heat_map.positions() { 58 | if 1.0 / (rng.random() * f64::from(heat_map[pos])) < 0.5 { 59 | loop { 60 | let walls = maze.walls(pos); 61 | let wall = walls[rng.range(0, walls.len())]; 62 | if maze.is_inside(maze.back((pos, wall)).0) { 63 | maze.open((pos, wall)); 64 | break; 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | maze 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /maker/src/types/heatmap_renderer.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use svg::Node; 4 | 5 | use maze_tools::image::Color; 6 | 7 | use crate::types::*; 8 | 9 | /// A full description of the heat map action. 10 | #[derive(Clone)] 11 | pub struct HeatMapRenderer { 12 | /// The heat map type. 13 | pub map_type: HeatMapType, 14 | 15 | /// The colour of cold regioins. 16 | pub from: Color, 17 | 18 | /// The colour of hot regions. 19 | pub to: Color, 20 | } 21 | 22 | impl FromStr for HeatMapRenderer { 23 | type Err = String; 24 | 25 | /// Converts a string to a heat map description. 26 | /// 27 | /// The string can be on three forms: 28 | /// 1. `map_type`: If only a value that can be made into a 29 | /// [`HeatMapType`](HeatMapType) is passed, the `from` and `to` values 30 | /// will be `#000000FF` and `#FFFF0000`. 31 | /// 2. `map_type,colour`: If only one colour is passed, the `from` and `to` 32 | /// values will be `#00000000` and the colour passed. 33 | /// 3. `map_type,from,to`: If two colours are passed, they are used as 34 | /// `from` and `to` values. 35 | fn from_str(s: &str) -> Result { 36 | let mut parts = s.split(',').map(str::trim); 37 | let map_type = parts.next().map(HeatMapType::from_str).unwrap()?; 38 | 39 | if let Some(part1) = parts.next() { 40 | if let Some(part2) = parts.next() { 41 | Ok(Self { 42 | map_type, 43 | from: Color::from_str(part1)?, 44 | to: Color::from_str(part2)?, 45 | }) 46 | } else { 47 | Ok(Self { 48 | map_type, 49 | from: Color::from_str(part1).map(Color::transparent)?, 50 | to: Color::from_str(part1)?, 51 | }) 52 | } 53 | } else { 54 | Ok(Self { 55 | map_type, 56 | from: Color { 57 | red: 0, 58 | green: 0, 59 | blue: 255, 60 | alpha: 0, 61 | }, 62 | to: Color { 63 | red: 255, 64 | green: 0, 65 | blue: 0, 66 | alpha: 255, 67 | }, 68 | }) 69 | } 70 | } 71 | } 72 | 73 | impl Renderer for HeatMapRenderer { 74 | /// Applies the heat map action. 75 | /// 76 | /// This action will calculate a heat map, and use the heat of each room to 77 | /// interpolate between the colours in `action`. 78 | /// 79 | /// # Arguments 80 | /// * `maze` - The maze. 81 | /// * `group` - The group to which to add the rooms. 82 | fn render(&self, maze: &Maze, group: &mut svg::node::element::Group) { 83 | let matrix = self.map_type.generate(maze); 84 | let max = *matrix.values().max().unwrap() as f32; 85 | group.append(draw_rooms(maze, |pos| { 86 | self.to.fade(self.from, matrix[pos] as f32 / max) 87 | })); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /maker/src/types/mask_initializer.rs: -------------------------------------------------------------------------------- 1 | use std::ops; 2 | use std::str::FromStr; 3 | 4 | use maze::physical; 5 | use maze_tools::cell::*; 6 | 7 | use super::*; 8 | 9 | /// A constant used as multiplier for individual colour values to get an 10 | /// intensity 11 | const D: f32 = 1.0 / 255.0 / 3.0; 12 | 13 | /// A masking image. 14 | #[derive(Clone)] 15 | pub struct MaskInitializer 16 | where 17 | R: initialize::Randomizer + Sized + Send + Sync, 18 | { 19 | /// The mask image. 20 | pub image: image::RgbImage, 21 | 22 | /// The intensity threshold 23 | pub threshold: f32, 24 | 25 | _marker: ::std::marker::PhantomData, 26 | } 27 | 28 | impl FromStr for MaskInitializer 29 | where 30 | R: initialize::Randomizer + Sized + Send + Sync, 31 | { 32 | type Err = String; 33 | 34 | /// Converts a string to an initialise mask description. 35 | /// 36 | /// The string must be on the form `path,threshold`, where `path` is the 37 | /// path to an image and `threshold` is a value between 0 and 1. 38 | fn from_str(s: &str) -> Result { 39 | let mut parts = s.split(',').map(str::trim); 40 | let path = parts 41 | .next() 42 | .map(|p| std::path::Path::new(p).to_path_buf()) 43 | .unwrap(); 44 | 45 | if let Some(part1) = parts.next() { 46 | if let Ok(threshold) = part1.parse() { 47 | Ok(Self { 48 | image: image::open(path) 49 | .map_err(|_| format!("failed to open {}", s))? 50 | .to_rgb8(), 51 | threshold, 52 | _marker: ::std::marker::PhantomData, 53 | }) 54 | } else { 55 | Err(format!("invalid threshold: {}", part1)) 56 | } 57 | } else { 58 | Err(format!("invalid mask: {}", s)) 59 | } 60 | } 61 | } 62 | 63 | impl Initializer for MaskInitializer 64 | where 65 | R: initialize::Randomizer + Sized + Send + Sync, 66 | { 67 | /// Applies the initialise action. 68 | /// 69 | /// This action will use the intensity of pixels to determine whether 70 | /// rooms should be part of the maze. 71 | /// 72 | /// # Arguments 73 | /// * `maze` - The maze to initialise. 74 | /// * `rng` - A random number generator. 75 | /// * `methods` - The initialisers to use to generate the maze. 76 | fn initialize(&self, maze: Maze, rng: &mut R, methods: Methods) -> Maze { 77 | let physical::ViewBox { width, height, .. } = maze.viewbox(); 78 | let (cols, rows) = self.image.dimensions(); 79 | let data = self 80 | .image 81 | .enumerate_pixels() 82 | .map(|(x, y, pixel)| { 83 | ( 84 | physical::Pos { 85 | x: width * (x as f32 / cols as f32), 86 | y: height * (y as f32 / rows as f32), 87 | }, 88 | Intermediate::from(pixel), 89 | ) 90 | }) 91 | .split_by(&maze.shape(), maze.width(), maze.height()) 92 | .map(|&v| v > self.threshold); 93 | 94 | methods.initialize(maze, rng, |pos| data[pos]) 95 | } 96 | } 97 | 98 | #[derive(Clone, Copy, Default)] 99 | struct Intermediate(f32); 100 | 101 | impl<'a, P> From<&'a P> for Intermediate 102 | where 103 | P: image::Pixel, 104 | { 105 | fn from(source: &'a P) -> Self { 106 | Intermediate(source.channels().iter().map(|&b| f32::from(b)).sum()) 107 | } 108 | } 109 | 110 | impl ops::Add for Intermediate { 111 | type Output = Self; 112 | 113 | fn add(self, other: Self) -> Self { 114 | Intermediate(self.0 + other.0) 115 | } 116 | } 117 | 118 | impl ops::Div for Intermediate { 119 | type Output = f32; 120 | 121 | fn div(self, divisor: usize) -> Self::Output { 122 | D * self.0 / divisor as f32 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /maker/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use rayon::prelude::*; 4 | use svg::Node; 5 | 6 | use maze::initialize; 7 | use maze::matrix; 8 | use maze_tools::image::Color; 9 | use maze_tools::voronoi; 10 | 11 | pub type Maze = maze::Maze<()>; 12 | 13 | pub mod background_renderer; 14 | pub use self::background_renderer::*; 15 | pub mod break_post_processor; 16 | pub use self::break_post_processor::*; 17 | pub mod heatmap_renderer; 18 | pub use self::heatmap_renderer::*; 19 | pub mod mask_initializer; 20 | pub use self::mask_initializer::*; 21 | pub mod solve_renderer; 22 | pub use solve_renderer::*; 23 | pub mod text_renderer; 24 | pub use self::text_renderer::*; 25 | 26 | /// A trait to initialise a maze. 27 | pub trait Initializer 28 | where 29 | R: initialize::Randomizer + Sized + Send + Sync, 30 | { 31 | /// Initialises a maze. 32 | /// 33 | /// # Arguments 34 | /// * `maze` - The maze to initialise. 35 | /// * `rng` - A random number generator. 36 | /// * `method` - The initialisation method to use. 37 | fn initialize(&self, maze: Maze, rng: &mut R, method: Methods) -> Maze; 38 | } 39 | 40 | impl Initializer for Option 41 | where 42 | R: initialize::Randomizer + Sized + Send + Sync, 43 | T: Initializer, 44 | { 45 | fn initialize(&self, maze: Maze, rng: &mut R, methods: Methods) -> Maze { 46 | if let Some(action) = self { 47 | action.initialize(maze, rng, methods) 48 | } else { 49 | methods.initialize(maze, rng, |_| true) 50 | } 51 | } 52 | } 53 | 54 | /// A trait to perform post-processing of a maze. 55 | pub trait PostProcessor 56 | where 57 | R: initialize::Randomizer + Sized + Send + Sync, 58 | { 59 | /// Performs post-processing of a maze. 60 | /// 61 | /// # Arguments 62 | /// * `maze` - The maze to post-process. 63 | /// * `rng` - A random number generator. 64 | fn post_process(&self, maze: Maze, rng: &mut R) -> Maze; 65 | } 66 | 67 | impl PostProcessor for Option 68 | where 69 | R: initialize::Randomizer + Sized + Send + Sync, 70 | T: PostProcessor, 71 | { 72 | fn post_process(&self, maze: Maze, rng: &mut R) -> Maze { 73 | if let Some(action) = self { 74 | action.post_process(maze, rng) 75 | } else { 76 | maze 77 | } 78 | } 79 | } 80 | 81 | #[derive(Clone)] 82 | pub struct Methods(pub voronoi::initialize::Methods) 83 | where 84 | R: initialize::Randomizer + Sized + Send + Sync; 85 | 86 | impl Default for Methods 87 | where 88 | R: initialize::Randomizer + Sized + Send + Sync, 89 | { 90 | fn default() -> Self { 91 | Self(voronoi::initialize::Methods::default()) 92 | } 93 | } 94 | 95 | impl FromStr for Methods 96 | where 97 | R: initialize::Randomizer + Sized + Send + Sync, 98 | { 99 | type Err = String; 100 | 101 | fn from_str(s: &str) -> Result { 102 | let mut methods = vec![]; 103 | for method in s.split(',') { 104 | methods.push(method.parse()?) 105 | } 106 | 107 | Ok(Self(voronoi::initialize::Methods::new(methods))) 108 | } 109 | } 110 | 111 | impl Methods 112 | where 113 | R: initialize::Randomizer + Sized + Send + Sync, 114 | { 115 | /// Wraps the inner initialiser. 116 | /// 117 | /// # Arguments 118 | /// * `maze` - The maze to initialise. 119 | /// * `rng` - A random number generator. 120 | /// * `filter` - An additional filter applied to all methods. 121 | #[allow(clippy::needless_collect)] // TODO: Wait for Clippy #6066 122 | pub fn initialize(self, maze: Maze, rng: &mut R, filter: F) -> Maze 123 | where 124 | F: Fn(matrix::Pos) -> bool, 125 | { 126 | let points = 127 | voronoi::initialize::Methods::random_points(maze.viewbox(), rng) 128 | .take(self.0.methods().len()) 129 | .collect::>(); 130 | self.0 131 | .initialize(maze, rng, filter, points.into_iter()) 132 | .into() 133 | } 134 | } 135 | 136 | /// A trait for rendering a maze. 137 | pub trait Renderer { 138 | /// Applies this action to a maze and SVG group. 139 | /// 140 | /// # Arguments 141 | /// * `maze` - The maze. 142 | /// * `group` - An SVG group. 143 | fn render(&self, maze: &Maze, group: &mut svg::node::element::Group); 144 | } 145 | 146 | impl Renderer for Option 147 | where 148 | T: Renderer, 149 | { 150 | fn render(&self, maze: &Maze, group: &mut svg::node::element::Group) { 151 | if let Some(action) = self { 152 | action.render(maze, group); 153 | } 154 | } 155 | } 156 | 157 | /// A type of heat map. 158 | #[derive(Clone)] 159 | pub enum HeatMapType { 160 | /// The heat map is generated by traversing vertically. 161 | Vertical, 162 | 163 | /// The heat map is generated by traversing horisontally. 164 | Horizontal, 165 | 166 | /// The heat map is generated by travesing from every edge room to the one 167 | /// on the opposite side. 168 | Full, 169 | } 170 | 171 | impl FromStr for HeatMapType { 172 | type Err = String; 173 | 174 | fn from_str(s: &str) -> Result { 175 | match s { 176 | "vertical" => Ok(HeatMapType::Vertical), 177 | "horizontal" => Ok(HeatMapType::Horizontal), 178 | "full" => Ok(HeatMapType::Full), 179 | _ => Err(format!("unknown heat map type: {}", s)), 180 | } 181 | } 182 | } 183 | 184 | impl HeatMapType { 185 | /// Generates a heat map based on this heat map type. 186 | /// 187 | /// # Arguments 188 | /// * `maze` - The maze for which to generate a heat map. 189 | pub fn generate(&self, maze: &Maze) -> maze::matrix::Matrix { 190 | match *self { 191 | HeatMapType::Vertical => self.create_heatmap( 192 | maze, 193 | (0..maze.width()).map(|col| { 194 | ( 195 | maze::matrix::Pos { 196 | col: col as isize, 197 | row: 0, 198 | }, 199 | maze::matrix::Pos { 200 | col: col as isize, 201 | row: maze.height() as isize - 1, 202 | }, 203 | ) 204 | }), 205 | ), 206 | HeatMapType::Horizontal => self.create_heatmap( 207 | maze, 208 | (0..maze.height()).map(|row| { 209 | ( 210 | maze::matrix::Pos { 211 | col: 0, 212 | row: row as isize, 213 | }, 214 | maze::matrix::Pos { 215 | col: maze.width() as isize - 1, 216 | row: row as isize, 217 | }, 218 | ) 219 | }), 220 | ), 221 | HeatMapType::Full => self.create_heatmap( 222 | maze, 223 | maze.positions() 224 | .filter(|&pos| pos.col == 0 || pos.row == 0) 225 | .map(|pos| { 226 | ( 227 | pos, 228 | maze::matrix::Pos { 229 | col: maze.width() as isize - 1 - pos.col, 230 | row: maze.height() as isize - 1 - pos.row, 231 | }, 232 | ) 233 | }), 234 | ), 235 | } 236 | } 237 | 238 | /// Generates a heat map for a maze and an iteration of positions. 239 | /// 240 | /// # Arguments 241 | /// * `maze` - The maze for which to generate a heat map. 242 | /// * `positions` - The positions for which to generate a heat map. These 243 | /// will be generated from the heat map type. 244 | fn create_heatmap(&self, maze: &Maze, positions: I) -> maze::HeatMap 245 | where 246 | I: Iterator, 247 | { 248 | let collected = positions.collect::>(); 249 | collected 250 | .chunks(collected.len() / rayon::current_num_threads()) 251 | .collect::>() 252 | .par_iter() 253 | .map(|positions| maze::heatmap(maze, positions.iter().cloned())) 254 | .reduce( 255 | || maze::HeatMap::new(maze.width(), maze.height()), 256 | std::ops::Add::add, 257 | ) 258 | } 259 | } 260 | 261 | /// A source of random values. 262 | #[derive(Clone)] 263 | pub enum Random { 264 | /// A source of random values from the operating system. 265 | OSRandom, 266 | 267 | /// A source of random values from an LFSR. 268 | Lfsr(initialize::LFSR), 269 | } 270 | 271 | impl Random { 272 | /// Creates a source of random values from the operating system. 273 | pub fn from_os() -> Self { 274 | Self::OSRandom 275 | } 276 | 277 | /// Creates a source of random values from an LFSR. 278 | /// 279 | /// # Arguments 280 | /// * `seed` The LFST seed. 281 | pub fn from_seed(seed: u64) -> Self { 282 | Self::Lfsr(seed.into()) 283 | } 284 | } 285 | 286 | impl initialize::Randomizer for Random { 287 | fn range(&mut self, a: usize, b: usize) -> usize { 288 | use Random::*; 289 | match self { 290 | OSRandom => rand::rngs::OsRng.range(a, b), 291 | Lfsr(lfsr) => lfsr.range(a, b), 292 | } 293 | } 294 | 295 | fn random(&mut self) -> f64 { 296 | use Random::*; 297 | match self { 298 | OSRandom => rand::rngs::OsRng.random(), 299 | Lfsr(lfsr) => lfsr.random(), 300 | } 301 | } 302 | } 303 | 304 | /// Draws all rooms of a maze. 305 | /// 306 | /// # Arguments 307 | /// * `maze` - The maze to draw. 308 | /// * `colors` - A function determining the colour of a room. 309 | pub fn draw_rooms(maze: &Maze, colors: F) -> svg::node::element::Group 310 | where 311 | F: Fn(maze::matrix::Pos) -> Color, 312 | { 313 | let mut group = svg::node::element::Group::new(); 314 | for pos in maze.positions().filter(|&pos| maze[pos].visited) { 315 | let color = colors(pos); 316 | let mut commands = maze 317 | .walls(pos) 318 | .iter() 319 | .enumerate() 320 | .map(|(i, wall)| { 321 | let (coords, _) = maze.corners((pos, wall)); 322 | if i == 0 { 323 | svg::node::element::path::Command::Move( 324 | svg::node::element::path::Position::Absolute, 325 | (coords.x, coords.y).into(), 326 | ) 327 | } else { 328 | svg::node::element::path::Command::Line( 329 | svg::node::element::path::Position::Absolute, 330 | (coords.x, coords.y).into(), 331 | ) 332 | } 333 | }) 334 | .collect::>(); 335 | commands.push(svg::node::element::path::Command::Close); 336 | 337 | group.append( 338 | svg::node::element::Path::new() 339 | .set("fill", color.to_string()) 340 | .set("fill-opacity", f32::from(color.alpha) / 255.0) 341 | .set("d", svg::node::element::path::Data::from(commands)), 342 | ); 343 | } 344 | 345 | group 346 | } 347 | -------------------------------------------------------------------------------- /maker/src/types/solve_renderer.rs: -------------------------------------------------------------------------------- 1 | use maze::render::svg::ToPath; 2 | 3 | use svg::Node; 4 | 5 | use crate::types::*; 6 | 7 | /// The maze solution. 8 | #[derive(Clone)] 9 | pub struct SolveRenderer { 10 | /// The colour of the solution marker. 11 | color: String, 12 | } 13 | 14 | impl FromStr for SolveRenderer { 15 | type Err = String; 16 | 17 | /// Converts a string to a string to render. 18 | /// 19 | /// The string must be a path. 20 | fn from_str(s: &str) -> Result { 21 | Ok(Self { color: s.into() }) 22 | } 23 | } 24 | 25 | impl Renderer for SolveRenderer { 26 | /// Renders the maze solution. 27 | /// 28 | /// # Arguments 29 | /// * `maze` - The maze. 30 | /// * `group` - The group to which to add the solution. 31 | fn render(&self, maze: &Maze, group: &mut svg::node::element::Group) { 32 | group.append( 33 | svg::node::element::Path::new() 34 | .set("fill", "none") 35 | .set("stroke", self.color.as_str()) 36 | .set("stroke-linecap", "round") 37 | .set("stroke-linejoin", "round") 38 | .set("stroke-width", 0.4) 39 | .set("vector-effect", "non-scaling-stroke") 40 | .set( 41 | "d", 42 | maze.walk( 43 | maze::matrix::Pos { col: 0, row: 0 }, 44 | maze::matrix::Pos { 45 | col: maze.width() as isize - 1, 46 | row: maze.height() as isize - 1, 47 | }, 48 | ) 49 | .unwrap() 50 | .to_path_d(), 51 | ), 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /maker/src/types/text_renderer.rs: -------------------------------------------------------------------------------- 1 | use std::ops; 2 | use std::str::FromStr; 3 | 4 | use svg::Node; 5 | 6 | use maze::physical; 7 | use maze_tools::alphabet; 8 | use maze_tools::cell::*; 9 | use maze_tools::image::Color; 10 | 11 | use crate::types::*; 12 | 13 | /// A text. 14 | #[derive(Clone)] 15 | pub struct TextRenderer { 16 | /// The string to render. 17 | text: String, 18 | } 19 | 20 | impl FromStr for TextRenderer { 21 | type Err = String; 22 | 23 | /// Converts a string to a string to render. 24 | /// 25 | /// The string must be a path. 26 | fn from_str(s: &str) -> Result { 27 | Ok(Self { text: s.into() }) 28 | } 29 | } 30 | 31 | impl Renderer for TextRenderer { 32 | /// Applies the text action. 33 | /// 34 | /// This action will render a string as background. 35 | /// 36 | /// # Arguments 37 | /// * `maze` - The maze. 38 | /// * `group` - The group to which to add the rooms. 39 | fn render(&self, maze: &Maze, group: &mut svg::node::element::Group) { 40 | let physical::ViewBox { width, height, .. } = maze.viewbox(); 41 | let columns = (self.text.len() as f32).sqrt().ceil() as usize; 42 | let rows = (self.text.len() as f32 / columns as f32).ceil() as usize; 43 | let data = alphabet::default::ALPHABET 44 | .render(&self.text, columns, 16 * maze.width()) 45 | .map(|(pos, v)| { 46 | ( 47 | physical::Pos { 48 | x: width * pos.x / columns as f32, 49 | y: height * pos.y / rows as f32, 50 | }, 51 | Intermediate::from(v), 52 | ) 53 | }) 54 | .split_by(&maze.shape(), maze.width(), maze.height()); 55 | 56 | group.append(draw_rooms(maze, |pos| data[pos])); 57 | } 58 | } 59 | 60 | #[derive(Clone, Copy, Default)] 61 | struct Intermediate(f32); 62 | 63 | impl From for Intermediate { 64 | fn from(source: f32) -> Self { 65 | Intermediate(source) 66 | } 67 | } 68 | 69 | impl ops::Add for Intermediate { 70 | type Output = Self; 71 | 72 | fn add(self, other: Self) -> Self { 73 | Intermediate(self.0 + other.0) 74 | } 75 | } 76 | 77 | impl ops::Div for Intermediate { 78 | type Output = Color; 79 | 80 | fn div(self, divisor: usize) -> Self::Output { 81 | Color { 82 | red: 0, 83 | green: 0, 84 | blue: 0, 85 | alpha: (255.0 * (1.0 - self.0 / divisor as f32)) as u8, 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /maze/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maze" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | maze-test = { path = "../test" } 9 | 10 | bit-set = { workspace = true } 11 | rand = { workspace = true, optional = true } 12 | serde = { workspace = true, optional = true } 13 | svg = { workspace = true, optional = true } 14 | 15 | [dev-dependencies] 16 | serde_json = { workspace = true } 17 | criterion = "0.5" 18 | 19 | [[bench]] 20 | name = "initialize" 21 | harness = false 22 | 23 | [[bench]] 24 | name = "walk" 25 | harness = false 26 | 27 | [features] 28 | default = ["rand", "serde", "svg"] 29 | -------------------------------------------------------------------------------- /maze/benches/initialize.rs: -------------------------------------------------------------------------------- 1 | use criterion::{ 2 | black_box, criterion_group, criterion_main, BenchmarkId, Criterion, 3 | }; 4 | use maze::initialize::{Method, LFSR}; 5 | use maze::{Maze, Shape}; 6 | 7 | pub fn initialize(c: &mut Criterion) { 8 | for &method in [Method::Braid, Method::Branching, Method::Winding].iter() { 9 | let mut group = c.benchmark_group(format!("initialize {}", method)); 10 | for shape in [Shape::Tri, Shape::Quad, Shape::Hex].iter() { 11 | group.bench_with_input( 12 | BenchmarkId::from_parameter(shape), 13 | shape, 14 | |b, &shape| { 15 | b.iter(|| { 16 | Maze::<()>::new(black_box(shape), 100, 100) 17 | .initialize(method, &mut LFSR::new(65)); 18 | }); 19 | }, 20 | ); 21 | } 22 | group.finish(); 23 | } 24 | } 25 | 26 | criterion_group!(benches, initialize); 27 | criterion_main!(benches); 28 | -------------------------------------------------------------------------------- /maze/benches/walk.rs: -------------------------------------------------------------------------------- 1 | use criterion::{ 2 | black_box, criterion_group, criterion_main, BenchmarkId, Criterion, 3 | }; 4 | use maze::initialize::{Method, LFSR}; 5 | use maze::{Maze, Shape}; 6 | 7 | pub fn walk(c: &mut Criterion) { 8 | for &method in [Method::Braid, Method::Branching, Method::Winding].iter() { 9 | let mut group = c.benchmark_group(format!("walk {}", method)); 10 | for shape in [Shape::Tri, Shape::Quad, Shape::Hex].iter() { 11 | let maze = Maze::<()>::new(black_box(*shape), 100, 100) 12 | .initialize(method, &mut LFSR::new(65)); 13 | let start = (0isize, 0isize).into(); 14 | let end = 15 | ((maze.width() - 1) as isize, (maze.height() - 1) as isize) 16 | .into(); 17 | group.bench_with_input( 18 | BenchmarkId::from_parameter(shape), 19 | shape, 20 | |b, _| { 21 | b.iter(|| { 22 | maze.walk(start, end); 23 | }); 24 | }, 25 | ); 26 | } 27 | group.finish(); 28 | } 29 | } 30 | 31 | criterion_group!(benches, walk); 32 | criterion_main!(benches); 33 | -------------------------------------------------------------------------------- /maze/src/initialize/braid.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | 3 | use crate::Maze; 4 | 5 | use crate::matrix; 6 | 7 | /// Initialises a maze using the _Braid_ algorithm. 8 | /// 9 | /// This method will leave no dead ends in the final maze; all rooms will have 10 | /// at least two open walls. 11 | /// 12 | /// # Arguments 13 | /// * `maze``- The maze to initialise. 14 | /// * `rng` - A random number generator. 15 | /// * `candidates` - A filter for the rooms to modify. 16 | pub(crate) fn initialize( 17 | mut maze: Maze, 18 | rng: &mut R, 19 | candidates: matrix::Matrix, 20 | ) -> Maze 21 | where 22 | R: super::Randomizer + Sized, 23 | T: Clone, 24 | { 25 | // First remove all inner walls 26 | for pos in maze.positions().filter(|&pos| candidates[pos]) { 27 | for wall in maze.walls(pos) { 28 | let (pos, wall) = maze.back((pos, wall)); 29 | if *candidates.get(pos).unwrap_or(&false) { 30 | maze.open((pos, wall)); 31 | } 32 | } 33 | } 34 | 35 | // List all possible walls 36 | let walls = maze 37 | .positions() 38 | .filter(|&pos| candidates[pos]) 39 | .flat_map(|pos| { 40 | maze.wall_positions(pos) 41 | .map(|wall_pos| (wall_pos, maze.back(wall_pos))) 42 | }) 43 | .filter(|(_, back)| *candidates.get(back.0).unwrap_or(&false)) 44 | .map(|(wall_pos, back)| { 45 | let dx = wall_pos.0.col - back.0.col; 46 | let dy = wall_pos.0.row - back.0.row; 47 | if dy < 0 || (dy == 0 && dx < 0) { 48 | wall_pos 49 | } else { 50 | back 51 | } 52 | }) 53 | .collect::>(); 54 | 55 | // Randomize the wall array 56 | let mut walls = walls.iter().collect::>(); 57 | let len = walls.len(); 58 | for i in 0..len { 59 | walls.swap(i, rng.range(0, len)); 60 | } 61 | 62 | // Attempt to add every wall, but make sure no dead-ends appear 63 | for &wall_pos in walls { 64 | let back = maze.back(wall_pos); 65 | if maze[wall_pos.0].open_walls() > 2 && maze[back.0].open_walls() > 2 { 66 | maze.close(wall_pos); 67 | } 68 | } 69 | 70 | super::connect_all(&mut maze, rng, |pos| { 71 | *candidates.get(pos).unwrap_or(&false) 72 | }); 73 | 74 | maze 75 | } 76 | -------------------------------------------------------------------------------- /maze/src/initialize/branching.rs: -------------------------------------------------------------------------------- 1 | use crate::Maze; 2 | 3 | use crate::matrix; 4 | 5 | /// Initialises a maze using the _Randomised Prim_ algorithm. 6 | /// 7 | /// # Arguments 8 | /// * `maze` - The maze to initialise. 9 | /// * `rng` - A random number generator. 10 | /// * `candidates` - A filter for the rooms to modify. 11 | pub(crate) fn initialize( 12 | mut maze: Maze, 13 | rng: &mut R, 14 | mut candidates: matrix::Matrix, 15 | ) -> Maze 16 | where 17 | R: super::Randomizer + Sized, 18 | T: Clone, 19 | { 20 | loop { 21 | // Start with all walls in a random room, except for those leading 22 | // out of the maze 23 | let mut walls = super::random_room(rng, &candidates) 24 | // Get all walls not leading out of the maze 25 | .map(|pos| { 26 | maze.walls(pos) 27 | .iter() 28 | .filter(|wall| maze.is_inside(maze.back((pos, wall)).0)) 29 | // Create a wall position 30 | .map(|wall| (pos, *wall)) 31 | .collect::>() 32 | }) 33 | .unwrap_or_default(); 34 | 35 | while !walls.is_empty() { 36 | // Get a random wall 37 | let index = rng.range(0, walls.len()); 38 | let wall_pos = walls.remove(index); 39 | 40 | // Walk through the wall if we have not visited the room on the 41 | // other side before 42 | let (next_pos, _) = maze.back(wall_pos); 43 | if candidates[next_pos] { 44 | // Mark the rooms as visited and open the door 45 | candidates[wall_pos.0] = false; 46 | candidates[next_pos] = false; 47 | maze.open(wall_pos); 48 | 49 | // Add all walls of the next room except those already 50 | // visited and those outside of the maze 51 | walls.extend( 52 | maze.walls(next_pos) 53 | .iter() 54 | .map(|w| maze.back((next_pos, w))) 55 | .filter(|&(pos, _)| { 56 | *candidates.get(pos).unwrap_or(&false) 57 | }) 58 | .map(|wall_pos| maze.back(wall_pos)) 59 | .filter(|&(pos, _)| candidates.is_inside(pos)), 60 | ); 61 | } 62 | } 63 | 64 | if candidates.values().all(|v| !v) { 65 | break; 66 | } 67 | } 68 | 69 | maze 70 | } 71 | -------------------------------------------------------------------------------- /maze/src/initialize/clear.rs: -------------------------------------------------------------------------------- 1 | use crate::Maze; 2 | 3 | use crate::matrix; 4 | 5 | /// Initialises a maze by clearing all inner walls. 6 | /// 7 | /// # Arguments 8 | /// * `maze``- The maze to initialise. 9 | /// * `_rng` - Not used. 10 | /// * `candidates` - A filter for the rooms to modify. 11 | pub(crate) fn initialize( 12 | mut maze: Maze, 13 | _rng: &mut R, 14 | candidates: matrix::Matrix, 15 | ) -> Maze 16 | where 17 | R: super::Randomizer + Sized, 18 | T: Clone, 19 | { 20 | for pos in maze.positions().filter(|&pos| candidates[pos]) { 21 | for wall in maze.walls(pos) { 22 | let (pos, wall) = maze.back((pos, wall)); 23 | if *candidates.get(pos).unwrap_or(&false) { 24 | maze.open((pos, wall)); 25 | } 26 | } 27 | } 28 | 29 | maze 30 | } 31 | -------------------------------------------------------------------------------- /maze/src/initialize/dividing.rs: -------------------------------------------------------------------------------- 1 | use crate::Maze; 2 | 3 | use crate::matrix; 4 | use crate::physical; 5 | 6 | /// Initialises a maze using the _Randomised Prim_ algorithm. 7 | /// 8 | /// # Arguments 9 | /// * `maze` - The maze to initialise. 10 | /// * `rng` - A random number generator. 11 | /// * `candidates` - A filter for the rooms to modify. 12 | pub(crate) fn initialize( 13 | mut maze: Maze, 14 | rng: &mut R, 15 | candidates: matrix::Matrix, 16 | ) -> Maze 17 | where 18 | R: super::Randomizer + Sized, 19 | T: Clone, 20 | { 21 | // We need to work with a clear maze 22 | for pos in maze.positions().filter(|&pos| candidates[pos]) { 23 | for wall in maze.walls(pos) { 24 | let (pos, wall) = maze.back((pos, wall)); 25 | if *candidates.get(pos).unwrap_or(&false) { 26 | maze.open((pos, wall)); 27 | } 28 | } 29 | } 30 | 31 | // Calculate the full view box of our candidate area 32 | let viewbox = maze 33 | .positions() 34 | .flat_map(|pos| maze.wall_positions(pos)) 35 | .map(|wall_pos| maze.corners(wall_pos).0) 36 | .collect::(); 37 | 38 | // Stop recursing when any split has a side less than twice the distance 39 | // between two rooms 40 | let threshold = 2.0 41 | * (maze.center((0isize, 0isize).into()) 42 | - maze.center((1isize, 1isize).into())) 43 | .value() 44 | .sqrt(); 45 | 46 | // Recursively split and rebuild 47 | Split::from_viewbox(viewbox, rng).apply( 48 | &mut maze, 49 | rng, 50 | &candidates, 51 | threshold, 52 | ); 53 | 54 | maze 55 | } 56 | 57 | enum Split { 58 | Horizontal(physical::ViewBox, f32), 59 | Vertical(physical::ViewBox, f32), 60 | } 61 | 62 | impl Split { 63 | pub fn from_viewbox(viewbox: physical::ViewBox, rng: &mut R) -> Self 64 | where 65 | R: super::Randomizer + Sized, 66 | { 67 | let cut = 0.8 * rng.random() as f32 + 0.2; 68 | if viewbox.width > viewbox.height { 69 | Self::Vertical(viewbox, viewbox.corner.x + cut * viewbox.width) 70 | } else { 71 | Self::Horizontal(viewbox, viewbox.corner.y + cut * viewbox.height) 72 | } 73 | } 74 | 75 | pub fn apply( 76 | self, 77 | maze: &mut Maze, 78 | rng: &mut R, 79 | candidates: &matrix::Matrix, 80 | threshold: f32, 81 | ) where 82 | R: super::Randomizer + Sized, 83 | T: Clone, 84 | { 85 | use Split::*; 86 | 87 | // Make a random cut 88 | let ranges = match self { 89 | Horizontal(viewbox, at) => { 90 | let (a, b) = ( 91 | maze.room_at((viewbox.corner.x, at).into()), 92 | maze.room_at((viewbox.corner.x + viewbox.width, at).into()), 93 | ); 94 | ((a.col - 1..a.col + 1), (a.row - 1..b.row + 1)) 95 | } 96 | Vertical(viewbox, at) => { 97 | let (a, b) = ( 98 | maze.room_at((at, viewbox.corner.y).into()), 99 | maze.room_at( 100 | (at, viewbox.corner.y + viewbox.height).into(), 101 | ), 102 | ); 103 | ((a.col - 1..a.col + 1), (a.row - 1..b.row + 1)) 104 | } 105 | }; 106 | 107 | // Close walls along the cut 108 | for pos in ranges 109 | .0 110 | .flat_map(|x| ranges.1.clone().map(move |y| (x, y).into())) 111 | .filter(|&pos| *candidates.get(pos).unwrap_or(&false)) 112 | { 113 | for a in maze.wall_positions(pos) { 114 | let b = maze.back(a); 115 | if *candidates.get(b.0).unwrap_or(&false) 116 | && (self.contains(maze.center(a.0)) 117 | != self.contains(maze.center(b.0))) 118 | { 119 | maze.close(a); 120 | } 121 | } 122 | } 123 | 124 | // Recurse 125 | let splits = self.split(rng); 126 | for split in [splits.0, splits.1] { 127 | let viewbox = split.viewbox(); 128 | if [viewbox.width, viewbox.height] 129 | .iter() 130 | .all(|&x| x > threshold * (1.0 + rng.random() as f32)) 131 | { 132 | split.apply(maze, rng, candidates, threshold); 133 | } 134 | } 135 | } 136 | 137 | fn split(self, rng: &mut R) -> (Self, Self) 138 | where 139 | R: super::Randomizer + Sized, 140 | { 141 | use Split::*; 142 | let viewboxes = match self { 143 | Horizontal(viewbox, at) => viewbox.split_horizontal(at), 144 | Vertical(viewbox, at) => viewbox.split_vertical(at), 145 | }; 146 | ( 147 | Self::from_viewbox(viewboxes.0, rng), 148 | Self::from_viewbox(viewboxes.1, rng), 149 | ) 150 | } 151 | 152 | fn viewbox(&self) -> &physical::ViewBox { 153 | use Split::*; 154 | match self { 155 | Horizontal(viewbox, _) | Vertical(viewbox, _) => viewbox, 156 | } 157 | } 158 | 159 | fn contains(&self, pos: physical::Pos) -> bool { 160 | use Split::*; 161 | match self { 162 | Horizontal(_, at) => pos.y < *at, 163 | Vertical(_, at) => pos.x < *at, 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /maze/src/initialize/mod.rs: -------------------------------------------------------------------------------- 1 | //! # Initialisation methods 2 | //! 3 | //! This module contains implementations of initialisation methods. These are 4 | //! used to open walls in a fully closed maze to make it navigable. 5 | 6 | use std::iter; 7 | use std::str; 8 | 9 | #[cfg(feature = "serde")] 10 | use serde::{Deserialize, Serialize}; 11 | 12 | use crate::Maze; 13 | 14 | use crate::matrix; 15 | 16 | mod braid; 17 | mod branching; 18 | mod clear; 19 | mod winding; 20 | 21 | /// The various supported initialisation method. 22 | #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] 23 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 24 | pub enum Method { 25 | /// Initialises a maze with no dead ends. 26 | /// 27 | /// A dead end is a room with only one open wall. 28 | /// 29 | /// This method starts with a fully cleared area, and adds walls until no 30 | /// longer possible without creating dead ends. A maze initialised with 31 | /// this method will contain loops. 32 | Braid, 33 | 34 | /// Initialises a maze by opening all walls inside the area. 35 | Clear, 36 | 37 | /// Initialises a maze using a branching algorithm. 38 | /// 39 | /// This method uses the _Randomised Prim_ algorithm to generate a maze, 40 | /// which yields mazes with a branching characteristic. A maze initialised 41 | /// with this method will not contain loops. 42 | /// 43 | /// See [Wikipedia] for a description of the algorithm. 44 | /// 45 | /// [Wikipedia]: https://en.wikipedia.org/wiki/Maze_generation_algorithm#Randomized_Prim's_algorithm 46 | Branching, 47 | 48 | /// Initialises a maze using a winding algorithm. 49 | /// 50 | /// This method uses a simple _Depth First_ algorithm to generate a maze, 51 | /// which yields mazes with long winding corridors. A maze initialised with 52 | /// this method will not contain loops. 53 | /// 54 | /// See [Wikipedia] for a description of the algorithm. 55 | /// 56 | /// [Wikipedia]: https://en.wikipedia.org/wiki/Maze_generation_algorithm#Depth-first_search 57 | Winding, 58 | } 59 | 60 | impl Default for Method { 61 | /// The default initialisation method is [`Branching`](Method::Branchin). 62 | fn default() -> Self { 63 | Method::Branching 64 | } 65 | } 66 | 67 | impl std::fmt::Display for Method { 68 | /// The opposite of [std::str::FromStr]. 69 | /// 70 | /// # Examples 71 | /// 72 | /// ``` 73 | /// # use maze::initialize::*; 74 | /// 75 | /// assert_eq!( 76 | /// Method::Braid.to_string().parse::(), 77 | /// Ok(Method::Braid), 78 | /// ); 79 | /// assert_eq!( 80 | /// Method::Branching.to_string().parse::(), 81 | /// Ok(Method::Branching), 82 | /// ); 83 | /// assert_eq!( 84 | /// Method::Clear.to_string().parse::(), 85 | /// Ok(Method::Clear), 86 | /// ); 87 | /// assert_eq!( 88 | /// Method::Winding.to_string().parse::(), 89 | /// Ok(Method::Winding), 90 | /// ); 91 | /// ``` 92 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 93 | use Method::*; 94 | match self { 95 | Braid => write!(f, "braid"), 96 | Clear => write!(f, "clear"), 97 | Branching => write!(f, "branching"), 98 | Winding => write!(f, "winding"), 99 | } 100 | } 101 | } 102 | 103 | impl str::FromStr for Method { 104 | type Err = String; 105 | 106 | /// Converts a string to an initialiser. 107 | /// 108 | /// The source strings are the lower case names of the initialisation 109 | /// methods. 110 | /// 111 | /// # Examples 112 | /// 113 | /// ``` 114 | /// # use maze::initialize::*; 115 | /// 116 | /// assert_eq!( 117 | /// "braid".parse::(), 118 | /// Ok(Method::Braid), 119 | /// ); 120 | /// assert_eq!( 121 | /// "branching".parse::(), 122 | /// Ok(Method::Branching), 123 | /// ); 124 | /// assert_eq!( 125 | /// "clear".parse::(), 126 | /// Ok(Method::Clear), 127 | /// ); 128 | /// assert_eq!( 129 | /// "winding".parse::(), 130 | /// Ok(Method::Winding), 131 | /// ); 132 | /// ``` 133 | fn from_str(source: &str) -> Result { 134 | match source { 135 | "braid" => Ok(Method::Braid), 136 | "clear" => Ok(Method::Clear), 137 | "branching" => Ok(Method::Branching), 138 | "winding" => Ok(Method::Winding), 139 | e => Err(e.to_owned()), 140 | } 141 | } 142 | } 143 | 144 | pub trait Randomizer { 145 | /// Generates a random value in the range `[low, high)`, where `low` and 146 | /// `high` are the low and high values of `a` and `b`. 147 | /// 148 | /// # Arguments 149 | /// * `a` - A number. 150 | /// * `b` - A number. 151 | fn range(&mut self, a: usize, b: usize) -> usize; 152 | 153 | /// Generates a random value in the range `[0, 1)`. 154 | fn random(&mut self) -> f64; 155 | } 156 | 157 | #[cfg(feature = "rand")] 158 | impl Randomizer for T 159 | where 160 | T: rand::Rng, 161 | { 162 | fn range(&mut self, a: usize, b: usize) -> usize { 163 | if a < b { 164 | self.gen_range(a..b) 165 | } else { 166 | self.gen_range(b..a) 167 | } 168 | } 169 | 170 | fn random(&mut self) -> f64 { 171 | self.gen() 172 | } 173 | } 174 | 175 | /// A linear feedback shift register. 176 | #[derive(Clone, Debug, Eq, PartialEq)] 177 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 178 | pub struct LFSR(u64); 179 | 180 | impl LFSR { 181 | /// Creates a new linear shift register. 182 | /// 183 | /// # Arguments 184 | /// * `seed` - The seed. This value will not be yielded. 185 | pub fn new(seed: u64) -> Self { 186 | Self(seed) 187 | } 188 | 189 | /// Advances this shift register by one `u64` and returns the bit mask. 190 | pub fn advance(&mut self) -> u64 { 191 | self.nth(63).unwrap(); 192 | 193 | self.0 194 | } 195 | } 196 | 197 | impl From for LFSR 198 | where 199 | T: Into, 200 | { 201 | fn from(source: T) -> Self { 202 | Self(source.into()) 203 | } 204 | } 205 | 206 | impl iter::Iterator for LFSR { 207 | type Item = bool; 208 | 209 | /// Returns the next bit. 210 | fn next(&mut self) -> Option { 211 | let bit = (self.0 ^ (self.0 >> 2) ^ (self.0 >> 3) ^ (self.0 >> 5)) & 1; 212 | 213 | self.0 = (self.0 >> 1) | (bit << 63); 214 | 215 | Some(bit != 0) 216 | } 217 | } 218 | 219 | impl Randomizer for LFSR { 220 | fn range(&mut self, a: usize, b: usize) -> usize { 221 | let val = self.advance() as usize; 222 | let (low, high) = if a < b { (a, b) } else { (b, a) }; 223 | if low == high { 224 | low 225 | } else { 226 | low + val % (high - low) 227 | } 228 | } 229 | 230 | fn random(&mut self) -> f64 { 231 | self.advance() as f64 / u64::MAX as f64 232 | } 233 | } 234 | 235 | impl Maze 236 | where 237 | T: Clone, 238 | { 239 | /// Initialises a maze using the selected algorithm. 240 | /// 241 | /// See [here](https://en.wikipedia.org/wiki/Maze_generation_algorithm) for 242 | /// a description of the algorithms. 243 | /// 244 | /// The maze should be fully closed; any already open walls will be 245 | /// ignored and kept. 246 | /// 247 | /// This method guarantees that the resulting maze is predictable if the 248 | /// _RNG_ is predictable. 249 | /// 250 | /// # Arguments 251 | /// * `method` - The initialisation method to use. 252 | /// * `rng` - A random number generator. 253 | pub fn initialize(self, method: Method, rng: &mut R) -> Self 254 | where 255 | R: Randomizer + Sized, 256 | { 257 | self.initialize_filter(method, rng, |_| true) 258 | } 259 | 260 | /// Initialises a maze using the selected algorithm. 261 | /// 262 | /// See [here](https://en.wikipedia.org/wiki/Maze_generation_algorithm) for 263 | /// a description of the algorithms. 264 | /// 265 | /// The maze should be fully closed; any already open walls will be 266 | /// ignored and kept. 267 | /// 268 | /// This method guarantees that the resulting maze is predictable if the 269 | /// _RNG_ is predictable. 270 | /// 271 | /// # Arguments 272 | /// * `method` - The initialisation method to use. 273 | /// * `rng` - A random number generator. 274 | /// * `filter` - A filter function used to ignore rooms. 275 | pub fn initialize_filter( 276 | self, 277 | method: Method, 278 | rng: &mut R, 279 | filter: F, 280 | ) -> Self 281 | where 282 | F: Fn(matrix::Pos) -> bool, 283 | R: Randomizer + Sized, 284 | { 285 | match matrix::filter(self.width(), self.height(), filter) { 286 | (count, filter) if count > 0 => match method { 287 | Method::Braid => braid::initialize(self, rng, filter), 288 | Method::Clear => clear::initialize(self, rng, filter), 289 | Method::Branching => branching::initialize(self, rng, filter), 290 | Method::Winding => winding::initialize(self, rng, filter), 291 | }, 292 | _ => self, 293 | } 294 | } 295 | } 296 | 297 | /// Returns a random unvisited room. 298 | /// 299 | /// # Arguments 300 | /// * `rng` - A random number generator. 301 | /// * `filter_matrix` - A matrix containing the rooms to consider. 302 | fn random_room( 303 | rng: &mut dyn Randomizer, 304 | filter_matrix: &matrix::Matrix, 305 | ) -> Option { 306 | let count = filter_matrix 307 | .positions() 308 | .filter(|&pos| filter_matrix[pos]) 309 | .count(); 310 | if count > 0 { 311 | filter_matrix 312 | .positions() 313 | .filter(|&pos| filter_matrix[pos]) 314 | .nth(rng.range(0, count)) 315 | } else { 316 | None 317 | } 318 | } 319 | 320 | /// Ensures all rooms are connected 321 | /// 322 | /// This function will find all closed areas and ensure they have one exit to 323 | /// each neighbouring area. 324 | /// 325 | /// # Arguments 326 | /// * `maze` - The maze to modify. 327 | /// * `filter` - A filter for rooms to consider. 328 | pub fn connect_all(maze: &mut Maze, rng: &mut R, filter: F) 329 | where 330 | F: Fn(matrix::Pos) -> bool, 331 | R: Randomizer + Sized, 332 | T: Clone, 333 | { 334 | // First find all non-connected areas by visiting all rooms and filling for 335 | // each filtered, non-filled room and then incrementing the area index 336 | let mut areas = matrix::Matrix::new(maze.width(), maze.height()); 337 | let mut index = 0; 338 | for pos in maze.positions() { 339 | // Ignore filtered and already visited rooms 340 | if !filter(pos) || areas[pos] > 0 { 341 | continue; 342 | } else { 343 | index += 1; 344 | areas.fill(pos, index, |pos| { 345 | maze.neighbors(pos).filter(|&pos| filter(pos)) 346 | }); 347 | } 348 | } 349 | 350 | // Then find all edges between separate areas and open a random wall 351 | for (_, edge) in areas 352 | .edges(|pos| maze.adjacent(pos)) 353 | .iter() 354 | .filter(|&((source, _), _)| *source > 0) 355 | { 356 | let wall_positions = edge 357 | .iter() 358 | .flat_map(|&(pos1, pos2)| maze.connecting_wall(pos1, pos2)) 359 | .collect::>(); 360 | maze.open(wall_positions[rng.range(0, wall_positions.len())]) 361 | } 362 | } 363 | 364 | #[cfg(test)] 365 | mod tests { 366 | use maze_test::maze_test; 367 | 368 | use super::*; 369 | use crate::test_utils::*; 370 | 371 | /// The various initialisation methods tested. 372 | const INITIALIZERS: &[Method] = 373 | &[Method::Braid, Method::Branching, Method::Winding]; 374 | 375 | /// Tests that range works as advertised. 376 | #[test] 377 | fn lfsr_range() { 378 | let mut lfsr = LFSR::new(12345); 379 | for a in 0..100 { 380 | for b in a..a + 100 { 381 | for _ in 0..100 { 382 | let v = lfsr.range(a, b); 383 | if !(a <= v && v < b) { 384 | println!("!({} <= {} < {})", a, v, b); 385 | } 386 | if b > a { 387 | assert!(a <= v && v < b); 388 | } else { 389 | assert!(a == v && v == b); 390 | } 391 | } 392 | } 393 | } 394 | } 395 | 396 | /// Tests that random gives a rectangular distribution. 397 | #[test] 398 | fn lfsr_random() { 399 | let mut lfsr = LFSR::new(12345); 400 | 401 | let buckets = 100; 402 | let iterations = 100 * 100 * buckets; 403 | let hist = (0..iterations).fold(vec![0; buckets], |mut hist, _| { 404 | hist[(buckets as f64 * lfsr.random()) as usize] += 1; 405 | hist 406 | }); 407 | 408 | let mid = iterations / buckets; 409 | let h = 400; 410 | for v in hist { 411 | assert!(mid - h < v && v < mid + h); 412 | } 413 | } 414 | 415 | #[test] 416 | fn random_room_none() { 417 | let width = 5; 418 | let height = 5; 419 | let mut rng = LFSR::new(12345); 420 | let (count, filter_matrix) = matrix::filter(width, height, |_| false); 421 | 422 | assert_eq!(0, count); 423 | 424 | let iterations = width * height * 100; 425 | for _ in 0..iterations { 426 | assert!(random_room(&mut rng, &filter_matrix).is_none()); 427 | } 428 | } 429 | 430 | #[test] 431 | fn random_room_some() { 432 | let width = 5; 433 | let height = 5; 434 | let mut rng = LFSR::new(12345); 435 | let (count, filter_matrix) = 436 | matrix::filter(width, height, |pos| pos.col as usize == width - 1); 437 | 438 | assert_eq!(height, count); 439 | 440 | let buckets = height; 441 | let iterations = 100 * 100 * buckets; 442 | let hist = (0..iterations).fold(vec![0; buckets], |mut hist, _| { 443 | hist[random_room(&mut rng, &filter_matrix).unwrap().row 444 | as usize] += 1; 445 | hist 446 | }); 447 | 448 | let mid = iterations / buckets; 449 | let h = 400; 450 | for v in hist { 451 | assert!(mid - h < v && v < mid + h); 452 | } 453 | } 454 | 455 | #[maze_test] 456 | fn initialize(maze: TestMaze) { 457 | for method in INITIALIZERS { 458 | let maze = 459 | maze.clone().initialize(*method, &mut rand::thread_rng()); 460 | 461 | let from = matrix_pos(0, 0); 462 | let to = matrix_pos( 463 | (maze.width() - 1) as isize, 464 | (maze.height() - 1) as isize, 465 | ); 466 | assert!(maze.walk(from, to).is_some()); 467 | } 468 | } 469 | 470 | #[maze_test] 471 | fn initialize_lfsr_stable(maze: TestMaze) { 472 | for method in INITIALIZERS { 473 | let seed = 12345; 474 | let mut rng1 = LFSR::new(seed); 475 | let mut rng2 = LFSR::new(seed); 476 | maze.clone().initialize(*method, &mut rng1); 477 | maze.clone().initialize(*method, &mut rng2); 478 | 479 | assert_eq!(rng1, rng2, "for method {:?}", method); 480 | } 481 | } 482 | 483 | #[maze_test] 484 | fn initialize_filter_most(maze: TestMaze) { 485 | for method in INITIALIZERS { 486 | let from = matrix_pos(0, 0); 487 | let other = matrix_pos(1, 0); 488 | let to = matrix_pos( 489 | (maze.width() - 1) as isize, 490 | (maze.height() - 1) as isize, 491 | ); 492 | let maze = maze.clone().initialize_filter( 493 | *method, 494 | &mut rand::thread_rng(), 495 | |pos| pos != from, 496 | ); 497 | 498 | assert!(maze.walk(from, to).is_none()); 499 | assert!(maze.walk(other, to).is_some()); 500 | } 501 | } 502 | 503 | #[maze_test] 504 | fn initialize_filter_all(maze: TestMaze) { 505 | for method in INITIALIZERS { 506 | let from = matrix_pos(0, 0); 507 | let other = matrix_pos(1, 0); 508 | let to = matrix_pos( 509 | (maze.width() - 1) as isize, 510 | (maze.height() - 1) as isize, 511 | ); 512 | let maze = maze.clone().initialize_filter( 513 | *method, 514 | &mut rand::thread_rng(), 515 | |_| false, 516 | ); 517 | 518 | assert!(maze.walk(from, to).is_none()); 519 | assert!(maze.walk(other, to).is_none()); 520 | } 521 | } 522 | 523 | #[maze_test] 524 | fn initialize_filter_picked(maze: TestMaze) { 525 | for method in INITIALIZERS { 526 | for _ in 0..1000 { 527 | let filter = |matrix::Pos { col, row }| col > row; 528 | let maze = maze.clone().initialize_filter( 529 | *method, 530 | &mut rand::thread_rng(), 531 | filter, 532 | ); 533 | 534 | for pos in maze.positions() { 535 | assert_eq!(filter(pos), maze[pos].visited); 536 | } 537 | } 538 | } 539 | } 540 | 541 | #[maze_test] 542 | fn initialize_filter_segmented(maze: TestMaze) { 543 | for method in INITIALIZERS { 544 | for _ in 0..1000 { 545 | let width = maze.width(); 546 | let height = maze.height(); 547 | let filter = |matrix::Pos { col, row }| { 548 | col as usize != width / 2 && row as usize != height / 2 549 | }; 550 | let maze = maze.clone().initialize_filter( 551 | *method, 552 | &mut rand::thread_rng(), 553 | filter, 554 | ); 555 | 556 | for pos in maze.positions() { 557 | assert_eq!(filter(pos), maze[pos].visited); 558 | } 559 | } 560 | } 561 | } 562 | } 563 | -------------------------------------------------------------------------------- /maze/src/initialize/spelunker.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::matrix; 4 | use crate::Maze; 5 | 6 | /// A list of instructions. 7 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 8 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 9 | pub struct Instructions(Vec); 10 | 11 | /// Instructions for the spelunker. 12 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 13 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 14 | pub enum Instruction { 15 | /// Move forward. 16 | Forward, 17 | 18 | /// Turn left. 19 | Left, 20 | 21 | /// Turn right. 22 | Right, 23 | 24 | /// Fork and let one spelunker start from the beginning of the instruction 25 | /// set after having turned left. 26 | ForkLeft, 27 | 28 | /// Fork and let one spelunker start from the beginning of the instruction 29 | /// set after having turned right. 30 | ForkRight, 31 | } 32 | 33 | /// Initialises a maze by clearing all inner walls. 34 | /// 35 | /// This method will ignore rooms for which `filter` returns `false`. 36 | /// 37 | /// # Arguments 38 | /// * `maze``- The maze to initialise. 39 | /// * `rng` - Not used. 40 | /// * `candidates` - A predicate filtering rooms to consider. 41 | /// * `instructions` - The spelunker instructions. 42 | pub(crate) fn initialize( 43 | mut maze: Maze, 44 | rng: &mut R, 45 | mut candidates: matrix::Matrix, 46 | instructions: &Instructions, 47 | ) -> Maze 48 | where 49 | R: super::Randomizer + Sized, 50 | T: Clone, 51 | { 52 | let mask = candidates.clone(); 53 | 54 | // The list of spelunker origins 55 | let mut origins = super::random_room(rng, &candidates) 56 | .and_then(|pos| super::random_wall(rng, &candidates, pos, &maze)) 57 | .into_iter() 58 | .collect::>(); 59 | 60 | loop { 61 | // Find the next wall position 62 | let mut wall_pos = if let Some(wall_pos) = origins.pop() { 63 | // Continue a previous fork 64 | wall_pos 65 | } else { 66 | if let Some(room_pos) = super::random_room(rng, &candidates) { 67 | candidates[room_pos] = false; 68 | if let Some(wall_pos) = 69 | super::random_wall(rng, &candidates, room_pos, &maze) 70 | { 71 | // We have selected a new origin 72 | wall_pos 73 | } else { 74 | // The rooms has no candidate walls 75 | continue; 76 | } 77 | } else { 78 | // All candidate rooms have been visited 79 | break; 80 | } 81 | }; 82 | 83 | // Execute the instructions 84 | for i in instructions.0.iter().cycle() { 85 | use Instruction::*; 86 | match i { 87 | Forward => { 88 | candidates[wall_pos.0] = false; 89 | 90 | let back = maze.back(wall_pos); 91 | if candidates.get(back.0).is_some_and(|&b| b) 92 | && !maze.rooms[back.0].visited 93 | { 94 | maze.open(wall_pos); 95 | wall_pos = maze 96 | .opposite(back) 97 | .map(|wall| (back.0, wall)) 98 | .unwrap(); 99 | candidates[wall_pos.0] = false; 100 | } else { 101 | break; 102 | } 103 | } 104 | Left => wall_pos = (wall_pos.0, wall_pos.1.previous), 105 | Right => wall_pos = (wall_pos.0, wall_pos.1.next), 106 | ForkLeft => origins.push((wall_pos.0, wall_pos.1.previous)), 107 | ForkRight => origins.push((wall_pos.0, wall_pos.1.next)), 108 | } 109 | } 110 | } 111 | 112 | println!("CONNECTING"); 113 | super::connect_all(&mut maze, rng, |pos| mask.get(pos).is_some_and(|&b| b)); 114 | 115 | maze 116 | } 117 | 118 | /// The prefix for the string version of this method. 119 | const PREFIX: &str = "spelunker("; 120 | 121 | /// The suffix for the string version of this method. 122 | const SUFFIX: &str = ")"; 123 | 124 | pub(crate) fn parse_method(s: &str) -> Result { 125 | if s.starts_with(PREFIX) && s.ends_with(SUFFIX) { 126 | Ok(super::Method::Spelunker { 127 | instructions: s[PREFIX.len()..s.len() - SUFFIX.len()].parse()?, 128 | }) 129 | } else { 130 | Err(()) 131 | } 132 | } 133 | 134 | pub(crate) fn display_method( 135 | f: &mut std::fmt::Formatter, 136 | instructions: &Instructions, 137 | ) -> std::fmt::Result { 138 | write!(f, "{}{}{}", PREFIX, instructions, SUFFIX) 139 | } 140 | 141 | impl From> for Instructions { 142 | fn from(source: Vec) -> Self { 143 | Self(source) 144 | } 145 | } 146 | 147 | impl std::fmt::Display for Instructions { 148 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 149 | for i in &self.0 { 150 | write!(f, "{}", i.char())?; 151 | } 152 | Ok(()) 153 | } 154 | } 155 | 156 | impl std::str::FromStr for Instructions { 157 | type Err = (); 158 | 159 | fn from_str(s: &str) -> Result { 160 | Ok(Self( 161 | s.chars() 162 | .map(|c| Instruction::try_from(c)) 163 | .collect::, _>>()?, 164 | )) 165 | } 166 | } 167 | 168 | impl Instruction { 169 | /// The character used to represent this instruction. 170 | pub const fn char(self) -> char { 171 | use Instruction::*; 172 | match self { 173 | Forward => '|', 174 | Left => '<', 175 | Right => '>', 176 | ForkLeft => '}', 177 | ForkRight => '{', 178 | } 179 | } 180 | } 181 | 182 | impl TryFrom for Instruction { 183 | type Error = (); 184 | 185 | fn try_from(source: char) -> Result { 186 | use Instruction::*; 187 | match source { 188 | c if c == Forward.char() => Ok(Forward), 189 | c if c == Left.char() => Ok(Left), 190 | c if c == Right.char() => Ok(Right), 191 | c if c == ForkLeft.char() => Ok(ForkLeft), 192 | c if c == ForkRight.char() => Ok(ForkRight), 193 | _ => Err(()), 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /maze/src/initialize/winding.rs: -------------------------------------------------------------------------------- 1 | use crate::Maze; 2 | 3 | use crate::matrix; 4 | 5 | /// Initialises a maze using the _Depth First_ algorithm. 6 | /// 7 | /// See [here](https://en.wikipedia.org/wiki/Maze_generation_algorithm) for a 8 | /// description of the algorithm. 9 | /// 10 | /// The maze should be fully closed; any already open walls will be ignored and 11 | /// kept. 12 | /// 13 | /// This method will ignore rooms for which `filter` returns `false`. 14 | /// 15 | /// # Arguments 16 | /// * `maze``- The maze to initialise. 17 | /// * `rng` - A random number generator. 18 | /// * `candidates` - A filter for the rooms to modify. 19 | pub(crate) fn initialize( 20 | mut maze: Maze, 21 | rng: &mut R, 22 | mut candidates: matrix::Matrix, 23 | ) -> Maze 24 | where 25 | R: super::Randomizer + Sized, 26 | T: Clone, 27 | { 28 | // The backracking path is initially empty 29 | let mut path = Vec::new(); 30 | 31 | // Start in a random room; we know that at least one candidate exists 32 | let mut current = super::random_room(rng, &candidates).unwrap(); 33 | 34 | loop { 35 | candidates[current] = false; 36 | 37 | // Find all non-visited neighbours as the tuple (neighbour-position, 38 | // wall-from-current) 39 | let neighbors = maze 40 | .walls(current) 41 | .iter() 42 | .map(|wall| maze.back((current, wall))) 43 | .filter(|&(pos, _)| *candidates.get(pos).unwrap_or(&false)) 44 | .map(|(pos, wall)| (pos, maze.back((pos, wall)).1)) 45 | .collect::>(); 46 | 47 | // If any exists, move to a random one and update the path, otherwise 48 | // backtrack to the previous room; since the maze may be segmented, we 49 | // must also attempt to find a new random room 50 | if !neighbors.is_empty() { 51 | let (next, wall) = neighbors[rng.range(0, neighbors.len())]; 52 | maze.open((current, wall)); 53 | path.push(current); 54 | current = next; 55 | } else if let Some(next) = 56 | path.pop().or_else(|| super::random_room(rng, &candidates)) 57 | { 58 | current = next; 59 | } else { 60 | break; 61 | } 62 | } 63 | 64 | maze 65 | } 66 | -------------------------------------------------------------------------------- /maze/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[cfg(test)] 7 | mod test_utils; 8 | 9 | #[macro_use] 10 | mod macros; 11 | 12 | pub mod wall; 13 | 14 | pub mod shape; 15 | pub use self::shape::Shape; 16 | 17 | pub mod initialize; 18 | pub mod matrix; 19 | pub mod physical; 20 | pub mod render; 21 | pub mod room; 22 | pub mod walk; 23 | 24 | /// A wall of a room. 25 | pub type WallPos = (matrix::Pos, &'static wall::Wall); 26 | 27 | /// A matrix of rooms. 28 | type Rooms = matrix::Matrix>; 29 | 30 | /// A maze contains rooms and has methods for managing paths and doors. 31 | #[derive(Clone)] 32 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 33 | pub struct Maze 34 | where 35 | T: Clone, 36 | { 37 | /// The shape of the rooms. 38 | shape: Shape, 39 | 40 | /// The actual rooms. 41 | rooms: Rooms, 42 | } 43 | 44 | impl Maze 45 | where 46 | T: Clone + Default, 47 | { 48 | /// Creates an uninitialised maze. 49 | /// 50 | /// # Arguments 51 | /// * `shape` - The shape of the rooms. 52 | /// * `width` - The width, in rooms, of the maze. 53 | /// * `height` - The height, in rooms, of the maze. 54 | pub fn new(shape: Shape, width: usize, height: usize) -> Self { 55 | let rooms = Rooms::new(width, height); 56 | Self { shape, rooms } 57 | } 58 | } 59 | 60 | impl Maze 61 | where 62 | T: Clone, 63 | { 64 | /// Creates an uninitialised maze. 65 | /// 66 | /// This method allows creating a maze initialised with data. 67 | /// 68 | /// # Arguments 69 | /// * `shape` - The shape of the rooms. 70 | /// * `width` - The width, in rooms, of the maze. 71 | /// * `height` - The height, in rooms, of the maze. 72 | /// * `data` - A function providing room data. 73 | pub fn new_with_data( 74 | shape: Shape, 75 | width: usize, 76 | height: usize, 77 | mut data: F, 78 | ) -> Self 79 | where 80 | F: FnMut(matrix::Pos) -> T, 81 | { 82 | let rooms = Rooms::new_with_data(width, height, |pos| data(pos).into()); 83 | Self { shape, rooms } 84 | } 85 | 86 | /// Maps each room, yielding a maze with the same layout but with 87 | /// transformed data. 88 | /// 89 | /// # Arguments 90 | /// * `data` - A function providing data for the new maze. 91 | pub fn map(&self, mut data: F) -> Maze 92 | where 93 | F: FnMut(matrix::Pos, T) -> U, 94 | U: Clone, 95 | { 96 | Maze { 97 | shape: self.shape, 98 | rooms: self.rooms.map_with_pos(|pos, value| { 99 | value.with_data(data(pos, value.data.clone())) 100 | }), 101 | } 102 | } 103 | 104 | /// The width of the maze. 105 | pub fn width(&self) -> usize { 106 | self.rooms.width 107 | } 108 | 109 | /// The height of the maze. 110 | pub fn height(&self) -> usize { 111 | self.rooms.height 112 | } 113 | 114 | /// The shape of the maze. 115 | pub fn shape(&self) -> Shape { 116 | self.shape 117 | } 118 | 119 | /// The data for a specific room. 120 | /// 121 | /// If the index is out of bounds, nothing is returned. 122 | /// 123 | /// # Arguments 124 | /// * `pos``- The room position. 125 | pub fn data(&self, pos: matrix::Pos) -> Option<&T> { 126 | self.rooms.get(pos).map(|room| &room.data) 127 | } 128 | 129 | /// The mutable data for a specific room. 130 | /// 131 | /// If the position is out of bounds, nothing is returned. 132 | /// 133 | /// # Arguments 134 | /// * `pos``- The room position. 135 | pub fn data_mut(&mut self, pos: matrix::Pos) -> Option<&mut T> { 136 | self.rooms.get_mut(pos).map(|room| &mut room.data) 137 | } 138 | 139 | /// Whether a position is inside of the maze. 140 | /// 141 | /// # Arguments 142 | /// * `pos` - The romm position. 143 | pub fn is_inside(&self, pos: matrix::Pos) -> bool { 144 | self.rooms.is_inside(pos) 145 | } 146 | 147 | /// Whether a wall is open. 148 | /// 149 | /// If the position is out of bounds, `false` is returned. 150 | /// 151 | /// # Arguments 152 | /// * `wall_pos` - The wall position. 153 | pub fn is_open(&self, wall_pos: WallPos) -> bool { 154 | self.rooms 155 | .get(wall_pos.0) 156 | .map(|room| room.is_open(wall_pos.1)) 157 | .unwrap_or(false) 158 | } 159 | 160 | /// Finds the wall connecting two rooms. 161 | /// 162 | /// The returned wall position, if it exists, will be in the room at `pos1`. 163 | /// 164 | /// # Arguments 165 | /// * `pos1` - The first room position. 166 | /// * `pos2` - The second room position. 167 | pub fn connecting_wall( 168 | &self, 169 | pos1: matrix::Pos, 170 | pos2: matrix::Pos, 171 | ) -> Option { 172 | self.walls(pos1) 173 | .iter() 174 | .find(|wall| { 175 | (pos1.col + wall.dir.0 == pos2.col) 176 | && (pos1.row + wall.dir.1 == pos2.row) 177 | }) 178 | .map(|&wall| (pos1, wall)) 179 | } 180 | 181 | /// Whether two rooms are connected. 182 | /// 183 | /// Two rooms are connected if there is an open wall between them, or if 184 | /// they are the same room. 185 | /// 186 | /// # Arguments 187 | /// * `pos1` - The first room. 188 | /// * `pos2` - The second room. 189 | pub fn connected(&self, pos1: matrix::Pos, pos2: matrix::Pos) -> bool { 190 | if pos1 == pos2 { 191 | true 192 | } else { 193 | self.connecting_wall(pos1, pos2) 194 | .map(|wall_pos| self.is_open(wall_pos)) 195 | .unwrap_or(false) 196 | } 197 | } 198 | 199 | /// Sets whether a wall is open. 200 | /// 201 | /// # Arguments 202 | /// * `wall_pos` - The wall position. 203 | /// * `value` - Whether to open the wall. 204 | pub fn set_open(&mut self, wall_pos: WallPos, value: bool) { 205 | // First modify the requested wall... 206 | if let Some(room) = self.rooms.get_mut(wall_pos.0) { 207 | room.set_open(wall_pos.1, value); 208 | } 209 | 210 | // ...and then sync the value on the back 211 | let other = self.back(wall_pos); 212 | if let Some(other_room) = self.rooms.get_mut(other.0) { 213 | other_room.set_open(other.1, value); 214 | } 215 | } 216 | 217 | /// Opens a wall. 218 | /// 219 | /// # Arguments 220 | /// * `wall_pos` - The wall position. 221 | pub fn open(&mut self, wall_pos: WallPos) { 222 | self.set_open(wall_pos, true); 223 | } 224 | 225 | /// Closes a wall. 226 | /// 227 | /// # Arguments 228 | /// * `wall_pos` - The wall position. 229 | pub fn close(&mut self, wall_pos: WallPos) { 230 | self.set_open(wall_pos, false); 231 | } 232 | 233 | /// Iterates over all room positions. 234 | /// 235 | /// The positions are visited row by row, starting from `(0, 0)` and ending 236 | /// with `(self.width() - 1, self.height - 1())`. 237 | pub fn positions(&self) -> impl Iterator { 238 | self.rooms.positions() 239 | } 240 | 241 | /// The physical positions of the two corners of a wall. 242 | /// 243 | /// # Arguments 244 | /// * `wall_pos` - The wall position. 245 | pub fn corners(&self, wall_pos: WallPos) -> (physical::Pos, physical::Pos) { 246 | let center = self.center(wall_pos.0); 247 | (center + wall_pos.1.span.0, center + wall_pos.1.span.1) 248 | } 249 | 250 | /// See [`Self::corner_walls_start`]. 251 | #[deprecated] 252 | pub fn corner_walls( 253 | &self, 254 | wall_pos: WallPos, 255 | ) -> impl DoubleEndedIterator { 256 | self.corner_walls_start(wall_pos) 257 | } 258 | 259 | /// All walls that meet in the corner where a wall has its start span. 260 | /// 261 | /// The walls are visited in counter-clockwise order. Only one side of each 262 | /// wall will be visited. Each consecutive wall will be in a room different 263 | /// from the previous one. 264 | /// 265 | /// This method will visit rooms outside of the maze for rooms on the edge. 266 | /// 267 | /// # Arguments 268 | /// * `wall_pos` - The wall position. 269 | pub fn corner_walls_start( 270 | &self, 271 | wall_pos: WallPos, 272 | ) -> impl DoubleEndedIterator { 273 | let (matrix::Pos { col, row }, wall) = wall_pos; 274 | std::iter::once(wall_pos).chain(wall.corner_wall_offsets.iter().map( 275 | move |&wall::Offset { dx, dy, wall }| { 276 | ( 277 | matrix::Pos { 278 | col: col + dx, 279 | row: row + dy, 280 | }, 281 | wall, 282 | ) 283 | }, 284 | )) 285 | } 286 | 287 | /// All walls that meet in the corner where a wall has its end span. 288 | /// 289 | /// The walls are visited in clockwise order. Only one side of each wall 290 | /// will be visited. Each consecutive wall will be in a room different from 291 | /// the previous one. 292 | /// 293 | /// This method will visit rooms outside of the maze for rooms on the edge. 294 | /// 295 | /// # Arguments 296 | /// * `wall_pos` - The wall position. 297 | pub fn corner_walls_end( 298 | &self, 299 | wall_pos: WallPos, 300 | ) -> impl DoubleEndedIterator { 301 | let shape = self.shape; 302 | let (matrix::Pos { col, row }, wall) = shape.back(wall_pos); 303 | std::iter::once(wall_pos).chain( 304 | wall.corner_wall_offsets.iter().rev().map( 305 | move |&wall::Offset { dx, dy, wall }| { 306 | shape.back(( 307 | matrix::Pos { 308 | col: col + dx, 309 | row: row + dy, 310 | }, 311 | wall, 312 | )) 313 | }, 314 | ), 315 | ) 316 | } 317 | 318 | /// Iterates over all wall positions of a room. 319 | /// 320 | /// # Arguments 321 | /// * `pos` - The room position. 322 | pub fn wall_positions( 323 | &self, 324 | pos: matrix::Pos, 325 | ) -> impl DoubleEndedIterator + '_ { 326 | self.walls(pos).iter().map(move |&wall| (pos, wall)) 327 | } 328 | 329 | /// Iterates over all open walls of a room. 330 | /// 331 | /// # Arguments 332 | /// * `pos` - The room position. 333 | pub fn doors( 334 | &self, 335 | pos: matrix::Pos, 336 | ) -> impl DoubleEndedIterator + '_ 337 | { 338 | self.walls(pos) 339 | .iter() 340 | .filter(move |&wall| self.is_open((pos, wall))) 341 | .copied() 342 | } 343 | 344 | /// Iterates over all adjacent rooms. 345 | /// 346 | /// This method will visit rooms outside of the maze for rooms on the edge. 347 | /// 348 | /// # Arguments 349 | /// * `pos` - The room position. 350 | pub fn adjacent( 351 | &self, 352 | pos: matrix::Pos, 353 | ) -> impl DoubleEndedIterator + '_ { 354 | self.walls(pos).iter().map(move |&wall| matrix::Pos { 355 | col: pos.col + wall.dir.0, 356 | row: pos.row + wall.dir.1, 357 | }) 358 | } 359 | 360 | /// Iterates over all reachable neighbours of a room. 361 | /// 362 | /// This method will visit rooms outside of the maze if an opening outside 363 | /// from the room exists. 364 | /// 365 | /// # Arguments 366 | /// * `pos` - The room position. 367 | pub fn neighbors( 368 | &self, 369 | pos: matrix::Pos, 370 | ) -> impl DoubleEndedIterator + '_ { 371 | self.doors(pos).map(move |wall| self.back((pos, wall)).0) 372 | } 373 | } 374 | 375 | impl std::ops::Index for Maze 376 | where 377 | T: Clone, 378 | { 379 | type Output = room::Room; 380 | 381 | fn index(&self, pos: matrix::Pos) -> &Self::Output { 382 | &self.rooms[pos] 383 | } 384 | } 385 | 386 | /// A matrix of scores for rooms. 387 | pub type HeatMap = matrix::Matrix; 388 | 389 | /// Generates a heat map where the value for each cell is the number of times it 390 | /// has been traversed when walking between the positions. 391 | /// 392 | /// Any position pairs with no path between them will be ignored. 393 | /// 394 | /// # Arguments 395 | /// * `positions` - The positions as the tuple `(from, to)`. These are used as 396 | /// positions between which to walk. 397 | pub fn heatmap(maze: &crate::Maze, positions: I) -> HeatMap 398 | where 399 | I: Iterator, 400 | T: Clone, 401 | { 402 | let mut result = matrix::Matrix::new(maze.width(), maze.height()); 403 | 404 | for (from, to) in positions { 405 | if let Some(path) = maze.walk(from, to) { 406 | for pos in path.into_iter() { 407 | result[pos] += 1; 408 | } 409 | } 410 | } 411 | 412 | result 413 | } 414 | 415 | #[cfg(test)] 416 | mod tests { 417 | use std::iter::once; 418 | 419 | use maze_test::maze_test; 420 | 421 | use super::test_utils::*; 422 | use super::*; 423 | 424 | #[test] 425 | fn data() { 426 | let mut maze = Shape::Quad.create::(5, 5); 427 | let pos = (0isize, 0isize).into(); 428 | assert_eq!(Some(&false), maze.data(pos)); 429 | *maze.data_mut(pos).unwrap() = true; 430 | assert_eq!(Some(&true), maze.data(pos)); 431 | } 432 | 433 | #[maze_test] 434 | fn is_inside_correct(maze: TestMaze) { 435 | assert!(maze.is_inside(matrix_pos(0, 0))); 436 | assert!(maze.is_inside(matrix_pos( 437 | maze.width() as isize - 1, 438 | maze.height() as isize - 1, 439 | ))); 440 | assert!(!maze.is_inside(matrix_pos(-1, -1))); 441 | assert!(!maze.is_inside(matrix_pos( 442 | maze.width() as isize, 443 | maze.height() as isize 444 | ))); 445 | } 446 | 447 | #[maze_test] 448 | fn can_open(mut maze: TestMaze) { 449 | let log = Navigator::new(&mut maze).down(true).stop(); 450 | let pos = log[0]; 451 | let next = log[1]; 452 | assert!( 453 | maze.walls(pos) 454 | .iter() 455 | .filter(|wall| maze.is_open((pos, wall))) 456 | .count() 457 | == 1 458 | ); 459 | assert!( 460 | maze.walls(next) 461 | .iter() 462 | .filter(|wall| maze.is_open((next, wall))) 463 | .count() 464 | == 1 465 | ); 466 | } 467 | 468 | #[maze_test] 469 | fn can_close(mut maze: TestMaze) { 470 | let log = Navigator::new(&mut maze).down(true).up(false).stop(); 471 | let pos = log.first().unwrap(); 472 | let next = log.last().unwrap(); 473 | assert!( 474 | maze.walls(*pos) 475 | .iter() 476 | .filter(|wall| maze.is_open((*pos, wall))) 477 | .count() 478 | == 0 479 | ); 480 | assert!( 481 | maze.walls(*next) 482 | .iter() 483 | .filter(|wall| maze.is_open((*next, wall))) 484 | .count() 485 | == 0 486 | ); 487 | } 488 | 489 | #[maze_test] 490 | fn connecting_wall_correct(maze: TestMaze) { 491 | for pos in maze.positions() { 492 | for &wall in maze.walls(pos) { 493 | assert!(maze 494 | .connecting_wall( 495 | pos, 496 | matrix::Pos { 497 | col: pos.col - 3, 498 | row: pos.row - 3 499 | } 500 | ) 501 | .is_none()); 502 | let wall_pos = (pos, wall); 503 | let other = matrix::Pos { 504 | col: pos.col + wall.dir.0, 505 | row: pos.row + wall.dir.1, 506 | }; 507 | assert_eq!(Some(wall_pos), maze.connecting_wall(pos, other)); 508 | } 509 | } 510 | } 511 | 512 | #[maze_test] 513 | fn connected_correct(mut maze: TestMaze) { 514 | for pos in maze.positions() { 515 | assert!(maze.connected(pos, pos)) 516 | } 517 | 518 | let pos1 = matrix_pos(1, 1); 519 | for wall in maze.walls(pos1) { 520 | let pos2 = matrix_pos(pos1.col + wall.dir.0, pos1.row + wall.dir.1); 521 | assert!(!maze.connected(pos1, pos2)); 522 | maze.open((pos1, wall)); 523 | assert!(maze.connected(pos1, pos2)); 524 | } 525 | } 526 | 527 | #[maze_test] 528 | fn corner_wall_sequence(maze: TestMaze) { 529 | for wall in maze.shape.all_walls() { 530 | let mut followed = vec![*wall]; 531 | loop { 532 | let current = followed.last().unwrap(); 533 | let next = current.corner_wall_offsets.first().unwrap().wall; 534 | if next == *wall { 535 | break; 536 | } else { 537 | followed.push(next); 538 | } 539 | } 540 | assert_eq!( 541 | followed, 542 | once(*wall) 543 | .chain(wall.corner_wall_offsets.iter().map(|o| o.wall)) 544 | .collect::>() 545 | ); 546 | } 547 | } 548 | 549 | #[maze_test] 550 | fn corner_walls_start(maze: TestMaze) { 551 | for pos in maze.positions() { 552 | for wall_pos in maze.wall_positions(pos) { 553 | let (center, _) = maze.corners(wall_pos); 554 | for corner_wall in maze.corner_walls_start(wall_pos) { 555 | let (corner, _) = maze.corners(corner_wall); 556 | assert!(is_close(corner, center)); 557 | } 558 | } 559 | } 560 | } 561 | 562 | #[maze_test] 563 | fn corner_walls_end(maze: TestMaze) { 564 | for pos in maze.positions() { 565 | for wall_pos in maze.wall_positions(pos) { 566 | let (_, center) = maze.corners(wall_pos); 567 | for corner_wall in maze.corner_walls_end(wall_pos) { 568 | let (_, corner) = maze.corners(corner_wall); 569 | assert!(is_close(corner, center)); 570 | } 571 | } 572 | } 573 | } 574 | 575 | #[maze_test] 576 | fn doors(mut maze: TestMaze) { 577 | let pos = matrix::Pos { col: 0, row: 0 }; 578 | assert_eq!( 579 | maze.doors(pos).collect::>(), 580 | Vec::<&'static wall::Wall>::new(), 581 | ); 582 | let walls = maze 583 | .walls(pos) 584 | .iter() 585 | .filter(|wall| maze.is_inside(maze.back((pos, wall)).0)).copied() 586 | .collect::>(); 587 | walls.iter().for_each(|wall| maze.open((pos, wall))); 588 | assert_eq!(maze.doors(pos).collect::>(), walls); 589 | } 590 | 591 | #[maze_test] 592 | fn adjacent(maze: TestMaze) { 593 | for pos1 in maze.positions() { 594 | for pos2 in maze.positions() { 595 | assert!( 596 | maze.connecting_wall(pos1, pos2).is_some() 597 | == maze.adjacent(pos1).any(|p| pos2 == p) 598 | ); 599 | } 600 | } 601 | } 602 | 603 | #[maze_test] 604 | fn neighbors(mut maze: TestMaze) { 605 | let pos = matrix::Pos { col: 0, row: 0 }; 606 | assert_eq!(maze.neighbors(pos).collect::>(), vec![]); 607 | maze.walls(pos) 608 | .iter() 609 | .for_each(|wall| maze.open((pos, wall))); 610 | assert_eq!( 611 | maze.neighbors(pos).collect::>(), 612 | maze.walls(pos) 613 | .iter() 614 | .map(|wall| matrix::Pos { 615 | col: pos.col + wall.dir.0, 616 | row: pos.row + wall.dir.1 617 | }) 618 | .collect::>(), 619 | ); 620 | } 621 | } 622 | -------------------------------------------------------------------------------- /maze/src/macros.rs: -------------------------------------------------------------------------------- 1 | /// Dispatches a function call for the current maze to a shape defined module. 2 | /// 3 | /// This is an internal library macro. 4 | macro_rules! dispatch { 5 | ($on:expr => $func:ident ( $($args:ident $(,)?)* ) ) => { 6 | match $on { 7 | crate::Shape::Hex => hex::$func($($args,)*), 8 | crate::Shape::Quad => quad::$func($($args,)*), 9 | crate::Shape::Tri => tri::$func($($args,)*), 10 | } 11 | } 12 | } 13 | 14 | /// Defines a wall module. 15 | /// 16 | /// This is an internal library macro. 17 | macro_rules! define_shape { 18 | ( << $name:ident >> $( $wall_name:ident ( $ordinal:expr ) = { 19 | $( $field:ident: $val:expr, )* 20 | } ),* ) => { 21 | #[allow(unused_imports, non_camel_case_types)] 22 | pub mod walls { 23 | use $crate::wall as wall; 24 | use super::*; 25 | 26 | pub enum WallIndex { 27 | $($wall_name,)* 28 | } 29 | 30 | $(pub static $wall_name: wall::Wall = wall::Wall { 31 | name: concat!(stringify!($name), ":", stringify!($wall_name)), 32 | shape: crate::shape::Shape::$name, 33 | index: WallIndex::$wall_name as usize, 34 | ordinal: $ordinal, 35 | $( $field: $val, )* 36 | } );*; 37 | 38 | pub static ALL: &[&'static wall::Wall] = &[$(&$wall_name),*]; 39 | } 40 | 41 | /// Returns all walls used in this type of maze. 42 | pub fn all_walls() -> &'static [&'static wall::Wall] { 43 | &walls::ALL 44 | } 45 | 46 | /// Returns the wall on the back of `wall_pos`. 47 | /// 48 | /// # Arguments 49 | /// * `wall_pos` - The wall for which to find the back. 50 | pub fn back(wall_pos: WallPos) -> WallPos { 51 | let (pos, wall) = wall_pos; 52 | let other = matrix::Pos { 53 | col: pos.col + wall.dir.0, 54 | row: pos.row + wall.dir.1, 55 | }; 56 | 57 | (other, walls::ALL[self::back_index(wall.index)]) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /maze/src/physical.rs: -------------------------------------------------------------------------------- 1 | //! # Aspects of the maze as laid out in a physical landscape 2 | //! 3 | //! When physically laying out the maze, rooms and edges have certain 4 | //! attributes. These are collected in this module. 5 | use std::ops; 6 | 7 | #[cfg(feature = "serde")] 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use crate::wall::Angle; 11 | 12 | /// A physical position. 13 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 14 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 15 | pub struct Pos { 16 | /// The X coordinate. 17 | pub x: f32, 18 | 19 | /// The Y coordinate. 20 | pub y: f32, 21 | } 22 | 23 | impl Pos { 24 | /// A scalar value signifying distance from _(0, 0)_. 25 | /// 26 | /// The length of the position vector is given by the square root of this 27 | /// value. 28 | /// 29 | /// # Example 30 | /// 31 | /// ``` 32 | /// # use maze::physical::*; 33 | /// 34 | /// let d = (0.1f32.cos(), 0.1f32.sin()); 35 | /// let v = ( 36 | /// Pos { x: 0.0, y: 0.0 }.value(), 37 | /// Pos { x: d.0 * 1.0, y: d.1 * 1.0 }.value(), 38 | /// Pos { x: d.0 * 2.0, y: d.1 * 2.0 }.value(), 39 | /// Pos { x: d.0 * 4.0, y: d.1 * 4.0 }.value(), 40 | /// ); 41 | /// 42 | /// assert!(v.0 < v.1); 43 | /// assert!(v.1 < v.2); 44 | /// assert!(v.2 < v.3); 45 | /// ``` 46 | pub fn value(self) -> f32 { 47 | self.x * self.x + self.y * self.y 48 | } 49 | } 50 | 51 | impl From<(T, T)> for Pos 52 | where 53 | T: Into, 54 | { 55 | /// Converts the tuple _(x, y)_ to `Pos { x, y }`. 56 | /// 57 | /// # Example 58 | /// 59 | /// ``` 60 | /// # use maze::physical::*; 61 | /// 62 | /// assert_eq!( 63 | /// Pos::from((1.0f32, 2.0f32)), 64 | /// Pos { x: 1.0, y: 2.0 }, 65 | /// ); 66 | /// assert_eq!( 67 | /// Pos::from((1i16, 2i16)), 68 | /// Pos { x: 1.0, y: 2.0 }, 69 | /// ); 70 | /// ``` 71 | fn from((x, y): (T, T)) -> Self { 72 | Pos { 73 | x: x.into(), 74 | y: y.into(), 75 | } 76 | } 77 | } 78 | 79 | impl ops::Add for Pos { 80 | type Output = Self; 81 | 82 | /// Adds the axis values of two positions. 83 | /// 84 | /// # Example 85 | /// 86 | /// ``` 87 | /// # use maze::physical::*; 88 | /// 89 | /// assert_eq!( 90 | /// Pos { x: 1.0, y: 2.0 } + Pos { x: 3.0, y: 4.0 }, 91 | /// Pos { x: 4.0, y: 6.0 }, 92 | /// ); 93 | /// ``` 94 | /// 95 | /// # Arguments 96 | /// * `other` - The other position to add. 97 | fn add(self, other: Self) -> Self::Output { 98 | Self { 99 | x: self.x + other.x, 100 | y: self.y + other.y, 101 | } 102 | } 103 | } 104 | 105 | impl ops::Sub for Pos { 106 | type Output = Self; 107 | 108 | /// Subtracts the axis values of another position from the axis values of 109 | /// this one. 110 | /// 111 | /// # Example 112 | /// 113 | /// ``` 114 | /// # use maze::physical::*; 115 | /// 116 | /// assert_eq!( 117 | /// Pos { x: 4.0, y: 6.0 } - Pos { x: 3.0, y: 4.0 }, 118 | /// Pos { x: 1.0, y: 2.0 }, 119 | /// ); 120 | /// ``` 121 | /// 122 | /// # Arguments 123 | /// * `other` - The other position to add. 124 | fn sub(self, other: Self) -> Self::Output { 125 | Self { 126 | x: self.x - other.x, 127 | y: self.y - other.y, 128 | } 129 | } 130 | } 131 | 132 | impl ops::Mul for Pos { 133 | type Output = Self; 134 | 135 | /// Multiplies the axis values of a position. 136 | /// 137 | /// # Example 138 | /// 139 | /// ``` 140 | /// # use maze::physical::*; 141 | /// 142 | /// assert_eq!( 143 | /// Pos { x: 1.0, y: 2.5 } * 2.0, 144 | /// Pos { x: 2.0, y: 5.0 }, 145 | /// ); 146 | /// ``` 147 | /// 148 | /// # Arguments 149 | /// * `other` - The multiplication factor. 150 | #[inline] 151 | fn mul(self, other: f32) -> Self::Output { 152 | Self { 153 | x: other * self.x, 154 | y: other * self.y, 155 | } 156 | } 157 | } 158 | 159 | impl ops::Mul for f32 { 160 | type Output = Pos; 161 | 162 | /// Multiplies the axis values of a position. 163 | /// 164 | /// # Example 165 | /// 166 | /// ``` 167 | /// # use maze::physical::*; 168 | /// 169 | /// assert_eq!( 170 | /// 2.0 * Pos { x: 1.0, y: 2.5 }, 171 | /// Pos { x: 2.0, y: 5.0 }, 172 | /// ); 173 | /// ``` 174 | /// 175 | /// # Arguments 176 | /// * `other` - The multiplication factor. 177 | #[inline] 178 | fn mul(self, other: Pos) -> Self::Output { 179 | other * self 180 | } 181 | } 182 | 183 | impl ops::Div for Pos { 184 | type Output = Self; 185 | 186 | /// Divides the axis values of a position. 187 | /// 188 | /// # Example 189 | /// 190 | /// ``` 191 | /// # use maze::physical::*; 192 | /// 193 | /// assert_eq!( 194 | /// Pos { x: 1.0, y: 3.0 } / 2.0, 195 | /// Pos { x: 0.5, y: 1.5 }, 196 | /// ); 197 | /// ``` 198 | /// 199 | /// # Arguments 200 | /// * `other` - The divisor. 201 | #[inline] 202 | fn div(self, other: f32) -> Self::Output { 203 | self * (1.0 / other) 204 | } 205 | } 206 | 207 | impl ops::Add for Pos { 208 | type Output = Self; 209 | 210 | /// Adds the delta values of an angle to this position. 211 | /// 212 | /// This is equal to to adding the position vector generated by the cosine 213 | /// and sine values of the angle. 214 | /// 215 | /// # Example 216 | /// 217 | /// ``` 218 | /// # use maze::physical::*; 219 | /// # use maze::wall::*; 220 | /// 221 | /// assert_eq!( 222 | /// Pos { x: 1.0, y: 2.0 } + Angle { a: 0.0, dx: 1.0, dy: 0.0 }, 223 | /// Pos { x: 2.0, y: 2.0 }, 224 | /// ); 225 | /// ``` 226 | /// 227 | /// # Arguments 228 | /// * `other` - The other position to add. 229 | fn add(self, other: Angle) -> Self::Output { 230 | Self { 231 | x: self.x + other.dx, 232 | y: self.y + other.dy, 233 | } 234 | } 235 | } 236 | 237 | /// A view box described by one corner and the width and height of the sides. 238 | /// 239 | /// The remaining corners are retrieved by adding the width and height the the 240 | /// corner coordinates. 241 | #[derive(Clone, Copy, Debug, PartialEq)] 242 | pub struct ViewBox { 243 | /// A corner. 244 | /// 245 | /// The coordinates of the remaining corners can be calculated by adding 246 | /// `width` and `height` to this value. 247 | pub corner: Pos, 248 | 249 | /// The width of the view box. 250 | pub width: f32, 251 | 252 | /// The height of the view box. 253 | pub height: f32, 254 | } 255 | 256 | impl ViewBox { 257 | /// Creates a view box centered around a point. 258 | /// 259 | /// # Arguments 260 | /// * `pos` - The centre. 261 | /// * `width` - The width of the view box. 262 | /// * `height` - The height of the view box. 263 | pub fn centered_at(pos: Pos, width: f32, height: f32) -> Self { 264 | Self { 265 | corner: Pos { 266 | x: pos.x - 0.5 * width, 267 | y: pos.y - 0.5 * height, 268 | }, 269 | width, 270 | height, 271 | } 272 | } 273 | 274 | /// Flattens this view box to the tuple `(x, y, width, height)`. 275 | /// 276 | /// # Example 277 | /// 278 | /// ``` 279 | /// # use maze::physical::*; 280 | /// 281 | /// assert_eq!( 282 | /// ViewBox { 283 | /// corner: Pos { 284 | /// x: 1.0, 285 | /// y: 2.0, 286 | /// }, 287 | /// width: 3.0, 288 | /// height: 4.0, 289 | /// }.tuple(), 290 | /// (1.0, 2.0, 3.0, 4.0), 291 | /// ); 292 | /// ``` 293 | pub fn tuple(self) -> (f32, f32, f32, f32) { 294 | (self.corner.x, self.corner.y, self.width, self.height) 295 | } 296 | 297 | /// Expands this view box with `d` units. 298 | /// 299 | /// The centre is maintained, but every side will be `d` units further from 300 | /// it. 301 | /// 302 | /// If `d` is a negative value, the view box will be contracted, which may 303 | /// lead to a view box with negative dimensions. 304 | /// 305 | /// # Example 306 | /// 307 | /// ``` 308 | /// # use maze::physical::*; 309 | /// 310 | /// assert_eq!( 311 | /// ViewBox::centered_at(Pos { x: 1.0, y: 1.0 }, 2.0, 2.0) 312 | /// .expand(1.0), 313 | /// ViewBox { 314 | /// corner: Pos { 315 | /// x: -1.0, 316 | /// y: -1.0, 317 | /// }, 318 | /// width: 4.0, 319 | /// height: 4.0, 320 | /// }, 321 | /// ); 322 | /// ``` 323 | /// 324 | /// # Arguments 325 | /// * `d` - The number of units to expand. 326 | pub fn expand(self, d: f32) -> Self { 327 | Self { 328 | corner: Pos { 329 | x: self.corner.x - d, 330 | y: self.corner.y - d, 331 | }, 332 | width: self.width + 2.0 * d, 333 | height: self.height + 2.0 * d, 334 | } 335 | } 336 | 337 | /// The centre of this view box. 338 | /// 339 | /// # Example 340 | /// 341 | /// ``` 342 | /// # use maze::physical::*; 343 | /// 344 | /// let viewbox = ViewBox { 345 | /// corner: Pos { x: 0.0, y: 0.0 }, 346 | /// width: 2.0, 347 | /// height: 2.0, 348 | /// }; 349 | /// 350 | /// assert_eq!( 351 | /// viewbox.center(), 352 | /// Pos { x: 1.0, y: 1.0 }, 353 | /// ); 354 | /// ``` 355 | pub fn center(self) -> Pos { 356 | Pos { 357 | x: self.corner.x + 0.5 * self.width, 358 | y: self.corner.y + 0.5 * self.height, 359 | } 360 | } 361 | 362 | /// Whether a point is inside this view box. 363 | /// 364 | /// Points along the edge of the view box are also considered to be inside. 365 | /// 366 | /// # Example 367 | /// 368 | /// ``` 369 | /// # use maze::physical::*; 370 | /// 371 | /// let viewbox = ViewBox { 372 | /// corner: Pos { x: 0.0, y: 0.0 }, 373 | /// width: 1.0, 374 | /// height: 1.0, 375 | /// }; 376 | /// assert!(viewbox.contains(Pos { x: 0.0, y: 0.0 })); 377 | /// assert!(viewbox.contains(Pos { x: 0.5, y: 0.5 })); 378 | /// assert!(viewbox.contains(Pos { x: 1.0, y: 1.0 })); 379 | /// assert!(!viewbox.contains(Pos { x: 2.0, y: 2.0 })); 380 | /// ``` 381 | /// 382 | /// # Arguments 383 | /// * `pos` - The position to check. 384 | pub fn contains(self, pos: Pos) -> bool { 385 | pos.x >= self.corner.x 386 | && pos.y >= self.corner.y 387 | && pos.x <= self.corner.x + self.width 388 | && pos.y <= self.corner.x + self.height 389 | } 390 | } 391 | 392 | impl ops::Mul for f32 { 393 | type Output = ViewBox; 394 | 395 | /// Scales every value in this view box by `rhs`. 396 | /// 397 | /// # Example 398 | /// 399 | /// ``` 400 | /// # use maze::physical::*; 401 | /// 402 | /// assert_eq!( 403 | /// 2.0 * ViewBox { 404 | /// corner: Pos { x: 1.0, y: 1.0 }, 405 | /// width: 2.0, 406 | /// height: 2.0, 407 | /// }, 408 | /// ViewBox { 409 | /// corner: Pos { x: 2.0, y: 2.0 }, 410 | /// width: 4.0, 411 | /// height: 4.0, 412 | /// }, 413 | /// ); 414 | /// ``` 415 | /// 416 | /// # Arguments 417 | /// * `rhs` - The view box to scale. 418 | fn mul(self, rhs: ViewBox) -> Self::Output { 419 | Self::Output { 420 | corner: Pos { 421 | x: rhs.corner.x * self, 422 | y: rhs.corner.y * self, 423 | }, 424 | width: rhs.width * self, 425 | height: rhs.height * self, 426 | } 427 | } 428 | } 429 | 430 | impl ops::Mul for ViewBox { 431 | type Output = Self; 432 | 433 | /// Scales every value in a view box by this value. 434 | fn mul(self, rhs: f32) -> Self::Output { 435 | rhs * self 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /maze/src/render/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::physical; 2 | use crate::Maze; 3 | 4 | impl Maze 5 | where 6 | T: Clone, 7 | { 8 | /// Calculates the _view box_ for an object when rendered. 9 | /// 10 | /// The returned value is the minimal rectangle that will contain this 11 | /// maze. 12 | pub fn viewbox(&self) -> physical::ViewBox { 13 | self.shape().viewbox(self.width(), self.height()) 14 | } 15 | } 16 | 17 | #[cfg(feature = "svg")] 18 | pub mod svg; 19 | -------------------------------------------------------------------------------- /maze/src/render/svg.rs: -------------------------------------------------------------------------------- 1 | use svg::node::element::path::{Command, Position}; 2 | 3 | use crate::Maze; 4 | use crate::WallPos; 5 | 6 | use crate::matrix; 7 | use crate::physical; 8 | use crate::wall; 9 | 10 | use crate::walk::*; 11 | 12 | pub trait ToPath { 13 | /// Generates an _SVG path d_ attribute value. 14 | fn to_path_d(&self) -> svg::node::element::path::Data; 15 | } 16 | 17 | impl ToPath for Maze 18 | where 19 | T: Clone, 20 | { 21 | fn to_path_d(&self) -> svg::node::element::path::Data { 22 | let mut commands = Vec::new(); 23 | let mut visitor = Visitor::new(self); 24 | 25 | // While a non-visited wall still exists, walk along it 26 | while let Some((next_pos, next_wall)) = visitor.next_wall() { 27 | for (i, (from, to)) in 28 | self.follow_wall((next_pos, next_wall)).enumerate() 29 | { 30 | // Ensure the wall has not been visited before 31 | if visitor.visited(from) { 32 | break; 33 | } else { 34 | visitor.visit(from); 35 | } 36 | 37 | // For the first wall, we need to move to the corner furthest 38 | // from the second wall, or just any corner if this is a 39 | // one-wall line 40 | if i == 0 { 41 | if let Some(next) = to { 42 | let (_, pos) = corners(self, from, center(self, next)); 43 | commands.push(Operation::Move(pos)); 44 | } else { 45 | let (pos, _) = self.corners(from); 46 | commands.push(Operation::Move(pos)); 47 | } 48 | } 49 | 50 | // Draw a line from the previous point to the point of the 51 | // current wall furthest away 52 | let (_, pos) = 53 | corners(self, from, commands.last().unwrap().pos()); 54 | commands.push(Operation::Line(pos)); 55 | 56 | // If the next room is outside of the maze, break 57 | if to.map(|(pos, _)| !self.is_inside(pos)).unwrap_or(false) { 58 | break; 59 | } 60 | } 61 | } 62 | 63 | svg::node::element::path::Data::from( 64 | commands 65 | .into_iter() 66 | .map(Into::into) 67 | .collect::>(), 68 | ) 69 | } 70 | } 71 | 72 | impl<'a, T> ToPath for Path<'a, T> 73 | where 74 | T: Clone, 75 | { 76 | fn to_path_d(&self) -> svg::node::element::path::Data { 77 | svg::node::element::path::Data::from( 78 | self.into_iter() 79 | .map(|pos| self.maze.center(pos)) 80 | .enumerate() 81 | .map(|(i, pos)| { 82 | if i == 0 { 83 | Operation::Move(pos).into() 84 | } else { 85 | Operation::Line(pos).into() 86 | } 87 | }) 88 | .collect::>(), 89 | ) 90 | } 91 | } 92 | 93 | /// A visitor for wall positions. 94 | /// 95 | /// This struct provides means to visit all wall positions of a maze. 96 | struct Visitor<'a, T> 97 | where 98 | T: Clone, 99 | { 100 | /// The maze whose walls are being visited. 101 | maze: &'a Maze, 102 | 103 | /// The visited walls. 104 | walls: matrix::Matrix, 105 | 106 | /// The current room. 107 | index: usize, 108 | } 109 | 110 | impl<'a, T> Visitor<'a, T> 111 | where 112 | T: Clone, 113 | { 114 | /// Creates a new visitor for a maze. 115 | /// 116 | /// # Arguments 117 | /// * `maze` - The maze whose walls to visit. 118 | pub fn new(maze: &'a Maze) -> Self { 119 | Self { 120 | maze, 121 | walls: matrix::Matrix::new(maze.width(), maze.height()), 122 | index: 0, 123 | } 124 | } 125 | 126 | /// Marks a wall and its back as visited. 127 | /// 128 | /// If the wall is outside of the maze, it is ignored. The back is likewise 129 | /// ignored if it is outside of the maze. 130 | /// 131 | /// # Arguments 132 | /// * `wall_pos` - The wall to mark as visited. 133 | fn visit(&mut self, wall_pos: WallPos) { 134 | if let Some(mask) = self.walls.get_mut(wall_pos.0) { 135 | *mask |= 1 << wall_pos.1.index; 136 | } 137 | 138 | let back = self.maze.back(wall_pos); 139 | if let Some(back_mask) = self.walls.get_mut(back.0) { 140 | *back_mask |= 1 << back.1.index; 141 | } 142 | } 143 | 144 | /// Determines whether a wall has been visited. 145 | /// 146 | /// # Arguments 147 | /// * `wall_pos` - The wall position to check. 148 | fn visited(&self, wall_pos: WallPos) -> bool { 149 | if let Some(mask) = self.walls.get(wall_pos.0) { 150 | (mask & (1 << wall_pos.1.index)) != 0 151 | } else { 152 | false 153 | } 154 | } 155 | 156 | /// Returns the next non-visited wall. 157 | fn next_wall(&mut self) -> Option { 158 | while let Some(pos) = self.pos() { 159 | if let Some(next) = self 160 | .maze 161 | .walls(pos) 162 | .iter() 163 | // Keep only closed walls that have not yet been drawn 164 | .filter(|&w| !self.maze.is_open((pos, w))) 165 | .filter(|&w| !self.visited((pos, *w))) 166 | .map(|&w| (pos, w)) 167 | .next() 168 | { 169 | return Some(next); 170 | } else { 171 | self.index += 1; 172 | } 173 | } 174 | 175 | None 176 | } 177 | 178 | /// Returns the current room. 179 | /// 180 | /// This function transforms the index to a room position. 181 | /// 182 | /// If the room corresponding to the current index has never been visited, 183 | /// the next room is checked until no rooms remain. 184 | fn pos(&mut self) -> Option { 185 | while self.index < self.maze.width() * self.maze.height() { 186 | let pos = matrix::Pos { 187 | col: (self.index % self.maze.width()) as isize, 188 | row: (self.index / self.maze.width()) as isize, 189 | }; 190 | 191 | if self 192 | .maze 193 | .rooms 194 | .get(pos) 195 | .map(|room| room.visited) 196 | .unwrap_or(false) 197 | { 198 | return Some(pos); 199 | } else { 200 | self.index += 1; 201 | } 202 | } 203 | 204 | None 205 | } 206 | } 207 | 208 | /// A line drawing operation. 209 | enum Operation { 210 | /// Move the current position without drawing a line. 211 | Move(physical::Pos), 212 | 213 | /// Draw a line from the old position to this position. 214 | Line(physical::Pos), 215 | } 216 | 217 | impl Operation { 218 | /// Extracts the position from this operation regardless of type. 219 | fn pos(&self) -> physical::Pos { 220 | match *self { 221 | Operation::Move(pos) | Operation::Line(pos) => pos, 222 | } 223 | } 224 | } 225 | 226 | impl From for Command { 227 | /// Converts a line drawing operation to an actual _SVG path command_. 228 | fn from(operation: Operation) -> Self { 229 | match operation { 230 | Operation::Move(pos) => { 231 | Command::Move(Position::Absolute, (pos.x, pos.y).into()) 232 | } 233 | Operation::Line(pos) => { 234 | Command::Line(Position::Absolute, (pos.x, pos.y).into()) 235 | } 236 | } 237 | } 238 | } 239 | 240 | /// Returns the center of a wall. 241 | /// 242 | /// The center of a wall is the point between its corners. 243 | /// 244 | /// # Arguments 245 | /// * `wall_pos` - The wall position. 246 | fn center(maze: &Maze, wall_pos: WallPos) -> physical::Pos 247 | where 248 | T: Clone, 249 | { 250 | let (corner1, corner2) = maze.corners(wall_pos); 251 | physical::Pos { 252 | x: (corner1.x + corner2.x) / 2.0, 253 | y: (corner1.y + corner2.y) / 2.0, 254 | } 255 | } 256 | 257 | /// Returns the physical positions of the two corners of a wall ordered by 258 | /// distance to another point. 259 | /// 260 | /// # Arguments 261 | /// * `from` - The wall position. 262 | /// * `origin` - The point or origin. The first element of the returned tuple 263 | /// will be the corner closest to this point. 264 | fn corners( 265 | maze: &Maze, 266 | from: WallPos, 267 | origin: physical::Pos, 268 | ) -> (physical::Pos, physical::Pos) 269 | where 270 | T: Clone, 271 | { 272 | let (pos1, pos2) = maze.corners(from); 273 | 274 | if (pos1 - origin).value() < (pos2 - origin).value() { 275 | (pos1, pos2) 276 | } else { 277 | (pos2, pos1) 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /maze/src/room.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serde")] 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::wall; 5 | 6 | /// A room is a part of a maze. 7 | /// 8 | /// It has walls, openings connecting it with other rooms, and asssociated data. 9 | /// 10 | /// It does not know its location. 11 | #[derive(Clone, Debug)] 12 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 13 | pub struct Room 14 | where 15 | T: Clone, 16 | { 17 | /// A bit mask of open walls. 18 | walls: wall::Mask, 19 | 20 | /// Whether this room has been visited. This is true if at least one wall 21 | /// has at any time been opened. 22 | pub visited: bool, 23 | 24 | /// The data associated with this room. 25 | pub data: T, 26 | } 27 | 28 | impl Default for Room 29 | where 30 | T: Clone + Default, 31 | { 32 | fn default() -> Self { 33 | Self { 34 | walls: wall::Mask::default(), 35 | visited: false, 36 | data: T::default(), 37 | } 38 | } 39 | } 40 | 41 | impl Room 42 | where 43 | T: Clone, 44 | { 45 | /// Whether a specified wall is open. 46 | /// 47 | /// # Example 48 | /// 49 | /// ``` 50 | /// # use maze::room::*; 51 | /// # let mut room: Room<_> = false.into(); 52 | /// # let wall = &maze::shape::quad::walls::LEFT; 53 | /// 54 | /// room.set_open(wall, true); 55 | /// assert!(room.is_open(wall)); 56 | /// ``` 57 | /// 58 | /// # Arguments 59 | /// * `wall` - The wall to check. 60 | pub fn is_open(&self, wall: &'static wall::Wall) -> bool { 61 | self.walls & wall.mask() != 0 62 | } 63 | 64 | /// Sets whether a wall is open. 65 | /// 66 | /// # Arguments 67 | /// * `wall` - The wall to set. 68 | /// * `state` - Whether the wall is open. 69 | pub fn set_open(&mut self, wall: &'static wall::Wall, value: bool) { 70 | if value { 71 | self.open(wall); 72 | } else { 73 | self.close(wall); 74 | } 75 | } 76 | 77 | /// Opens a wall. 78 | /// 79 | /// # Arguments 80 | /// * `wall` - The wall to open. 81 | pub fn open(&mut self, wall: &'static wall::Wall) { 82 | self.walls |= wall.mask(); 83 | self.visited = true; 84 | } 85 | 86 | /// Closes a wall. 87 | /// 88 | /// # Arguments 89 | /// * `wall` - The wall to close. 90 | pub fn close(&mut self, wall: &'static wall::Wall) { 91 | self.walls &= !wall.mask(); 92 | } 93 | 94 | /// Returns the number of open walls. 95 | pub fn open_walls(&self) -> usize { 96 | self.walls.count_ones() as usize 97 | } 98 | 99 | /// Creates a copy of this room with new data. 100 | /// 101 | /// # Arguments 102 | /// * `data` - The new data. 103 | pub fn with_data(&self, data: U) -> Room 104 | where 105 | U: Clone, 106 | { 107 | Room { 108 | walls: self.walls, 109 | visited: self.visited, 110 | data, 111 | } 112 | } 113 | } 114 | 115 | impl From for Room 116 | where 117 | T: Clone, 118 | { 119 | /// Constructs a non-visited room with data. 120 | /// 121 | /// # Arguments 122 | /// * `source` - The data content. 123 | fn from(source: T) -> Self { 124 | Self { 125 | walls: 0, 126 | visited: false, 127 | data: source, 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /maze/src/shape/quad.rs: -------------------------------------------------------------------------------- 1 | use crate::matrix; 2 | use crate::physical; 3 | use crate::wall; 4 | 5 | use crate::wall::{Angle, Offset}; 6 | use crate::WallPos; 7 | 8 | use super::{COS_45, SIN_45}; 9 | 10 | /// A span step angle 11 | /// 12 | /// This is half the angle span used by a single wall. 13 | const D: f32 = 2.0 * std::f32::consts::PI / 8.0; 14 | 15 | /// The scale factor when converting maze coordinates to physical coordinates 16 | const MULTIPLICATOR: f32 = 2.0 / std::f32::consts::SQRT_2; 17 | 18 | define_shape! { 19 | << Quad >> 20 | 21 | UP(1) = { 22 | corner_wall_offsets: &[ 23 | Offset { dx: 0, dy: -1, wall: &LEFT }, 24 | Offset { dx: -1, dy: -1, wall: &DOWN }, 25 | Offset { dx: -1, dy: 0, wall: &RIGHT }, 26 | ], 27 | dir: (0, -1), 28 | span: ( 29 | Angle { 30 | a: 5.0 * D, 31 | dx: -COS_45, 32 | dy: -SIN_45, 33 | }, 34 | Angle { 35 | a: 7.0 * D, 36 | dx: COS_45, 37 | dy: -SIN_45, 38 | }, 39 | ), 40 | previous: &LEFT, 41 | next: &RIGHT, 42 | }, 43 | LEFT(0) = { 44 | corner_wall_offsets: &[ 45 | Offset { dx: -1, dy: 0, wall: &DOWN }, 46 | Offset { dx: -1, dy: 1, wall: &RIGHT }, 47 | Offset { dx: 0, dy: 1, wall: &UP }, 48 | ], 49 | dir: (-1, 0), 50 | span: ( 51 | Angle { 52 | a: 3.0 * D, 53 | dx: -COS_45, 54 | dy: SIN_45, 55 | }, 56 | Angle { 57 | a: 5.0 * D, 58 | dx: -COS_45, 59 | dy: -SIN_45, 60 | }, 61 | ), 62 | previous: &DOWN, 63 | next: &UP, 64 | }, 65 | DOWN(3) = { 66 | corner_wall_offsets: &[ 67 | Offset { dx: 0, dy: 1, wall: &RIGHT }, 68 | Offset { dx: 1, dy: 1, wall: &UP }, 69 | Offset { dx: 1, dy: 0, wall: &LEFT }, 70 | ], 71 | dir: (0, 1), 72 | span: ( 73 | Angle { 74 | a: D, 75 | dx: COS_45, 76 | dy: SIN_45, 77 | }, 78 | Angle { 79 | a: 3.0 * D, 80 | dx: -COS_45, 81 | dy: SIN_45, 82 | }, 83 | ), 84 | previous: &RIGHT, 85 | next: &LEFT, 86 | }, 87 | RIGHT(2) = { 88 | corner_wall_offsets: &[ 89 | Offset { dx: 1, dy: 0, wall: &UP }, 90 | Offset { dx: 1, dy: -1, wall: &LEFT }, 91 | Offset { dx: 0, dy: -1, wall: &DOWN }, 92 | ], 93 | dir: (1, 0), 94 | span: ( 95 | Angle { 96 | a: 7.0 * D, 97 | dx: COS_45, 98 | dy: -SIN_45, 99 | }, 100 | Angle { 101 | a: 1.0 * D, 102 | dx: COS_45, 103 | dy: SIN_45, 104 | }, 105 | ), 106 | previous: &UP, 107 | next: &DOWN, 108 | } 109 | } 110 | 111 | /// The walls 112 | static WALLS: &[&wall::Wall] = 113 | &[&walls::LEFT, &walls::UP, &walls::RIGHT, &walls::DOWN]; 114 | 115 | pub fn minimal_dimensions(width: f32, height: f32) -> (usize, usize) { 116 | let height = (height.max(MULTIPLICATOR) / MULTIPLICATOR).ceil() as usize; 117 | 118 | let width = (width.max(MULTIPLICATOR) / MULTIPLICATOR).ceil() as usize; 119 | 120 | (width, height) 121 | } 122 | 123 | pub fn back_index(wall: usize) -> usize { 124 | wall ^ 0b0010 125 | } 126 | 127 | pub fn opposite(wall_pos: WallPos) -> Option<&'static wall::Wall> { 128 | let (_, wall) = wall_pos; 129 | Some(walls::ALL[(wall.index + walls::ALL.len() / 2) % walls::ALL.len()]) 130 | } 131 | 132 | pub fn walls(_pos: matrix::Pos) -> &'static [&'static wall::Wall] { 133 | WALLS 134 | } 135 | 136 | pub fn cell_to_physical(pos: matrix::Pos) -> physical::Pos { 137 | physical::Pos { 138 | x: (pos.col as f32 + 0.5) * MULTIPLICATOR, 139 | y: (pos.row as f32 + 0.5) * MULTIPLICATOR, 140 | } 141 | } 142 | 143 | pub fn physical_to_cell(pos: physical::Pos) -> matrix::Pos { 144 | matrix::Pos { 145 | col: (pos.x / MULTIPLICATOR).floor() as isize, 146 | row: (pos.y / MULTIPLICATOR).floor() as isize, 147 | } 148 | } 149 | 150 | #[allow(clippy::collapsible_else_if)] 151 | pub fn physical_to_wall_pos(pos: physical::Pos) -> WallPos { 152 | let matrix_pos = physical_to_cell(pos); 153 | let center = cell_to_physical(matrix_pos); 154 | let (dx, dy) = (pos.x - center.x, pos.y - center.y); 155 | 156 | let wall = if dx > dy { 157 | if dy > -dx { 158 | &walls::RIGHT 159 | } else { 160 | &walls::UP 161 | } 162 | } else { 163 | if dy > -dx { 164 | &walls::DOWN 165 | } else { 166 | &walls::LEFT 167 | } 168 | }; 169 | 170 | (matrix_pos, wall) 171 | } 172 | 173 | #[cfg(test)] 174 | mod tests { 175 | use maze_test::maze_test; 176 | 177 | use super::*; 178 | use crate::test_utils::*; 179 | use crate::WallPos; 180 | 181 | #[maze_test(quad)] 182 | fn back(maze: TestMaze) { 183 | assert_eq!( 184 | maze.back((matrix_pos(1, 1), &walls::LEFT)), 185 | (matrix_pos(0, 1), &walls::RIGHT) 186 | ); 187 | assert_eq!( 188 | maze.back((matrix_pos(1, 1), &walls::UP)), 189 | (matrix_pos(1, 0), &walls::DOWN) 190 | ); 191 | assert_eq!( 192 | maze.back((matrix_pos(1, 1), &walls::RIGHT)), 193 | (matrix_pos(2, 1), &walls::LEFT) 194 | ); 195 | assert_eq!( 196 | maze.back((matrix_pos(1, 1), &walls::DOWN)), 197 | (matrix_pos(1, 2), &walls::UP) 198 | ); 199 | } 200 | 201 | #[maze_test(quad)] 202 | fn opposite(maze: TestMaze) { 203 | assert_eq!( 204 | maze.opposite((matrix_pos(1, 1), &walls::LEFT)).unwrap(), 205 | &walls::RIGHT 206 | ); 207 | assert_eq!( 208 | maze.opposite((matrix_pos(1, 1), &walls::UP)).unwrap(), 209 | &walls::DOWN 210 | ); 211 | assert_eq!( 212 | maze.opposite((matrix_pos(1, 1), &walls::RIGHT)).unwrap(), 213 | &walls::LEFT 214 | ); 215 | assert_eq!( 216 | maze.opposite((matrix_pos(1, 1), &walls::DOWN)).unwrap(), 217 | &walls::UP 218 | ); 219 | } 220 | 221 | #[maze_test(quad)] 222 | fn corner_walls(maze: TestMaze) { 223 | assert_eq!( 224 | maze.corner_walls_start((matrix_pos(1, 1), &walls::UP)) 225 | .collect::>(), 226 | vec![ 227 | (matrix_pos(1, 1), &walls::UP), 228 | (matrix_pos(1, 0), &walls::LEFT), 229 | (matrix_pos(0, 0), &walls::DOWN), 230 | (matrix_pos(0, 1), &walls::RIGHT), 231 | ], 232 | ); 233 | 234 | assert_eq!( 235 | maze.corner_walls_start((matrix_pos(1, 1), &walls::LEFT)) 236 | .collect::>(), 237 | vec![ 238 | (matrix_pos(1, 1), &walls::LEFT), 239 | (matrix_pos(0, 1), &walls::DOWN), 240 | (matrix_pos(0, 2), &walls::RIGHT), 241 | (matrix_pos(1, 2), &walls::UP), 242 | ], 243 | ); 244 | 245 | assert_eq!( 246 | maze.corner_walls_start((matrix_pos(1, 1), &walls::DOWN)) 247 | .collect::>(), 248 | vec![ 249 | (matrix_pos(1, 1), &walls::DOWN), 250 | (matrix_pos(1, 2), &walls::RIGHT), 251 | (matrix_pos(2, 2), &walls::UP), 252 | (matrix_pos(2, 1), &walls::LEFT), 253 | ], 254 | ); 255 | 256 | assert_eq!( 257 | maze.corner_walls_start((matrix_pos(1, 1), &walls::RIGHT)) 258 | .collect::>(), 259 | vec![ 260 | (matrix_pos(1, 1), &walls::RIGHT), 261 | (matrix_pos(2, 1), &walls::UP), 262 | (matrix_pos(2, 0), &walls::LEFT), 263 | (matrix_pos(1, 0), &walls::DOWN), 264 | ], 265 | ); 266 | } 267 | 268 | #[maze_test(quad)] 269 | fn follow_no_wall(mut maze: TestMaze) { 270 | let log = Navigator::new(&mut maze).down(true).stop(); 271 | 272 | let start_pos = (*log.first().unwrap(), &walls::DOWN); 273 | let expected = vec![]; 274 | assert_eq!(maze.follow_wall(start_pos).collect::>(), expected); 275 | } 276 | 277 | #[maze_test(quad)] 278 | fn follow_wall_single_room(maze: TestMaze) { 279 | assert_eq!( 280 | vec![ 281 | (matrix_pos(0, 0), &walls::LEFT), 282 | (matrix_pos(0, 0), &walls::UP), 283 | (matrix_pos(0, 0), &walls::RIGHT), 284 | (matrix_pos(0, 0), &walls::DOWN), 285 | ], 286 | maze.follow_wall((matrix_pos(0, 0), &walls::LEFT)) 287 | .map(|(from, _)| from) 288 | .collect::>() 289 | ); 290 | } 291 | 292 | #[maze_test(quad)] 293 | fn follow_wall(mut maze: TestMaze) { 294 | Navigator::new(&mut maze) 295 | .from(matrix_pos(0, 0)) 296 | .down(true) 297 | .right(true) 298 | .up(true); 299 | 300 | assert_eq!( 301 | vec![ 302 | (matrix_pos(0, 0), &walls::LEFT), 303 | (matrix_pos(0, 0), &walls::UP), 304 | (matrix_pos(0, 0), &walls::RIGHT), 305 | (matrix_pos(1, 0), &walls::LEFT), 306 | (matrix_pos(1, 0), &walls::UP), 307 | (matrix_pos(1, 0), &walls::RIGHT), 308 | (matrix_pos(1, 1), &walls::RIGHT), 309 | (matrix_pos(1, 1), &walls::DOWN), 310 | (matrix_pos(0, 1), &walls::DOWN), 311 | (matrix_pos(0, 1), &walls::LEFT), 312 | ], 313 | maze.follow_wall((matrix_pos(0, 0), &walls::LEFT)) 314 | .map(|(from, _)| from) 315 | .collect::>() 316 | ); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /maze/src/shape/tri.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts::PI; 2 | 3 | use crate::WallPos; 4 | 5 | use crate::matrix; 6 | use crate::physical; 7 | use crate::wall; 8 | 9 | use crate::wall::{Angle, Offset}; 10 | 11 | use super::{COS_30, SIN_30}; 12 | 13 | /// A span step angle 14 | const D: f32 = 2.0 * PI / 12.0; 15 | 16 | /// The distance between the centre of a room and the centre of a room on the 17 | /// next row. 18 | const HORIZONTAL_MULTIPLICATOR: f32 = COS_30; 19 | 20 | /// The distance between the centre of a room and the centre of a room on the 21 | /// next column. 22 | const VERTICAL_MULTIPLICATOR: f32 = 2.0 - 1.0f32 / 2.0f32; 23 | 24 | /// The vertical offset for the centre of rooms. 25 | const OFFSET: f32 = 1.0f32 / 4.0f32; 26 | 27 | define_shape! { 28 | << Tri >> 29 | 30 | LEFT0(0) = { 31 | corner_wall_offsets: &[ 32 | Offset { dx: -1, dy: 0, wall: &DOWN }, 33 | Offset { dx: -1, dy: 1, wall: &RIGHT0 }, 34 | Offset { dx: 0, dy: 1, wall: &RIGHT1 }, 35 | Offset { dx: 1, dy: 1, wall: &UP }, 36 | Offset { dx: 1, dy: 0, wall: &LEFT1 }, 37 | ], 38 | dir: (-1, 0), 39 | span: ( 40 | Angle { 41 | a: 3.0 * D, 42 | dx: 0.0, 43 | dy: 1.0, 44 | }, 45 | Angle { 46 | a: 7.0 * D, 47 | dx: -COS_30, 48 | dy: -SIN_30, 49 | }, 50 | ), 51 | previous: &RIGHT0, 52 | next: &UP, 53 | }, 54 | RIGHT1(1) = { 55 | corner_wall_offsets: &[ 56 | Offset { dx: 1, dy: 0, wall: &UP }, 57 | Offset { dx: 1, dy: -1, wall: &LEFT1 }, 58 | Offset { dx: 0, dy: -1, wall: &LEFT0 }, 59 | Offset { dx: -1, dy: -1, wall: &DOWN }, 60 | Offset { dx: -1, dy: 0, wall: &RIGHT0 }, 61 | ], 62 | dir: (1, 0), 63 | span: ( 64 | Angle { 65 | a: 9.0 * D, 66 | dx: 0.0, 67 | dy: -1.0, 68 | }, 69 | Angle { 70 | a: 1.0 * D, 71 | dx: COS_30, 72 | dy: SIN_30, 73 | }, 74 | ), 75 | previous: &LEFT1, 76 | next: &DOWN, 77 | }, 78 | 79 | LEFT1(0) = { 80 | corner_wall_offsets: &[ 81 | Offset { dx: -1, dy: 0, wall: &LEFT0 }, 82 | Offset { dx: -2, dy: 0, wall: &DOWN }, 83 | Offset { dx: -2, dy: 1, wall: &RIGHT0 }, 84 | Offset { dx: -1, dy: 1, wall: &RIGHT1 }, 85 | Offset { dx: 0, dy: 1, wall: &UP }, 86 | ], 87 | dir: (-1, 0), 88 | span: ( 89 | Angle { 90 | a: 5.0 * D, 91 | dx: -COS_30, 92 | dy: SIN_30, 93 | }, 94 | Angle { 95 | a: 9.0 * D, 96 | dx: 0.0, 97 | dy: -1.0, 98 | }, 99 | ), 100 | previous: &DOWN, 101 | next: &RIGHT1, 102 | }, 103 | RIGHT0(2) = { 104 | corner_wall_offsets: &[ 105 | Offset { dx: 1, dy: 0, wall: &RIGHT1 }, 106 | Offset { dx: 2, dy: 0, wall: &UP }, 107 | Offset { dx: 2, dy: -1, wall: &LEFT1 }, 108 | Offset { dx: 1, dy: -1, wall: &LEFT0 }, 109 | Offset { dx: 0, dy: -1, wall: &DOWN }, 110 | ], 111 | dir: (1, 0), 112 | span: ( 113 | Angle { 114 | a: 11.0 * D, 115 | dx: COS_30, 116 | dy: -SIN_30, 117 | }, 118 | Angle { 119 | a: 3.0 * D, 120 | dx: 0.0, 121 | dy: 1.0, 122 | }, 123 | ), 124 | previous: &UP, 125 | next: &LEFT0, 126 | }, 127 | 128 | UP(1) = { 129 | corner_wall_offsets: &[ 130 | Offset { dx: 0, dy: -1, wall: &LEFT1 }, 131 | Offset { dx: -1, dy: -1, wall: &LEFT0 }, 132 | Offset { dx: -2, dy: -1, wall: &DOWN }, 133 | Offset { dx: -2, dy: 0, wall: &RIGHT0 }, 134 | Offset { dx: -1, dy: 0, wall: &RIGHT1 }, 135 | ], 136 | dir: (0, -1), 137 | span: ( 138 | Angle { 139 | a: 7.0 * D, 140 | dx: -COS_30, 141 | dy: -SIN_30, 142 | }, 143 | Angle { 144 | a: 11.0 * D, 145 | dx: COS_30, 146 | dy: -SIN_30, 147 | }, 148 | ), 149 | previous: &LEFT0, 150 | next: &RIGHT0, 151 | }, 152 | DOWN(2) = { 153 | corner_wall_offsets: &[ 154 | Offset { dx: 0, dy: 1, wall: &RIGHT0 }, 155 | Offset { dx: 1, dy: 1, wall: &RIGHT1 }, 156 | Offset { dx: 2, dy: 1, wall: &UP }, 157 | Offset { dx: 2, dy: 0, wall: &LEFT1 }, 158 | Offset { dx: 1, dy: 0, wall: &LEFT0 }, 159 | ], 160 | dir: (0, 1), 161 | span: ( 162 | Angle { 163 | a: 1.0 * D, 164 | dx: COS_30, 165 | dy: SIN_30, 166 | }, 167 | Angle { 168 | a: 5.0 * D, 169 | dx: -COS_30, 170 | dy: SIN_30, 171 | }, 172 | ), 173 | previous: &RIGHT1, 174 | next: &LEFT1, 175 | } 176 | } 177 | 178 | /// The walls for even rows 179 | static WALLS_EVEN: &[&wall::Wall] = 180 | &[&walls::LEFT0, &walls::UP, &walls::RIGHT0]; 181 | 182 | /// The walls for odd rows 183 | static WALLS_ODD: &[&wall::Wall] = 184 | &[&walls::LEFT1, &walls::RIGHT1, &walls::DOWN]; 185 | 186 | /// Returns whether a room is reversed. 187 | /// 188 | /// # Arguments 189 | /// * `pos` - the room position. 190 | fn is_reversed(pos: matrix::Pos) -> bool { 191 | (pos.col + pos.row) & 1 != 0 192 | } 193 | 194 | pub fn minimal_dimensions(width: f32, height: f32) -> (usize, usize) { 195 | let height = (height.max(VERTICAL_MULTIPLICATOR) / VERTICAL_MULTIPLICATOR) 196 | .ceil() as usize; 197 | 198 | let width = (width.max(HORIZONTAL_MULTIPLICATOR) / HORIZONTAL_MULTIPLICATOR) 199 | .floor() as usize; 200 | 201 | (width, height) 202 | } 203 | 204 | pub fn back_index(wall: usize) -> usize { 205 | wall ^ 0b0001 206 | } 207 | 208 | pub fn opposite(_pos: WallPos) -> Option<&'static wall::Wall> { 209 | // There is no opposite wall in a room with an odd number of walls 210 | None 211 | } 212 | 213 | pub fn walls(pos: matrix::Pos) -> &'static [&'static wall::Wall] { 214 | if is_reversed(pos) { 215 | WALLS_ODD 216 | } else { 217 | WALLS_EVEN 218 | } 219 | } 220 | 221 | pub fn cell_to_physical(pos: matrix::Pos) -> physical::Pos { 222 | physical::Pos { 223 | x: (pos.col as f32 + 1.0) * HORIZONTAL_MULTIPLICATOR, 224 | y: (pos.row as f32 + 0.5) * VERTICAL_MULTIPLICATOR 225 | + if is_reversed(pos) { OFFSET } else { -OFFSET }, 226 | } 227 | } 228 | 229 | pub fn physical_to_cell(pos: physical::Pos) -> matrix::Pos { 230 | // Calculate approximations of the room position 231 | let (i, f) = matrix::partition(pos.y / VERTICAL_MULTIPLICATOR); 232 | let odd_row = i & 1 == 1; 233 | let approx_row = i; 234 | let rel_y = if odd_row { 1.0 - f } else { f }; 235 | let (i, f) = matrix::partition(pos.x / (2.0 * HORIZONTAL_MULTIPLICATOR)); 236 | let approx_col = i * 2; 237 | let rel_x = f; 238 | 239 | let past_center = rel_x > 0.5; 240 | 241 | matrix::Pos { 242 | col: approx_col 243 | + if past_center && 2.0 * (1.0 - rel_x) < rel_y { 244 | 1 245 | } else if !past_center && 2.0 * rel_x < rel_y { 246 | -1 247 | } else { 248 | 0 249 | }, 250 | row: approx_row, 251 | } 252 | } 253 | 254 | #[allow(clippy::collapsible_if)] 255 | pub fn physical_to_wall_pos(pos: physical::Pos) -> WallPos { 256 | let matrix_pos = physical_to_cell(pos); 257 | let flipped = (matrix_pos.col + matrix_pos.row) & 1 == 1; 258 | let center = cell_to_physical(matrix_pos); 259 | let (dx, dy) = ( 260 | pos.x - center.x, 261 | if flipped { 262 | center.y - pos.y 263 | } else { 264 | pos.y - center.y 265 | }, 266 | ); 267 | 268 | let either = |a, b| if flipped { a } else { b }; 269 | 270 | let wall = if dx > 0.0 { 271 | let t = dx; 272 | if dy > t * walls::UP.span.0.dy { 273 | either(&walls::RIGHT1, &walls::RIGHT0) 274 | } else { 275 | either(&walls::DOWN, &walls::UP) 276 | } 277 | } else { 278 | let t = -dx; 279 | if dy > t * walls::UP.span.0.dy { 280 | either(&walls::LEFT1, &walls::LEFT0) 281 | } else { 282 | either(&walls::DOWN, &walls::UP) 283 | } 284 | }; 285 | 286 | (matrix_pos, wall) 287 | } 288 | 289 | #[cfg(test)] 290 | mod tests { 291 | use maze_test::maze_test; 292 | 293 | use super::*; 294 | use crate::test_utils::*; 295 | use crate::WallPos; 296 | 297 | #[maze_test(tri)] 298 | fn back(maze: TestMaze) { 299 | assert_eq!( 300 | maze.back((matrix_pos(2, 0), &walls::LEFT0)), 301 | (matrix_pos(1, 0), &walls::RIGHT1) 302 | ); 303 | assert_eq!( 304 | maze.back((matrix_pos(2, 0), &walls::RIGHT0)), 305 | (matrix_pos(3, 0), &walls::LEFT1) 306 | ); 307 | assert_eq!( 308 | maze.back((matrix_pos(1, 0), &walls::LEFT1)), 309 | (matrix_pos(0, 0), &walls::RIGHT0) 310 | ); 311 | assert_eq!( 312 | maze.back((matrix_pos(1, 1), &walls::UP)), 313 | (matrix_pos(1, 0), &walls::DOWN) 314 | ); 315 | assert_eq!( 316 | maze.back((matrix_pos(1, 0), &walls::RIGHT1)), 317 | (matrix_pos(2, 0), &walls::LEFT0) 318 | ); 319 | assert_eq!( 320 | maze.back((matrix_pos(1, 0), &walls::DOWN)), 321 | (matrix_pos(1, 1), &walls::UP) 322 | ); 323 | } 324 | 325 | #[maze_test(tri)] 326 | fn corner_walls(maze: TestMaze) { 327 | assert_eq!( 328 | maze.corner_walls_start((matrix_pos(2, 0), &walls::LEFT0)) 329 | .collect::>(), 330 | vec![ 331 | (matrix_pos(2, 0), &walls::LEFT0), 332 | (matrix_pos(1, 0), &walls::DOWN), 333 | (matrix_pos(1, 1), &walls::RIGHT0), 334 | (matrix_pos(2, 1), &walls::RIGHT1), 335 | (matrix_pos(3, 1), &walls::UP), 336 | (matrix_pos(3, 0), &walls::LEFT1), 337 | ], 338 | ); 339 | assert_eq!( 340 | maze.corner_walls_start((matrix_pos(2, 0), &walls::RIGHT0)) 341 | .collect::>(), 342 | vec![ 343 | (matrix_pos(2, 0), &walls::RIGHT0), 344 | (matrix_pos(3, 0), &walls::RIGHT1), 345 | (matrix_pos(4, 0), &walls::UP), 346 | (matrix_pos(4, -1), &walls::LEFT1), 347 | (matrix_pos(3, -1), &walls::LEFT0), 348 | (matrix_pos(2, -1), &walls::DOWN), 349 | ], 350 | ); 351 | assert_eq!( 352 | maze.corner_walls_start((matrix_pos(1, 0), &walls::LEFT1)) 353 | .collect::>(), 354 | vec![ 355 | (matrix_pos(1, 0), &walls::LEFT1), 356 | (matrix_pos(0, 0), &walls::LEFT0), 357 | (matrix_pos(-1, 0), &walls::DOWN), 358 | (matrix_pos(-1, 1), &walls::RIGHT0), 359 | (matrix_pos(0, 1), &walls::RIGHT1), 360 | (matrix_pos(1, 1), &walls::UP), 361 | ], 362 | ); 363 | assert_eq!( 364 | maze.corner_walls_start((matrix_pos(1, 1), &walls::UP)) 365 | .collect::>(), 366 | vec![ 367 | (matrix_pos(1, 1), &walls::UP), 368 | (matrix_pos(1, 0), &walls::LEFT1), 369 | (matrix_pos(0, 0), &walls::LEFT0), 370 | (matrix_pos(-1, 0), &walls::DOWN), 371 | (matrix_pos(-1, 1), &walls::RIGHT0), 372 | (matrix_pos(0, 1), &walls::RIGHT1), 373 | ], 374 | ); 375 | assert_eq!( 376 | maze.corner_walls_start((matrix_pos(1, 0), &walls::RIGHT1)) 377 | .collect::>(), 378 | vec![ 379 | (matrix_pos(1, 0), &walls::RIGHT1), 380 | (matrix_pos(2, 0), &walls::UP), 381 | (matrix_pos(2, -1), &walls::LEFT1), 382 | (matrix_pos(1, -1), &walls::LEFT0), 383 | (matrix_pos(0, -1), &walls::DOWN), 384 | (matrix_pos(0, 0), &walls::RIGHT0), 385 | ], 386 | ); 387 | assert_eq!( 388 | maze.corner_walls_start((matrix_pos(1, 0), &walls::DOWN)) 389 | .collect::>(), 390 | vec![ 391 | (matrix_pos(1, 0), &walls::DOWN), 392 | (matrix_pos(1, 1), &walls::RIGHT0), 393 | (matrix_pos(2, 1), &walls::RIGHT1), 394 | (matrix_pos(3, 1), &walls::UP), 395 | (matrix_pos(3, 0), &walls::LEFT1), 396 | (matrix_pos(2, 0), &walls::LEFT0), 397 | ], 398 | ); 399 | } 400 | 401 | #[maze_test(tri)] 402 | fn follow_no_wall(mut maze: TestMaze) { 403 | let log = Navigator::new(&mut maze).down(true).stop(); 404 | 405 | let start_pos = (*log.first().unwrap(), &walls::DOWN); 406 | let expected = vec![]; 407 | assert_eq!(maze.follow_wall(start_pos).collect::>(), expected); 408 | } 409 | 410 | #[maze_test(tri)] 411 | fn follow_wall_single_room(maze: TestMaze) { 412 | assert_eq!( 413 | vec![ 414 | (matrix_pos(0, 0), &walls::LEFT0), 415 | (matrix_pos(0, 0), &walls::UP), 416 | (matrix_pos(0, 0), &walls::RIGHT0), 417 | ], 418 | maze.follow_wall((matrix_pos(0, 0), &walls::LEFT0)) 419 | .map(|(from, _)| from) 420 | .collect::>() 421 | ); 422 | } 423 | 424 | #[maze_test(tri)] 425 | fn follow_wall(mut maze: TestMaze) { 426 | Navigator::new(&mut maze) 427 | .from(matrix_pos(1, 0)) 428 | .down(true) 429 | .right(true) 430 | .right(true) 431 | .up(true) 432 | .left(true); 433 | 434 | assert_eq!( 435 | vec![ 436 | (matrix_pos(1, 0), &walls::RIGHT1), 437 | (matrix_pos(2, 0), &walls::LEFT0), 438 | (matrix_pos(2, 0), &walls::UP), 439 | (matrix_pos(3, 0), &walls::RIGHT1), 440 | (matrix_pos(3, 1), &walls::RIGHT0), 441 | (matrix_pos(2, 1), &walls::DOWN), 442 | (matrix_pos(1, 1), &walls::LEFT0), 443 | (matrix_pos(1, 0), &walls::LEFT1), 444 | ], 445 | maze.follow_wall((matrix_pos(1, 0), &walls::RIGHT1)) 446 | .map(|(from, _)| from) 447 | .collect::>() 448 | ); 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /maze/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub type TestMaze = Maze<()>; 4 | 5 | /// Determines whether two physical locations are close enough to be 6 | /// considered equal. 7 | /// 8 | /// # Arguments 9 | /// * `expected` - The expected location. 10 | /// * `actual` - Another location. 11 | pub fn is_close(expected: physical::Pos, actual: physical::Pos) -> bool { 12 | let d = (expected.x - actual.x, expected.y - actual.y); 13 | (d.0 * d.0 + d.1 * d.1).sqrt() < 0.00001 14 | } 15 | 16 | /// Determines whether two floating point values are close enough to be 17 | /// considered equal. 18 | /// 19 | /// This function lowers the resolution to `std::f32::EPSILON * 4.0`. 20 | /// 21 | /// # Arguments 22 | /// * `a` - One value. 23 | /// * `b` - Another value. 24 | pub fn nearly_equal(a: f32, b: f32) -> bool { 25 | a == b || (a - b).abs() < std::f32::EPSILON * 4.0 26 | } 27 | 28 | /// A simple helper to create a matrix position. 29 | /// 30 | /// # Arguments 31 | /// * `col` - The column. 32 | /// * `row` - The row. 33 | pub fn matrix_pos(col: isize, row: isize) -> matrix::Pos { 34 | matrix::Pos { col, row } 35 | } 36 | 37 | /// A navigator through a maze. 38 | /// 39 | /// This struct provides utility methods to open and close doors based on 40 | /// directions. 41 | pub struct Navigator<'a> { 42 | maze: &'a mut TestMaze, 43 | pos: Option, 44 | log: Vec, 45 | } 46 | 47 | impl<'a> Navigator<'a> { 48 | /// Creates a new navigator for a specific maze. 49 | /// 50 | /// # Arguments 51 | /// * `maze` - The maze to modify. 52 | pub fn new(maze: &'a mut TestMaze) -> Navigator<'a> { 53 | Navigator { 54 | maze, 55 | pos: None, 56 | log: Vec::new(), 57 | } 58 | } 59 | 60 | /// Moves the navigator to a specific room. 61 | /// 62 | /// # Arguments 63 | /// * `pos` - The new position. 64 | pub fn from(mut self, pos: matrix::Pos) -> Self { 65 | self.pos = Some(pos); 66 | self 67 | } 68 | 69 | /// Opens or closes a wall leading _up_. 70 | /// 71 | /// The current room position is also updated. 72 | /// 73 | /// # Arguments 74 | /// * `open` - Whether to open the wall. 75 | /// 76 | /// # Panics 77 | /// This method panics if there is no wall leading up from the current 78 | /// room. 79 | pub fn up(self, open: bool) -> Self { 80 | self.navigate(|wall| wall.dir == (0, -1), open) 81 | } 82 | 83 | /// Opens or closes a wall leading _down_. 84 | /// 85 | /// The current room position is also updated. 86 | /// 87 | /// # Arguments 88 | /// * `open` - Whether to open the wall. 89 | /// 90 | /// # Panics 91 | /// This method panics if there is no wall leading down from the current 92 | /// room. 93 | pub fn down(self, open: bool) -> Self { 94 | self.navigate(|wall| wall.dir == (0, 1), open) 95 | } 96 | 97 | /// Opens or closes a wall leading _left_. 98 | /// 99 | /// The current room position is also updated. 100 | /// 101 | /// # Arguments 102 | /// * `open` - Whether to open the wall. 103 | /// 104 | /// # Panics 105 | /// This method panics if there is no wall leading left from the current 106 | /// room. 107 | pub fn left(self, open: bool) -> Self { 108 | self.navigate(|wall| wall.dir == (-1, 0), open) 109 | } 110 | 111 | /// Opens or closes a wall leading _right_. 112 | /// 113 | /// The current room position is also updated. 114 | /// 115 | /// # Arguments 116 | /// * `open` - Whether to open the wall. 117 | /// 118 | /// # Panics 119 | /// This method panics if there is no wall leading right from the 120 | /// current room. 121 | pub fn right(self, open: bool) -> Self { 122 | self.navigate(|wall| wall.dir == (1, 0), open) 123 | } 124 | 125 | /// Stops and freezes this navigator. 126 | pub fn stop(mut self) -> Vec { 127 | self.log.push(self.pos.unwrap()); 128 | self.log 129 | } 130 | 131 | /// Opens or closes a wall. 132 | /// 133 | /// The current room position is also updated. 134 | /// 135 | /// # Arguments 136 | /// * `open` - Whether to open the wall. 137 | /// 138 | /// The wall selected is the first one for which `predicate` returns 139 | /// `true`. 140 | /// 141 | /// # Panics 142 | /// This method panics if there is no wall for which the predicate 143 | /// returns `true`. 144 | pub fn navigate

(mut self, mut predicate: P, open: bool) -> Self 145 | where 146 | for<'r> P: FnMut(&'r &&wall::Wall) -> bool, 147 | { 148 | if self.pos.is_none() { 149 | self.pos = self 150 | .maze 151 | .positions() 152 | .filter(|&pos| { 153 | self.maze.walls(pos).iter().any(|wall| predicate(&wall)) 154 | }) 155 | .next(); 156 | } 157 | let pos = self.pos.unwrap(); 158 | self.log.push(pos); 159 | 160 | let wall = self 161 | .maze 162 | .walls(pos) 163 | .iter() 164 | .filter(predicate) 165 | .filter(|wall| { 166 | self.maze.is_inside(matrix_pos( 167 | pos.col + wall.dir.0, 168 | pos.row + wall.dir.1, 169 | )) 170 | }) 171 | .next() 172 | .unwrap(); 173 | self.maze.set_open((pos, wall), open); 174 | self.pos = Some(matrix_pos(pos.col + wall.dir.0, pos.row + wall.dir.1)); 175 | self 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /maze/src/walk.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BinaryHeap; 2 | 3 | use bit_set::BitSet; 4 | 5 | use crate::matrix; 6 | 7 | use crate::matrix::Matrix; 8 | use crate::Maze; 9 | use crate::WallPos; 10 | 11 | /// The tuple `(current_wall, next_wall)`. 12 | /// 13 | /// The second value can be used to determine whether the end has been reached; 14 | /// it will be `None` for the last wall. 15 | pub type FollowWallItem = (WallPos, Option); 16 | 17 | impl Maze 18 | where 19 | T: Clone, 20 | { 21 | /// Walks from `from` to `to` along the shortest path. 22 | /// 23 | /// If the rooms are connected, the return value will iterate over the 24 | /// minimal set of rooms required to pass through to get from start to 25 | /// finish, including `from` and ` to`. 26 | /// 27 | /// # Example 28 | /// 29 | /// ``` 30 | /// # use maze::matrix; 31 | /// # use maze::walk::*; 32 | /// # let maze = maze::Shape::Hex.create::(5, 5) 33 | /// # .initialize( 34 | /// # maze::initialize::Method::Winding, 35 | /// # &mut maze::initialize::LFSR::new(12345), 36 | /// # ); 37 | /// 38 | /// for (i, pos) in maze 39 | /// .walk( 40 | /// matrix::Pos { col: 0, row: 0 }, 41 | /// matrix::Pos { 42 | /// col: maze.width() as isize - 1, 43 | /// row: maze.height() as isize - 1, 44 | /// }, 45 | /// ) 46 | /// .unwrap() 47 | /// .into_iter() 48 | /// .enumerate() 49 | /// { 50 | /// println!("{:?} is room #{} on the path", pos, i); 51 | /// } 52 | /// 53 | /// ``` 54 | /// 55 | /// # Arguments 56 | /// * `from` - The starting position. 57 | /// * `to` - The desired goal. 58 | pub fn walk(&self, from: matrix::Pos, to: matrix::Pos) -> Option> { 59 | // Reverse the positions to return the rooms in correct order 60 | let (start, end) = (to, from); 61 | 62 | // Assume that the distance between the centres of adjacent rooms is 63 | // consistent 64 | let distance = (self.center((0isize, 0isize).into()) 65 | - self.center((0isize, 1isize).into())) 66 | .value(); 67 | 68 | // The heuristic for a room position 69 | let target = self.center(end); 70 | let h = 71 | |pos: matrix::Pos| Priority((target - self.center(pos)).value()); 72 | 73 | // The room positions pending evaluation and their cost 74 | let mut open_set = OpenSet::new(self.width(), self.height()); 75 | open_set.push(Priority(f32::MAX), start); 76 | 77 | let mut rooms = Matrix::::new(self.width(), self.height()); 78 | rooms[start].g = Priority(0.0); 79 | rooms[start].f = h(start); 80 | 81 | while let Some(current) = open_set.pop() { 82 | // Have we reached the target? 83 | if current == end { 84 | return Some(Path::new(self, start, end, rooms)); 85 | } 86 | 87 | rooms[current].visited = true; 88 | for wall in self.doors(current) { 89 | // Find the next room, and continue if we have already evaluated 90 | // it to a better distance, or it is outside of the maze 91 | let (next, _) = self.back((current, wall)); 92 | if !self.is_inside(next) 93 | || (rooms[next].visited 94 | && rooms[next].g > rooms[current].g + distance) 95 | { 96 | continue; 97 | } 98 | 99 | // The cost to get to this room is one more that the room from 100 | // which we came 101 | let g = rooms[current].g + distance; 102 | let f = g + h(next); 103 | 104 | let current_in_open_set = open_set.contains(current); 105 | if !current_in_open_set || g >= rooms[current].g { 106 | rooms[next].g = g; 107 | rooms[next].f = f; 108 | rooms[next].came_from = Some(current); 109 | 110 | if !current_in_open_set { 111 | open_set.push(f, next); 112 | } 113 | } 114 | } 115 | } 116 | 117 | None 118 | } 119 | 120 | /// Follows a wall. 121 | /// 122 | /// This method will follow a wall without passing through any walls. When 123 | /// the starting wall is encountered, no more walls will be returned. 124 | /// 125 | /// The direction of walking along a wall is from the point where its span 126 | /// starts to where it ends. 127 | /// 128 | /// If the starting position is an open wall, the iterator will contain no 129 | /// elements. 130 | /// 131 | /// # Arguments 132 | /// * `wall_pos` - The starting wall position. 133 | pub fn follow_wall( 134 | &self, 135 | wall_pos: WallPos, 136 | ) -> impl Iterator + '_ { 137 | Follower::new(self, wall_pos) 138 | } 139 | } 140 | 141 | /// A path through a maze. 142 | /// 143 | /// This struct describes the path through a maze by maintaining a mapping from 144 | /// a room position to the next room. 145 | pub struct Path<'a, T> 146 | where 147 | T: Clone, 148 | { 149 | /// The maze being walked. 150 | pub(crate) maze: &'a Maze, 151 | 152 | /// The backing room matrix. 153 | rooms: matrix::Matrix, 154 | 155 | /// The start position. 156 | a: matrix::Pos, 157 | 158 | /// The end position. 159 | b: matrix::Pos, 160 | } 161 | 162 | impl<'a, T> Path<'a, T> 163 | where 164 | T: Clone, 165 | { 166 | /// Stores a path in a maze. 167 | /// 168 | /// # Arguments 169 | /// * `maze` - The maze being walked. 170 | /// * `start` - The start position. 171 | /// * `rooms` - The backing room matrix. 172 | pub(self) fn new( 173 | maze: &'a Maze, 174 | start: matrix::Pos, 175 | end: matrix::Pos, 176 | rooms: matrix::Matrix, 177 | ) -> Self { 178 | Path { 179 | maze, 180 | rooms, 181 | a: end, 182 | b: start, 183 | } 184 | } 185 | } 186 | 187 | impl<'a, T> IntoIterator for &'a Path<'a, T> 188 | where 189 | T: Clone, 190 | { 191 | type Item = matrix::Pos; 192 | type IntoIter = as IntoIterator>::IntoIter; 193 | 194 | /// Backtraces a path by following the `came_from` fields. 195 | /// 196 | /// To generate 197 | /// 198 | /// # Arguments 199 | /// * `start` - The starting position. 200 | /// * `end` - The end position. 201 | /// 202 | /// # Panics 203 | /// If the backing room matrix is incomplete. 204 | fn into_iter(self) -> Self::IntoIter { 205 | let (a, b) = (self.a, self.b); 206 | let mut result = Vec::new(); 207 | result.push(a); 208 | 209 | let mut current = a; 210 | while current != b { 211 | if let Some(next) = self.rooms[current].came_from { 212 | result.push(next); 213 | if current == b { 214 | break; 215 | } else { 216 | current = next; 217 | } 218 | } else { 219 | panic!("attempted to backtrace an incomplete path!"); 220 | } 221 | } 222 | 223 | result.into_iter() 224 | } 225 | } 226 | 227 | /// A rooms description for the walk algorithm. 228 | #[derive(Clone)] 229 | struct Room { 230 | /// The F score. 231 | /// 232 | ///This is the cost from start to a room along the best known path 233 | f: Priority, 234 | 235 | /// The G score. 236 | /// 237 | /// This is the estimated cost from start to end through a room. 238 | g: Priority, 239 | 240 | /// Whether the rooms has been visited. 241 | visited: bool, 242 | 243 | /// The room from which we came. 244 | /// 245 | /// When the algorithm has competed, this will be the room on the shortest 246 | /// path. 247 | came_from: Option, 248 | } 249 | 250 | impl Default for Room { 251 | fn default() -> Self { 252 | Room { 253 | f: Priority(f32::MAX), 254 | g: Priority(f32::MAX), 255 | visited: false, 256 | came_from: None, 257 | } 258 | } 259 | } 260 | 261 | /// Follows a wall. 262 | struct Follower<'a, T> 263 | where 264 | T: Clone, 265 | { 266 | /// The maze. 267 | maze: &'a Maze, 268 | 269 | /// The starting position. 270 | start_pos: WallPos, 271 | 272 | /// The current position. 273 | current: WallPos, 274 | 275 | /// Whether we have finished following walls. 276 | finished: bool, 277 | } 278 | 279 | impl<'a, T> Follower<'a, T> 280 | where 281 | T: Clone, 282 | { 283 | pub(self) fn new(maze: &'a Maze, start_pos: WallPos) -> Self { 284 | Self { 285 | maze, 286 | start_pos, 287 | current: start_pos, 288 | finished: maze.is_open(start_pos), 289 | } 290 | } 291 | 292 | /// Retrieves the next wall position. 293 | /// 294 | /// The next wall position will be reachable from `wall_pos` without passing 295 | /// through any walls, and it will share a corner. Repeatedly calling this 296 | /// method will yield walls clockwise inside a cavity in the maze. 297 | /// 298 | /// # Arguments 299 | /// * `wall_pos`- The wall position for which to retrieve a room. 300 | fn next_wall_pos(&self, wall_pos: WallPos) -> WallPos { 301 | self.maze 302 | .corner_walls_start((wall_pos.0, wall_pos.1.next)) 303 | .find(|&next| !self.maze.is_open(next)) 304 | .unwrap_or_else(|| self.maze.back(wall_pos)) 305 | } 306 | } 307 | 308 | impl<'a, T> Iterator for Follower<'a, T> 309 | where 310 | T: Clone, 311 | { 312 | type Item = FollowWallItem; 313 | 314 | /// Iterates over all wall positions. 315 | /// 316 | /// Wall positions are returned in the pair _(from, to)_. The last iteration 317 | /// before this iterator is exhausted will return _to_ as `None`. 318 | fn next(&mut self) -> Option { 319 | if self.finished { 320 | None 321 | } else { 322 | let previous = self.current; 323 | self.current = self.next_wall_pos(self.current); 324 | self.finished = self.current == self.start_pos; 325 | Some(( 326 | previous, 327 | if self.finished { 328 | None 329 | } else { 330 | Some(self.current) 331 | }, 332 | )) 333 | } 334 | } 335 | } 336 | 337 | /// A priority in an open set. 338 | #[derive(Clone, Copy)] 339 | struct Priority(f32); 340 | 341 | impl ::std::cmp::PartialEq for Priority { 342 | fn eq(&self, other: &Self) -> bool { 343 | self.0 == other.0 344 | } 345 | } 346 | 347 | impl ::std::cmp::Eq for Priority {} 348 | 349 | impl ::std::cmp::PartialOrd for Priority { 350 | /// Compares priorities. 351 | /// 352 | /// Note that this operation is the inverse of comparing the wrapped `f32` 353 | /// values. 354 | /// 355 | /// # Arguments 356 | /// * `other` - The other value. 357 | fn partial_cmp(&self, other: &Self) -> Option { 358 | Some(self.cmp(other)) 359 | } 360 | } 361 | 362 | impl ::std::cmp::Ord for Priority { 363 | fn cmp(&self, other: &Self) -> ::std::cmp::Ordering { 364 | other.0.partial_cmp(&self.0).expect("comparable priorities") 365 | } 366 | } 367 | 368 | impl ::std::ops::Add for Priority { 369 | type Output = Self; 370 | 371 | fn add(self, rhs: f32) -> Self::Output { 372 | Self(self.0 + rhs) 373 | } 374 | } 375 | 376 | impl ::std::ops::Add for Priority { 377 | type Output = Self; 378 | 379 | fn add(self, rhs: Priority) -> Self::Output { 380 | Self(self.0 + rhs.0) 381 | } 382 | } 383 | 384 | /// A room position with a priority. 385 | type PriorityPos = (Priority, matrix::Pos); 386 | 387 | /// A set of rooms and priorities. 388 | /// 389 | /// This struct supports adding a position with a priority, retrieving the 390 | /// position with the highest priority and querying whether a position is in the 391 | /// set. 392 | struct OpenSet { 393 | /// The width of the set. 394 | width: usize, 395 | 396 | /// The height of the set. 397 | height: usize, 398 | 399 | /// The heap containing prioritised positions. 400 | heap: BinaryHeap, 401 | 402 | /// The positions present in the heap. 403 | present: BitSet, 404 | } 405 | 406 | impl OpenSet { 407 | /// Creates a new open set. 408 | pub fn new(width: usize, height: usize) -> OpenSet { 409 | OpenSet { 410 | width, 411 | height, 412 | heap: BinaryHeap::new(), 413 | present: BitSet::with_capacity(width * height), 414 | } 415 | } 416 | 417 | /// Adds a position with a priority. 418 | /// 419 | /// # Arguments 420 | /// * priority` - The priority of the position. 421 | /// * pos` - The position. 422 | pub fn push(&mut self, priority: Priority, pos: matrix::Pos) { 423 | if let Some(index) = self.index(pos) { 424 | self.heap.push((priority, pos)); 425 | self.present.insert(index); 426 | } 427 | } 428 | 429 | /// Pops the room with the highest priority. 430 | pub fn pop(&mut self) -> Option { 431 | if let Some(pos) = self.heap.pop().map(|(_, pos)| pos) { 432 | if let Some(index) = self.index(pos) { 433 | self.present.remove(index); 434 | } 435 | Some(pos) 436 | } else { 437 | None 438 | } 439 | } 440 | 441 | /// Checks whether a position is in the set. 442 | /// 443 | /// # Arguments 444 | /// * `pos` - The position. 445 | pub fn contains(&mut self, pos: matrix::Pos) -> bool { 446 | self.index(pos) 447 | .map(|i| self.present.contains(i)) 448 | .unwrap_or(false) 449 | } 450 | 451 | /// Calculates the index of a position. 452 | /// 453 | /// If the position is outside of this set, nothing is returned. 454 | /// 455 | /// # Arguments 456 | /// * `pos` - The position. 457 | fn index(&self, pos: matrix::Pos) -> Option { 458 | if pos.col >= 0 459 | && pos.row >= 0 460 | && pos.col < self.width as isize 461 | && pos.row < self.height as isize 462 | { 463 | Some(pos.col as usize + pos.row as usize * self.width) 464 | } else { 465 | None 466 | } 467 | } 468 | } 469 | 470 | #[cfg(test)] 471 | mod tests { 472 | use maze_test::maze_test; 473 | 474 | use super::*; 475 | use crate::test_utils::*; 476 | 477 | #[maze_test] 478 | fn walk_single(maze: TestMaze) { 479 | let map = Matrix::::new_with_data(10, 10, |_| Room { 480 | f: Priority(0.0), 481 | ..Default::default() 482 | }); 483 | 484 | assert_eq!( 485 | Path::new(&maze, matrix_pos(0, 0), matrix_pos(0, 0), map) 486 | .into_iter() 487 | .collect::>(), 488 | vec![matrix_pos(0, 0)] 489 | ); 490 | } 491 | 492 | #[maze_test] 493 | fn walk_path(maze: TestMaze) { 494 | let mut map = Matrix::::new_with_data(10, 10, |_| Room { 495 | f: Priority(0.0), 496 | ..Default::default() 497 | }); 498 | map[matrix_pos(1, 1)].came_from = Some(matrix_pos(2, 2)); 499 | map[matrix_pos(2, 2)].came_from = Some(matrix_pos(2, 3)); 500 | map[matrix_pos(2, 3)].came_from = Some(matrix_pos(2, 4)); 501 | 502 | assert_eq!( 503 | Path::new(&maze, matrix_pos(2, 4), matrix_pos(1, 1), map) 504 | .into_iter() 505 | .collect::>(), 506 | vec![ 507 | matrix_pos(1, 1), 508 | matrix_pos(2, 2), 509 | matrix_pos(2, 3), 510 | matrix_pos(2, 4) 511 | ] 512 | ); 513 | } 514 | 515 | #[maze_test] 516 | fn walk_disconnected(maze: TestMaze) { 517 | assert!(maze.walk(matrix_pos(0, 0), matrix_pos(0, 1)).is_none()); 518 | } 519 | 520 | #[maze_test] 521 | fn walk_same(maze: TestMaze) { 522 | let from = matrix_pos(0, 0); 523 | let to = matrix_pos(0, 0); 524 | let expected = vec![matrix_pos(0, 0)]; 525 | assert_eq!( 526 | maze.walk(from, to) 527 | .unwrap() 528 | .into_iter() 529 | .collect::>(), 530 | expected, 531 | ); 532 | } 533 | 534 | #[maze_test] 535 | fn walk_simple(mut maze: TestMaze) { 536 | let log = Navigator::new(&mut maze).down(true).stop(); 537 | 538 | let from = log.first().unwrap(); 539 | let to = log.last().unwrap(); 540 | let expected = vec![*from, *to]; 541 | assert_eq!( 542 | maze.walk(*from, *to) 543 | .unwrap() 544 | .into_iter() 545 | .collect::>(), 546 | expected, 547 | ); 548 | } 549 | 550 | #[maze_test] 551 | fn walk_shortest(mut maze: TestMaze) { 552 | let log = Navigator::new(&mut maze) 553 | .down(true) 554 | .right(true) 555 | .right(true) 556 | .up(true) 557 | .stop(); 558 | 559 | let from = log.first().unwrap(); 560 | let to = log.last().unwrap(); 561 | assert!( 562 | maze.walk(*from, *to) 563 | .unwrap() 564 | .into_iter() 565 | .collect::>() 566 | .len() 567 | <= log.len() 568 | ); 569 | } 570 | 571 | #[maze_test] 572 | fn follow_wall_order(maze: TestMaze) { 573 | let start = 574 | maze.wall_positions((0isize, 0isize).into()).next().unwrap(); 575 | 576 | for (a, b) in maze.follow_wall(start) { 577 | if let Some(b) = b { 578 | assert!(is_close( 579 | maze.center(a.0) + a.1.span.1, 580 | maze.center(b.0) + b.1.span.0, 581 | )); 582 | } 583 | } 584 | } 585 | 586 | #[test] 587 | fn pop_empty() { 588 | let mut os = OpenSet::new(10, 10); 589 | 590 | assert!(os.pop().is_none()); 591 | } 592 | 593 | #[test] 594 | fn pop_nonempty() { 595 | let mut os = OpenSet::new(10, 10); 596 | 597 | os.push(Priority(0.0), matrix_pos(0, 0)); 598 | assert!(os.pop().is_some()); 599 | } 600 | 601 | #[test] 602 | fn pop_correct() { 603 | let mut os = OpenSet::new(10, 10); 604 | let expected = (Priority(0.0), matrix_pos(1, 2)); 605 | 606 | os.push(expected.0, expected.1); 607 | os.push(Priority(5.0), matrix_pos(5, 6)); 608 | os.push(Priority(10.0), matrix_pos(3, 4)); 609 | assert_eq!(os.pop(), Some(expected.1)); 610 | } 611 | 612 | #[test] 613 | fn contains_same() { 614 | let mut os = OpenSet::new(10, 10); 615 | let expected = (Priority(10.0), matrix_pos(1, 2)); 616 | 617 | assert!(!os.contains(expected.1)); 618 | os.push(Priority(0.0), matrix_pos(3, 4)); 619 | assert!(!os.contains(expected.1)); 620 | os.push(expected.0, expected.1); 621 | assert!(os.contains(expected.1)); 622 | os.push(Priority(5.0), matrix_pos(5, 6)); 623 | assert!(os.contains(expected.1)); 624 | os.pop(); 625 | assert!(os.contains(expected.1)); 626 | os.pop(); 627 | assert!(os.contains(expected.1)); 628 | os.pop(); 629 | assert!(!os.contains(expected.1)); 630 | } 631 | } 632 | -------------------------------------------------------------------------------- /maze/src/wall.rs: -------------------------------------------------------------------------------- 1 | use std::f32::consts::TAU; 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; 5 | 6 | use crate::shape::Shape; 7 | 8 | /// A wall index. 9 | pub type Index = usize; 10 | 11 | /// A bit mask for a wall. 12 | pub type Mask = u32; 13 | 14 | /// An offset from a wall to its corner neighbours. 15 | #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] 16 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 17 | pub struct Offset { 18 | /// The horisontal offset. 19 | pub dx: isize, 20 | 21 | /// The vertical offset. 22 | pub dy: isize, 23 | 24 | /// The neighbour index. 25 | pub wall: &'static Wall, 26 | } 27 | 28 | /// An angle in a span. 29 | #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] 30 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 31 | pub struct Angle { 32 | /// The angle. 33 | pub a: f32, 34 | 35 | /// cos(a). 36 | pub dx: f32, 37 | 38 | /// sin(a). 39 | pub dy: f32, 40 | } 41 | 42 | /// A wall. 43 | /// 44 | /// Walls have an index, which is used by [`Room`](crate::room::Room) to 45 | /// generate bit masks, and a direction, which indicates the position of the 46 | /// room on the other side of a wall, relative to the room to which the wall 47 | /// belongs. 48 | #[derive(Clone)] 49 | pub struct Wall { 50 | /// The name of this wall. 51 | pub name: &'static str, 52 | 53 | /// The shape to which this wall belongs. 54 | pub shape: Shape, 55 | 56 | /// The ordinal of this wall. 57 | /// 58 | /// The ordinals will be in the range _[0, n)_, where _n_ is the number of 59 | /// walls for the shape. When listing the walls of a room, the sequence 60 | /// number of a wall will be equal to this number. 61 | pub ordinal: usize, 62 | 63 | /// The index of this wall, used to generate the bit mask. 64 | pub index: Index, 65 | 66 | /// Offsets to other walls in the first corner of this wall. 67 | pub corner_wall_offsets: &'static [Offset], 68 | 69 | /// The horizontal and vertical offset of the room on the other side of this 70 | /// wall. 71 | pub dir: (isize, isize), 72 | 73 | /// The span, in radians, of the wall. 74 | /// 75 | /// The first value is the start of the span, and the second value the end. 76 | /// The second value will be smaller if the span wraps around _2𝜋_. 77 | pub span: (Angle, Angle), 78 | 79 | /// The previous wall, clock-wise. 80 | pub previous: &'static Wall, 81 | 82 | /// The next wall, clock-wise. 83 | pub next: &'static Wall, 84 | } 85 | 86 | impl Wall { 87 | /// The bit mask for this wall. 88 | pub fn mask(&self) -> Mask { 89 | 1 << self.index 90 | } 91 | 92 | /// Normalises an angle to be in the bound _[0, 2𝜋)_. 93 | /// 94 | /// # Arguments 95 | /// * `angle` - The angle to normalise. 96 | pub fn normalized_angle(angle: f32) -> f32 { 97 | if (0.0..TAU).contains(&angle) { 98 | angle 99 | } else { 100 | let t = angle % TAU; 101 | if t >= 0.0 { 102 | t 103 | } else { 104 | t + TAU 105 | } 106 | } 107 | } 108 | 109 | /// Whether an angle is in the span of this wall. 110 | /// 111 | /// The angle will be normalised. 112 | /// 113 | /// # Arguments 114 | /// * `angle` - The angle in radians. 115 | pub fn in_span(&self, angle: f32) -> bool { 116 | let normalized = Wall::normalized_angle(angle); 117 | 118 | if self.span.0.a < self.span.1.a { 119 | (self.span.0.a <= normalized) && (normalized < self.span.1.a) 120 | } else { 121 | (self.span.0.a <= normalized) || (normalized < self.span.1.a) 122 | } 123 | } 124 | } 125 | 126 | impl PartialEq for Wall { 127 | fn eq(&self, other: &Self) -> bool { 128 | self.shape == other.shape 129 | && self.index == other.index 130 | && self.dir == other.dir 131 | } 132 | } 133 | 134 | impl Eq for Wall {} 135 | 136 | impl std::hash::Hash for Wall { 137 | fn hash(&self, state: &mut H) 138 | where 139 | H: std::hash::Hasher, 140 | { 141 | self.shape.hash(state); 142 | self.index.hash(state); 143 | self.dir.hash(state); 144 | } 145 | } 146 | 147 | impl std::fmt::Debug for Wall { 148 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 149 | f.write_str(self.name) 150 | } 151 | } 152 | 153 | impl Ord for Wall { 154 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 155 | self.index.cmp(&other.index) 156 | } 157 | } 158 | 159 | impl PartialOrd for Wall { 160 | fn partial_cmp(&self, other: &Self) -> Option { 161 | Some(self.cmp(other)) 162 | } 163 | } 164 | 165 | #[cfg(feature = "serde")] 166 | impl<'de> Deserialize<'de> for &'static Wall { 167 | fn deserialize(deserializer: D) -> Result 168 | where 169 | D: Deserializer<'de>, 170 | { 171 | let wall_name = String::deserialize(deserializer)?; 172 | crate::shape::hex::walls::ALL 173 | .iter() 174 | .chain(crate::shape::quad::walls::ALL.iter()) 175 | .chain(crate::shape::tri::walls::ALL.iter()) 176 | .find(|wall| wall.name == wall_name) 177 | .copied() 178 | .ok_or_else(|| D::Error::custom("expected a wall name")) 179 | } 180 | } 181 | 182 | #[cfg(feature = "serde")] 183 | impl Serialize for Wall { 184 | fn serialize(&self, serializer: S) -> Result 185 | where 186 | S: Serializer, 187 | { 188 | serializer.serialize_str(self.name) 189 | } 190 | } 191 | 192 | #[cfg(test)] 193 | mod tests { 194 | use std::collections::HashSet; 195 | use std::f32::consts::PI; 196 | 197 | use maze_test::maze_test; 198 | 199 | use super::*; 200 | use crate::*; 201 | use test_utils::*; 202 | 203 | #[maze_test] 204 | fn unique(maze: TestMaze) { 205 | let walls = maze.walls(matrix_pos(0, 1)); 206 | assert_eq!( 207 | walls 208 | .iter() 209 | .cloned() 210 | .collect::>() 211 | .len(), 212 | walls.len() 213 | ); 214 | } 215 | 216 | #[maze_test] 217 | fn ordinal(maze: TestMaze) { 218 | for pos in maze.positions() { 219 | for (i, wall) in maze.walls(pos).iter().enumerate() { 220 | assert_eq!(i, wall.ordinal, "invalid ordinal for {:?}", wall); 221 | } 222 | } 223 | } 224 | 225 | #[maze_test] 226 | fn span(maze: TestMaze) { 227 | fn assert_span(wall: &'static Wall, angle: f32) { 228 | assert!( 229 | wall.in_span(angle), 230 | "{} was not in span ({} - {}) for {:?}", 231 | angle, 232 | wall.span.0.a, 233 | wall.span.1.a, 234 | wall, 235 | ); 236 | } 237 | 238 | fn assert_not_span(wall: &'static Wall, angle: f32) { 239 | assert!( 240 | !wall.in_span(angle), 241 | "{} was in span ({} - {}) for {:?}", 242 | angle, 243 | wall.span.0.a, 244 | wall.span.1.a, 245 | wall, 246 | ); 247 | } 248 | 249 | for pos in maze.positions() { 250 | for wall in maze.walls(pos) { 251 | let d = 16.0 * std::f32::EPSILON; 252 | assert_span(wall, wall.span.0.a + d); 253 | assert_not_span(wall, wall.span.0.a - d); 254 | assert_span(wall.previous, wall.span.0.a - d); 255 | assert_span(wall, wall.span.1.a - d); 256 | assert_not_span(wall, wall.span.1.a + d); 257 | assert_span(wall.next, wall.span.1.a + d); 258 | 259 | assert!( 260 | nearly_equal(wall.span.0.a.cos(), wall.span.0.dx), 261 | "{} span 0 dx invalid ({} != {})", 262 | wall.name, 263 | wall.span.0.a.cos(), 264 | wall.span.0.dx, 265 | ); 266 | assert!( 267 | nearly_equal(wall.span.0.a.sin(), wall.span.0.dy), 268 | "{} span 0 dy invalid ({} != {})", 269 | wall.name, 270 | wall.span.0.a.sin(), 271 | wall.span.0.dy, 272 | ); 273 | assert!( 274 | nearly_equal(wall.span.1.a.cos(), wall.span.1.dx), 275 | "{} span 1 dx invalid ({} != {})", 276 | wall.name, 277 | wall.span.1.a.cos(), 278 | wall.span.1.dx, 279 | ); 280 | assert!( 281 | nearly_equal(wall.span.1.a.sin(), wall.span.1.dy), 282 | "{} span 1 dy invalid ({} != {})", 283 | wall.name, 284 | wall.span.1.a.sin(), 285 | wall.span.1.dy, 286 | ); 287 | } 288 | } 289 | } 290 | 291 | #[maze_test] 292 | fn order(maze: TestMaze) { 293 | for pos in maze.positions() { 294 | let walls = maze.walls(pos); 295 | for wall in walls { 296 | let d = 16.0 * std::f32::EPSILON; 297 | assert!( 298 | wall.in_span(wall.previous.span.1.a + d), 299 | "invalid wall order {:?}: {:?} <=> {:?}", 300 | walls, 301 | wall.previous, 302 | wall, 303 | ); 304 | assert!( 305 | wall.in_span(wall.next.span.0.a - d), 306 | "invalid wall order {:?}: {:?} <=> {:?}", 307 | walls, 308 | wall, 309 | wall.next, 310 | ); 311 | } 312 | } 313 | } 314 | 315 | #[maze_test] 316 | fn wall_serialization(maze: TestMaze) { 317 | for wall in maze.all_walls() { 318 | let serialized = serde_json::to_string(wall).unwrap(); 319 | let deserialized: &'static Wall = 320 | serde_json::from_str(&serialized).unwrap(); 321 | assert_eq!(*wall, deserialized); 322 | } 323 | } 324 | 325 | #[maze_test] 326 | fn in_span(maze: TestMaze) { 327 | let mut failures = Vec::new(); 328 | let count = 100_000; 329 | 330 | // Test for two different rooms to ensure we cover all room types 331 | for col in 0..=1 { 332 | failures.extend( 333 | (0..=count) 334 | .map(|t| 2.0 * (TAU * (t as f32 / count as f32) - PI)) 335 | .filter(|&a| { 336 | maze.walls(matrix::Pos { col, row: 0 }) 337 | .iter().find(|wall| wall.in_span(a)) 338 | .is_none() 339 | }), 340 | ); 341 | } 342 | 343 | assert_eq!( 344 | Vec::::new(), 345 | failures, 346 | "not all angles were in the span of a wall ({}% failed)", 347 | 100.0 * (failures.len() as f32 / (2.0 * count as f32)), 348 | ); 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maze-test" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | -------------------------------------------------------------------------------- /test/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use std::collections::HashSet; 4 | 5 | use proc_macro::{ 6 | Delimiter, Group, Ident, Punct, Spacing, Span, TokenStream, TokenTree, 7 | }; 8 | 9 | /// The different shapes of mazes for which to generate tests. 10 | const SHAPES: &[&str] = &["hex", "quad", "tri"]; 11 | 12 | /// Marks a function as a test for a maze. 13 | /// 14 | /// Adding this attribute macro will ensure that the function is run as a test 15 | /// for all kinds of mazes. 16 | /// 17 | /// The annotated function should take one argument, which is the maze 18 | /// instance. 19 | #[proc_macro_attribute] 20 | pub fn maze_test(attr: TokenStream, item: TokenStream) -> TokenStream { 21 | // Extract the interesting parts of the original function 22 | let (span, name, args, inner_body) = split(item); 23 | 24 | // Extract the shapes for which to generate tests 25 | let shapes = shapes(attr); 26 | 27 | // Generate the body of the new function 28 | let body = { 29 | let mut body = 30 | function(span, Ident::new("inner", span), args, inner_body); 31 | 32 | // Iterate through known shapes for consistent ordering 33 | for shape in SHAPES { 34 | if shapes.iter().any(|s| s == shape) { 35 | body.extend( 36 | format!( 37 | "inner(\"{}\".parse::() 38 | .unwrap().create(10, 5));", 39 | shape, 40 | ) 41 | .parse::() 42 | .unwrap(), 43 | ); 44 | } 45 | } 46 | body 47 | }; 48 | 49 | let mut result = TokenStream::new(); 50 | result.extend(test_attr(span)); 51 | result.extend(function( 52 | span, 53 | name, 54 | Group::new(Delimiter::Parenthesis, TokenStream::new()), 55 | Group::new(Delimiter::Brace, body), 56 | )); 57 | result 58 | } 59 | 60 | /// Splits a token stream into the components we use. 61 | /// 62 | /// This function expects a function definition. It does not validate the 63 | /// function arguments. 64 | /// 65 | /// # Arguments 66 | /// * `item` - The token stream to split. 67 | /// 68 | /// # Panics 69 | /// This function will panic if the token stream does not contain the expected 70 | /// tokens. 71 | fn split(item: TokenStream) -> (Span, Ident, Group, Group) { 72 | let mut items = item.into_iter(); 73 | 74 | match (items.next(), items.next(), items.next(), items.next()) { 75 | ( 76 | Some(TokenTree::Ident(head)), 77 | Some(TokenTree::Ident(name)), 78 | Some(TokenTree::Group(args)), 79 | Some(TokenTree::Group(body)), 80 | ) => { 81 | if head.to_string() != "fn" { 82 | panic!("Expected function") 83 | } else { 84 | (head.span(), name, args, body) 85 | } 86 | } 87 | _ => panic!("Expected function"), 88 | } 89 | } 90 | 91 | /// Generates a set of shapes. 92 | /// 93 | /// If the attribute is empty, a set containing all shapes will be returned 94 | /// instead. 95 | /// 96 | /// # Panics 97 | /// This function panics if the token stream is not a comma separated list of 98 | /// identifiers, or if any identifier is not in `SHAPES`. 99 | fn shapes(attr: TokenStream) -> HashSet { 100 | let shapes = attr 101 | .into_iter() 102 | .flat_map(|tree| match tree { 103 | TokenTree::Ident(ref shape) => Some(shape.to_string()), 104 | TokenTree::Punct(ref punct) if punct.as_char() == ',' => None, 105 | _ => panic!("Unexpected token: {}", tree), 106 | }) 107 | .collect::>(); 108 | if shapes.is_empty() { 109 | SHAPES.iter().cloned().map(String::from).collect() 110 | } else { 111 | for shape in shapes.iter() { 112 | if !SHAPES.iter().any(|&s| s == shape) { 113 | panic!("Unknown shape: {}", shape); 114 | } 115 | } 116 | shapes 117 | } 118 | } 119 | 120 | /// Generates a test attribute. 121 | /// 122 | /// # Arguments 123 | /// * `span` - The span of the original function. 124 | fn test_attr(span: Span) -> TokenStream { 125 | vec![ 126 | TokenTree::Punct(Punct::new('#', Spacing::Alone)), 127 | TokenTree::Group(Group::new( 128 | Delimiter::Bracket, 129 | vec![TokenTree::Ident(Ident::new("test", span))] 130 | .into_iter() 131 | .collect(), 132 | )), 133 | ] 134 | .into_iter() 135 | .collect() 136 | } 137 | 138 | /// Generates a function. 139 | /// 140 | /// # Arguments 141 | /// * `span` - The span of the original function. 142 | /// * `name` - The function name. 143 | /// * `args` - The function arguments. 144 | /// * `body` - The function body. 145 | fn function(span: Span, name: Ident, args: Group, body: Group) -> TokenStream { 146 | vec![ 147 | TokenTree::Ident(Ident::new("fn", span)), 148 | TokenTree::Ident(name), 149 | TokenTree::Group(args), 150 | TokenTree::Group(body), 151 | ] 152 | .into_iter() 153 | .collect::() 154 | } 155 | -------------------------------------------------------------------------------- /tools/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maze-tools" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | maze = { path = "../maze" } 9 | 10 | lazy_static = { workspace = true } 11 | -------------------------------------------------------------------------------- /tools/src/alphabet/macros.rs: -------------------------------------------------------------------------------- 1 | /// Defines a full character bitmap. 2 | /// 3 | /// The expected format is: 4 | /// ```ignore 5 | /// character!( 6 | /// O X O X O X O X 7 | /// X O X O X O X O 8 | /// O X O X O X O X 9 | /// X O X O X O X O 10 | /// O X O X O X O X 11 | /// X O X O X O X O 12 | /// O X O X O X O X 13 | /// X O X O X O X O 14 | /// ); 15 | /// ``` 16 | macro_rules! character { 17 | (O) => { 18 | false 19 | }; 20 | (X) => { 21 | true 22 | }; 23 | ($($a:ident $b:ident $c:ident $d:ident $e:ident $f:ident $g:ident $h:ident)*) => { 24 | [ 25 | $([ 26 | character!($a), 27 | character!($b), 28 | character!($c), 29 | character!($d), 30 | character!($e), 31 | character!($f), 32 | character!($g), 33 | character!($h), 34 | ],)* 35 | ] 36 | }; 37 | } 38 | 39 | /// Defines the full mapping from character to bitmap. 40 | /// 41 | /// The expected format is: 42 | /// ```ignore 43 | /// let alphabet = alphabet! { 44 | /// 'A' => [ 45 | /// O X O X O X O X 46 | /// X O X O X O X O 47 | /// O X O X O X O X 48 | /// X O X O X O X O 49 | /// O X O X O X O X 50 | /// X O X O X O X O 51 | /// O X O X O X O X 52 | /// X O X O X O X O 53 | /// ], 54 | /// _ => [ 55 | /// O X O X O X O X 56 | /// X O X O X O X O 57 | /// O X O X O X O X 58 | /// X O X O X O X O 59 | /// O X O X O X O X 60 | /// X O X O X O X O 61 | /// O X O X O X O X 62 | /// X O X O X O X O 63 | /// ] 64 | /// }; 65 | /// ``` 66 | macro_rules! alphabet { 67 | ($($name:expr => [$($bits:ident)*],)* _ => [$($default:ident)*] ) => { 68 | { 69 | let mut map = ::std::collections::HashMap::new(); 70 | $(map.insert( 71 | $name, 72 | crate::alphabet::Character(character!($($bits)*)), 73 | );)* 74 | crate::alphabet::Alphabet { 75 | default: crate::alphabet::Character( 76 | character!($($default)*), 77 | ), 78 | map, 79 | } 80 | } 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /tools/src/alphabet/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use maze::{matrix, physical}; 4 | 5 | #[macro_use] 6 | mod macros; 7 | pub mod default; 8 | 9 | /// The width of a character bitmap. 10 | const WIDTH: usize = 8; 11 | 12 | /// The height of a character bitmap. 13 | const HEIGHT: usize = 8; 14 | 15 | /// A character bitmap. 16 | pub struct Character(pub(self) [[bool; WIDTH]; HEIGHT]); 17 | 18 | impl Character { 19 | /// Retrieves an interpolated bit from the bitmap. 20 | /// 21 | /// Positions outside of the bitmap are considered to be `0.0f32`. 22 | /// 23 | /// The bit at `(0, 0)` will have the greatest impact at the physical 24 | /// position `(0.5, 0.5)`. 25 | /// 26 | /// # Arguments 27 | /// * `pos` - The position. 28 | pub fn interpolated(&self, pos: physical::Pos) -> f32 { 29 | // Since values are centered around (0.5, 0.5), we do not need to 30 | // interpolate values outside of the range 31 | if pos.x < 0.0 32 | || pos.y < 0.0 33 | || pos.x > WIDTH as f32 34 | || pos.y > HEIGHT as f32 35 | { 36 | 0.0 37 | } else { 38 | let (col, dx) = matrix::partition(pos.x - 0.5); 39 | let (row, dy) = matrix::partition(pos.y - 0.5); 40 | 41 | let tl = self.get(matrix::Pos { col, row }); 42 | let tr = self.get(matrix::Pos { col: col + 1, row }); 43 | let t = tl * (1.0 - dx) + tr * dx; 44 | 45 | let bl = self.get(matrix::Pos { col, row: row + 1 }); 46 | let br = self.get(matrix::Pos { 47 | col: col + 1, 48 | row: row + 1, 49 | }); 50 | let b = bl * (1.0 - dx) + br * dx; 51 | 52 | t * (1.0 - dy) + b * dy 53 | } 54 | } 55 | 56 | /// Reads a specific bit. 57 | /// 58 | /// If the position is outside of the bitmap, `0.0f32` is returned. 59 | /// 60 | /// # Arguments 61 | /// * `pos` - The position to read. 62 | fn get(&self, pos: matrix::Pos) -> f32 { 63 | if pos.col >= 0 64 | && pos.row >= 0 65 | && pos.col < WIDTH as isize 66 | && pos.row < HEIGHT as isize 67 | { 68 | if self.0[pos.row as usize][pos.col as usize] { 69 | 1.0 70 | } else { 71 | 0.0 72 | } 73 | } else { 74 | 0.0 75 | } 76 | } 77 | } 78 | 79 | /// The bitmaps of an alphabet. 80 | pub struct Alphabet { 81 | /// The default character used when a string contains unknown characters. 82 | pub(self) default: Character, 83 | 84 | /// A mapping from character to bitmap. 85 | pub(self) map: HashMap, 86 | } 87 | 88 | impl Alphabet { 89 | /// Generates an iterator over the pixels of a string rendered by this 90 | /// alphabet. 91 | /// 92 | /// # Arguments 93 | /// * `text` - The text to render. 94 | /// * `columns` - The number of columns. This determines the horisontal 95 | /// size of the image. When reached, a line break will be added. 96 | /// * `resolution` - The number of samples to generate horisontally. 97 | pub fn render( 98 | &self, 99 | text: &str, 100 | columns: usize, 101 | horizontal_resolution: usize, 102 | ) -> AlphabetRenderer<'_> { 103 | let rows = (text.len() as f32 / columns as f32).ceil() as usize; 104 | let text = text.chars().collect(); 105 | let resolution = horizontal_resolution / columns; 106 | let current = 0; 107 | let limit = columns * rows * resolution * resolution; 108 | AlphabetRenderer { 109 | alphabet: self, 110 | text, 111 | columns, 112 | resolution, 113 | current, 114 | limit, 115 | } 116 | } 117 | } 118 | 119 | /// An iterator over bit samples for a rendered text. 120 | pub struct AlphabetRenderer<'a> { 121 | /// The alphabet to use. 122 | alphabet: &'a Alphabet, 123 | 124 | /// The characters of the text. 125 | text: Vec, 126 | 127 | /// The number of columns. 128 | columns: usize, 129 | 130 | /// The number of samples per character in each direction. 131 | resolution: usize, 132 | 133 | /// The current index. 134 | current: usize, 135 | 136 | /// The maximum number of samples. 137 | limit: usize, 138 | } 139 | 140 | impl<'a> AlphabetRenderer<'a> { 141 | /// Returns the current position. 142 | /// 143 | /// The position is represented as the tuple 144 | /// `(column * resolution, row * resolution)`. 145 | fn position(&self) -> (usize, usize) { 146 | let x = self.current % (self.columns * self.resolution); 147 | let y = self.current / (self.columns * self.resolution); 148 | (x, y) 149 | } 150 | } 151 | 152 | impl<'a> Iterator for AlphabetRenderer<'a> { 153 | type Item = (physical::Pos, f32); 154 | 155 | fn next(&mut self) -> Option { 156 | if self.current < self.limit { 157 | // Get the position 158 | let (ix, iy) = AlphabetRenderer::position(self); 159 | self.current += 1; 160 | 161 | // Calculate the character index 162 | let col = ix / self.resolution; 163 | let row = iy / self.resolution; 164 | let i = row * self.columns + col; 165 | 166 | // Calculate the physical position 167 | let x = ix as f32 / self.resolution as f32; 168 | let y = iy as f32 / self.resolution as f32; 169 | 170 | // Calculate the relative position within the character cell 171 | let rx = WIDTH as f32 * (ix - col * self.resolution) as f32 172 | / self.resolution as f32; 173 | let ry = HEIGHT as f32 * (iy - row * self.resolution) as f32 174 | / self.resolution as f32; 175 | 176 | Some(( 177 | physical::Pos { x, y }, 178 | self.text 179 | .get(i) 180 | .map(|&c| self.alphabet.get(c)) 181 | .map(|c| c.interpolated(physical::Pos { x: rx, y: ry })) 182 | .unwrap_or(0.0), 183 | )) 184 | } else { 185 | None 186 | } 187 | } 188 | } 189 | 190 | impl Alphabet { 191 | /// Retrieves the bitmap for a character, or the default one if none exists. 192 | /// 193 | /// # Arguments 194 | /// * `character` - The character for which to retrieve a bitmap. 195 | fn get(&self, character: char) -> &Character { 196 | self.map.get(&character).unwrap_or(&self.default) 197 | } 198 | } 199 | 200 | #[cfg(test)] 201 | mod tests { 202 | use super::*; 203 | 204 | #[test] 205 | fn character_interpolated() { 206 | let character = Character(character! { 207 | O O O O X X X X 208 | O O O O X X X X 209 | O O O O X X X X 210 | O O O O X X X X 211 | X X X X X X X X 212 | X X X X X X X X 213 | X X X X X X X X 214 | X X X X X X X X 215 | }); 216 | assert_eq!( 217 | character.interpolated(physical::Pos { x: -0.5, y: -0.5 }), 218 | 0.0 219 | ); 220 | assert_eq!( 221 | character.interpolated(physical::Pos { x: -0.5, y: 5.5 }), 222 | 0.0 223 | ); 224 | assert_eq!( 225 | character.interpolated(physical::Pos { x: -0.5, y: 9.5 }), 226 | 0.0 227 | ); 228 | assert_eq!( 229 | character.interpolated(physical::Pos { x: 8.5, y: -0.5 }), 230 | 0.0 231 | ); 232 | assert_eq!( 233 | character.interpolated(physical::Pos { x: 8.5, y: 5.5 }), 234 | 0.0 235 | ); 236 | assert_eq!( 237 | character.interpolated(physical::Pos { x: 8.5, y: 9.5 }), 238 | 0.0 239 | ); 240 | assert_eq!( 241 | character.interpolated(physical::Pos { x: 4.5, y: -0.5 }), 242 | 0.0 243 | ); 244 | assert_eq!( 245 | character.interpolated(physical::Pos { x: 4.5, y: 9.5 }), 246 | 0.0 247 | ); 248 | assert_eq!( 249 | character.interpolated(physical::Pos { x: 6.5, y: 2.5 }), 250 | 1.0 251 | ); 252 | assert_eq!( 253 | character.interpolated(physical::Pos { x: 4.0, y: 2.5 }), 254 | 0.5 255 | ); 256 | assert_eq!( 257 | character.interpolated(physical::Pos { x: 2.5, y: 4.0 }), 258 | 0.5 259 | ); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /tools/src/cell/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ops; 2 | 3 | use maze::matrix; 4 | use maze::physical; 5 | 6 | /// Translates physical positions to cells. 7 | pub trait Cells { 8 | /// Translates a physical position to a matrix cell. 9 | /// 10 | /// # Arguments 11 | /// * `pos` - The physical position to translate. 12 | fn cell(&self, pos: physical::Pos) -> matrix::Pos; 13 | } 14 | 15 | impl Cells for maze::Shape { 16 | fn cell(&self, pos: physical::Pos) -> matrix::Pos { 17 | self.physical_to_cell(pos) 18 | } 19 | } 20 | 21 | /// Splits values into matrix cells. 22 | pub trait Splitter 23 | where 24 | C: Cells, 25 | T: Copy, 26 | U: Copy + ops::Add + ops::Div, 27 | { 28 | /// Passes values through cells and collects their average in a matrix. 29 | /// 30 | /// # Arguments 31 | /// * `cells` - The cells used to translate physical coordinates to matrix 32 | /// coordinates. 33 | /// * `width` - The expected width of the resulting matrix. 34 | /// * `height` - The expected height of the resulting matrix. 35 | fn split_by( 36 | self, 37 | cells: &C, 38 | width: usize, 39 | height: usize, 40 | ) -> matrix::Matrix; 41 | } 42 | 43 | impl<'a, C, I, T, U> Splitter for &'a mut I 44 | where 45 | C: Cells, 46 | I: Iterator, 47 | T: Copy, 48 | U: Copy + Default + ops::Add + ops::Div, 49 | { 50 | fn split_by( 51 | self, 52 | cells: &C, 53 | width: usize, 54 | height: usize, 55 | ) -> matrix::Matrix { 56 | self.fold( 57 | matrix::Matrix::<(usize, U)>::new(width, height), 58 | |mut acc, (physical_pos, value)| { 59 | let matrix_pos = cells.cell(physical_pos); 60 | if let Some((count, previous)) = acc.get(matrix_pos) { 61 | acc[matrix_pos] = (count + 1, *previous + value); 62 | } 63 | acc 64 | }, 65 | ) 66 | .map(|(count, value)| *value / *count) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tools/src/image/mod.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | 3 | /// A colour. 4 | #[derive(Clone, Copy, Default)] 5 | pub struct Color { 6 | // The red component. 7 | pub red: u8, 8 | 9 | // The green component. 10 | pub green: u8, 11 | 12 | // The blue component. 13 | pub blue: u8, 14 | 15 | // The alpha component. 16 | pub alpha: u8, 17 | } 18 | 19 | impl Color { 20 | /// Returns a fully transparent version of this colour. 21 | pub fn transparent(self) -> Self { 22 | Self { alpha: 0, ..self } 23 | } 24 | 25 | /// Fades one colour to another. 26 | /// 27 | /// # Arguments 28 | /// * `other` - The other colour. 29 | /// * `w` - The weight of this colour. If this is `1.0` or greater, `self` 30 | /// colour is returned; if this is 0.0 or less, `other` is returned; 31 | /// otherwise a linear interpolation between the colours is returned. 32 | pub fn fade(self, other: Self, w: f32) -> Color { 33 | if w >= 1.0 { 34 | self 35 | } else if w <= 0.0 { 36 | other 37 | } else { 38 | let n = 1.0 - w; 39 | Color { 40 | red: (f32::from(self.red) * w + f32::from(other.red) * n) as u8, 41 | green: (f32::from(self.green) * w + f32::from(other.green) * n) 42 | as u8, 43 | blue: (f32::from(self.blue) * w + f32::from(other.blue) * n) 44 | as u8, 45 | alpha: (f32::from(self.alpha) * w + f32::from(other.alpha) * n) 46 | as u8, 47 | } 48 | } 49 | } 50 | } 51 | 52 | impl str::FromStr for Color { 53 | type Err = String; 54 | 55 | /// Converts a string to a colour. 56 | /// 57 | /// This method supports colours on the form `#RRGGBB` and `#RRGGBBAA`, 58 | /// where `RR`, `GG`, `BB` and `AA` are the red, green, blue and alpha 59 | /// components hex encoded. 60 | /// 61 | /// # Arguments 62 | /// * `s` - The string to convert. 63 | fn from_str(s: &str) -> Result { 64 | if !s.starts_with('#') || s.len() % 2 == 0 { 65 | Err(format!("unknown colour value: {}", s)) 66 | } else { 67 | let data = s 68 | .bytes() 69 | // Skip the initial '#' 70 | .skip(1) 71 | // Hex decode and create list 72 | .map(|c| { 73 | if c.is_ascii_digit() { 74 | Some(c - b'0') 75 | } else if (b'A'..=b'F').contains(&c) { 76 | Some(c - b'A' + 10) 77 | } else if (b'a'..=b'f').contains(&c) { 78 | Some(c - b'a' + 10) 79 | } else { 80 | None 81 | } 82 | }) 83 | .collect::>() 84 | // Join every byte 85 | .chunks(2) 86 | .map(|c| { 87 | if let (Some(msb), Some(lsb)) = (c[0], c[1]) { 88 | Some(msb << 4 | lsb) 89 | } else { 90 | None 91 | } 92 | }) 93 | // Ensure all values are valid 94 | .take_while(Option::is_some) 95 | .map(Option::unwrap) 96 | .collect::>(); 97 | 98 | match data.len() { 99 | 3 => Ok(Color { 100 | red: data[0], 101 | green: data[1], 102 | blue: data[2], 103 | alpha: 255, 104 | }), 105 | 4 => Ok(Color { 106 | red: data[1], 107 | green: data[2], 108 | blue: data[3], 109 | alpha: data[0], 110 | }), 111 | _ => Err(format!("invalid colour format: {}", s)), 112 | } 113 | } 114 | } 115 | } 116 | 117 | impl ::std::fmt::Display for Color { 118 | /// Converts a colour to a string. 119 | /// 120 | /// This method ignores the alpha component. 121 | fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { 122 | write!(f, "#{:02.X}{:02.X}{:02.X}", self.red, self.green, self.blue) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tools/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate lazy_static; 3 | 4 | pub mod alphabet; 5 | pub mod cell; 6 | pub mod image; 7 | pub mod voronoi; 8 | -------------------------------------------------------------------------------- /tools/src/voronoi/initialize.rs: -------------------------------------------------------------------------------- 1 | use std::iter; 2 | 3 | use maze::initialize; 4 | use maze::matrix; 5 | use maze::physical; 6 | 7 | /// A container struct for multiple initialisation methods. 8 | #[derive(Clone, Debug)] 9 | pub struct Methods 10 | where 11 | R: initialize::Randomizer + Sized, 12 | { 13 | methods: Vec, 14 | 15 | _marker: ::std::marker::PhantomData, 16 | } 17 | 18 | /// A description of an initialised maze and its areas. 19 | pub struct InitializedMaze 20 | where 21 | T: Clone, 22 | { 23 | /// The initialised maze. 24 | pub maze: maze::Maze, 25 | 26 | /// A mapping from room position to the index of its initialiser in the 27 | /// initialisation vector. 28 | pub areas: matrix::Matrix, 29 | } 30 | 31 | impl InitializedMaze 32 | where 33 | T: Clone, 34 | { 35 | /// Maps each room of the maze, yielding a maze with the same layout but 36 | /// with transformed data. 37 | /// 38 | /// This method allows for incorporating are information into the new maze. 39 | /// 40 | /// # Arguments 41 | /// * `data` - A function providing data for the new maze. 42 | pub fn map(&self, mut data: F) -> maze::Maze 43 | where 44 | F: FnMut(matrix::Pos, T, usize) -> U, 45 | U: Clone, 46 | { 47 | self.maze 48 | .map(|pos, value| data(pos, value, self.areas[pos])) 49 | } 50 | } 51 | 52 | impl From> for maze::Maze 53 | where 54 | T: Clone, 55 | { 56 | fn from(source: InitializedMaze) -> Self { 57 | source.maze 58 | } 59 | } 60 | 61 | impl Methods 62 | where 63 | R: initialize::Randomizer + Sized, 64 | { 65 | /// Creates an initialiser for a list of initialisation methods. 66 | /// 67 | /// # Arguments 68 | /// * `methods` - The initialisation methods to use. 69 | pub fn new(methods: Vec) -> Self { 70 | Self { 71 | methods, 72 | _marker: ::std::marker::PhantomData, 73 | } 74 | } 75 | 76 | /// The initialisation methods. 77 | pub fn methods(&self) -> &Vec { 78 | &self.methods 79 | } 80 | 81 | /// Initialises a maze by applying all methods defined for this collection. 82 | /// 83 | /// This method generates a Voronoi diagram for all methods with centres and 84 | /// weights from `points`, and uses that and the `filter` argument to limit 85 | /// each initialisation method. 86 | /// 87 | /// The matrix returned is the Voronoi diagram used, where values are 88 | /// indices in the `methods` vector. 89 | /// 90 | /// # Arguments 91 | /// * `maze` - The maze to initialise. 92 | /// * `rng` - A random number generator. 93 | /// * `filter` - An additional filter applied to all methods. 94 | /// * `points` - The points and weights to generate a Voronoi diagram. 95 | pub fn initialize( 96 | self, 97 | maze: maze::Maze, 98 | rng: &mut R, 99 | filter: F, 100 | points: P, 101 | ) -> InitializedMaze 102 | where 103 | F: Fn(matrix::Pos) -> bool, 104 | T: Clone, 105 | P: Iterator>, 106 | { 107 | // Generate the areas 108 | let areas = 109 | super::matrix(&maze, points.take(self.methods.len()).collect()); 110 | 111 | // Use a different initialisation method for each segment 112 | let mut maze = self.methods.into_iter().enumerate().fold( 113 | maze, 114 | |maze, (i, method)| { 115 | maze.initialize_filter(method, rng, |pos| { 116 | filter(pos) && areas[pos] == i 117 | }) 118 | }, 119 | ); 120 | 121 | // Make sure all segments are connected 122 | initialize::connect_all(&mut maze, rng, filter); 123 | 124 | InitializedMaze { maze, areas } 125 | } 126 | 127 | /// Generates an infinite enumeration of random points and weights. 128 | /// 129 | /// The value of the points yielded is their index. 130 | /// 131 | /// # Arguments 132 | /// * `viewbox` - The viewbox to which to constrain the points. 133 | /// * `rng``- A random number generator. 134 | pub fn random_points( 135 | viewbox: physical::ViewBox, 136 | rng: &mut R, 137 | ) -> impl Iterator> + '_ { 138 | iter::repeat_with(move || { 139 | ( 140 | physical::Pos { 141 | x: viewbox.corner.x + rng.random() as f32 * viewbox.width, 142 | y: viewbox.corner.y + rng.random() as f32 * viewbox.height, 143 | }, 144 | (rng.random() as f32) + 0.5, 145 | ) 146 | }) 147 | .enumerate() 148 | } 149 | } 150 | 151 | impl Default for Methods 152 | where 153 | R: initialize::Randomizer + Sized, 154 | { 155 | fn default() -> Self { 156 | Self { 157 | methods: vec![initialize::Method::default()], 158 | _marker: ::std::marker::PhantomData, 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /tools/src/voronoi/mod.rs: -------------------------------------------------------------------------------- 1 | use std::f32; 2 | 3 | use maze::matrix; 4 | use maze::physical; 5 | 6 | pub mod initialize; 7 | 8 | /// A point description in the Voronoi diagram initialisation vector. 9 | /// 10 | /// The first item is the physical location of this point and the second its 11 | /// weight. The final item is the actual value. 12 | pub type Point = (V, (physical::Pos, f32)); 13 | 14 | pub fn matrix( 15 | maze: &maze::Maze, 16 | points: Vec>, 17 | ) -> matrix::Matrix 18 | where 19 | V: Clone + Default, 20 | T: Clone, 21 | { 22 | let mut result = matrix::Matrix::new(maze.width(), maze.height()); 23 | 24 | // For each cell in the resulting matrix, retrieve the value of the point 25 | // closest to the centre of the room corresponding to the cell by iterating 26 | // over all points and selecting the one where the distance / weight ratio 27 | // is the smallest 28 | for pos in result.positions() { 29 | let center = maze.center(pos); 30 | if let Some(val) = points 31 | .iter() 32 | .map(|(val, (p, w))| ((*p - center).value() / w, val)) 33 | // We assume that that the weights are not exotic enough to cause 34 | // this to fail 35 | .min_by(|v1, v2| v1.0.partial_cmp(&v2.0).unwrap()) 36 | .map(|(_, val)| val) 37 | { 38 | result[pos] = val.clone(); 39 | } 40 | } 41 | 42 | result 43 | } 44 | -------------------------------------------------------------------------------- /web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "maze-web" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | maze = { path = "../maze" } 9 | 10 | actix-web = { workspace = true } 11 | futures-util = { workspace = true } 12 | rand = { workspace = true } 13 | serde = { workspace = true } 14 | svg = { workspace = true } 15 | 16 | [dev-dependencies] 17 | serde_urlencoded = { workspace = true } 18 | -------------------------------------------------------------------------------- /web/src/main.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{get, web, App, HttpResponse, HttpServer, Responder}; 2 | use serde::Deserialize; 3 | 4 | mod types; 5 | 6 | #[derive(Deserialize)] 7 | struct Query { 8 | seed: Option, 9 | solve: Option, 10 | } 11 | #[get("/{maze_type}/{dimensions}/image.svg")] 12 | async fn maze_svg( 13 | (path, query): ( 14 | web::Path<(types::MazeType, types::Dimensions)>, 15 | web::Query, 16 | ), 17 | ) -> impl Responder { 18 | let (maze_type, dimensions) = path.into_inner(); 19 | let Query { seed, solve } = query.into_inner(); 20 | HttpResponse::from(types::Maze { 21 | maze_type, 22 | dimensions, 23 | seed: seed.unwrap_or_else(types::Seed::random), 24 | solve: solve.unwrap_or(false), 25 | }) 26 | } 27 | 28 | #[actix_web::main] 29 | async fn main() -> std::io::Result<()> { 30 | HttpServer::new(|| App::new().service(maze_svg)) 31 | .bind("0.0.0.0:8000") 32 | .unwrap() 33 | .run() 34 | .await 35 | } 36 | -------------------------------------------------------------------------------- /web/src/types/dimensions.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | /// Dimensions of a maze. 4 | #[derive(Debug, Deserialize, Eq, PartialEq)] 5 | #[serde(try_from = "String")] 6 | pub struct Dimensions { 7 | /// The width. 8 | pub width: usize, 9 | 10 | /// The height. 11 | pub height: usize, 12 | } 13 | 14 | impl TryFrom for Dimensions { 15 | type Error = String; 16 | 17 | fn try_from(value: String) -> Result { 18 | let mut parts = value.split('x'); 19 | let width = parts 20 | .next() 21 | .unwrap() 22 | .parse::() 23 | .map_err(|_| String::from("invalid width"))?; 24 | let height = parts 25 | .next() 26 | .ok_or_else(|| String::from("no height specified"))? 27 | .parse::() 28 | .map_err(|_| String::from("invalid height"))?; 29 | Ok(Self { width, height }) 30 | } 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use super::*; 36 | 37 | #[test] 38 | fn deserialize() { 39 | assert_eq!( 40 | Dimensions { 41 | width: 1, 42 | height: 2, 43 | }, 44 | String::from("1x2").try_into().unwrap(), 45 | ); 46 | assert_eq!( 47 | Err(String::from("no height specified")), 48 | Dimensions::try_from(String::from("1")), 49 | ); 50 | assert_eq!( 51 | Err(String::from("invalid width")), 52 | Dimensions::try_from(String::from("ax2")), 53 | ); 54 | assert_eq!( 55 | Err(String::from("invalid height")), 56 | Dimensions::try_from(String::from("1xb")), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /web/src/types/maze_type.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | /// A maze type, convertible from a query string. 4 | #[derive(Deserialize)] 5 | #[serde(transparent)] 6 | pub struct MazeType(maze::Shape); 7 | 8 | impl MazeType { 9 | pub fn create(self, dimensions: super::Dimensions) -> maze::Maze 10 | where 11 | T: Clone + Copy + Default, 12 | { 13 | self.0.create(dimensions.width, dimensions.height) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /web/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | use actix_web::HttpResponse; 2 | use svg::Node; 3 | 4 | use maze::initialize; 5 | use maze::render::svg::ToPath; 6 | 7 | mod maze_type; 8 | pub use self::maze_type::*; 9 | mod dimensions; 10 | pub use self::dimensions::*; 11 | mod seed; 12 | pub use self::seed::*; 13 | 14 | /// The maximum nmber of rooms. 15 | const MAX_ROOMS: usize = 1000; 16 | 17 | /// A responder providing an image of a maze. 18 | pub struct Maze { 19 | pub maze_type: MazeType, 20 | pub dimensions: Dimensions, 21 | pub seed: Seed, 22 | pub solve: bool, 23 | } 24 | 25 | impl From for HttpResponse { 26 | fn from(mut source: Maze) -> Self { 27 | let room_count = source.dimensions.width * source.dimensions.height; 28 | if room_count > MAX_ROOMS { 29 | HttpResponse::InsufficientStorage() 30 | .body("the requested maze is too large") 31 | } else { 32 | let maze = source 33 | .maze_type 34 | .create::<()>(source.dimensions) 35 | .initialize(initialize::Method::Branching, &mut source.seed); 36 | 37 | let mut container = svg::node::element::Group::new(); 38 | container.append( 39 | svg::node::element::Path::new() 40 | .set("class", "walls") 41 | .set("d", maze.to_path_d()), 42 | ); 43 | if source.solve { 44 | container.append( 45 | svg::node::element::Path::new().set("class", "path").set( 46 | "d", 47 | maze.walk( 48 | maze::matrix::Pos { col: 0, row: 0 }, 49 | maze::matrix::Pos { 50 | col: maze.width() as isize - 1, 51 | row: maze.height() as isize - 1, 52 | }, 53 | ) 54 | .unwrap() 55 | .to_path_d(), 56 | ), 57 | ); 58 | } 59 | let data = svg::Document::new() 60 | .set("viewBox", maze.viewbox().tuple()) 61 | .add(container) 62 | .to_string(); 63 | HttpResponse::Ok().content_type("image/svg+xml").body(data) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /web/src/types/seed.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use maze::initialize; 4 | 5 | /// A random seed. 6 | #[derive(Debug, Deserialize, Eq, PartialEq)] 7 | #[serde(transparent)] 8 | pub struct Seed { 9 | /// The LFSR initialised with the seed. 10 | lfsr: initialize::LFSR, 11 | } 12 | 13 | impl Seed { 14 | pub fn random() -> Self { 15 | Self { 16 | lfsr: initialize::LFSR::new(rand::random()), 17 | } 18 | } 19 | } 20 | 21 | impl initialize::Randomizer for Seed { 22 | fn range(&mut self, a: usize, b: usize) -> usize { 23 | self.lfsr.range(a, b) 24 | } 25 | 26 | fn random(&mut self) -> f64 { 27 | self.lfsr.random() 28 | } 29 | } 30 | 31 | #[cfg(test)] 32 | mod tests { 33 | use super::*; 34 | 35 | #[test] 36 | fn deserialize() { 37 | assert_eq!( 38 | Seed { 39 | lfsr: initialize::LFSR::new(1234) 40 | }, 41 | serde_urlencoded::from_str::>("seed=1234") 42 | .unwrap()[0] 43 | .1, 44 | ); 45 | } 46 | } 47 | --------------------------------------------------------------------------------