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::{
|
use crate::{GeneratorType, HexMaze};
|
||||||
generator::{generate_backtracking, GeneratorType},
|
|
||||||
HexMaze,
|
|
||||||
};
|
|
||||||
use hexx::Hex;
|
use hexx::Hex;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
@ -186,21 +183,14 @@ impl MazeBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !maze.is_empty() {
|
if !maze.is_empty() {
|
||||||
self.generate_maze(&mut maze);
|
self.generator_type
|
||||||
|
.generate(&mut maze, self.start_position, self.seed);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(maze)
|
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 mut maze = HexMaze::new();
|
||||||
let radius = i32::from(radius);
|
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