use crate::dungeon::{DungeonGrid, DungeonType, DynDungeonGrid, TileType};
use crate::grid_coord_2d::{GetCoordinateBounds2D, GridCoord2D};
use rand::prelude::IndexedRandom;
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DungeonGenerationStep {
PlaceFloor { coord: GridCoord2D },
StampRoom {
center: GridCoord2D,
half_width: usize,
half_height: usize,
},
PlaceWall { coord: GridCoord2D },
SetExit { coord: GridCoord2D },
Complete,
}
pub trait DungeonGenerationVisitor {
fn on_step(&mut self, step: &DungeonGenerationStep);
}
#[derive(Default)]
pub struct VecDungeonGenerationVisitor {
steps: Vec<DungeonGenerationStep>,
}
impl VecDungeonGenerationVisitor {
pub fn into_steps(self) -> Vec<DungeonGenerationStep> {
self.steps
}
}
impl DungeonGenerationVisitor for VecDungeonGenerationVisitor {
fn on_step(&mut self, step: &DungeonGenerationStep) {
self.steps.push(step.clone());
}
}
struct NoOpVisitor;
impl DungeonGenerationVisitor for NoOpVisitor {
#[inline]
fn on_step(&mut self, _step: &DungeonGenerationStep) {
}
}
pub struct DungeonGenerationSteps {
inner: std::vec::IntoIter<DungeonGenerationStep>,
}
impl DungeonGenerationSteps {
pub fn new(steps: Vec<DungeonGenerationStep>) -> Self {
Self {
inner: steps.into_iter(),
}
}
}
impl Iterator for DungeonGenerationSteps {
type Item = DungeonGenerationStep;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
}
pub trait DungeonGenerator {
fn new_random() -> Self
where
Self: Sized;
fn new_from_seed(seed: u64) -> Self
where
Self: Sized;
fn generate(&self, width: usize, height: usize, floor_count: usize) -> DungeonGrid;
fn generate_steps(
&self,
width: usize,
height: usize,
floor_count: usize,
) -> DungeonGenerationSteps {
let _ = self.generate(width, height, floor_count);
DungeonGenerationSteps::new(vec![DungeonGenerationStep::Complete])
}
fn dungeon_type(&self) -> DungeonType;
fn name(&self) -> &'static str {
self.dungeon_type().name()
}
fn description(&self) -> &'static str {
self.dungeon_type().description()
}
}
pub struct DungeonWalkGenerator {
rng_seed: u64,
dungeon_type: DungeonType,
winding_hall_probability: u8,
long_walk_min: usize,
long_walk_max: usize,
dynamic_resize: bool,
initial_grid_size: usize,
trim_padding: usize,
}
struct WalkContext<'a, V> {
rng: &'a mut StdRng,
grid: &'a mut DungeonGrid,
visitor: &'a mut V,
width: usize,
height: usize,
last_floor: GridCoord2D,
}
struct DynWalkContext<'a, V> {
rng: &'a mut StdRng,
dyn_grid: &'a mut DynDungeonGrid,
visitor: &'a mut V,
last_floor_world: (isize, isize),
initial_size: usize,
}
impl DungeonWalkGenerator {
pub fn new_random(dungeon_type: DungeonType) -> Self {
Self {
rng_seed: rand::random(),
dungeon_type,
winding_hall_probability: 50, long_walk_min: 9, long_walk_max: 18, dynamic_resize: false,
initial_grid_size: 32,
trim_padding: 0,
}
}
pub fn new_from_seed(dungeon_type: DungeonType, seed: u64) -> Self {
let rng_seed = if seed == 0 { rand::random() } else { seed };
Self {
rng_seed,
dungeon_type,
winding_hall_probability: 50,
long_walk_min: 9,
long_walk_max: 18,
dynamic_resize: false,
initial_grid_size: 32,
trim_padding: 0,
}
}
pub fn with_winding_probability(mut self, probability: u8) -> Self {
self.winding_hall_probability = probability.min(99);
self
}
pub fn with_long_walk_range(mut self, min: usize, max_exclusive: usize) -> Self {
let min = min.max(1); let max_exclusive = max_exclusive.max(min + 1); self.long_walk_min = min;
self.long_walk_max = max_exclusive;
self
}
pub fn with_dynamic_resize(mut self, enabled: bool) -> Self {
self.dynamic_resize = enabled;
self
}
pub fn with_initial_grid_size(mut self, size: usize) -> Self {
self.initial_grid_size = size.max(8); self
}
pub fn with_trim_padding(mut self, padding: usize) -> Self {
self.trim_padding = padding;
self
}
pub fn generate(&self, width: usize, height: usize, floor_count: usize) -> DungeonGrid {
self.generate_internal(width, height, floor_count, &mut NoOpVisitor, false)
}
pub fn generate_steps(
&self,
width: usize,
height: usize,
floor_count: usize,
) -> DungeonGenerationSteps {
let mut visitor = VecDungeonGenerationVisitor::default();
let _ = self.generate_internal(width, height, floor_count, &mut visitor, true);
DungeonGenerationSteps::new(visitor.into_steps())
}
fn generate_internal<V: DungeonGenerationVisitor>(
&self,
width: usize,
height: usize,
floor_count: usize,
visitor: &mut V,
emit_wall_steps: bool,
) -> DungeonGrid {
let mut rng = StdRng::seed_from_u64(self.rng_seed);
if width == 0 || height == 0 || floor_count == 0 {
let grid = DungeonGrid::new(width, height);
visitor.on_step(&DungeonGenerationStep::Complete);
return grid;
}
if self.dynamic_resize {
self.generate_dynamic(floor_count, visitor, emit_wall_steps, &mut rng)
} else {
self.generate_fixed(
width,
height,
floor_count,
visitor,
emit_wall_steps,
&mut rng,
)
}
}
fn generate_fixed<V: DungeonGenerationVisitor>(
&self,
width: usize,
height: usize,
floor_count: usize,
visitor: &mut V,
emit_wall_steps: bool,
rng: &mut StdRng,
) -> DungeonGrid {
let mut grid = DungeonGrid::new(width, height);
let max_possible_floor = width.saturating_mul(height).saturating_mul(9) / 10; let target_floor_count = floor_count.min(max_possible_floor);
let mut walker_pos = GridCoord2D::new(width / 2, height / 2);
let mut last_floor_pos = walker_pos;
grid.set(walker_pos, TileType::Floor);
visitor.on_step(&DungeonGenerationStep::PlaceFloor { coord: walker_pos });
let mut iterations_since_progress = 0;
let max_stalled_iterations = target_floor_count
.max(width.saturating_mul(height) / 10)
.max(1000);
while grid.floor_count() < target_floor_count {
let floor_count_before = grid.floor_count();
iterations_since_progress += 1;
if iterations_since_progress > max_stalled_iterations {
break;
}
match self.dungeon_type {
DungeonType::Caverns => {
walker_pos = self.take_step(rng, walker_pos, width, height);
if !grid.is_floor(walker_pos) {
grid.set(walker_pos, TileType::Floor);
visitor.on_step(&DungeonGenerationStep::PlaceFloor { coord: walker_pos });
last_floor_pos = walker_pos;
}
}
DungeonType::Rooms | DungeonType::Winding => {
let mut ctx = WalkContext {
rng,
grid: &mut grid,
visitor,
width,
height,
last_floor: last_floor_pos,
};
walker_pos = self.take_long_walk(&mut ctx, walker_pos, target_floor_count);
let should_stamp_room = if self.dungeon_type == DungeonType::Rooms {
true
} else {
let roll = ctx.rng.random_range(0..100);
roll > self.winding_hall_probability
};
if should_stamp_room && ctx.grid.floor_count() < target_floor_count {
self.stamp_room(&mut ctx, walker_pos, target_floor_count);
}
last_floor_pos = ctx.last_floor;
}
}
if grid.floor_count() > floor_count_before {
iterations_since_progress = 0;
}
}
grid.set_exit(last_floor_pos);
grid = grid.trim(self.trim_padding);
if emit_wall_steps {
for y in 0..grid.height() {
for x in 0..grid.width() {
let coord = GridCoord2D::new(x, y);
if grid.get(coord).unwrap().is_wall() {
visitor.on_step(&DungeonGenerationStep::PlaceWall { coord });
}
}
}
}
let trimmed_exit = grid.exit().unwrap_or(last_floor_pos);
visitor.on_step(&DungeonGenerationStep::SetExit {
coord: trimmed_exit,
});
visitor.on_step(&DungeonGenerationStep::Complete);
grid
}
fn generate_dynamic<V: DungeonGenerationVisitor>(
&self,
floor_count: usize,
visitor: &mut V,
emit_wall_steps: bool,
rng: &mut StdRng,
) -> DungeonGrid {
let initial_size = self.initial_grid_size;
let mut dyn_grid = DynDungeonGrid::new(initial_size, initial_size);
let max_possible_floor = initial_size.saturating_mul(initial_size).saturating_mul(9) / 10;
let target_floor_count = floor_count.min(max_possible_floor.max(10000));
let mut walker_world = (0isize, 0isize);
let mut last_floor_world = (0isize, 0isize);
dyn_grid.ensure_bounds(0, 0, 0, 0);
dyn_grid.set_world(0, 0, TileType::Floor);
dyn_grid.update_floor_bounds(0, 0);
visitor.on_step(&DungeonGenerationStep::PlaceFloor {
coord: GridCoord2D::new(initial_size / 2, initial_size / 2),
});
let mut iterations_since_progress = 0;
let max_stalled_iterations = target_floor_count.max(5000);
while dyn_grid.inner().floor_count() < target_floor_count {
let floor_count_before = dyn_grid.inner().floor_count();
iterations_since_progress += 1;
if iterations_since_progress > max_stalled_iterations {
break;
}
match self.dungeon_type {
DungeonType::Caverns => {
walker_world = self.take_step_world(rng, walker_world);
dyn_grid.ensure_bounds(
walker_world.0,
walker_world.1,
walker_world.0,
walker_world.1,
);
if !dyn_grid.is_floor_world(walker_world.0, walker_world.1) {
dyn_grid.set_world(walker_world.0, walker_world.1, TileType::Floor);
dyn_grid.update_floor_bounds(walker_world.0, walker_world.1);
visitor.on_step(&DungeonGenerationStep::PlaceFloor {
coord: GridCoord2D::new(initial_size / 2, initial_size / 2),
});
last_floor_world = walker_world;
}
}
DungeonType::Rooms | DungeonType::Winding => {
let mut ctx = DynWalkContext {
rng,
dyn_grid: &mut dyn_grid,
visitor,
last_floor_world,
initial_size,
};
walker_world =
self.take_long_walk_world(&mut ctx, walker_world, target_floor_count);
let should_stamp_room = if self.dungeon_type == DungeonType::Rooms {
true
} else {
let roll = ctx.rng.random_range(0..100);
roll > self.winding_hall_probability
};
if should_stamp_room && ctx.dyn_grid.inner().floor_count() < target_floor_count
{
self.stamp_room_world(&mut ctx, walker_world, target_floor_count);
}
last_floor_world = ctx.last_floor_world;
}
}
if dyn_grid.inner().floor_count() > floor_count_before {
iterations_since_progress = 0;
}
}
dyn_grid.set_exit_world(last_floor_world.0, last_floor_world.1);
visitor.on_step(&DungeonGenerationStep::SetExit {
coord: GridCoord2D::new(initial_size / 2, initial_size / 2),
});
let final_grid = dyn_grid.finalize(self.trim_padding);
if emit_wall_steps {
for y in 0..final_grid.height() {
for x in 0..final_grid.width() {
let coord = GridCoord2D::new(x, y);
if final_grid.get(coord).unwrap().is_wall() {
visitor.on_step(&DungeonGenerationStep::PlaceWall { coord });
}
}
}
}
visitor.on_step(&DungeonGenerationStep::Complete);
final_grid
}
fn take_step_world(&self, rng: &mut StdRng, pos: (isize, isize)) -> (isize, isize) {
let directions: [(isize, isize); 4] = [
(0, -1), (1, 0), (0, 1), (-1, 0), ];
let &(dx, dy) = directions.choose(rng).unwrap();
(pos.0 + dx, pos.1 + dy)
}
fn take_step(
&self,
rng: &mut StdRng,
pos: GridCoord2D,
width: usize,
height: usize,
) -> GridCoord2D {
let directions: [(isize, isize); 4] = [
(0, -1), (1, 0), (0, 1), (-1, 0), ];
let &(dx, dy) = directions.choose(rng).unwrap();
let new_x = (pos.x as isize + dx).max(0).min(width as isize - 1) as usize;
let new_y = (pos.y as isize + dy).max(0).min(height as isize - 1) as usize;
GridCoord2D::new(new_x, new_y)
}
fn take_long_walk<V: DungeonGenerationVisitor>(
&self,
ctx: &mut WalkContext<V>,
start: GridCoord2D,
target_floor_count: usize,
) -> GridCoord2D {
let walk_length = ctx.rng.random_range(self.long_walk_min..self.long_walk_max);
let directions: [(isize, isize); 4] = [
(0, -1), (1, 0), (0, 1), (-1, 0), ];
let &(dx, dy) = directions.choose(ctx.rng).unwrap();
let mut pos = start;
for _ in 0..walk_length {
if ctx.grid.floor_count() >= target_floor_count {
break;
}
let new_x = (pos.x as isize + dx).max(0).min(ctx.width as isize - 1) as usize;
let new_y = (pos.y as isize + dy).max(0).min(ctx.height as isize - 1) as usize;
pos = GridCoord2D::new(new_x, new_y);
if !ctx.grid.is_floor(pos) {
ctx.grid.set(pos, TileType::Floor);
ctx.visitor
.on_step(&DungeonGenerationStep::PlaceFloor { coord: pos });
ctx.last_floor = pos;
}
}
pos
}
fn stamp_room<V: DungeonGenerationVisitor>(
&self,
ctx: &mut WalkContext<V>,
center: GridCoord2D,
target_floor_count: usize,
) {
let half_width = ctx.rng.random_range(1..5); let half_height = ctx.rng.random_range(1..5);
ctx.visitor.on_step(&DungeonGenerationStep::StampRoom {
center,
half_width,
half_height,
});
let min_x = center.x.saturating_sub(half_width);
let max_x = (center.x + half_width).min(ctx.grid.width() - 1);
let min_y = center.y.saturating_sub(half_height);
let max_y = (center.y + half_height).min(ctx.grid.height() - 1);
for y in min_y..=max_y {
for x in min_x..=max_x {
if ctx.grid.floor_count() >= target_floor_count {
return;
}
let coord = GridCoord2D::new(x, y);
if !ctx.grid.is_floor(coord) {
ctx.grid.set(coord, TileType::Floor);
ctx.visitor
.on_step(&DungeonGenerationStep::PlaceFloor { coord });
ctx.last_floor = coord;
}
}
}
}
fn take_long_walk_world<V: DungeonGenerationVisitor>(
&self,
ctx: &mut DynWalkContext<V>,
start: (isize, isize),
target_floor_count: usize,
) -> (isize, isize) {
let walk_length = ctx.rng.random_range(self.long_walk_min..self.long_walk_max);
let directions: [(isize, isize); 4] = [
(0, -1), (1, 0), (0, 1), (-1, 0), ];
let &(dx, dy) = directions.choose(ctx.rng).unwrap();
let mut pos = start;
for _ in 0..walk_length {
if ctx.dyn_grid.inner().floor_count() >= target_floor_count {
break;
}
pos = (pos.0 + dx, pos.1 + dy);
ctx.dyn_grid.ensure_bounds(pos.0, pos.1, pos.0, pos.1);
if !ctx.dyn_grid.is_floor_world(pos.0, pos.1) {
ctx.dyn_grid.set_world(pos.0, pos.1, TileType::Floor);
ctx.dyn_grid.update_floor_bounds(pos.0, pos.1);
ctx.visitor.on_step(&DungeonGenerationStep::PlaceFloor {
coord: GridCoord2D::new(ctx.initial_size / 2, ctx.initial_size / 2),
});
ctx.last_floor_world = pos;
}
}
pos
}
fn stamp_room_world<V: DungeonGenerationVisitor>(
&self,
ctx: &mut DynWalkContext<V>,
center: (isize, isize),
target_floor_count: usize,
) {
let half_width = ctx.rng.random_range(1..5);
let half_height = ctx.rng.random_range(1..5);
ctx.visitor.on_step(&DungeonGenerationStep::StampRoom {
center: GridCoord2D::new(ctx.initial_size / 2, ctx.initial_size / 2),
half_width,
half_height,
});
let min_x = center.0 - half_width as isize;
let max_x = center.0 + half_width as isize;
let min_y = center.1 - half_height as isize;
let max_y = center.1 + half_height as isize;
ctx.dyn_grid.ensure_bounds(min_x, min_y, max_x, max_y);
for y in min_y..=max_y {
for x in min_x..=max_x {
if ctx.dyn_grid.inner().floor_count() >= target_floor_count {
return;
}
if !ctx.dyn_grid.is_floor_world(x, y) {
ctx.dyn_grid.set_world(x, y, TileType::Floor);
ctx.dyn_grid.update_floor_bounds(x, y);
ctx.visitor.on_step(&DungeonGenerationStep::PlaceFloor {
coord: GridCoord2D::new(ctx.initial_size / 2, ctx.initial_size / 2),
});
ctx.last_floor_world = (x, y);
}
}
}
}
}
impl DungeonGenerator for DungeonWalkGenerator {
fn new_random() -> Self {
Self::new_random(DungeonType::Caverns)
}
fn new_from_seed(seed: u64) -> Self {
Self::new_from_seed(DungeonType::Caverns, seed)
}
fn generate(&self, width: usize, height: usize, floor_count: usize) -> DungeonGrid {
DungeonWalkGenerator::generate(self, width, height, floor_count)
}
fn generate_steps(
&self,
width: usize,
height: usize,
floor_count: usize,
) -> DungeonGenerationSteps {
DungeonWalkGenerator::generate_steps(self, width, height, floor_count)
}
fn dungeon_type(&self) -> DungeonType {
self.dungeon_type
}
}
#[cfg(test)]
mod generator_tests {
use super::*;
#[test]
fn test_generator_dynamic_produces_tight_grid() {
let generator = DungeonWalkGenerator::new_random(DungeonType::Caverns)
.with_dynamic_resize(true)
.with_initial_grid_size(32)
.with_trim_padding(0);
let grid = generator.generate(40, 30, 500);
assert!(grid.width() <= 100);
assert!(grid.height() <= 100);
assert!(grid.floor_count() > 0);
}
#[test]
fn test_generator_dynamic_with_padding() {
let generator = DungeonWalkGenerator::new_random(DungeonType::Caverns)
.with_dynamic_resize(true)
.with_initial_grid_size(32)
.with_trim_padding(2);
let grid = generator.generate(40, 30, 100);
assert!(grid.width() > 0);
assert!(grid.height() > 0);
assert!(grid.floor_count() > 0);
}
#[test]
fn test_generator_fixed_mode_trims_output() {
let generator = DungeonWalkGenerator::new_from_seed(DungeonType::Caverns, 42)
.with_dynamic_resize(false)
.with_trim_padding(0);
let grid = generator.generate(40, 30, 200);
assert!(grid.width() <= 40);
assert!(grid.height() <= 30);
assert!(grid.floor_count() > 0);
assert!(grid.width() < 40 || grid.height() < 30);
}
#[test]
fn test_generator_fixed_mode_with_padding() {
let generator = DungeonWalkGenerator::new_random(DungeonType::Caverns)
.with_dynamic_resize(false)
.with_trim_padding(2);
let grid = generator.generate(40, 30, 200);
assert!(grid.width() > 0);
assert!(grid.height() > 0);
assert!(grid.floor_count() > 0);
}
#[test]
fn test_generator_dynamic_rooms_type() {
let generator = DungeonWalkGenerator::new_random(DungeonType::Rooms)
.with_dynamic_resize(true)
.with_initial_grid_size(32);
let grid = generator.generate(40, 30, 300);
assert!(grid.floor_count() > 0);
assert!(grid.width() > 0);
assert!(grid.height() > 0);
}
#[test]
fn test_generator_dynamic_winding_type() {
let generator = DungeonWalkGenerator::new_random(DungeonType::Winding)
.with_dynamic_resize(true)
.with_initial_grid_size(32)
.with_winding_probability(50);
let grid = generator.generate(40, 30, 300);
assert!(grid.floor_count() > 0);
assert!(grid.width() > 0);
assert!(grid.height() > 0);
}
}