Compare commits

...

7 Commits
v0.5.3 ... main

Author SHA1 Message Date
81991027bb
fix: clippy warnings
Some checks failed
CI / build-and-test (push) Has been cancelled
2025-09-25 11:40:42 +03:00
b874f05471 docs: fix method name 2025-01-20 21:11:55 +02:00
5585966da6 docs(pathfinding): add docstrings 2025-01-14 13:06:47 +02:00
cafcb3545f
Merge pull request #4 from kristoferssolo/feature/pathfinding 2025-01-14 12:43:27 +02:00
e0f180aeee test(pathfinding): add tests 2025-01-14 12:42:21 +02:00
4eab4d1198 feat(pathfinding): use pathfinding crate 2025-01-14 11:58:59 +02:00
6cd7550086 feat(pathfinding): add A* 2025-01-14 11:35:20 +02:00
16 changed files with 296 additions and 137 deletions

View File

@ -9,76 +9,30 @@ env:
RUSTFLAGS: --deny warnings RUSTFLAGS: --deny warnings
RUSTDOCFLAGS: --deny warnings RUSTDOCFLAGS: --deny warnings
jobs: jobs:
# Run tests. build-and-test:
test:
name: Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 env:
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps: steps:
- name: Checkout repository - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Install Rust toolchain - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Install dependencies
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev
- name: Populate target directory from cache
uses: Leafwing-Studios/cargo-cache@v2
with: with:
sweep-cache: true toolchain: stable
- name: Run tests components: clippy, rustfmt
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.9
- name: Install cargo-nextest
uses: taiki-e/install-action@cargo-nextest
- name: Run Clippy
run: cargo clippy --locked --workspace --all-targets --all-features -- -D warnings
- name: Run formatting
run: cargo fmt --all --check
- name: Run Tests
run: | run: |
cargo test --locked --workspace --all-features --all-targets cargo nextest run --all-features --all-targets
# Workaround for https://github.com/rust-lang/cargo/issues/6669
cargo test --locked --workspace --all-features --doc cargo test --locked --workspace --all-features --doc
# Run clippy lints. - name: Check Documentation
clippy:
name: Clippy
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Install dependencies
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev
- name: Populate target directory from cache
uses: Leafwing-Studios/cargo-cache@v2
with:
sweep-cache: true
- name: Run clippy lints
run: cargo clippy --locked --workspace --all-features -- --deny warnings
# Check formatting.
format:
name: Format
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: Run cargo fmt
run: cargo fmt --all -- --check
# Check documentation.
doc:
name: Docs
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install dependencies
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev
- name: Populate target directory from cache
uses: Leafwing-Studios/cargo-cache@v2
with:
sweep-cache: true
- name: Check documentation
run: cargo doc --locked --workspace --all-features --document-private-items --no-deps run: cargo doc --locked --workspace --all-features --document-private-items --no-deps

60
Cargo.lock generated
View File

@ -1248,7 +1248,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"regex", "regex",
"rustc-hash", "rustc-hash 1.1.0",
"shlex", "shlex",
"syn", "syn",
] ]
@ -1631,7 +1631,7 @@ dependencies = [
"log", "log",
"rangemap", "rangemap",
"rayon", "rayon",
"rustc-hash", "rustc-hash 1.1.0",
"rustybuzz", "rustybuzz",
"self_cell", "self_cell",
"swash", "swash",
@ -1743,6 +1743,18 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
[[package]]
name = "deprecate-until"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a3767f826efbbe5a5ae093920b58b43b01734202be697e1354914e862e8e704"
dependencies = [
"proc-macro2",
"quote",
"semver",
"syn",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "1.0.0" version = "1.0.0"
@ -2338,7 +2350,7 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]] [[package]]
name = "hexlab" name = "hexlab"
version = "0.5.3" version = "0.6.1"
dependencies = [ dependencies = [
"bevy", "bevy",
"bevy_reflect", "bevy_reflect",
@ -2346,6 +2358,7 @@ dependencies = [
"claims", "claims",
"glam", "glam",
"hexx", "hexx",
"pathfinding",
"rand", "rand",
"rstest", "rstest",
"serde", "serde",
@ -2354,9 +2367,9 @@ dependencies = [
[[package]] [[package]]
name = "hexx" name = "hexx"
version = "0.19.0" version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34b47b6f7ee46bba869534a92306b7e7f549bec38114a4ba0288b9321617db22" checksum = "4b450e02a24a4a981c895be4cd2752e2401996c545971309730c4e812b984691"
dependencies = [ dependencies = [
"bevy_reflect", "bevy_reflect",
"glam", "glam",
@ -2420,6 +2433,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "integer-sqrt"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "io-kit-sys" name = "io-kit-sys"
version = "0.4.1" version = "0.4.1"
@ -2687,7 +2709,7 @@ dependencies = [
"indexmap", "indexmap",
"log", "log",
"pp-rs", "pp-rs",
"rustc-hash", "rustc-hash 1.1.0",
"spirv", "spirv",
"termcolor", "termcolor",
"thiserror 1.0.69", "thiserror 1.0.69",
@ -2708,7 +2730,7 @@ dependencies = [
"once_cell", "once_cell",
"regex", "regex",
"regex-syntax 0.8.5", "regex-syntax 0.8.5",
"rustc-hash", "rustc-hash 1.1.0",
"thiserror 1.0.69", "thiserror 1.0.69",
"tracing", "tracing",
"unicode-ident", "unicode-ident",
@ -3166,6 +3188,20 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathfinding"
version = "4.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301ad6aa19104eeb9af172b3d6a4ab8a5ea26234890baf2fcb1cbbc3f05f674b"
dependencies = [
"deprecate-until",
"indexmap",
"integer-sqrt",
"num-traits",
"rustc-hash 2.1.0",
"thiserror 2.0.3",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@ -3567,6 +3603,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@ -4283,7 +4325,7 @@ dependencies = [
"parking_lot", "parking_lot",
"profiling", "profiling",
"raw-window-handle", "raw-window-handle",
"rustc-hash", "rustc-hash 1.1.0",
"smallvec", "smallvec",
"thiserror 1.0.69", "thiserror 1.0.69",
"wgpu-hal", "wgpu-hal",
@ -4325,7 +4367,7 @@ dependencies = [
"range-alloc", "range-alloc",
"raw-window-handle", "raw-window-handle",
"renderdoc-sys", "renderdoc-sys",
"rustc-hash", "rustc-hash 1.1.0",
"smallvec", "smallvec",
"thiserror 1.0.69", "thiserror 1.0.69",
"wasm-bindgen", "wasm-bindgen",

View File

@ -1,7 +1,7 @@
[package] [package]
name = "hexlab" name = "hexlab"
authors = ["Kristofers Solo <dev@kristofers.xyz>"] authors = ["Kristofers Solo <dev@kristofers.xyz>"]
version = "0.5.3" version = "0.6.1"
edition = "2021" edition = "2021"
description = "A hexagonal maze generation and manipulation library" description = "A hexagonal maze generation and manipulation library"
repository = "https://github.com/kristoferssolo/hexlab" repository = "https://github.com/kristoferssolo/hexlab"
@ -26,6 +26,7 @@ thiserror = "2.0"
bevy = { version = "0.15", optional = true } bevy = { version = "0.15", optional = true }
bevy_utils = { version = "0.15", optional = true } bevy_utils = { version = "0.15", optional = true }
glam = { version = "0.29", optional = true } glam = { version = "0.29", optional = true }
pathfinding = { version = "4.13", optional = true }
[dependencies.bevy_reflect] [dependencies.bevy_reflect]
@ -48,7 +49,8 @@ bevy_reflect = [
"hexx/bevy_reflect", "hexx/bevy_reflect",
"dep:glam", "dep:glam",
] ]
full = ["serde", "bevy"] pathfinding = ["dep:pathfinding"]
full = ["serde", "bevy", "pathfinding"]
[profile.dev] [profile.dev]
opt-level = 1 # Better compile times with some optimization opt-level = 1 # Better compile times with some optimization

View File

@ -42,7 +42,7 @@ fn main() {
.with_radius(5) .with_radius(5)
.build() .build()
.expect("Failed to create maze"); .expect("Failed to create maze");
println!("Maze size: {}", maze.len()); println!("Maze size: {}", maze.count());
} }
``` ```

View File

@ -20,7 +20,7 @@ use hexx::Hex;
/// ///
/// // A radius of 5 creates 61 hexagonal tiles /// // A radius of 5 creates 61 hexagonal tiles
/// assert!(!maze.is_empty()); /// assert!(!maze.is_empty());
/// assert_eq!(maze.len(), 91); /// assert_eq!(maze.count(), 91);
/// ``` /// ```
/// ///
/// Using a seed for reproducible results: /// Using a seed for reproducible results:
@ -40,7 +40,7 @@ use hexx::Hex;
/// .expect("Failed to create maze"); /// .expect("Failed to create maze");
/// ///
/// // Same seed should produce identical mazes /// // Same seed should produce identical mazes
/// assert_eq!(maze1.len(), maze2.len()); /// assert_eq!(maze1.count(), maze2.count());
/// assert_eq!(maze1, maze2); /// assert_eq!(maze1, maze2);
/// ``` /// ```
/// ///
@ -190,7 +190,7 @@ pub fn create_hex_maze(radius: u16) -> Maze {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use claims::assert_gt; use claims::{assert_gt, assert_some};
use rstest::rstest; use rstest::rstest;
#[test] #[test]
@ -249,8 +249,8 @@ mod test {
Hex::new(0, -2), Hex::new(0, -2),
Hex::new(1, -2), Hex::new(1, -2),
]; ];
for pos in expected_positions.iter() { for pos in &expected_positions {
assert!(maze.get(pos).is_some(), "Expected tile at {:?}", pos); assert_some!(maze.get(pos), "Expected tile at {pos:?}");
} }
} }
} }

View File

@ -44,6 +44,7 @@ fn recursive_backtrack<R: Rng>(
mod test { mod test {
use super::*; use super::*;
use crate::builder::create_hex_maze; use crate::builder::create_hex_maze;
use claims::assert_some;
use rstest::rstest; use rstest::rstest;
#[rstest] #[rstest]
@ -72,7 +73,7 @@ mod test {
recursive_backtrack(&mut maze, start, &mut visited, &mut rng); recursive_backtrack(&mut maze, start, &mut visited, &mut rng);
for &pos in maze.keys() { for &pos in maze.keys() {
let walls = maze.get_walls(&pos).unwrap(); let walls = assert_some!(maze.get_walls(&pos));
assert!( assert!(
walls.count() < 6, walls.count() < 6,
"At least one wall should be removed for each tile" "At least one wall should be removed for each tile"

View File

@ -15,6 +15,7 @@ pub enum GeneratorType {
#[default] #[default]
RecursiveBacktracking, RecursiveBacktracking,
} }
impl GeneratorType { impl GeneratorType {
pub fn generate(&self, maze: &mut Maze, start_pos: Option<Hex>, seed: Option<u64>) { pub fn generate(&self, maze: &mut Maze, start_pos: Option<Hex>, seed: Option<u64>) {
match self { match self {

View File

@ -20,7 +20,7 @@
//! .build() //! .build()
//! .expect("Failed to create maze"); //! .expect("Failed to create maze");
//! //!
//! assert_eq!(maze.len(), 37); // A radius of 3 should create 37 tiles //! assert_eq!(maze.count(), 37); // A radius of 3 should create 37 tiles
//!``` //!```
//! //!
//! Customizing maze generation: //! Customizing maze generation:
@ -52,6 +52,8 @@ mod builder;
pub mod errors; pub mod errors;
mod generator; mod generator;
mod maze; mod maze;
#[cfg(feature = "pathfinding")]
mod pathfinding;
mod tile; mod tile;
pub mod traits; pub mod traits;
mod walls; mod walls;

View File

@ -34,7 +34,7 @@ impl Maze {
/// let maze = Maze::new(); /// let maze = Maze::new();
/// ///
/// assert!(maze.is_empty()); /// assert!(maze.is_empty());
/// assert_eq!(maze.len(), 0); /// assert_eq!(maze.count(), 0);
/// ``` /// ```
#[inline] #[inline]
#[must_use] #[must_use]

84
src/pathfinding.rs Normal file
View File

@ -0,0 +1,84 @@
//! Maze pathfinding implementation for hexagonal grids.
//!
//! This module provides functionality for finding paths through a hexagonal maze
//! using the A* pathfinding algorithm. The maze is represented as a collection of
//! hexagonal cells, where each cell may have walls on any of its six edges.
//!
//! # Examples
//!
//! ```
//! use hexlab::prelude::*;
//!
//! let maze = MazeBuilder::new()
//! .with_radius(3)
//! .with_seed(12345)
//! .build()
//! .expect("Failed to create maze");
//! assert!(maze.find_path(Hex::ZERO, Hex::new(-1, 3)).is_some());
//! ```
//!
//! # Implementation Details
//!
//! The pathfinding algorithm uses Manhattan distance as a heuristic and considers
//! walls between cells when determining valid paths. Each step between adjacent
//! cells has a cost of 1.
use hexx::{EdgeDirection, Hex};
use pathfinding::prelude::*;
use crate::Maze;
impl Maze {
#[must_use]
/// Finds the shortest path between two hexagonal positions in the maze using A* pathfinding.
///
/// This function calculates the optimal path while taking into account walls between cells.
/// The path cost between adjacent cells is always 1, and Manhattan distance is used as the
/// heuristic for pathfinding.
///
/// # Arguments
///
/// * `from` - The starting hexagonal position
/// * `to` - The target hexagonal position
///
/// # Returns
///
/// * `Some(Vec<Hex>)` - A vector of hexagonal positions representing the path from start to target
/// * `None` - If no valid path exists between the positions
///
/// # Examples
///
/// ```
/// use hexlab::prelude::*;
///
/// let maze = MazeBuilder::new()
/// .with_radius(3)
/// .with_seed(12345)
/// .build()
/// .expect("Failed to create maze");
/// assert!(maze.find_path(Hex::ZERO, Hex::new(-1, 3)).is_some());
/// ```
pub fn find_path(&self, from: Hex, to: Hex) -> Option<Vec<Hex>> {
let successors = |pos: &Hex| {
{
EdgeDirection::ALL_DIRECTIONS.iter().filter_map(|&dir| {
let neighbor = pos.neighbor(dir);
if let Some(current_tile) = self.get(pos) {
if self.get(&neighbor).is_some() && !current_tile.walls.contains(dir) {
return Some((neighbor, 1)); // Cost of 1 for each step
}
}
None
})
}
.collect::<Vec<_>>()
};
let heuristic = |pos: &Hex| {
// Manhatan distance
let diff = *pos - to;
(diff.x.abs() + diff.y.abs() + diff.z().abs()) / 2
};
astar(&from, successors, heuristic, |pos| *pos == to).map(|(path, _)| path)
}
}

View File

@ -236,7 +236,7 @@ mod test {
let layout = HexLayout::default(); let layout = HexLayout::default();
let tile = Tile::new(Hex::new(1, 0)); let tile = Tile::new(Hex::new(1, 0));
let vec2 = tile.to_vec2(&layout); let vec2 = tile.to_vec2(&layout);
assert_eq!(vec2, Vec2::new(1.5, -0.8660254)); assert_eq!(vec2, Vec2::new(1.5, -0.866_025_4));
} }
#[test] #[test]
@ -244,7 +244,7 @@ mod test {
let layout = HexLayout::default(); let layout = HexLayout::default();
let tile = Tile::new(Hex::new(0, 1)); let tile = Tile::new(Hex::new(0, 1));
let vec3 = tile.to_vec3(&layout); let vec3 = tile.to_vec3(&layout);
assert_eq!(vec3, Vec3::new(0.0, 0.0, -1.7320508)); assert_eq!(vec3, Vec3::new(0.0, 0.0, -1.732_050_8));
} }
} }
} }

View File

@ -375,7 +375,7 @@ mod test {
let walls = Walls::all_directions(); let walls = Walls::all_directions();
assert!(walls.is_enclosed()); assert!(walls.is_enclosed());
assert!(!walls.is_empty()); assert!(!walls.is_empty());
assert_eq!(walls.as_bits(), 0b111111); assert_eq!(walls.as_bits(), 0b11_1111);
} }
// as_bits // as_bits
@ -389,7 +389,7 @@ mod test {
fn as_bits_single_wall() { fn as_bits_single_wall() {
let mut walls = Walls::empty(); let mut walls = Walls::empty();
walls.insert(EdgeDirection::FLAT_NORTH); walls.insert(EdgeDirection::FLAT_NORTH);
assert_eq!(walls.as_bits(), 0b010000); assert_eq!(walls.as_bits(), 0b01_0000);
} }
#[test] #[test]
@ -397,13 +397,13 @@ mod test {
let mut walls = Walls::empty(); let mut walls = Walls::empty();
walls.insert(EdgeDirection::FLAT_NORTH); walls.insert(EdgeDirection::FLAT_NORTH);
walls.insert(EdgeDirection::FLAT_SOUTH); walls.insert(EdgeDirection::FLAT_SOUTH);
assert_eq!(walls.as_bits(), 0b010010); assert_eq!(walls.as_bits(), 0b01_0010);
} }
#[test] #[test]
fn as_bits_all_walls() { fn as_bits_all_walls() {
let walls = Walls::new(); let walls = Walls::new();
assert_eq!(walls.as_bits(), 0b111111); assert_eq!(walls.as_bits(), 0b11_1111);
} }
// new // new
@ -411,7 +411,7 @@ mod test {
fn new_created_closed_walls() { fn new_created_closed_walls() {
let walls = Walls::new(); let walls = Walls::new();
assert!(walls.is_enclosed()); assert!(walls.is_enclosed());
assert_eq!(walls.as_bits(), 0b111111); assert_eq!(walls.as_bits(), 0b11_1111);
} }
// empty // empty
@ -520,7 +520,7 @@ mod test {
fn default_creates_closed_walls() { fn default_creates_closed_walls() {
let walls = Walls::default(); let walls = Walls::default();
assert!(walls.is_enclosed()); assert!(walls.is_enclosed());
assert_eq!(walls.as_bits(), 0b111111); assert_eq!(walls.as_bits(), 0b11_1111);
} }
#[test] #[test]
@ -542,53 +542,53 @@ mod test {
// Test single bit operations // Test single bit operations
walls.insert(EdgeDirection::FLAT_NORTH); walls.insert(EdgeDirection::FLAT_NORTH);
assert_eq!(walls.as_bits(), 0b010000); assert_eq!(walls.as_bits(), 0b01_0000);
walls.insert(EdgeDirection::FLAT_SOUTH); walls.insert(EdgeDirection::FLAT_SOUTH);
assert_eq!(walls.as_bits(), 0b010010); assert_eq!(walls.as_bits(), 0b01_0010);
// Test removing middle bit // Test removing middle bit
walls.insert(EdgeDirection::FLAT_SOUTH_EAST); walls.insert(EdgeDirection::FLAT_SOUTH_EAST);
assert_eq!(walls.as_bits(), 0b010011); assert_eq!(walls.as_bits(), 0b01_0011);
walls.remove(EdgeDirection::FLAT_SOUTH); walls.remove(EdgeDirection::FLAT_SOUTH);
assert_eq!(walls.as_bits(), 0b010001); assert_eq!(walls.as_bits(), 0b01_0001);
} }
// From<EdgeDirection> tests // From<EdgeDirection> tests
#[test] #[test]
fn from_edge_direction_flat_south_east() { fn from_edge_direction_flat_south_east() {
let walls = Walls::from(EdgeDirection::FLAT_SOUTH_EAST); let walls = Walls::from(EdgeDirection::FLAT_SOUTH_EAST);
assert_eq!(walls.as_bits(), 0b000001); assert_eq!(walls.as_bits(), 0b00_0001);
} }
#[test] #[test]
fn from_edge_direction_flat_south() { fn from_edge_direction_flat_south() {
let walls = Walls::from(EdgeDirection::FLAT_SOUTH); let walls = Walls::from(EdgeDirection::FLAT_SOUTH);
assert_eq!(walls.as_bits(), 0b000010); assert_eq!(walls.as_bits(), 0b00_0010);
} }
#[test] #[test]
fn from_edge_direction_flat_south_west() { fn from_edge_direction_flat_south_west() {
let walls = Walls::from(EdgeDirection::FLAT_SOUTH_WEST); let walls = Walls::from(EdgeDirection::FLAT_SOUTH_WEST);
assert_eq!(walls.as_bits(), 0b000100); assert_eq!(walls.as_bits(), 0b00_0100);
} }
#[test] #[test]
fn from_edge_direction_flat_north_west() { fn from_edge_direction_flat_north_west() {
let walls = Walls::from(EdgeDirection::FLAT_NORTH_WEST); let walls = Walls::from(EdgeDirection::FLAT_NORTH_WEST);
assert_eq!(walls.as_bits(), 0b001000); assert_eq!(walls.as_bits(), 0b00_1000);
} }
#[test] #[test]
fn from_edge_direction_flat_north() { fn from_edge_direction_flat_north() {
let walls = Walls::from(EdgeDirection::FLAT_NORTH); let walls = Walls::from(EdgeDirection::FLAT_NORTH);
assert_eq!(walls.as_bits(), 0b010000); assert_eq!(walls.as_bits(), 0b01_0000);
} }
#[test] #[test]
fn from_edge_direction_flat_east() { fn from_edge_direction_flat_east() {
let walls = Walls::from(EdgeDirection::FLAT_NORTH_EAST); let walls = Walls::from(EdgeDirection::FLAT_NORTH_EAST);
assert_eq!(walls.as_bits(), 0b100000); assert_eq!(walls.as_bits(), 0b10_0000);
} }
// FromIterator tests // FromIterator tests
@ -603,7 +603,7 @@ mod test {
let walls = vec![EdgeDirection::FLAT_SOUTH] let walls = vec![EdgeDirection::FLAT_SOUTH]
.into_iter() .into_iter()
.collect::<Walls>(); .collect::<Walls>();
assert_eq!(walls.as_bits(), 0b000010); assert_eq!(walls.as_bits(), 0b00_0010);
} }
#[test] #[test]
@ -611,7 +611,7 @@ mod test {
let walls = vec![EdgeDirection::FLAT_NORTH, EdgeDirection::FLAT_SOUTH] let walls = vec![EdgeDirection::FLAT_NORTH, EdgeDirection::FLAT_SOUTH]
.into_iter() .into_iter()
.collect::<Walls>(); .collect::<Walls>();
assert_eq!(walls.as_bits(), 0b010010); assert_eq!(walls.as_bits(), 0b01_0010);
} }
#[test] #[test]
@ -623,13 +623,13 @@ mod test {
] ]
.into_iter() .into_iter()
.collect::<Walls>(); .collect::<Walls>();
assert_eq!(walls.as_bits(), 0b010010); assert_eq!(walls.as_bits(), 0b01_0010);
} }
#[test] #[test]
fn from_iterator_all_directions() { fn from_iterator_all_directions() {
let walls = EdgeDirection::iter().collect::<Walls>(); let walls = EdgeDirection::iter().collect::<Walls>();
assert_eq!(walls.as_bits(), 0b111111); assert_eq!(walls.as_bits(), 0b11_1111);
} }
// From<[EdgeDirection; N]> tests // From<[EdgeDirection; N]> tests
@ -642,13 +642,13 @@ mod test {
#[test] #[test]
fn from_array_single() { fn from_array_single() {
let walls = Walls::from([EdgeDirection::FLAT_NORTH]); let walls = Walls::from([EdgeDirection::FLAT_NORTH]);
assert_eq!(walls.as_bits(), 0b010000); assert_eq!(walls.as_bits(), 0b01_0000);
} }
#[test] #[test]
fn from_array_multiple() { fn from_array_multiple() {
let walls = Walls::from([EdgeDirection::FLAT_NORTH, EdgeDirection::FLAT_SOUTH]); let walls = Walls::from([EdgeDirection::FLAT_NORTH, EdgeDirection::FLAT_SOUTH]);
assert_eq!(walls.as_bits(), 0b010010); assert_eq!(walls.as_bits(), 0b01_0010);
} }
#[test] #[test]
@ -658,7 +658,7 @@ mod test {
EdgeDirection::FLAT_NORTH, EdgeDirection::FLAT_NORTH,
EdgeDirection::FLAT_SOUTH, EdgeDirection::FLAT_SOUTH,
]); ]);
assert_eq!(walls.as_bits(), 0b010010); assert_eq!(walls.as_bits(), 0b01_0010);
} }
#[test] #[test]
@ -671,6 +671,6 @@ mod test {
EdgeDirection::FLAT_SOUTH_WEST, EdgeDirection::FLAT_SOUTH_WEST,
EdgeDirection::FLAT_NORTH_WEST, EdgeDirection::FLAT_NORTH_WEST,
]); ]);
assert_eq!(walls.as_bits(), 0b111111); assert_eq!(walls.as_bits(), 0b11_1111);
} }
} }

View File

@ -67,28 +67,24 @@ fn maze_connectivity() {
let maze = assert_ok!(MazeBuilder::new().with_radius(3).build()); let maze = assert_ok!(MazeBuilder::new().with_radius(3).build());
// Helper function to count accessible neighbors // Helper function to count accessible neighbors
fn count_accessible_neighbors(maze: &Maze, pos: Hex) -> usize { let count_accessible_neighbors = |pos: Hex| -> usize {
hexx::EdgeDirection::ALL_DIRECTIONS hexx::EdgeDirection::ALL_DIRECTIONS
.iter() .iter()
.filter(|&&dir| { .filter(|&&dir| {
let neighbor = pos + dir; let neighbor = pos + dir;
if let Some(walls) = maze.get_walls(&pos) { maze.get_walls(&pos)
!walls.contains(dir) && maze.get(&neighbor).is_some() .is_some_and(|walls| !walls.contains(dir) && maze.get(&neighbor).is_some())
} else {
false
}
}) })
.count() .count()
} };
// Check that each tile has at least one connection // Check that each tile has at least one connection
for &pos in maze.keys() { for &pos in maze.keys() {
let accessible_neighbors = count_accessible_neighbors(&maze, pos); let accessible_neighbors = count_accessible_neighbors(pos);
claims::assert_gt!( assert_gt!(
accessible_neighbors, accessible_neighbors,
0, 0,
"Tile at {:?} has no accessible neighbors", "Tile at {pos:?} has no accessible neighbors",
pos
); );
} }
} }
@ -96,10 +92,9 @@ fn maze_connectivity() {
#[test] #[test]
fn maze_boundaries() { fn maze_boundaries() {
let radius = 3; let radius = 3;
let maze = MazeBuilder::new() let maze = assert_ok!(MazeBuilder::new().with_radius(radius).build());
.with_radius(radius as u16)
.build() let radius = i32::from(radius);
.unwrap();
// Test that tiles exist within the radius // Test that tiles exist within the radius
for q in -radius..=radius { for q in -radius..=radius {
@ -108,8 +103,7 @@ fn maze_boundaries() {
if q.abs() + r.abs() <= radius { if q.abs() + r.abs() <= radius {
assert!( assert!(
maze.get(&pos).is_some(), maze.get(&pos).is_some(),
"Expected tile at {:?} to exist", "Expected tile at {pos:?} to exist",
pos
); );
} }
} }

View File

@ -1,3 +1,4 @@
use claims::assert_some;
use hexlab::prelude::*; use hexlab::prelude::*;
use rstest::rstest; use rstest::rstest;
@ -46,11 +47,10 @@ fn generator_type(
// Check that each tile has at least one open wall // Check that each tile has at least one open wall
for &pos in maze.keys() { for &pos in maze.keys() {
let walls = maze.get_walls(&pos).unwrap(); let walls = assert_some!(maze.get_walls(&pos));
assert!( assert!(
walls.count() < 6, walls.count() < 6,
"Tile at {:?} should have at least one open wall", "Tile at {pos:?} should have at least one open wall",
pos
); );
} }
} }

View File

@ -1,3 +1,4 @@
use claims::assert_some;
use hexlab::prelude::*; use hexlab::prelude::*;
#[test] #[test]
@ -10,9 +11,8 @@ fn hex_maze_creation_and_basic_operations() {
assert_eq!(maze.count(), 1); assert_eq!(maze.count(), 1);
assert!(!maze.is_empty()); assert!(!maze.is_empty());
let tile = maze.get(&center); let tile = assert_some!(maze.get(&center));
assert!(tile.is_some()); assert_eq!(tile.pos(), center);
assert_eq!(tile.unwrap().pos(), center);
} }
#[test] #[test]
@ -26,7 +26,7 @@ fn hex_maze_wall_operations() {
let _ = maze.add_tile_wall(&center, direction); let _ = maze.add_tile_wall(&center, direction);
} }
let walls = maze.get_walls(&center).unwrap(); let walls = assert_some!(maze.get_walls(&center));
assert_eq!(walls.count(), 6); assert_eq!(walls.count(), 6);
// Remove walls // Remove walls
@ -34,7 +34,7 @@ fn hex_maze_wall_operations() {
let _ = maze.remove_tile_wall(&center, direction); let _ = maze.remove_tile_wall(&center, direction);
} }
let walls = maze.get_walls(&center).unwrap(); let walls = assert_some!(maze.get_walls(&center));
assert_eq!(walls.count(), 0); assert_eq!(walls.count(), 0);
} }

79
tests/pathfinding.rs Normal file
View File

@ -0,0 +1,79 @@
use claims::*;
use hexlab::MazeBuilder;
use hexx::{hex, EdgeDirection, Hex};
#[test]
fn basic_path() {
let maze = assert_ok!(MazeBuilder::new().with_seed(12345).with_radius(5).build());
let start = Hex::new(0, 0);
let goal = Hex::new(2, 0);
assert_some_eq!(
maze.find_path(start, goal),
vec![start, hex(1, 0), hex(1, 1), hex(2, 1), goal]
);
}
#[test]
fn path_with_walls() {
let mut maze = assert_ok!(MazeBuilder::new().with_seed(12345).with_radius(5).build());
let start = Hex::new(0, 0);
let goal = Hex::new(2, 0);
// Block direct path with wall
assert_ok!(maze.add_tile_wall(&start, EdgeDirection::FLAT_SOUTH));
// Should find alternative path or no path
let path = maze.find_path(start, goal);
if let Some(path) = path {
// If path exists, verify it's valid
assert!(path.len() > 3); // Should be longer than direct path
assert_eq!(path.first(), Some(&start));
assert_eq!(path.last(), Some(&goal));
}
}
#[test]
fn path_to_self() {
let maze = assert_ok!(MazeBuilder::new().with_seed(12345).with_radius(5).build());
let pos = Hex::new(0, 0);
assert_some_eq!(maze.find_path(pos, pos), vec![pos]);
}
#[test]
fn no_path_exists() {
let mut maze = assert_ok!(MazeBuilder::new().with_seed(12345).with_radius(5).build());
let start = Hex::new(0, 0);
let goal = Hex::new(2, 0);
// Surround start with walls
for dir in EdgeDirection::ALL_DIRECTIONS {
assert_ok!(maze.add_tile_wall(&start, dir));
}
assert_none!(maze.find_path(start, goal));
}
#[test]
fn path_in_larger_maze() {
let maze = assert_ok!(MazeBuilder::new().with_seed(12345).with_radius(10).build());
let start = Hex::new(-5, -5);
let goal = Hex::new(5, 5);
let path = assert_some!(maze.find_path(start, goal));
// Basic path properties
assert_eq!(path.first(), Some(&start));
assert_eq!(path.last(), Some(&goal));
// Path should be continuous
for window in path.windows(2) {
let current = window[0];
let next = window[1];
assert!(EdgeDirection::ALL_DIRECTIONS
.iter()
.any(|&dir| current.neighbor(dir) == next));
}
}