Compare commits

...

24 Commits
v0.3.0 ... 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
fae8e91b54 BREAKING CHANGE: deprecate Maze::len() in favor of Maze::count() 2025-01-02 22:32:47 +02:00
d66e4c4bb2 fix(walls): pass tests 2024-12-28 18:04:17 +02:00
2b3a375c4f fix(walls): contains de-reference 2024-12-28 17:11:26 +02:00
dcbe06fb8c Merge branch 'feature/add-traits' 2024-12-26 18:15:59 +02:00
91b1326bd4 feat(traits): add traits 2024-12-26 18:15:46 +02:00
434a23b15e
Merge pull request #3 from kristoferssolo/tests/increase-coverage
Tests/increase coverage
2024-12-26 01:03:48 +02:00
a562333b90 fix(bevy): imports 2024-12-26 00:54:47 +02:00
9740ce1a5a test(walls): 93% coverage 2024-12-25 22:57:13 +02:00
43a669dee8 test(maze): 88% coverage 2024-12-25 21:24:55 +02:00
012d1e5cca refactor: rename files 2024-12-25 20:56:22 +02:00
6660b4613d test(generator): 89% coverage 2024-12-25 20:51:45 +02:00
7cacf92014 test(builder): 100% builder tests 2024-12-25 20:18:12 +02:00
389c8ee1fd docs: add README.md 2024-12-25 19:23:42 +02:00
cd4f369108 docs: update 2024-12-25 19:01:43 +02:00
83f2e47e27 fix: typo 2024-12-25 18:01:20 +02:00
7e5abb9a79 chore: bump version 2024-12-25 17:07:27 +02:00
dd6111dce3 docs: fix typos 2024-12-25 17:06:22 +02:00
21 changed files with 1898 additions and 1030 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

231
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",
] ]
@ -1347,18 +1347,18 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.20.0" version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
dependencies = [ dependencies = [
"bytemuck_derive", "bytemuck_derive",
] ]
[[package]] [[package]]
name = "bytemuck_derive" name = "bytemuck_derive"
version = "1.8.0" version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1399,9 +1399,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.3" version = "1.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@ -1441,6 +1441,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "claims"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bba18ee93d577a8428902687bcc2b6b45a56b1981a1f6d779731c86cc4c5db18"
[[package]] [[package]]
name = "clang-sys" name = "clang-sys"
version = "1.8.1" version = "1.8.1"
@ -1519,9 +1525,9 @@ dependencies = [
[[package]] [[package]]
name = "const_panic" name = "const_panic"
version = "0.2.10" version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "013b6c2c3a14d678f38cd23994b02da3a1a1b6a5d1eedddfe63a5a5f11b13a81" checksum = "53857514f72ee4a2b583de67401e3ff63a5472ca4acf289d09a9ea7636dfec17"
[[package]] [[package]]
name = "const_soft_float" name = "const_soft_float"
@ -1625,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",
@ -1671,18 +1677,18 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.13" version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
dependencies = [ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.5" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [ dependencies = [
"crossbeam-epoch", "crossbeam-epoch",
"crossbeam-utils", "crossbeam-utils",
@ -1699,9 +1705,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.20" version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]] [[package]]
name = "crunchy" name = "crunchy"
@ -1737,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"
@ -1945,9 +1963,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "foldhash" name = "foldhash"
version = "0.1.3" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]] [[package]]
name = "font-types" name = "font-types"
@ -2042,6 +2060,43 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-macro",
"futures-task",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]] [[package]]
name = "gethostname" name = "gethostname"
version = "0.4.3" version = "0.4.3"
@ -2217,13 +2272,13 @@ dependencies = [
[[package]] [[package]]
name = "gpu-descriptor" name = "gpu-descriptor"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557" checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"gpu-descriptor-types", "gpu-descriptor-types",
"hashbrown 0.14.5", "hashbrown 0.15.1",
] ]
[[package]] [[package]]
@ -2295,20 +2350,26 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]] [[package]]
name = "hexlab" name = "hexlab"
version = "0.3.0" version = "0.6.1"
dependencies = [ dependencies = [
"bevy", "bevy",
"bevy_reflect",
"bevy_utils",
"claims",
"glam",
"hexx", "hexx",
"pathfinding",
"rand", "rand",
"rstest",
"serde", "serde",
"thiserror 2.0.3", "thiserror 2.0.3",
] ]
[[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",
@ -2372,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"
@ -2616,9 +2686,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.0" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
dependencies = [ dependencies = [
"adler2", "adler2",
"simd-adler32", "simd-adler32",
@ -2626,9 +2696,9 @@ dependencies = [
[[package]] [[package]]
name = "naga" name = "naga"
version = "23.0.0" version = "23.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d5941e45a15b53aad4375eedf02033adb7a28931eedc31117faffa52e6a857e" checksum = "364f94bc34f61332abebe8cad6f6cd82a5b65cff22c828d05d0968911462ca4f"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bit-set 0.8.0", "bit-set 0.8.0",
@ -2639,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",
@ -2660,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",
@ -3118,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"
@ -3162,6 +3246,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "piper" name = "piper"
version = "0.2.4" version = "0.2.4"
@ -3181,9 +3271,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]] [[package]]
name = "png" name = "png"
version = "0.17.15" version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67582bd5b65bdff614270e2ea89a1cf15bef71245cc1e5f7ea126977144211d" checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags 1.3.2",
"crc32fast", "crc32fast",
@ -3436,6 +3526,12 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "relative-path"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]] [[package]]
name = "renderdoc-sys" name = "renderdoc-sys"
version = "1.1.0" version = "1.1.0"
@ -3471,12 +3567,57 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]]
name = "rstest"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03e905296805ab93e13c1ec3a03f4b6c4f35e9498a3d5fa96dc626d22c03cd89"
dependencies = [
"futures-timer",
"futures-util",
"rstest_macros",
"rustc_version",
]
[[package]]
name = "rstest_macros"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef0053bbffce09062bee4bcc499b0fbe7a57b879f1efe088d6d8d4c7adcdef9b"
dependencies = [
"cfg-if",
"glob",
"proc-macro-crate",
"proc-macro2",
"quote",
"regex",
"relative-path",
"rustc_version",
"syn",
"unicode-ident",
]
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "1.1.0" 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]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.42" version = "0.38.42"
@ -3543,6 +3684,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 = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe"
[[package]]
name = "semver"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
[[package]] [[package]]
name = "send_wrapper" name = "send_wrapper"
version = "0.6.0" version = "0.6.0"
@ -3571,9 +3718,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.133" version = "1.0.134"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@ -3799,9 +3946,9 @@ dependencies = [
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.8.0" version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8"
dependencies = [ dependencies = [
"tinyvec_macros", "tinyvec_macros",
] ]
@ -3946,9 +4093,9 @@ checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e"
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.17" version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]] [[package]]
name = "unicode-bidi-mirroring" name = "unicode-bidi-mirroring"
@ -4178,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",
@ -4220,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",
@ -4616,9 +4763,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "winit" name = "winit"
version = "0.30.5" version = "0.30.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0be9e76a1f1077e04a411f0b989cbd3c93339e1771cb41e71ac4aee95bfd2c67" checksum = "dba50bc8ef4b6f1a75c9274fb95aa9a8f63fbc66c56f391bd85cf68d51e7b1a3"
dependencies = [ dependencies = [
"android-activity", "android-activity",
"atomic-waker", "atomic-waker",

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.3.0" 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"
@ -16,23 +16,41 @@ categories = [
"data-structures", "data-structures",
] ]
exclude = ["/.github", "/.gitignore", "/tests", "*.png", "*.md"] exclude = ["/.github", "/.gitignore", "/tests", "*.png", "*.md"]
readme = "README.md"
[dependencies] [dependencies]
bevy = { version = "0.15", optional = true }
hexx = { version = "0.19" } hexx = { version = "0.19" }
rand = "0.8" rand = "0.8"
serde = { version = "1.0", features = ["derive"], optional = true } serde = { version = "1.0", features = ["derive"], optional = true }
thiserror = "2.0" thiserror = "2.0"
bevy = { version = "0.15", optional = true }
bevy_utils = { version = "0.15", optional = true }
glam = { version = "0.29", optional = true }
pathfinding = { version = "4.13", optional = true }
[dependencies.bevy_reflect]
version = "0.15"
optional = true
default-features = false
features = ["glam"]
[dev-dependencies] [dev-dependencies]
claims = "0.8"
rstest = "0.24"
[features] [features]
default = [] default = []
serde = ["dep:serde", "hexx/serde"] serde = ["dep:serde", "hexx/serde"]
bevy = ["bevy_reflect"] bevy = ["dep:bevy", "bevy_reflect"]
bevy_reflect = ["dep:bevy", "hexx/bevy_reflect"] bevy_reflect = [
full = ["serde", "bevy"] "dep:bevy_reflect",
"dep:bevy_utils",
"hexx/bevy_reflect",
"dep:glam",
]
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
@ -47,8 +65,23 @@ panic = "abort" # Smaller binary size
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]
[profile.dev.package."*"]
opt-level = 3
# Override some settings for native builds.
[profile.release-native]
# Default to release profile values.
inherits = "release"
# Optimize with performance in mind.
opt-level = 3
# Keep debug information in the binary.
strip = "none"
[lints.clippy] [lints.clippy]
pedantic = "warn" pedantic = "warn"
nursery = "warn" nursery = "warn"
unwrap_used = "warn" unwrap_used = "warn"
expect_used = "warn" expect_used = "warn"
[package.metadata.nextest]
slow-timeout = { period = "120s", terminate-after = 3 }

89
README.md Normal file
View File

@ -0,0 +1,89 @@
# Hexlab
<!-- toc -->
- [Features](#features)
- [Installation](#installation)
- [Getting Started](#getting-started)
- [Usage](#usage)
- [Documentation](#documentation)
- [Contributing](#contributing)
- [Acknowledgements](#acknowledgements)
- [License](#license)
<!-- tocstop -->
Hexlab is a Rust library for generating and manipulating hexagonal mazes.
## Features
- Create hexagonal mazes of configurable size
- Customizable maze properties (radius, start position, seed)
- Efficient bit-flag representation of walls for optimized memory usage
- Multiple maze generation algorithms (WIP)
- Maze builder pattern for easy and flexible maze creation
## Installation
Add `hexlab` as a dependency:
```sh
cargo add hexlab
```
## Getting Started
```rust
use hexlab::prelude::*;
fn main() {
// Create a new maze with radius 5
let maze = MazeBuilder::new()
.with_radius(5)
.build()
.expect("Failed to create maze");
println!("Maze size: {}", maze.count());
}
```
## Usage
```rust
use hexlab::prelude::*;
// Create a new maze
let maze = MazeBuilder::new()
.with_radius(5)
.build()
.expect("Failed to create maze");
// Get a specific tile
let tile = maze.get_tile(&Hex::new(1, -1)).unwrap();
// Check if a wall exists
let has_wall = tile.walls().contains(EdgeDirection::FLAT_NORTH);
```
## Documentation
Full documentation is available at [docs.rs](https://docs.rs/hexlab).
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## Acknowledgements
Hexlab relies on the excellent [hexx](https://github.com/ManevilleF/hexx)
library for handling hexagonal grid mathematics, coordinates, and related
operations. We're grateful for the robust foundation it provides for working
with hexagonal grids.
## License
This project is dual-licensed under either:
- MIT License ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
at your option.

View File

@ -1,38 +1,16 @@
use crate::{ use crate::{errors::MazeBuilderError, GeneratorType, Maze};
generator::{generate_backtracking, GeneratorType},
HexMaze,
};
use hexx::Hex; use hexx::Hex;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MazeBuilderError {
/// Occurs when attempting to build a maze without specifying a radius.
#[error("Radius must be specified to build a maze")]
NoRadius,
/// Occurs when the specified radius is too large.
#[error("Radius {0} is too large. Maximum allowed radius is {1}")]
RadiusTooLarge(u32, u32),
/// Occurs when the specified start position is outside the maze bounds.
#[error("Start position {0:?} is outside maze bounds")]
InvalidStartPosition(Hex),
/// Occurs when maze generation fails.
#[error("Failed to generate maze: {0}")]
GenerationError(String),
}
/// A builder pattern for creating hexagonal mazes. /// A builder pattern for creating hexagonal mazes.
/// ///
/// This struct provides a fluent interface for configuring and building hexagonal mazes. /// This struct provides a fluent interface for configuring and building hexagonal mazes.
/// It offers flexibility in specifying the maze size, random seed, and generation algorithm. /// It offers flexibility in specifying the maze size, random seed, generation algorithm,
/// and starting position.
/// ///
/// # Examples /// # Examples
/// ///
/// Basic usage: /// Basic usage:
/// ```rust /// ```
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// let maze = MazeBuilder::new() /// let maze = MazeBuilder::new()
@ -42,11 +20,11 @@ pub enum MazeBuilderError {
/// ///
/// // 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:
/// ```rust /// ```
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// let maze1 = MazeBuilder::new() /// let maze1 = MazeBuilder::new()
@ -62,12 +40,12 @@ pub enum MazeBuilderError {
/// .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);
/// ``` /// ```
/// ///
/// Specifying a custom generator: /// Specifying a custom generator:
/// ```rust /// ```
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// let maze = MazeBuilder::new() /// let maze = MazeBuilder::new()
@ -79,14 +57,14 @@ pub enum MazeBuilderError {
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]
#[derive(Default)] #[derive(Default)]
pub struct MazeBuilder { pub struct MazeBuilder {
radius: Option<u32>, radius: Option<u16>,
seed: Option<u64>, seed: Option<u64>,
generator_type: GeneratorType, generator_type: GeneratorType,
start_position: Option<Hex>, start_position: Option<Hex>,
} }
impl MazeBuilder { impl MazeBuilder {
/// Creates a new [`MazeBuilder`] instance. /// Creates a new [`MazeBuilder`] instance with default settings.
#[inline] #[inline]
#[must_use] #[must_use]
pub fn new() -> Self { pub fn new() -> Self {
@ -95,21 +73,28 @@ impl MazeBuilder {
/// Sets the radius for the hexagonal maze. /// Sets the radius for the hexagonal maze.
/// ///
/// The radius determines the size of the maze, specifically the number of tiles
/// from the center (0,0) to the edge of the hexagon, not including the center tile.
/// For example, a radius of 3 would create a maze with 3 tiles from center to edge,
/// resulting in a total diameter of 7 tiles (3 + 1 + 3).
///
/// # Arguments /// # Arguments
/// ///
/// * `radius` - The size of the maze (number of tiles along one edge). /// - `radius` - The number of tiles from the center to the edge of the hexagon.
#[inline] #[inline]
#[must_use] #[must_use]
pub const fn with_radius(mut self, radius: u32) -> Self { pub const fn with_radius(mut self, radius: u16) -> Self {
self.radius = Some(radius); self.radius = Some(radius);
self self
} }
/// Sets the random seed for maze generation. /// Sets the random seed for maze generation.
/// ///
/// Using the same seed will produce identical mazes, allowing for reproducible results.
///
/// # Arguments /// # Arguments
/// ///
/// * `seed` - The random seed value. /// - `seed` - The random seed value.
#[inline] #[inline]
#[must_use] #[must_use]
pub const fn with_seed(mut self, seed: u64) -> Self { pub const fn with_seed(mut self, seed: u64) -> Self {
@ -123,14 +108,18 @@ impl MazeBuilder {
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `generator_type` - The maze generation algorithm to use. /// - `generator_type` - The maze generation algorithm to use.
#[inline] #[inline]
#[must_use] #[must_use]
pub const fn with_generator(mut self, generator_type: GeneratorType) -> Self { pub const fn with_generator(mut self, generator_type: GeneratorType) -> Self {
self.generator_type = generator_type; self.generator_type = generator_type;
self self
} }
/// Sets the starting position for maze generation.
///
/// # Arguments
///
/// - `pos` - The hexagonal coordinates for the starting position.
#[inline] #[inline]
#[must_use] #[must_use]
pub const fn with_start_position(mut self, pos: Hex) -> Self { pub const fn with_start_position(mut self, pos: Hex) -> Self {
@ -147,7 +136,7 @@ impl MazeBuilder {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// // Should fail without radius /// // Should fail without radius
@ -163,41 +152,35 @@ impl MazeBuilder {
/// let maze = result.unwrap(); /// let maze = result.unwrap();
/// assert!(!maze.is_empty()); /// assert!(!maze.is_empty());
/// ``` /// ```
pub fn build(self) -> Result<HexMaze, MazeBuilderError> { pub fn build(self) -> Result<Maze, MazeBuilderError> {
let radius = self.radius.ok_or(MazeBuilderError::NoRadius)?; let radius = self.radius.ok_or(MazeBuilderError::NoRadius)?;
let mut maze = create_hex_maze(radius); let mut maze = create_hex_maze(radius);
if let Some(start_pos) = self.start_position { if let Some(start_pos) = self.start_position {
if maze.get_tile(&start_pos).is_none() { if maze.get(&start_pos).is_none() {
return Err(MazeBuilderError::InvalidStartPosition(start_pos)); return Err(MazeBuilderError::InvalidStartPosition(start_pos));
} }
} }
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: u32) -> HexMaze {
let mut maze = HexMaze::new(); pub fn create_hex_maze(radius: u16) -> Maze {
let radius = i32::try_from(radius).unwrap_or(5); let mut maze = Maze::new();
let radius = i32::from(radius);
for q in -radius..=radius { for q in -radius..=radius {
let r1 = (-radius).max(-q - radius); let r1 = (-radius).max(-q - radius);
let r2 = radius.min(-q + radius); let r2 = radius.min(-q + radius);
for r in r1..=r2 { for r in r1..=r2 {
let pos = Hex::new(q, r); let pos = Hex::new(q, r);
maze.add_tile(pos); maze.insert(pos);
} }
} }
@ -206,194 +189,68 @@ fn create_hex_maze(radius: u32) -> HexMaze {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use hexx::EdgeDirection;
use super::*; use super::*;
use claims::{assert_gt, assert_some};
/// Helper function to count the number of tiles for a given radius use rstest::rstest;
fn calculate_hex_tiles(radius: u32) -> usize {
let r = radius as i32;
(3 * r * r + 3 * r + 1) as usize
}
#[test] #[test]
fn new_builder() { fn maze_builder_new() {
let builder = MazeBuilder::new(); let builder = MazeBuilder::new();
assert!(builder.radius.is_none()); assert_eq!(builder.radius, None);
assert!(builder.seed.is_none()); assert_eq!(builder.seed, None);
assert!(builder.start_position.is_none()); assert_eq!(builder.generator_type, GeneratorType::default());
assert_eq!(builder.start_position, None);
}
#[rstest]
#[case(0, 1)] // Minimum size is 1 tile
#[case(1, 7)]
#[case(2, 19)]
#[case(3, 37)]
#[case(10, 331)]
#[case(100, 30301)]
fn create_hex_maze_various_radii(#[case] radius: u16, #[case] expected_size: usize) {
let maze = create_hex_maze(radius);
assert_eq!(maze.count(), expected_size);
} }
#[test] #[test]
fn builder_with_radius() { fn create_hex_maze_large_radius() {
let radius = 5; let large_radius = 1000;
let maze = MazeBuilder::new().with_radius(radius).build().unwrap(); let maze = create_hex_maze(large_radius);
assert_gt!(maze.count(), 0);
assert_eq!(maze.len(), calculate_hex_tiles(radius)); // Calculate expected size for this radius
assert!(maze.get_tile(&Hex::ZERO).is_some()); let expected_size = 3 * (large_radius as usize).pow(2) + 3 * large_radius as usize + 1;
assert_eq!(maze.count(), expected_size);
} }
#[test] #[test]
fn builder_without_radius() { fn create_hex_maze_tile_positions() {
let maze = MazeBuilder::new().build(); let maze = create_hex_maze(2);
assert!(matches!(maze, Err(MazeBuilderError::NoRadius))); let expected_positions = [
} Hex::new(0, 0),
Hex::new(1, -1),
#[test] Hex::new(1, 0),
fn builder_with_seed() { Hex::new(0, 1),
let radius = 3; Hex::new(-1, 1),
let seed = 12345; Hex::new(-1, 0),
Hex::new(0, -1),
let maze1 = MazeBuilder::new() Hex::new(2, -2),
.with_radius(radius) Hex::new(2, -1),
.with_seed(seed) Hex::new(2, 0),
.build() Hex::new(1, 1),
.unwrap(); Hex::new(0, 2),
Hex::new(-1, 2),
let maze2 = MazeBuilder::new() Hex::new(-2, 2),
.with_radius(radius) Hex::new(-2, 1),
.with_seed(seed) Hex::new(-2, 0),
.build() Hex::new(-1, -1),
.unwrap(); Hex::new(0, -2),
Hex::new(1, -2),
// Same seed should produce identical mazes ];
assert_eq!(maze1, maze2); for pos in &expected_positions {
} assert_some!(maze.get(pos), "Expected tile at {pos:?}");
#[test]
fn different_seeds_produce_different_mazes() {
let radius = 3;
let maze1 = MazeBuilder::new()
.with_radius(radius)
.with_seed(12345)
.build()
.unwrap();
let maze2 = MazeBuilder::new()
.with_radius(radius)
.with_seed(54321)
.build()
.unwrap();
// Different seeds should produce different mazes
assert_ne!(maze1, maze2);
}
#[test]
fn maze_connectivity() {
let radius = 3;
let maze = MazeBuilder::new().with_radius(radius).build().unwrap();
// Helper function to count accessible neighbors
fn count_accessible_neighbors(maze: &HexMaze, pos: Hex) -> usize {
EdgeDirection::ALL_DIRECTIONS
.iter()
.filter(|&&dir| {
let neighbor = pos + dir;
if let Some(walls) = maze.get_walls(&pos) {
!walls.contains(dir) && maze.get_tile(&neighbor).is_some()
} else {
false
}
})
.count()
}
// Check that each tile has at least one connection
for &pos in maze.keys() {
let accessible_neighbors = count_accessible_neighbors(&maze, pos);
assert!(
accessible_neighbors > 0,
"Tile at {:?} has no accessible neighbors",
pos
);
}
}
#[test]
fn start_position() {
let radius = 3;
let start_pos = Hex::new(1, 1);
let maze = MazeBuilder::new()
.with_radius(radius)
.with_start_position(start_pos)
.build()
.unwrap();
assert!(maze.get_tile(&start_pos).is_some());
}
#[test]
fn invalid_start_position() {
let maze = MazeBuilder::new()
.with_radius(3)
.with_start_position(Hex::new(10, 10))
.build();
assert!(matches!(
maze,
Err(MazeBuilderError::InvalidStartPosition(_))
));
}
#[test]
fn maze_boundaries() {
let radius = 3;
let maze = MazeBuilder::new().with_radius(radius).build().unwrap();
// Test that tiles exist within the radius
for q in -(radius as i32)..=(radius as i32) {
for r in -(radius as i32)..=(radius as i32) {
let pos = Hex::new(q, r);
if q.abs() + r.abs() <= radius as i32 {
assert!(
maze.get_tile(&pos).is_some(),
"Expected tile at {:?} to exist",
pos
);
}
}
}
}
#[test]
fn different_radii() {
for radius in 1..=5 {
let maze = MazeBuilder::new().with_radius(radius).build().unwrap();
assert_eq!(
maze.len(),
calculate_hex_tiles(radius),
"Incorrect number of tiles for radius {}",
radius
);
}
}
#[test]
fn wall_consistency() {
let radius = 3;
let maze = MazeBuilder::new().with_radius(radius).build().unwrap();
// Check that if tile A has no wall to tile B,
// then tile B has no wall to tile A
for &pos in maze.keys() {
for &dir in &EdgeDirection::ALL_DIRECTIONS {
let neighbor = pos + dir;
if let (Some(walls), Some(neighbor_walls)) =
(maze.get_walls(&pos), maze.get_walls(&neighbor))
{
assert_eq!(
walls.contains(dir),
neighbor_walls.contains(dir.const_neg()),
"Wall inconsistency between {:?} and {:?}",
pos,
neighbor
);
}
}
} }
} }
} }

41
src/errors.rs Normal file
View File

@ -0,0 +1,41 @@
use hexx::{EdgeDirection, Hex};
use thiserror::Error;
use crate::Tile;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum MazeBuilderError {
/// Occurs when attempting to build a maze without specifying a radius.
#[error("Radius must be specified to build a maze")]
NoRadius,
/// Occurs when the specified start position is outside the maze bounds.
#[error("Start position {0:?} is outside maze bounds")]
InvalidStartPosition(Hex),
/// Occurs when maze generation fails.
#[error("Failed to generate maze: {0}")]
GenerationError(String),
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum MazeError {
/// Error when attempting to access or modify a tile at a non-existent coordinate.
#[error("Invalid coordinate: {0:?}")]
InvalidCoordinate(Hex),
/// Error when a tile's internal position doesn't match its insertion coordinate.
#[error("Tile position ({tile_pos:?}) does not match insertion coordinates ({insert_pos:?})")]
PositionMismatch { tile_pos: Hex, insert_pos: Hex },
/// Error when attempting to insert a tile at an already occupied position.
#[error("A tile {old_tile:?} already exists at position {pos:?}")]
TileAlreadyExists { pos: Hex, old_tile: Tile },
/// Error when a wall operation fails at the specified coordinate and direction.
#[error("Cannot add wall at {coord:?} in direction {direction:?}")]
WallOperationFailed {
coord: Hex,
direction: EdgeDirection,
},
}

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)]
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);
}
}
}

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

@ -0,0 +1,116 @@
use crate::Maze;
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 Maze, 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 Maze,
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(&neighbor).is_some() && !visited.contains(&neighbor) {
let _ = maze.remove_tile_wall(&current, direction);
let _ = 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 claims::assert_some;
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 = assert_some!(maze.get_walls(&pos));
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(&neighbor).is_some() {
to_visit.push(neighbor);
}
}
}
}
assert_eq!(
connected.len(),
maze.count(),
"All tiles should be connected"
);
}
}

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

@ -0,0 +1,25 @@
mod backtrack;
use crate::Maze;
use backtrack::generate_backtracking;
#[cfg(feature = "bevy")]
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(bevy_reflect::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 Maze, start_pos: Option<Hex>, seed: Option<u64>) {
match self {
Self::RecursiveBacktracking => generate_backtracking(maze, start_pos, seed),
}
}
}

View File

@ -1,254 +0,0 @@
use super::{HexTile, Walls};
#[cfg(feature = "bevy_reflect")]
use bevy::prelude::*;
#[cfg(feature = "bevy_reflect")]
use bevy::utils::HashMap;
use hexx::{EdgeDirection, Hex};
#[cfg(not(feature = "bevy_reflect"))]
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
/// Represents a hexagonal maze with tiles and walls
#[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, Default, PartialEq, Eq)]
pub struct HexMaze(HashMap<Hex, HexTile>);
impl HexMaze {
/// Creates a new empty maze
#[inline]
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Adds a new tile at the specified coordinates
pub fn add_tile(&mut self, coords: Hex) {
let tile = HexTile::new(coords);
self.0.insert(coords, tile);
}
/// Adds a wall in the specified direction at the given coordinates
pub fn add_wall(&mut self, coord: Hex, direction: EdgeDirection) {
if let Some(tile) = self.0.get_mut(&coord) {
tile.walls.add(direction);
}
}
/// Returns a reference to the tile at the specified coordinates
#[inline]
#[must_use]
pub fn get_tile(&self, coord: &Hex) -> Option<&HexTile> {
self.0.get(coord)
}
/// Returns a reference to the walls at the specified coordinates
pub fn get_walls(&self, coord: &Hex) -> Option<&Walls> {
self.0.get(coord).map(HexTile::walls)
}
/// Returns the number of tiles in the maze
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
/// Returns true if the maze is empty
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn remove_tile_wall(&mut self, coord: &Hex, direction: EdgeDirection) {
if let Some(tile) = self.0.get_mut(coord) {
tile.walls.remove(direction);
}
}
}
impl Deref for HexMaze {
type Target = HashMap<Hex, HexTile>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for HexMaze {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_maze() {
let maze = HexMaze::default();
assert!(maze.is_empty(), "New maze should be empty");
assert_eq!(maze.len(), 0, "New maze should have zero tiles");
}
#[test]
fn add_tile() {
let mut maze = HexMaze::default();
let coords = [Hex::ZERO, Hex::new(1, -1), Hex::new(-1, 1)];
// Add tiles
for &coord in &coords {
maze.add_tile(coord);
assert!(
maze.get_tile(&coord).is_some(),
"Tile should exist after adding"
);
}
assert_eq!(
maze.len(),
coords.len(),
"Maze should contain all added tiles"
);
}
#[test]
fn wall_operations() {
let mut maze = HexMaze::default();
let coord = Hex::ZERO;
maze.add_tile(coord);
// Test adding walls
let directions = [
EdgeDirection::FLAT_TOP,
EdgeDirection::FLAT_BOTTOM,
EdgeDirection::POINTY_TOP_RIGHT,
];
for &direction in &directions {
maze.add_wall(coord, direction);
assert!(
maze.get_walls(&coord).unwrap().contains(direction),
"Wall should exist after adding"
);
}
}
#[test]
fn tile_iteration() {
let mut maze = HexMaze::default();
let coords = [Hex::ZERO, Hex::new(1, 0), Hex::new(0, 1)];
// Add tiles
for &coord in &coords {
maze.add_tile(coord);
}
// Test iterator
let collected = maze.iter().map(|(_, tile)| tile).collect::<Vec<_>>();
assert_eq!(
collected.len(),
coords.len(),
"Iterator should yield all tiles"
);
}
#[test]
fn maze_clone() {
let mut maze = HexMaze::default();
let coord = Hex::ZERO;
maze.add_tile(coord);
maze.add_wall(coord, EdgeDirection::FLAT_TOP);
// Test cloning
let cloned_maze = maze.clone();
assert_eq!(
maze.len(),
cloned_maze.len(),
"Cloned maze should have same size"
);
assert!(
cloned_maze
.get_walls(&coord)
.unwrap()
.contains(EdgeDirection::FLAT_TOP),
"Cloned maze should preserve wall state"
);
}
#[test]
fn empty_tile_operations() {
let mut maze = HexMaze::default();
let coord = Hex::ZERO;
// Operations on non-existent tile
assert!(
maze.get_tile(&coord).is_none(),
"Should return None for non-existent tile"
);
assert!(
maze.get_walls(&coord).is_none(),
"Should return None for non-existent walls"
);
// Adding wall to non-existent tile should not panic
maze.add_wall(coord, EdgeDirection::FLAT_TOP);
}
#[test]
fn maze_boundaries() {
let mut maze = HexMaze::default();
let extreme_coords = [
Hex::new(i32::MAX, i32::MIN),
Hex::new(i32::MIN, i32::MAX),
Hex::new(0, i32::MAX),
Hex::new(0, i32::MIN),
Hex::new(i32::MAX, 0),
Hex::new(i32::MIN, 0),
];
// Test with extreme coordinates
for &coord in &extreme_coords {
maze.add_tile(coord);
assert!(
maze.get_tile(&coord).is_some(),
"Should handle extreme coordinates"
);
}
}
#[test]
fn iterator_consistency() {
let mut maze = HexMaze::default();
let coords = [Hex::ZERO, Hex::new(1, -1), Hex::new(-1, 1)];
// Add tiles
for &coord in &coords {
maze.add_tile(coord);
}
// Verify iterator
let iter_coords = maze.iter().map(|(coord, _)| *coord).collect::<Vec<_>>();
assert_eq!(
iter_coords.len(),
coords.len(),
"Iterator should yield all coordinates"
);
for coord in coords {
assert!(
iter_coords.contains(&coord),
"Iterator should contain all added coordinates"
);
}
}
#[test]
fn empty_maze() {
let maze = HexMaze::default();
assert!(maze.is_empty(), "New maze should be empty");
}
}

View File

@ -1,211 +0,0 @@
use super::Walls;
#[cfg(feature = "bevy_reflect")]
use bevy::prelude::*;
use hexx::Hex;
#[cfg(feature = "bevy_reflect")]
use hexx::HexLayout;
use std::fmt::Display;
/// Represents a single hexagonal tile in the maze
#[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, Default, PartialEq, Eq)]
pub struct HexTile {
pub(crate) pos: Hex,
pub(crate) walls: Walls,
}
impl HexTile {
/// Creates a new tile with pos and default walls
#[must_use]
pub fn new(pos: Hex) -> Self {
Self {
pos,
walls: Walls::default(),
}
}
/// Returns a reference to the tile's walls
#[inline]
#[must_use]
pub const fn walls(&self) -> &Walls {
&self.walls
}
/// Returns position of the tile
#[inline]
#[must_use]
pub const fn pos(&self) -> Hex {
self.pos
}
#[cfg(feature = "bevy_reflect")]
#[inline]
#[must_use]
pub fn to_vec2(&self, layout: &HexLayout) -> Vec2 {
layout.hex_to_world_pos(self.pos)
}
#[cfg(feature = "bevy_reflect")]
#[inline]
#[must_use]
pub fn to_vec3(&self, layout: &HexLayout) -> Vec3 {
let pos = self.to_vec2(layout);
Vec3::new(pos.x, 0., pos.y)
}
}
impl From<Hex> for HexTile {
fn from(value: Hex) -> Self {
Self {
pos: value,
walls: Walls::default(),
}
}
}
impl Display for HexTile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({},{})", self.pos.x, self.pos.y)
}
}
#[cfg(test)]
mod tests {
use hexx::EdgeDirection;
use super::*;
#[test]
fn new_tile() {
let pos = Hex::ZERO;
let tile = HexTile::new(pos);
assert_eq!(tile.pos, pos, "Position should match constructor argument");
assert_eq!(
tile.walls,
Walls::default(),
"Walls should be initialized to default"
);
}
#[test]
fn tile_walls_accessor() {
let pos = Hex::new(1, -1);
let tile = HexTile::new(pos);
// Test walls accessor method
let walls_ref = tile.walls();
assert_eq!(
walls_ref, &tile.walls,
"Walls accessor should return reference to walls"
);
}
#[test]
fn tile_modification() {
let pos = Hex::new(2, 3);
let mut tile = HexTile::new(pos);
// Modify walls
tile.walls.remove(EdgeDirection::FLAT_TOP);
assert!(
!tile.walls.contains(EdgeDirection::FLAT_TOP),
"Wall should be removed"
);
tile.walls.add(EdgeDirection::FLAT_TOP);
assert!(
tile.walls.contains(EdgeDirection::FLAT_TOP),
"Wall should be added back"
);
}
#[test]
fn tile_clone() {
let pos = Hex::new(0, -2);
let tile = HexTile::new(pos);
// Test Clone trait
let cloned_tile = tile.clone();
assert_eq!(tile, cloned_tile, "Cloned tile should equal original");
}
#[test]
fn tile_debug() {
let pos = Hex::ZERO;
let tile = HexTile::new(pos);
// Test Debug trait
let debug_string = format!("{:?}", tile);
assert!(
debug_string.contains("HexTile"),
"Debug output should contain struct name"
);
}
#[test]
fn different_positions() {
let positions = [Hex::ZERO, Hex::new(1, 0), Hex::new(-1, 1), Hex::new(2, -2)];
// Create tiles at different positions
let tiles = positions
.iter()
.map(|&pos| HexTile::new(pos))
.collect::<Vec<_>>();
// Verify each tile has correct position
for (tile, &pos) in tiles.iter().zip(positions.iter()) {
assert_eq!(
tile.pos, pos,
"Tile position should match constructor argument"
);
}
}
#[test]
fn tile_equality() {
let pos1 = Hex::new(1, 1);
let pos2 = Hex::new(1, 1);
let pos3 = Hex::new(2, 1);
let tile1 = HexTile::new(pos1);
let tile2 = HexTile::new(pos2);
let tile3 = HexTile::new(pos3);
assert_eq!(tile1, tile2, "Tiles with same position should be equal");
assert_ne!(
tile1, tile3,
"Tiles with different positions should not be equal"
);
// Test with modified walls
let mut tile4 = HexTile::new(pos1);
tile4.walls.remove(EdgeDirection::FLAT_TOP);
assert_ne!(
tile1, tile4,
"Tiles with different walls should not be equal"
);
}
#[test]
fn hex_boundaries() {
// Test with extreme coordinate values
let extreme_positions = [
Hex::new(i32::MAX, i32::MIN),
Hex::new(i32::MIN, i32::MAX),
Hex::new(0, i32::MAX),
Hex::new(i32::MIN, 0),
];
for pos in extreme_positions {
let tile = HexTile::new(pos);
assert_eq!(
tile.pos, pos,
"Tile should handle extreme coordinate values"
);
}
}
}

View File

@ -1,16 +1,73 @@
//! Hexlab is a library for generating and manipulating hexagonal mazes.
//!
//! # Features
//!
//! - Create hexagonal mazes of configurable size
//! - Customizable maze properties (radius, start position, seed)
//! - Efficient bit-flag representation of walls
//! - Multiple maze generation algorithms
//! - Maze builder pattern for easy maze creation
//!
//! # Examples
//!
//! Here's a quick example to create a simple hexagonal maze:
//!
//!```
//! use hexlab::prelude::*;
//!
//! let maze = MazeBuilder::new()
//! .with_radius(3)
//! .build()
//! .expect("Failed to create maze");
//!
//! assert_eq!(maze.count(), 37); // A radius of 3 should create 37 tiles
//!```
//!
//! Customizing maze generation:
//!
//!```
//! use hexlab::prelude::*;
//!
//! let maze = MazeBuilder::new()
//! .with_radius(2)
//! .with_seed(12345)
//! .with_start_position(Hex::new(1, -1))
//! .build()
//! .expect("Failed to create maze");
//!
//! assert!(maze.get(&Hex::new(1, -1)).is_some());
//!```
//!
//! Manipulating walls:
//!
//!```
//! use hexlab::prelude::*;
//!
//! let mut walls = Walls::empty();
//! assert!(!walls.insert(EdgeDirection::FLAT_NORTH));
//! assert!(walls.contains(EdgeDirection::FLAT_NORTH));
//! assert!(!walls.contains(EdgeDirection::FLAT_SOUTH));
//!```
mod builder; mod builder;
pub mod errors;
mod generator; mod generator;
mod hex_maze; mod maze;
mod hex_tile; #[cfg(feature = "pathfinding")]
mod pathfinding;
mod tile;
pub mod traits;
mod walls; mod walls;
pub use builder::{MazeBuilder, MazeBuilderError}; pub use builder::MazeBuilder;
pub use errors::*;
pub use generator::GeneratorType; pub use generator::GeneratorType;
pub use hex_maze::HexMaze; pub use maze::Maze;
pub use hex_tile::HexTile; pub use tile::Tile;
pub use traits::*;
pub use walls::Walls; pub use walls::Walls;
/// Prelude module containing commonly used types
pub mod prelude { pub mod prelude {
pub use super::{GeneratorType, HexMaze, HexTile, MazeBuilder, MazeBuilderError, Walls}; pub use super::{errors::*, traits::*, GeneratorType, Maze, MazeBuilder, Tile, Walls};
pub use hexx::{EdgeDirection, Hex, HexLayout}; pub use hexx::{EdgeDirection, Hex, HexLayout};
} }

380
src/maze.rs Normal file
View File

@ -0,0 +1,380 @@
use super::{Tile, Walls};
use crate::{
errors::MazeError,
traits::{TilePosition, WallStorage},
};
#[cfg(feature = "bevy")]
use bevy::prelude::*;
#[cfg(feature = "bevy_reflect")]
use bevy_utils::HashMap;
use hexx::{EdgeDirection, Hex};
#[cfg(not(feature = "bevy_reflect"))]
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
/// Represents a hexagonal maze with tiles and walls.
///
/// This struct stores the layout of a hexagonal maze, including the positions
/// of tiles and their associated walls.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
#[cfg_attr(feature = "bevy", derive(Component))]
#[cfg_attr(feature = "bevy", reflect(Component))]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Maze(HashMap<Hex, Tile>);
impl Maze {
/// Creates a new empty maze
///
/// # Examples
///
/// ```
/// use hexlab::prelude::*;
///
/// let maze = Maze::new();
///
/// assert!(maze.is_empty());
/// assert_eq!(maze.count(), 0);
/// ```
#[inline]
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Inserts a new tile at the specified coordinates.
///
/// If the map did not have this key present, [`None`] is returned.
///
/// If the map did have this key present, the value is updated, and the old
/// value is returned.
///
/// # Arguments
///
/// - `coords` - The hexagonal coordinates where the tile should be added.
///
/// # Examples
///
/// ```
/// use hexlab::prelude::*;
///
/// let mut maze = Maze::new();
/// let coord = Hex::ZERO;
///
/// assert_eq!(maze.insert(coord), None);
/// assert_eq!(maze.insert(coord), Some(Tile::new(coord)));
/// ```
pub fn insert(&mut self, coords: Hex) -> Option<Tile> {
let tile = Tile::new(coords);
self.0.insert(coords, tile)
}
/// Adds a new tile at the specified coordinates. It is recommended to use [`insert`].
///
/// [`insert`]: Maze::insert
///
/// # Arguments
///
/// - `coords` - The hexagonal coordinates where the tile should be added.
/// - `tile` - The tile to insert to.
///
/// # Errors
///
/// Returns [`MazeError::PositionMismatch`] if the tile's position doesn't match the insertion coordinates.
/// Returns [`MazeError::TileAlreadyExists`] if a tile already exists at the specified coordinates.
///
/// # Examples
///
/// ```
/// use hexlab::prelude::*;
///
/// let mut maze = Maze::new();
///
/// assert_eq!(
/// maze.insert_with_tile(Hex::new(2, 2), Tile::new(Hex::ZERO)),
/// Err(MazeError::PositionMismatch {
/// tile_pos: Hex::ZERO,
/// insert_pos: Hex::new(2, 2)
/// })
/// );
/// let tile = Tile::new(Hex::ZERO);
/// assert_eq!(maze.insert_with_tile(Hex::ZERO, tile.clone()), Ok(tile.clone()));
/// assert_eq!(
/// maze.insert_with_tile(Hex::ZERO, tile.clone()),
/// Err(MazeError::TileAlreadyExists {
/// pos: Hex::ZERO,
/// old_tile: tile
/// })
/// );
/// ```
pub fn insert_with_tile(&mut self, coords: Hex, tile: Tile) -> Result<Tile, MazeError> {
if tile.pos != coords {
return Err(MazeError::PositionMismatch {
tile_pos: tile.pos,
insert_pos: coords,
});
}
self.0
.insert(coords, tile.clone())
.map_or(Ok(tile), |old_tile| {
Err(MazeError::TileAlreadyExists {
pos: coords,
old_tile,
})
})
}
/// Returns a reference to the tile at the specified coordinates.
///
/// # Arguments
///
/// - `coord` - The hexagonal coordinates of the tile to retrieve.
///
/// # Examples
///
/// ```
/// use hexlab::prelude::*;
///
/// let mut maze = Maze::new();
/// let coord = Hex::ZERO;
/// maze.insert(coord);
///
/// assert!(maze.get(&coord).is_some());
/// assert!(maze.get(&Hex::new(1, 1)).is_none());
/// ```
#[inline]
#[must_use]
pub fn get(&self, coord: &Hex) -> Option<&Tile> {
self.0.get(coord)
}
#[inline]
#[must_use]
pub fn get_mut(&mut self, coord: &Hex) -> Option<&mut Tile> {
self.0.get_mut(coord)
}
/// Returns an optional mutable reference to the walls at the specified coordinates.
///
/// # Arguments
///
/// - `coord` - The hexagonal coordinates of the tile whose walls to retrieve.
///
/// # Examples
///
/// ```
/// use hexlab::prelude::*;
///
/// let mut maze = Maze::new();
/// let coord = Hex::new(0, 0);
/// maze.insert(coord);
///
/// maze.add_tile_wall(&coord, EdgeDirection::FLAT_NORTH);
/// let walls = maze.get_walls(&coord).unwrap();
/// assert!(walls.contains(EdgeDirection::FLAT_NORTH));
/// ```
#[inline]
#[must_use]
pub fn get_walls(&self, coord: &Hex) -> Option<&Walls> {
self.0.get(coord).map(Tile::walls)
}
/// Returns an optional mutable reference to the walls at the specified coordinates.
///
/// # Arguments
///
/// - `coord` - The hexagonal coordinates of the tile whose walls to retrieve.
///
/// # Examples
///
/// ```
/// use hexlab::prelude::*;
///
/// let mut maze = Maze::new();
/// let coord = Hex::new(0, 0);
/// maze.insert(coord);
///
/// maze.add_tile_wall(&coord, EdgeDirection::FLAT_NORTH);
/// let mut walls = maze.get_walls_mut(&coord).unwrap();
/// assert!(walls.remove(EdgeDirection::FLAT_NORTH));
/// ```
#[inline]
#[must_use]
pub fn get_walls_mut(&mut self, coord: &Hex) -> Option<&mut Walls> {
self.0.get_mut(coord).map(Tile::walls_mut)
}
/// Returns the number of tiles in the maze.
///
/// # Examples
///
/// ```
/// use hexlab::prelude::*;
///
/// let mut maze = Maze::new();
/// assert_eq!(maze.count(), 0);
///
/// maze.insert(Hex::new(0, 0));
/// assert_eq!(maze.count(), 1);
///
/// maze.insert(Hex::new(1, -1));
/// assert_eq!(maze.count(), 2);
/// ```
#[inline]
#[must_use]
pub fn count(&self) -> usize {
self.0.len()
}
/// Returns `true` if the maze contains no tiles.
///
/// # Examples
///
/// ```
/// use hexlab::prelude::*;
///
/// let mut maze = Maze::new();
/// assert!(maze.is_empty());
///
/// maze.insert(Hex::ZERO);
/// assert!(!maze.is_empty());
/// ```
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Adds a wall from a tile in the specified direction.
///
/// # Arguments
///
/// - `coord` - The hexagonal coordinates of the tile.
/// - `direction` - The direction of the wall to remove.
///
/// # Errors
///
/// Returns `MazeError::InvalidCoordinate` if the specified coordinate does not exist in the maze.
///
/// # Examples
///
/// ```
/// use hexlab::prelude::*;
///
/// // Create a maze with a single tile at the origin
/// let mut tile = Tile::new(Hex::ZERO);
/// tile.walls_mut().toggle(Walls::all_directions());
/// let mut maze = Maze::from([tile]);
///
/// // Initially, the tile should have no walls
/// assert!(maze.get_walls(&Hex::ZERO).unwrap().is_empty());
///
/// // Add a wall to the north
/// assert!(maze.add_tile_wall(&Hex::ZERO, EdgeDirection::FLAT_NORTH).is_ok());
///
/// // Check that the wall was added
/// let walls = maze.get_walls(&Hex::ZERO).unwrap();
/// assert!(walls.contains(EdgeDirection::FLAT_NORTH));
/// assert_eq!(walls.count(), 1);
///
/// // Adding the same wall again should return true (no change)
/// assert_eq!(maze.add_tile_wall(&Hex::ZERO, EdgeDirection::FLAT_NORTH), Ok(true));
///
/// // Adding a wall to a non-existent tile should return an error
/// let invalid_coord = Hex::new(1, 1);
/// assert_eq!(
/// maze.add_tile_wall(&invalid_coord, EdgeDirection::FLAT_NORTH),
/// Err(MazeError::InvalidCoordinate(invalid_coord))
/// );
/// ```
pub fn add_tile_wall(
&mut self,
coord: &Hex,
direction: EdgeDirection,
) -> Result<bool, MazeError> {
self.0
.get_mut(coord)
.map(|tile| tile.walls.insert(direction))
.ok_or(MazeError::InvalidCoordinate(*coord))
}
/// Removes a wall from a tile in the specified direction.
///
/// # Arguments
///
/// - `coord` - The hexagonal coordinates of the tile.
/// - `direction` - The direction of the wall to remove.
///
/// # Errors
///
/// Returns `MazeError::InvalidCoordinate` if the specified coordinate does not exist in the maze.
///
/// # Examples
///
/// ```
/// use hexlab::prelude::*;
///
/// let mut maze = Maze::new();
/// let coord = Hex::ZERO;
/// maze.insert(coord);
///
/// maze.add_tile_wall(&coord, EdgeDirection::FLAT_NORTH);
/// maze.remove_tile_wall(&coord, EdgeDirection::FLAT_NORTH);
///
/// let walls = maze.get_walls(&coord).unwrap();
/// assert!(!walls.contains(EdgeDirection::FLAT_NORTH));
/// ```
pub fn remove_tile_wall(
&mut self,
coord: &Hex,
direction: EdgeDirection,
) -> Result<bool, MazeError> {
self.0
.get_mut(coord)
.map(|tile| tile.walls.remove(direction))
.ok_or(MazeError::InvalidCoordinate(*coord))
}
}
impl FromIterator<Hex> for Maze {
fn from_iter<T: IntoIterator<Item = Hex>>(iter: T) -> Self {
Self(iter.into_iter().map(|hex| (hex, Tile::new(hex))).collect())
}
}
impl FromIterator<Tile> for Maze {
fn from_iter<T: IntoIterator<Item = Tile>>(iter: T) -> Self {
Self(iter.into_iter().map(|tile| (tile.pos(), tile)).collect())
}
}
impl FromIterator<(Hex, Tile)> for Maze {
fn from_iter<T: IntoIterator<Item = (Hex, Tile)>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
impl<const N: usize> From<[Hex; N]> for Maze {
fn from(value: [Hex; N]) -> Self {
value.into_iter().collect()
}
}
impl<const N: usize> From<[Tile; N]> for Maze {
fn from(value: [Tile; N]) -> Self {
value.into_iter().collect()
}
}
impl Deref for Maze {
type Target = HashMap<Hex, Tile>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Maze {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

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)
}
}

250
src/tile.rs Normal file
View File

@ -0,0 +1,250 @@
use super::Walls;
#[cfg(feature = "bevy_reflect")]
use crate::traits::WorldPositionable;
use crate::traits::{TilePosition, WallStorage};
#[cfg(feature = "bevy")]
use bevy::prelude::*;
use hexx::Hex;
#[cfg(feature = "bevy_reflect")]
use hexx::HexLayout;
use std::fmt::Display;
/// Represents a single hexagonal tile in the maze
///
/// Each tile has a position and a set of walls defining its boundaries.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
#[cfg_attr(feature = "bevy", derive(Component))]
#[cfg_attr(feature = "bevy", reflect(Component))]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Tile {
pub(crate) pos: Hex,
pub(crate) walls: Walls,
}
impl TilePosition for Tile {
/// Returns position of the tile
///
/// # Examples
///
/// ```
/// use hexlab::prelude::*;
///
/// let tile = Tile::new(Hex::new(2, -2));
/// assert_eq!(tile.pos(), Hex::new(2, -2));
/// ```
#[inline]
fn pos(&self) -> Hex {
self.pos
}
}
impl WallStorage for Tile {
/// Returns an immutable reference to the tile's walls
///
/// # Examples
///
/// ```
/// use hexlab::prelude::*;
///
/// let tile = Tile::new(Hex::ZERO);
/// assert_eq!(*tile.walls(), Walls::default());
/// ```
#[inline]
fn walls(&self) -> &Walls {
&self.walls
}
/// Returns a mutable reference to the tile's walls
///
/// # Examples
///
/// ```
/// use hexlab::prelude::*;
///
/// let tile = Tile::new(Hex::ZERO);
/// assert_eq!(*tile.walls(), Walls::default());
/// ```
#[inline]
fn walls_mut(&mut self) -> &mut Walls {
&mut self.walls
}
}
#[cfg(feature = "bevy_reflect")]
impl WorldPositionable for Tile {
/// Converts the tile's position to a 2D vector based on the given layout.
///
/// # Arguments
///
/// - `layout` - The hexagonal layout used for conversion.
#[inline]
fn to_vec2(&self, layout: &HexLayout) -> glam::Vec2 {
layout.hex_to_world_pos(self.pos)
}
/// Converts the tile's position to a 3D vector based on the given layout.
///
/// # Arguments
///
/// - `layout` - The hexagonal layout used for conversion.
#[inline]
fn to_vec3(&self, layout: &HexLayout) -> glam::Vec3 {
let pos = self.to_vec2(layout);
glam::Vec3::new(pos.x, 0., pos.y)
}
}
impl Tile {
/// Creates a new tile with the given position and default walls.
///
/// # Arguments
///
/// - `pos` - The hexagonal coordinates of the tile.
///
/// # Examples
///
/// ```
/// use hexlab::prelude::*;
///
/// let tile = Tile::new(Hex::new(1, -1));
/// assert_eq!(tile.pos(), Hex::new(1, -1));
/// assert_eq!(*tile.walls(), Walls::default());
/// ```
#[must_use]
pub fn new(pos: Hex) -> Self {
Self {
pos,
walls: Walls::default(),
}
}
}
impl From<Hex> for Tile {
fn from(value: Hex) -> Self {
Self {
pos: value,
walls: Walls::default(),
}
}
}
impl Display for Tile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({},{})", self.pos.x, self.pos.y)
}
}
#[cfg(test)]
mod test {
use super::*;
use hexx::EdgeDirection;
use rand::{thread_rng, Rng};
fn random_hex() -> Hex {
let mut rng = thread_rng();
Hex::new(rng.gen(), rng.gen())
}
#[test]
fn different_positions() {
let positions = [Hex::ZERO, Hex::new(1, 0), Hex::new(-1, 1), Hex::new(2, -2)];
// Create tiles at different positions
let tiles = positions
.iter()
.map(|&pos| Tile::new(pos))
.collect::<Vec<_>>();
// Verify each tile has correct position
for (tile, &pos) in tiles.iter().zip(positions.iter()) {
assert_eq!(tile.pos, pos);
}
}
#[test]
fn hex_boundaries() {
// Test with extreme coordinate values
let extreme_positions = [
Hex::new(i32::MAX, i32::MIN),
Hex::new(i32::MIN, i32::MAX),
Hex::new(0, i32::MAX),
Hex::new(i32::MIN, 0),
];
for pos in extreme_positions {
let tile = Tile::new(pos);
assert_eq!(tile.pos, pos);
}
}
#[test]
fn hex_tile_creation_and_properties() {
let hex = random_hex();
let tile = Tile::new(hex);
assert_eq!(tile.pos(), hex);
assert!(tile.walls().is_enclosed());
}
#[test]
fn hex_tile_from_hex() {
let hex = random_hex();
let tile = Tile::from(hex);
assert_eq!(tile.pos, hex);
assert_eq!(tile.walls, Walls::default());
}
#[test]
fn hex_hex_into_tile() {
let hex = random_hex();
let tile: Tile = hex.into();
assert_eq!(tile.pos, hex);
assert_eq!(tile.walls, Walls::default());
}
#[test]
fn hex_tile_display() {
let tile = Tile::new(Hex::new(3, -3));
assert_eq!(format!("{tile}"), "(3,-3)");
}
#[test]
fn hex_tile_wall_modifications() {
let mut tile = Tile::new(Hex::ZERO);
for direction in EdgeDirection::ALL_DIRECTIONS {
tile.walls.insert(direction);
}
assert_eq!(tile.walls.count(), 6);
for direction in EdgeDirection::ALL_DIRECTIONS {
tile.walls.remove(direction);
}
assert_eq!(tile.walls.count(), 0);
}
#[cfg(feature = "bevy_reflect")]
mod bevy_tests {
use super::*;
use glam::{Vec2, Vec3};
#[test]
fn hex_tile_to_vec2() {
let layout = HexLayout::default();
let tile = Tile::new(Hex::new(1, 0));
let vec2 = tile.to_vec2(&layout);
assert_eq!(vec2, Vec2::new(1.5, -0.866_025_4));
}
#[test]
fn hex_tile_to_vec3() {
let layout = HexLayout::default();
let tile = Tile::new(Hex::new(0, 1));
let vec3 = tile.to_vec3(&layout);
assert_eq!(vec3, Vec3::new(0.0, 0.0, -1.732_050_8));
}
}
}

22
src/traits.rs Normal file
View File

@ -0,0 +1,22 @@
use crate::Walls;
use hexx::Hex;
pub trait TilePosition {
/// Returns position of the tile
#[must_use]
fn pos(&self) -> Hex;
}
#[cfg(feature = "bevy_reflect")]
pub trait WorldPositionable {
#[must_use]
fn to_vec2(&self, layout: &hexx::HexLayout) -> glam::Vec2;
#[must_use]
fn to_vec3(&self, layout: &hexx::HexLayout) -> glam::Vec3;
}
pub trait WallStorage {
#[must_use]
fn walls(&self) -> &Walls;
fn walls_mut(&mut self) -> &mut Walls;
}

View File

@ -1,4 +1,4 @@
#[cfg(feature = "bevy_reflect")] #[cfg(feature = "bevy")]
use bevy::prelude::*; use bevy::prelude::*;
use hexx::EdgeDirection; use hexx::EdgeDirection;
@ -11,39 +11,24 @@ use hexx::EdgeDirection;
/// # Examples /// # Examples
/// ///
/// Creating and manipulating walls: /// Creating and manipulating walls:
/// ```rust /// ```
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// // Create a hexagon with all walls /// // Create a hexagon with all walls
/// let walls = Walls::new(); /// let walls = Walls::new();
/// assert!(walls.is_closed()); /// assert!(walls.is_enclosed());
/// ///
/// // Create a hexagon with no walls /// // Create a hexagon with no walls
/// let mut walls = Walls::empty(); /// let mut walls = Walls::empty();
/// assert!(walls.is_empty()); /// assert!(walls.is_empty());
/// ///
/// // Add specific walls /// // Add specific walls
/// walls.add(EdgeDirection::FLAT_NORTH); /// walls.insert(EdgeDirection::FLAT_NORTH);
/// walls.add(EdgeDirection::FLAT_SOUTH); /// walls.insert(EdgeDirection::FLAT_SOUTH);
/// assert_eq!(walls.count(), 2); /// assert_eq!(walls.count(), 2);
/// ``` /// ```
///
/// Using walls in game logic:
///
/// ```rust
/// use hexlab::prelude::*;
/// let mut walls = Walls::empty();
///
/// // Add walls to create a corner
/// walls.add(EdgeDirection::FLAT_NORTH);
/// walls.add(EdgeDirection::FLAT_SOUTH_EAST);
///
/// // Check if a specific direction has a wall
/// assert!(walls.contains(EdgeDirection::FLAT_NORTH));
/// assert!(!walls.contains(EdgeDirection::FLAT_SOUTH));
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
#[cfg_attr(feature = "bevy", derive(Component))] #[cfg_attr(feature = "bevy", derive(Component))]
#[cfg_attr(feature = "bevy", reflect(Component))] #[cfg_attr(feature = "bevy", reflect(Component))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -56,12 +41,11 @@ impl Walls {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// let walls = Walls::new(); /// let walls = Walls::new();
/// assert!(walls.is_closed()); /// assert!(walls.is_enclosed());
/// assert_eq!(walls.count(), 6);
/// ``` /// ```
#[inline] #[inline]
#[must_use] #[must_use]
@ -73,12 +57,11 @@ impl Walls {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// let walls = Walls::empty(); /// let walls = Walls::empty();
/// assert!(walls.is_empty()); /// assert!(walls.is_empty());
/// assert_eq!(walls.count(), 0);
/// ``` /// ```
#[inline] #[inline]
#[must_use] #[must_use]
@ -86,19 +69,15 @@ impl Walls {
Self(0) Self(0)
} }
/// Checks if the walls are currently empty /// Checks if the walls are currently empty (no walls present).
/// ///
/// Returns `true` if all directions have no walls set.
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// let walls = Walls::empty(); /// let walls = Walls::empty();
/// assert!(walls.is_empty()); /// assert!(walls.is_empty());
///
/// let walls = Walls::new();
/// assert!(!walls.is_empty());
/// ``` /// ```
#[inline] #[inline]
#[must_use] #[must_use]
@ -106,98 +85,118 @@ impl Walls {
self.0 == 0 self.0 == 0
} }
/// Adds a wall in the specified direction /// Insert a wall in the specified direction.
/// ///
/// This method uses bitwise operations to efficiently set the wall flag /// # Arguments
/// for the given direction. Multiple walls can be added to the same hexagon. ///
/// - `direction` - The direction in which to insert the wall.
///
/// # Returns
///
/// Returns `true` if a wall was present, `false` otherwise.
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// let mut walls = Walls::empty(); /// let mut walls = Walls::empty();
/// walls.add(EdgeDirection::FLAT_NORTH); /// assert_eq!(walls.count(), 0);
/// assert!(walls.contains(EdgeDirection::FLAT_NORTH));
/// assert!(!walls.contains(EdgeDirection::FLAT_SOUTH));
/// ///
/// walls.add(EdgeDirection::FLAT_SOUTH); /// assert!(!walls.insert(1));
/// assert!(walls.contains(EdgeDirection::FLAT_SOUTH)); /// assert_eq!(walls.count(), 1);
///
/// assert!(walls.insert(1));
/// assert_eq!(walls.count(), 1);
///
/// assert!(!walls.insert(EdgeDirection::FLAT_NORTH));
/// assert_eq!(walls.count(), 2); /// assert_eq!(walls.count(), 2);
/// ``` /// ```
#[inline] #[inline]
pub fn add<T>(&mut self, direction: T) pub fn insert<T>(&mut self, direction: T) -> bool
where where
T: Into<Self> + Copy, T: Into<Self>,
{ {
self.0 |= direction.into().0; let mask = direction.into().0;
let was_present = self.0 & mask != 0;
self.0 |= mask;
was_present
} }
/// Removes a wall in the specified direction /// Removes a wall in the specified direction.
/// ///
/// Returns `true` if a wall was actually removed, `false` if there was no wall /// # Arguments
/// in the specified direction.
/// ///
/// # Exmaples /// - `direction` - The direction from which to remove the wall.
/// ///
/// ```rust /// # Returns
///
/// Returns `true` if a wall was present and removed, `false` otherwise.
///
/// # Examples
///
/// ```
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// let mut walls = Walls::new(); /// let mut walls = Walls::new();
/// assert!(walls.remove(EdgeDirection::FLAT_NORTH));
/// assert!(!walls.contains(EdgeDirection::FLAT_NORTH));
/// ///
/// // Removing a non-existent wall returns false /// assert!(walls.remove(1));
/// assert!(!walls.remove(EdgeDirection::FLAT_NORTH)); /// assert_eq!(walls.count(), 5);
///
/// assert!(!walls.remove(1));
/// assert_eq!(walls.count(), 5);
///
/// assert!(walls.remove(EdgeDirection::FLAT_NORTH));
/// assert_eq!(walls.count(), 4);
/// ``` /// ```
#[inline] #[inline]
pub fn remove<T>(&mut self, direction: T) -> bool pub fn remove<T>(&mut self, direction: T) -> bool
where where
T: Into<Self> + Copy, T: Into<Self>,
{ {
let was_removed = self.contains(direction); let mask = direction.into().0;
if was_removed { let was_present = self.0 & mask != 0;
self.0 &= !direction.into().0; self.0 &= !mask;
} was_present
was_removed
} }
/// Returns true if there is a wall in the specified direction /// Checks if there is a wall in the specified direction.
/// ///
/// Uses efficient bitwise operations to check for the presence of a wall. /// # Arguments
/// ///
/// # Exmaples /// - `other` - The direction to check for a wall.
/// ///
/// ```rust /// # Examples
///
/// ```
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// let mut walls = Walls::empty(); /// let mut walls = Walls::empty();
/// walls.insert(EdgeDirection::FLAT_NORTH);
/// ///
/// walls.add(EdgeDirection::FLAT_NORTH);
/// assert!(walls.contains(EdgeDirection::FLAT_NORTH)); /// assert!(walls.contains(EdgeDirection::FLAT_NORTH));
/// assert!(!walls.contains(EdgeDirection::FLAT_SOUTH)); /// assert!(!walls.contains(EdgeDirection::FLAT_SOUTH));
/// ``` /// ```
#[inline] #[inline]
pub fn contains<T>(&self, other: T) -> bool pub fn contains<T>(&self, direction: T) -> bool
where where
T: Into<Self> + Copy, T: Into<Self>,
{ {
self.0 & other.into().0 != 0 self.0 & direction.into().0 != 0
} }
/// Returns the raw bit representation of the walls /// Returns the raw bit representation of the walls
/// ///
/// This method provides access to the underlying bit flags for advanced usage. /// # Examples
/// The bits are ordered according to the `EdgeDirection` indices.
/// ///
/// # Exmaples /// ```
///
/// ```rust
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// let mut walls = Walls::new(); /// let walls = Walls::new();
/// assert_eq!(walls.as_bits(), 0b11_1111);
/// ///
/// assert_eq!(walls.as_bits(), 0b111111); /// let walls = Walls::empty();
/// assert_eq!(walls.as_bits(), 0);
/// ``` /// ```
#[inline] #[inline]
#[must_use] #[must_use]
@ -207,19 +206,18 @@ impl Walls {
/// Returns the total number of walls present /// Returns the total number of walls present
/// ///
/// Efficiently counts the number of set bits in the internal representation. /// # Examples
/// ///
/// # Exmaples /// ```
///
/// ```rust
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// let mut walls = Walls::empty(); /// let mut walls = Walls::empty();
/// assert!(walls.is_empty());
/// ///
/// assert_eq!(walls.count(), 0); /// walls.insert(0);
/// assert_eq!(walls.count(), 1);
/// ///
/// walls.add(EdgeDirection::FLAT_NORTH); /// walls.insert(1);
/// walls.add(EdgeDirection::FLAT_SOUTH);
/// assert_eq!(walls.count(), 2); /// assert_eq!(walls.count(), 2);
/// ``` /// ```
#[inline] #[inline]
@ -228,19 +226,14 @@ impl Walls {
u8::try_from(self.0.count_ones()).unwrap_or_default() u8::try_from(self.0.count_ones()).unwrap_or_default()
} }
/// Returns all possible directions as a `Walls` value /// Returns a `Walls` value representing all possible directions.
/// ///
/// This represents a hexagon with walls in all six directions. /// # Examples
/// ///
/// # Exmaples /// ```
///
/// ```rust
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// let all_walls = Walls::all_directions(); /// assert_eq!(Walls::all_directions().as_bits(), 0b11_1111);
///
/// assert_eq!(all_walls.count(), 6);
/// assert!(all_walls.is_closed());
/// ``` /// ```
#[inline] #[inline]
#[must_use] #[must_use]
@ -252,59 +245,58 @@ impl Walls {
/// ///
/// If a wall exists in the given direction, it will be removed. /// If a wall exists in the given direction, it will be removed.
/// If no wall exists, one will be added. /// If no wall exists, one will be added.
/// Returns the previous state (`true` if a wall was present). ///
/// # Arguments
///
/// - `direction` - The direction in which to toggle the wall.
///
/// # Returns
///
/// The previous state (`true` if a wall was present before toggling, `false` otherwise).
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// let mut walls = Walls::empty(); /// let mut walls = Walls::empty();
/// ///
/// assert!(!walls.toggle(EdgeDirection::FLAT_NORTH)); // Returns false, wall was not present /// assert!(!walls.toggle(0));
/// assert!(walls.contains(EdgeDirection::FLAT_NORTH)); // Wall is now present /// assert_eq!(walls.count(), 1);
/// ///
/// let mut walls = Walls::new(); /// assert!(walls.toggle(0));
/// /// assert_eq!(walls.count(), 0);
/// assert!(walls.toggle(EdgeDirection::FLAT_NORTH)); // Returns true, wall was present
/// assert!(!walls.contains(EdgeDirection::FLAT_NORTH)); // Wall is now removed
/// ``` /// ```
pub fn toggle<T>(&mut self, direction: T) -> bool pub fn toggle<T>(&mut self, direction: T) -> bool
where where
T: Into<Self> + Copy, T: Into<Self> + Copy,
{ {
let is_present = self.contains(direction); let mask = direction.into().0;
if is_present { let was_present = self.0 & mask != 0;
self.remove(direction); self.0 ^= mask;
} else { was_present
self.add(direction);
}
is_present
} }
/// Checks if walls are present in all six directions. /// Checks if walls are present in all six directions.
/// ///
/// Returns `true` if the hexagon has all possible walls, making it completely enclosed. /// # Returns
///
/// `true` if the hexagon has all possible walls, making it completely enclosed.
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// let walls = Walls::new(); /// let mut walls = Walls::new();
/// assert!(walls.is_closed()); /// assert!(walls.is_enclosed());
/// ///
/// let mut walls = Walls::empty(); /// walls.remove(0);
/// assert!(!walls.is_closed()); /// assert!(!walls.is_enclosed());
/// // Add all walls manually
/// for direction in EdgeDirection::iter() {
/// walls.add(direction);
/// }
/// assert!(walls.is_closed());
/// ``` /// ```
#[inline] #[inline]
#[must_use] #[must_use]
pub fn is_closed(&self) -> bool { pub fn is_enclosed(&self) -> bool {
self.count() == 6 self.count() == 6
} }
@ -313,15 +305,19 @@ impl Walls {
/// This method efficiently adds multiple walls in a single operation while /// This method efficiently adds multiple walls in a single operation while
/// preserving any existing walls not specified in the input. /// preserving any existing walls not specified in the input.
/// ///
/// # Arguments
///
/// - `other` - The walls to insert, specified as a `Walls` instance or any type
/// that can be converted into `Walls`.
///
///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use hexlab::prelude::*; /// use hexlab::prelude::*;
/// ///
/// let mut walls = Walls::empty(); /// let mut walls = Walls::empty();
/// walls.add(EdgeDirection::FLAT_NORTH); /// walls.fill([EdgeDirection::FLAT_NORTH ,EdgeDirection::FLAT_SOUTH, EdgeDirection::FLAT_SOUTH_EAST]);
///
/// walls.fill([EdgeDirection::FLAT_SOUTH, EdgeDirection::FLAT_SOUTH_EAST]);
/// ///
/// assert!(walls.contains(EdgeDirection::FLAT_SOUTH)); /// assert!(walls.contains(EdgeDirection::FLAT_SOUTH));
/// assert_eq!(walls.count(), 3); /// assert_eq!(walls.count(), 3);
@ -370,16 +366,16 @@ impl Default for Walls {
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod test {
use super::*; use super::*;
// all_directions // all_directions
#[test] #[test]
fn all_directions_creates_closed_walls() { fn all_directions_creates_closed_walls() {
let walls = Walls::all_directions(); let walls = Walls::all_directions();
assert!(walls.is_closed()); 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
@ -392,30 +388,30 @@ mod tests {
#[test] #[test]
fn as_bits_single_wall() { fn as_bits_single_wall() {
let mut walls = Walls::empty(); let mut walls = Walls::empty();
walls.add(EdgeDirection::FLAT_NORTH); walls.insert(EdgeDirection::FLAT_NORTH);
assert_eq!(walls.as_bits(), 0b010000); assert_eq!(walls.as_bits(), 0b01_0000);
} }
#[test] #[test]
fn as_bits_multiple_walls() { fn as_bits_multiple_walls() {
let mut walls = Walls::empty(); let mut walls = Walls::empty();
walls.add(EdgeDirection::FLAT_NORTH); walls.insert(EdgeDirection::FLAT_NORTH);
walls.add(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
#[test] #[test]
fn new_created_closed_walls() { fn new_created_closed_walls() {
let walls = Walls::new(); let walls = Walls::new();
assert!(walls.is_closed()); assert!(walls.is_enclosed());
assert_eq!(walls.as_bits(), 0b111111); assert_eq!(walls.as_bits(), 0b11_1111);
} }
// empty // empty
@ -426,11 +422,11 @@ mod tests {
assert_eq!(walls.as_bits(), 0); assert_eq!(walls.as_bits(), 0);
} }
// add // insert
#[test] #[test]
fn add_single_wall() { fn insert_single_wall() {
let mut walls = Walls::empty(); let mut walls = Walls::empty();
walls.add(EdgeDirection::FLAT_NORTH); walls.insert(EdgeDirection::FLAT_NORTH);
assert!(walls.contains(EdgeDirection::FLAT_NORTH)); assert!(walls.contains(EdgeDirection::FLAT_NORTH));
assert_eq!(walls.count(), 1); assert_eq!(walls.count(), 1);
} }
@ -447,13 +443,13 @@ mod tests {
fn remove_nonexistent_wall() { fn remove_nonexistent_wall() {
let mut walls = Walls::empty(); let mut walls = Walls::empty();
assert!(!walls.remove(EdgeDirection::FLAT_NORTH)); assert!(!walls.remove(EdgeDirection::FLAT_NORTH));
walls.add(EdgeDirection::FLAT_NORTH); walls.insert(EdgeDirection::FLAT_NORTH);
assert!(walls.remove(EdgeDirection::FLAT_NORTH)); assert!(walls.remove(EdgeDirection::FLAT_NORTH));
} }
// toggle // toggle
#[test] #[test]
fn toggle_adds_wall() { fn toggle_wall() {
let mut walls = Walls::empty(); let mut walls = Walls::empty();
assert!(!walls.toggle(EdgeDirection::FLAT_NORTH)); assert!(!walls.toggle(EdgeDirection::FLAT_NORTH));
assert!(walls.contains(EdgeDirection::FLAT_NORTH)); assert!(walls.contains(EdgeDirection::FLAT_NORTH));
@ -479,7 +475,7 @@ mod tests {
#[test] #[test]
fn fill_preserves_existing_walls() { fn fill_preserves_existing_walls() {
let mut walls = Walls::empty(); let mut walls = Walls::empty();
walls.add(EdgeDirection::FLAT_NORTH); walls.insert(EdgeDirection::FLAT_NORTH);
walls.fill([EdgeDirection::FLAT_SOUTH, EdgeDirection::FLAT_SOUTH_EAST]); walls.fill([EdgeDirection::FLAT_SOUTH, EdgeDirection::FLAT_SOUTH_EAST]);
assert!(walls.contains(EdgeDirection::FLAT_NORTH)); assert!(walls.contains(EdgeDirection::FLAT_NORTH));
assert!(walls.contains(EdgeDirection::FLAT_SOUTH)); assert!(walls.contains(EdgeDirection::FLAT_SOUTH));
@ -523,8 +519,8 @@ mod tests {
#[test] #[test]
fn default_creates_closed_walls() { fn default_creates_closed_walls() {
let walls = Walls::default(); let walls = Walls::default();
assert!(walls.is_closed()); assert!(walls.is_enclosed());
assert_eq!(walls.as_bits(), 0b111111); assert_eq!(walls.as_bits(), 0b11_1111);
} }
#[test] #[test]
@ -545,54 +541,54 @@ mod tests {
let mut walls = Walls::empty(); let mut walls = Walls::empty();
// Test single bit operations // Test single bit operations
walls.add(EdgeDirection::FLAT_NORTH); walls.insert(EdgeDirection::FLAT_NORTH);
assert_eq!(walls.as_bits(), 0b010000); assert_eq!(walls.as_bits(), 0b01_0000);
walls.add(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.add(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
@ -607,7 +603,7 @@ mod tests {
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]
@ -615,7 +611,7 @@ mod tests {
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]
@ -627,13 +623,13 @@ mod tests {
] ]
.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
@ -646,13 +642,13 @@ mod tests {
#[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]
@ -662,7 +658,7 @@ mod tests {
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]
@ -675,6 +671,6 @@ mod tests {
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);
} }
} }

124
tests/builder.rs Normal file
View File

@ -0,0 +1,124 @@
use claims::{assert_err, assert_gt, assert_matches, assert_ok, assert_some};
use hexlab::prelude::*;
use rstest::rstest;
#[rstest]
#[case(1, 7)]
#[case(2, 19)]
#[case(3, 37)]
#[case(4, 61)]
#[case(5, 91)]
fn maze_size(#[case] radius: u16, #[case] expected_size: usize) {
let maze = assert_ok!(MazeBuilder::new().with_radius(radius).build());
assert_eq!(maze.count(), expected_size);
}
#[test]
fn builder_without_radius() {
let result = MazeBuilder::new().build();
assert_err!(&result);
assert_matches!(result, Err(MazeBuilderError::NoRadius));
}
#[rstest]
#[case(Hex::ZERO)]
#[case(Hex::new(1,-1))]
#[case(Hex::new(-2,1))]
fn valid_start_position(#[case] start_pos: Hex) {
let maze = assert_ok!(MazeBuilder::new()
.with_radius(3)
.with_start_position(start_pos)
.build());
assert_some!(maze.get(&start_pos));
}
#[test]
fn invalid_start_position() {
let maze = MazeBuilder::new()
.with_radius(3)
.with_start_position(Hex::new(10, 10))
.build();
assert_err!(&maze);
assert_matches!(maze, Err(MazeBuilderError::InvalidStartPosition(_)));
}
#[test]
fn maze_with_seed() {
let maze1 = assert_ok!(MazeBuilder::new().with_radius(3).with_seed(12345).build());
let maze2 = assert_ok!(MazeBuilder::new().with_radius(3).with_seed(12345).build());
assert_eq!(maze1, maze2, "Mazes with the same seed should be identical");
}
#[test]
fn different_seeds_produce_different_mazes() {
let maze1 = assert_ok!(MazeBuilder::new().with_radius(3).with_seed(12345).build());
let maze2 = assert_ok!(MazeBuilder::new().with_radius(3).with_seed(54321).build());
assert_ne!(
maze1, maze2,
"Mazes with different seeds should be different"
);
}
#[test]
fn maze_connectivity() {
let maze = assert_ok!(MazeBuilder::new().with_radius(3).build());
// Helper function to count accessible neighbors
let count_accessible_neighbors = |pos: Hex| -> usize {
hexx::EdgeDirection::ALL_DIRECTIONS
.iter()
.filter(|&&dir| {
let neighbor = pos + dir;
maze.get_walls(&pos)
.is_some_and(|walls| !walls.contains(dir) && maze.get(&neighbor).is_some())
})
.count()
};
// Check that each tile has at least one connection
for &pos in maze.keys() {
let accessible_neighbors = count_accessible_neighbors(pos);
assert_gt!(
accessible_neighbors,
0,
"Tile at {pos:?} has no accessible neighbors",
);
}
}
#[test]
fn maze_boundaries() {
let radius = 3;
let maze = assert_ok!(MazeBuilder::new().with_radius(radius).build());
let radius = i32::from(radius);
// Test that tiles exist within the radius
for q in -radius..=radius {
for r in -radius..=radius {
let pos = Hex::new(q, r);
if q.abs() + r.abs() <= radius {
assert!(
maze.get(&pos).is_some(),
"Expected tile at {pos:?} to exist",
);
}
}
}
}
#[rstest]
#[case(GeneratorType::RecursiveBacktracking)]
fn generate_maze_with_different_types(#[case] generator: GeneratorType) {
// TODO: Add more generator types when they become available
let maze = assert_ok!(MazeBuilder::new()
.with_radius(3)
.with_generator(generator)
.build());
assert_gt!(maze.count(), 0);
}

66
tests/generator.rs Normal file
View File

@ -0,0 +1,66 @@
use claims::assert_some;
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 = Maze::new();
for q in -3..=3 {
for r in -3..=3 {
let hex = Hex::new(q, r);
if hex.length() <= 3 {
maze.insert(hex);
}
}
}
let initial_size = maze.count();
generator.generate(&mut maze, start_pos, seed);
assert_eq!(maze.count(), 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(&neighbor).is_some() {
to_visit.push(neighbor);
}
}
}
}
assert_eq!(visited.len(), maze.count(), "All tiles should be connected");
// Check that each tile has at least one open wall
for &pos in maze.keys() {
let walls = assert_some!(maze.get_walls(&pos));
assert!(
walls.count() < 6,
"Tile at {pos:?} should have at least one open wall",
);
}
}
#[test]
fn test_empty_maze() {
let mut maze = Maze::new();
GeneratorType::RecursiveBacktracking.generate(&mut maze, None, None);
assert!(
maze.is_empty(),
"Empty maze should remain empty after generation"
);
}

68
tests/maze.rs Normal file
View File

@ -0,0 +1,68 @@
use claims::assert_some;
use hexlab::prelude::*;
#[test]
fn hex_maze_creation_and_basic_operations() {
let mut maze = Maze::new();
assert!(maze.is_empty());
let center = Hex::ZERO;
maze.insert(center);
assert_eq!(maze.count(), 1);
assert!(!maze.is_empty());
let tile = assert_some!(maze.get(&center));
assert_eq!(tile.pos(), center);
}
#[test]
fn hex_maze_wall_operations() {
let mut maze = Maze::new();
let center = Hex::ZERO;
maze.insert(center);
// Add walls
for direction in EdgeDirection::ALL_DIRECTIONS {
let _ = maze.add_tile_wall(&center, direction);
}
let walls = assert_some!(maze.get_walls(&center));
assert_eq!(walls.count(), 6);
// Remove walls
for direction in EdgeDirection::ALL_DIRECTIONS {
let _ = maze.remove_tile_wall(&center, direction);
}
let walls = assert_some!(maze.get_walls(&center));
assert_eq!(walls.count(), 0);
}
#[test]
fn hex_maze_multiple_tiles() {
let mut maze = Maze::new();
let tiles = [Hex::ZERO, Hex::new(1, -1), Hex::new(0, 1), Hex::new(-1, 1)];
for &tile in &tiles {
maze.insert(tile);
}
assert_eq!(maze.count(), tiles.len());
for &tile in &tiles {
assert!(maze.get(&tile).is_some());
}
}
#[test]
fn hex_maze_edge_cases() {
let mut maze = Maze::new();
let non_existent = Hex::new(10, 10);
// Operations on non-existent tiles should not panic
let _ = maze.add_tile_wall(&non_existent, EdgeDirection::FLAT_NORTH);
let _ = maze.remove_tile_wall(&non_existent, EdgeDirection::FLAT_NORTH);
assert!(maze.get(&non_existent).is_none());
assert!(maze.get_walls(&non_existent).is_none());
}

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));
}
}