test(generator): 89% coverage

This commit is contained in:
Kristofers Solo 2024-12-25 20:51:45 +02:00
parent 7cacf92014
commit 6660b4613d
5 changed files with 205 additions and 69 deletions

View File

@ -1,7 +1,4 @@
use crate::{
generator::{generate_backtracking, GeneratorType},
HexMaze,
};
use crate::{GeneratorType, HexMaze};
use hexx::Hex;
use thiserror::Error;
@ -186,21 +183,14 @@ impl MazeBuilder {
}
if !maze.is_empty() {
self.generate_maze(&mut maze);
self.generator_type
.generate(&mut maze, self.start_position, self.seed);
}
Ok(maze)
}
fn generate_maze(&self, maze: &mut HexMaze) {
match self.generator_type {
GeneratorType::RecursiveBacktracking => {
generate_backtracking(maze, self.start_position, self.seed);
}
}
}
}
fn create_hex_maze(radius: u16) -> HexMaze {
pub(crate) fn create_hex_maze(radius: u16) -> HexMaze {
let mut maze = HexMaze::new();
let radius = i32::from(radius);

View File

@ -1,55 +0,0 @@
#[cfg(feature = "bevy_reflect")]
use bevy::prelude::*;
use hexx::{EdgeDirection, Hex};
use rand::{rngs::StdRng, seq::SliceRandom, thread_rng, Rng, RngCore, SeedableRng};
use std::collections::HashSet;
use crate::HexMaze;
#[allow(clippy::module_name_repetitions)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy", derive(Component))]
#[cfg_attr(feature = "bevy", reflect(Component))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum GeneratorType {
#[default]
RecursiveBacktracking,
}
pub fn generate_backtracking(maze: &mut HexMaze, start_pos: Option<Hex>, seed: Option<u64>) {
if maze.is_empty() {
return;
}
let start = start_pos.unwrap_or(Hex::ZERO);
let mut visited = HashSet::new();
let mut rng: Box<dyn RngCore> = seed.map_or_else(
|| Box::new(thread_rng()) as Box<dyn RngCore>,
|seed| Box::new(StdRng::seed_from_u64(seed)) as Box<dyn RngCore>,
);
recursive_backtrack(maze, start, &mut visited, &mut rng);
}
fn recursive_backtrack<R: Rng>(
maze: &mut HexMaze,
current: Hex,
visited: &mut HashSet<Hex>,
rng: &mut R,
) {
visited.insert(current);
let mut directions = EdgeDirection::ALL_DIRECTIONS;
directions.shuffle(rng);
for direction in directions {
let neighbor = current + direction;
if maze.get_tile(&neighbor).is_some() && !visited.contains(&neighbor) {
maze.remove_tile_wall(&current, direction);
maze.remove_tile_wall(&neighbor, direction.const_neg());
recursive_backtrack(maze, neighbor, visited, rng);
}
}
}

111
src/generator/backtrack.rs Normal file
View File

@ -0,0 +1,111 @@
use crate::HexMaze;
use hexx::{EdgeDirection, Hex};
use rand::{rngs::StdRng, seq::SliceRandom, thread_rng, Rng, RngCore, SeedableRng};
use std::collections::HashSet;
pub(super) fn generate_backtracking(maze: &mut HexMaze, start_pos: Option<Hex>, seed: Option<u64>) {
if maze.is_empty() {
return;
}
let start = start_pos.unwrap_or(Hex::ZERO);
let mut visited = HashSet::new();
let mut rng: Box<dyn RngCore> = seed.map_or_else(
|| Box::new(thread_rng()) as Box<dyn RngCore>,
|seed| Box::new(StdRng::seed_from_u64(seed)) as Box<dyn RngCore>,
);
recursive_backtrack(maze, start, &mut visited, &mut rng);
}
fn recursive_backtrack<R: Rng>(
maze: &mut HexMaze,
current: Hex,
visited: &mut HashSet<Hex>,
rng: &mut R,
) {
visited.insert(current);
let mut directions = EdgeDirection::ALL_DIRECTIONS;
directions.shuffle(rng);
for direction in directions {
let neighbor = current + direction;
if maze.get_tile(&neighbor).is_some() && !visited.contains(&neighbor) {
maze.remove_tile_wall(&current, direction);
maze.remove_tile_wall(&neighbor, direction.const_neg());
recursive_backtrack(maze, neighbor, visited, rng);
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::builder::create_hex_maze;
use rstest::rstest;
#[rstest]
#[case(Hex::ZERO)]
#[case(Hex::new(1, -1))]
#[case(Hex::new(-2, 2))]
fn recursive_backtrack_start_visited(#[case] start: Hex) {
let mut maze = create_hex_maze(3);
let mut rng = StdRng::seed_from_u64(12345);
let mut visited = HashSet::new();
recursive_backtrack(&mut maze, start, &mut visited, &mut rng);
assert!(visited.contains(&start), "Start position should be visited");
}
#[rstest]
#[case(Hex::ZERO)]
#[case(Hex::new(1, -1))]
#[case(Hex::new(-2, 2))]
fn recursive_backtrack_walls_removed(#[case] start: Hex) {
let mut maze = create_hex_maze(3);
let mut rng = StdRng::seed_from_u64(12345);
let mut visited = HashSet::new();
recursive_backtrack(&mut maze, start, &mut visited, &mut rng);
for &pos in maze.keys() {
let walls = maze.get_walls(&pos).unwrap();
assert!(
walls.count() < 6,
"At least one wall should be removed for each tile"
);
}
}
#[rstest]
#[case(Hex::ZERO)]
#[case(Hex::new(1, -1))]
#[case(Hex::new(-2, 2))]
fn recursive_backtrack_connectivity(#[case] start: Hex) {
let mut maze = create_hex_maze(3);
let mut rng = StdRng::seed_from_u64(12345);
let mut visited = HashSet::new();
recursive_backtrack(&mut maze, start, &mut visited, &mut rng);
let mut to_visit = vec![start];
let mut connected = HashSet::new();
while let Some(current) = to_visit.pop() {
if !connected.insert(current) {
continue;
}
for dir in EdgeDirection::ALL_DIRECTIONS {
let neighbor = current + dir;
if let Some(walls) = maze.get_walls(&current) {
if !walls.contains(dir) && maze.get_tile(&neighbor).is_some() {
to_visit.push(neighbor);
}
}
}
}
assert_eq!(connected.len(), maze.len(), "All tiles should be connected");
}
}

24
src/generator/mod.rs Normal file
View File

@ -0,0 +1,24 @@
mod backtrack;
use crate::HexMaze;
use backtrack::generate_backtracking;
#[cfg(feature = "bevy_reflect")]
use bevy::prelude::*;
use hexx::Hex;
#[allow(clippy::module_name_repetitions)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy", derive(Component))]
#[cfg_attr(feature = "bevy", reflect(Component))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum GeneratorType {
#[default]
RecursiveBacktracking,
}
impl GeneratorType {
pub fn generate(&self, maze: &mut HexMaze, start_pos: Option<Hex>, seed: Option<u64>) {
match self {
Self::RecursiveBacktracking => generate_backtracking(maze, start_pos, seed),
}
}
}

66
tests/generator.rs Normal file
View File

@ -0,0 +1,66 @@
use hexlab::prelude::*;
use rstest::rstest;
#[rstest]
#[case(GeneratorType::RecursiveBacktracking, None, None)]
#[case(GeneratorType::RecursiveBacktracking, Some(Hex::new(1, -1)), None)]
#[case(GeneratorType::RecursiveBacktracking, None, Some(12345))]
fn generator_type(
#[case] generator: GeneratorType,
#[case] start_pos: Option<Hex>,
#[case] seed: Option<u64>,
) {
let mut maze = HexMaze::new();
for q in -3..=3 {
for r in -3..=3 {
let hex = Hex::new(q, r);
if hex.length() <= 3 {
maze.add_tile(hex);
}
}
}
let initial_size = maze.len();
generator.generate(&mut maze, start_pos, seed);
assert_eq!(maze.len(), initial_size, "Maze size should not change");
// Check maze connectivity
let start = start_pos.unwrap_or(Hex::ZERO);
let mut to_visit = vec![start];
let mut visited = std::collections::HashSet::new();
while let Some(current) = to_visit.pop() {
if !visited.insert(current) {
continue;
}
for dir in EdgeDirection::ALL_DIRECTIONS {
let neighbor = current + dir;
if let Some(walls) = maze.get_walls(&current) {
if !walls.contains(dir) && maze.get_tile(&neighbor).is_some() {
to_visit.push(neighbor);
}
}
}
}
assert_eq!(visited.len(), maze.len(), "All tiles should be connected");
// Check that each tile has at least one open wall
for &pos in maze.keys() {
let walls = maze.get_walls(&pos).unwrap();
assert!(
walls.count() < 6,
"Tile at {:?} should have at least one open wall",
pos
);
}
}
#[test]
fn test_empty_maze() {
let mut maze = HexMaze::new();
GeneratorType::RecursiveBacktracking.generate(&mut maze, None, None);
assert!(
maze.is_empty(),
"Empty maze should remain empty after generation"
);
}