From 5e147b40c2c9c45048cea00c457d42b80597ab27 Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Mon, 6 Dec 2021 15:49:40 -0500 Subject: [PATCH] Complete section 4.7, using maze generation to build a map --- src/map_builders/maze.rs | 274 +++++++++++++++++++++++++++++++++++++++ src/map_builders/mod.rs | 5 +- 2 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 src/map_builders/maze.rs diff --git a/src/map_builders/maze.rs b/src/map_builders/maze.rs new file mode 100644 index 0000000..7001596 --- /dev/null +++ b/src/map_builders/maze.rs @@ -0,0 +1,274 @@ +use super::MapBuilder; +use super::{generate_voronoi_spawn_regions, remove_unreachable_areas_returning_most_distant}; +use crate::{spawner, Map, Position, TileType, SHOW_MAPGEN_VISUALIZER}; +use rltk::RandomNumberGenerator; +use specs::prelude::*; +use std::cmp::{max, min}; +use std::collections::HashMap; + +pub struct MazeBuilder { + map: Map, + starting_position: Position, + depth: i32, + history: Vec, + noise_areas: HashMap>, +} + +impl MapBuilder for MazeBuilder { + fn build_map(&mut self) { + self.build(); + } + + fn spawn_entities(&mut self, ecs: &mut World) { + for area in self.noise_areas.iter() { + spawner::spawn_region(ecs, area.1, self.depth); + } + } + + fn get_map(&self) -> Map { + self.map.clone() + } + + fn get_starting_position(&self) -> Position { + self.starting_position + } + + fn get_snapshot_history(&self) -> Vec { + self.history.clone() + } + + fn take_snapshot(&mut self) { + if SHOW_MAPGEN_VISUALIZER { + let mut snapshot = self.map.clone(); + for v in snapshot.revealed_tiles.iter_mut() { + *v = true; + } + self.history.push(snapshot); + } + } +} + +impl MazeBuilder { + pub fn new(new_depth: i32) -> MazeBuilder { + MazeBuilder { + map: Map::new(new_depth), + starting_position: Position { x: 0, y: 0 }, + depth: new_depth, + history: Vec::new(), + noise_areas: HashMap::new(), + } + } + + #[allow(clippy::map_entry)] + fn build(&mut self) { + let mut rng = RandomNumberGenerator::new(); + + // Maze gen + let mut maze = Grid::new( + (self.map.width / 2) - 2, + (self.map.height / 2) - 2, + &mut rng, + ); + maze.generate_maze(self); + + self.starting_position = Position { x: 2, y: 2 }; + let start_idx = self + .map + .xy_idx(self.starting_position.x, self.starting_position.y); + self.take_snapshot(); + + // Find all tiles we can reach from the starting point + let exit_tile = remove_unreachable_areas_returning_most_distant(&mut self.map, start_idx); + self.take_snapshot(); + + // Place the stairs + self.map.tiles[exit_tile] = TileType::DownStairs; + self.take_snapshot(); + + // Now we build a noise map for use in spawning entities later + self.noise_areas = generate_voronoi_spawn_regions(&self.map, &mut rng); + } +} + +const TOP: usize = 0; +const RIGHT: usize = 1; +const BOTTOM: usize = 2; +const LEFT: usize = 3; + +#[derive(Copy, Clone)] +struct Cell { + row: i32, + column: i32, + walls: [bool; 4], + visited: bool, +} + +impl Cell { + fn new(row: i32, column: i32) -> Cell { + Cell { + row, + column, + walls: [true, true, true, true], + visited: false, + } + } + + fn remove_walls(&mut self, next: &mut Cell) { + let x = self.column - next.column; + let y = self.row - next.row; + + if x == 1 { + self.walls[LEFT] = false; + next.walls[RIGHT] = false; + } else if x == -1 { + self.walls[RIGHT] = false; + next.walls[LEFT] = false; + } else if y == 1 { + self.walls[TOP] = false; + next.walls[BOTTOM] = false; + } else if y == -1 { + self.walls[BOTTOM] = false; + next.walls[TOP] = false; + } + } +} + +struct Grid<'a> { + width: i32, + height: i32, + cells: Vec, + backtrace: Vec, + current: usize, + rng: &'a mut RandomNumberGenerator, +} + +impl<'a> Grid<'a> { + fn new(width: i32, height: i32, rng: &mut RandomNumberGenerator) -> Grid { + let mut grid = Grid { + width, + height, + cells: Vec::new(), + backtrace: Vec::new(), + current: 0, + rng, + }; + + for row in 0..height { + for column in 0..width { + grid.cells.push(Cell::new(row, column)); + } + } + + grid + } + + fn calculate_index(&self, row: i32, column: i32) -> i32 { + if row < 0 || column < 0 || column > self.width - 1 || row > self.height - 1 { + -1 + } else { + column + (row * self.width) + } + } + + fn get_available_neighbors(&self) -> Vec { + let mut neighbors: Vec = Vec::new(); + + let current_row = self.cells[self.current].row; + let current_column = self.cells[self.current].column; + + let neighbor_indices: [i32; 4] = [ + self.calculate_index(current_row - 1, current_column), + self.calculate_index(current_row, current_column + 1), + self.calculate_index(current_row + 1, current_column), + self.calculate_index(current_row, current_column - 1), + ]; + + for i in neighbor_indices.iter() { + if *i != -1 && !self.cells[*i as usize].visited { + neighbors.push(*i as usize); + } + } + + neighbors + } + + fn find_next_cell(&mut self) -> Option { + let neighbors = self.get_available_neighbors(); + if !neighbors.is_empty() { + return if neighbors.len() == 1 { + Some(neighbors[0]) + } else { + Some(neighbors[(self.rng.roll_dice(1, neighbors.len() as i32) - 1) as usize]) + }; + } + + None + } + + fn generate_maze(&mut self, generator: &mut MazeBuilder) { + let mut i = 0; + loop { + self.cells[self.current].visited = true; + + match self.find_next_cell() { + Some(next) => { + self.cells[next].visited = true; + self.backtrace.push(self.current); + + // __lower_part__ __higher_part_ + // / \ / \ + // --------cell1------ | cell2----------- + let (lower_part, higher_part) = + self.cells.split_at_mut(max(self.current, next)); + let cell1 = &mut lower_part[min(self.current, next)]; + let cell2 = &mut higher_part[0]; + + cell1.remove_walls(cell2); + self.current = next; + } + None => { + if !self.backtrace.is_empty() { + self.current = self.backtrace[0]; + self.backtrace.remove(0); + } else { + break; + } + } + } + + if i % 50 == 0 { + self.copy_to_map(&mut generator.map); + generator.take_snapshot(); + } + i += 1; + } + } + + fn copy_to_map(&self, map: &mut Map) { + // Clear the map + for i in map.tiles.iter_mut() { + *i = TileType::Wall; + } + + for cell in self.cells.iter() { + let x = cell.column + 1; + let y = cell.row + 1; + let idx = map.xy_idx(x * 2, y * 2); + + map.tiles[idx] = TileType::Floor; + + if !cell.walls[TOP] { + map.tiles[idx - map.width as usize] = TileType::Floor; + } + if !cell.walls[RIGHT] { + map.tiles[idx + 1] = TileType::Floor + } + if !cell.walls[BOTTOM] { + map.tiles[idx + map.width as usize] = TileType::Floor + } + if !cell.walls[LEFT] { + map.tiles[idx - 1] = TileType::Floor + } + } + } +} diff --git a/src/map_builders/mod.rs b/src/map_builders/mod.rs index 2925c72..2a64a9a 100644 --- a/src/map_builders/mod.rs +++ b/src/map_builders/mod.rs @@ -3,6 +3,7 @@ mod bsp_interior; mod cellular_automata; mod common; mod drunkard; +mod maze; mod simple_map; use crate::{Map, Position}; @@ -11,6 +12,7 @@ use bsp_interior::BspInteriorBuilder; use cellular_automata::CellularAutomataBuilder; use common::*; use drunkard::DrunkardsWalkBuilder; +use maze::MazeBuilder; use simple_map::SimpleMapBuilder; use specs::prelude::*; @@ -25,13 +27,14 @@ pub trait MapBuilder { pub fn random_builder(new_depth: i32) -> Box { let mut rng = rltk::RandomNumberGenerator::new(); - match rng.roll_dice(1, 7) { + match rng.roll_dice(1, 8) { 1 => Box::new(BspDungeonBuilder::new(new_depth)), 2 => Box::new(BspInteriorBuilder::new(new_depth)), 3 => Box::new(CellularAutomataBuilder::new(new_depth)), 4 => Box::new(DrunkardsWalkBuilder::open_area(new_depth)), 5 => Box::new(DrunkardsWalkBuilder::open_halls(new_depth)), 6 => Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)), + 7 => Box::new(MazeBuilder::new(new_depth)), _ => Box::new(SimpleMapBuilder::new(new_depth)), } }