mirror of
https://github.com/kristoferssolo/hexlab.git
synced 2025-10-21 19:40:34 +00:00
test(generator): 89% coverage
This commit is contained in:
parent
7cacf92014
commit
6660b4613d
@ -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);
|
||||
|
||||
|
||||
@ -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(¤t, direction);
|
||||
maze.remove_tile_wall(&neighbor, direction.const_neg());
|
||||
recursive_backtrack(maze, neighbor, visited, rng);
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/generator/backtrack.rs
Normal file
111
src/generator/backtrack.rs
Normal 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(¤t, 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(¤t) {
|
||||
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
24
src/generator/mod.rs
Normal 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
66
tests/generator.rs
Normal 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(¤t) {
|
||||
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"
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user