├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── benches ├── comparison_bench.rs └── single_bench.rs ├── examples ├── benchmark_runner.rs ├── heuristic_factor.rs ├── multiple_goals.rs ├── paths_and_waypoints.rs ├── simple_4.rs └── simple_8.rs ├── grid_pathfinding_benchmark ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── src ├── astar_jps.rs └── lib.rs └── tests └── fuzz_test.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose --lib 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea 3 | scenarios/ 4 | maps/ -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anes" 16 | version = "0.1.6" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" 19 | 20 | [[package]] 21 | name = "atty" 22 | version = "0.2.14" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 25 | dependencies = [ 26 | "hermit-abi", 27 | "libc", 28 | "winapi", 29 | ] 30 | 31 | [[package]] 32 | name = "autocfg" 33 | version = "1.4.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 36 | 37 | [[package]] 38 | name = "base64" 39 | version = "0.13.1" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 42 | 43 | [[package]] 44 | name = "bitflags" 45 | version = "1.3.2" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 48 | 49 | [[package]] 50 | name = "bumpalo" 51 | version = "3.16.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 54 | 55 | [[package]] 56 | name = "byteorder" 57 | version = "1.5.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 60 | 61 | [[package]] 62 | name = "cast" 63 | version = "0.3.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" 66 | 67 | [[package]] 68 | name = "cfg-if" 69 | version = "1.0.0" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 72 | 73 | [[package]] 74 | name = "ciborium" 75 | version = "0.2.2" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 78 | dependencies = [ 79 | "ciborium-io", 80 | "ciborium-ll", 81 | "serde", 82 | ] 83 | 84 | [[package]] 85 | name = "ciborium-io" 86 | version = "0.2.2" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 89 | 90 | [[package]] 91 | name = "ciborium-ll" 92 | version = "0.2.2" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 95 | dependencies = [ 96 | "ciborium-io", 97 | "half", 98 | ] 99 | 100 | [[package]] 101 | name = "clap" 102 | version = "3.2.25" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" 105 | dependencies = [ 106 | "bitflags", 107 | "clap_lex", 108 | "indexmap 1.9.3", 109 | "textwrap", 110 | ] 111 | 112 | [[package]] 113 | name = "clap_lex" 114 | version = "0.2.4" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 117 | dependencies = [ 118 | "os_str_bytes", 119 | ] 120 | 121 | [[package]] 122 | name = "criterion" 123 | version = "0.4.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" 126 | dependencies = [ 127 | "anes", 128 | "atty", 129 | "cast", 130 | "ciborium", 131 | "clap", 132 | "criterion-plot", 133 | "itertools 0.10.5", 134 | "lazy_static", 135 | "num-traits", 136 | "oorandom", 137 | "plotters", 138 | "rayon", 139 | "regex", 140 | "serde", 141 | "serde_derive", 142 | "serde_json", 143 | "tinytemplate", 144 | "walkdir", 145 | ] 146 | 147 | [[package]] 148 | name = "criterion-plot" 149 | version = "0.5.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" 152 | dependencies = [ 153 | "cast", 154 | "itertools 0.10.5", 155 | ] 156 | 157 | [[package]] 158 | name = "crossbeam-deque" 159 | version = "0.8.6" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 162 | dependencies = [ 163 | "crossbeam-epoch", 164 | "crossbeam-utils", 165 | ] 166 | 167 | [[package]] 168 | name = "crossbeam-epoch" 169 | version = "0.9.18" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 172 | dependencies = [ 173 | "crossbeam-utils", 174 | ] 175 | 176 | [[package]] 177 | name = "crossbeam-utils" 178 | version = "0.8.21" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 181 | 182 | [[package]] 183 | name = "crunchy" 184 | version = "0.2.2" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 187 | 188 | [[package]] 189 | name = "csv" 190 | version = "1.3.1" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" 193 | dependencies = [ 194 | "csv-core", 195 | "itoa", 196 | "ryu", 197 | "serde", 198 | ] 199 | 200 | [[package]] 201 | name = "csv-core" 202 | version = "0.1.11" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" 205 | dependencies = [ 206 | "memchr", 207 | ] 208 | 209 | [[package]] 210 | name = "either" 211 | version = "1.13.0" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 214 | 215 | [[package]] 216 | name = "equivalent" 217 | version = "1.0.1" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 220 | 221 | [[package]] 222 | name = "fixedbitset" 223 | version = "0.4.2" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" 226 | 227 | [[package]] 228 | name = "fxhash" 229 | version = "0.2.1" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 232 | dependencies = [ 233 | "byteorder", 234 | ] 235 | 236 | [[package]] 237 | name = "getrandom" 238 | version = "0.2.15" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 241 | dependencies = [ 242 | "cfg-if", 243 | "libc", 244 | "wasi", 245 | ] 246 | 247 | [[package]] 248 | name = "grid_pathfinding" 249 | version = "0.2.1" 250 | dependencies = [ 251 | "criterion", 252 | "fxhash", 253 | "grid_pathfinding_benchmark", 254 | "grid_util", 255 | "indexmap 2.7.0", 256 | "itertools 0.13.0", 257 | "log", 258 | "num-traits", 259 | "petgraph", 260 | "rand", 261 | "smallvec", 262 | ] 263 | 264 | [[package]] 265 | name = "grid_pathfinding_benchmark" 266 | version = "0.1.0" 267 | dependencies = [ 268 | "criterion", 269 | "csv", 270 | "grid_util", 271 | "serde", 272 | "walkdir", 273 | ] 274 | 275 | [[package]] 276 | name = "grid_util" 277 | version = "0.1.1" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "2b7f09fe015ff50963bc5b9810f73e54d69c8770b1986f3c8c475ba04a347662" 280 | dependencies = [ 281 | "num_enum", 282 | "rand", 283 | "rand_derive2", 284 | "ron", 285 | "serde", 286 | "strum", 287 | "strum_macros", 288 | ] 289 | 290 | [[package]] 291 | name = "half" 292 | version = "2.4.1" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" 295 | dependencies = [ 296 | "cfg-if", 297 | "crunchy", 298 | ] 299 | 300 | [[package]] 301 | name = "hashbrown" 302 | version = "0.12.3" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 305 | 306 | [[package]] 307 | name = "hashbrown" 308 | version = "0.15.2" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 311 | 312 | [[package]] 313 | name = "heck" 314 | version = "0.3.3" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 317 | dependencies = [ 318 | "unicode-segmentation", 319 | ] 320 | 321 | [[package]] 322 | name = "hermit-abi" 323 | version = "0.1.19" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 326 | dependencies = [ 327 | "libc", 328 | ] 329 | 330 | [[package]] 331 | name = "indexmap" 332 | version = "1.9.3" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 335 | dependencies = [ 336 | "autocfg", 337 | "hashbrown 0.12.3", 338 | ] 339 | 340 | [[package]] 341 | name = "indexmap" 342 | version = "2.7.0" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" 345 | dependencies = [ 346 | "equivalent", 347 | "hashbrown 0.15.2", 348 | ] 349 | 350 | [[package]] 351 | name = "itertools" 352 | version = "0.10.5" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 355 | dependencies = [ 356 | "either", 357 | ] 358 | 359 | [[package]] 360 | name = "itertools" 361 | version = "0.13.0" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 364 | dependencies = [ 365 | "either", 366 | ] 367 | 368 | [[package]] 369 | name = "itoa" 370 | version = "1.0.14" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 373 | 374 | [[package]] 375 | name = "js-sys" 376 | version = "0.3.76" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" 379 | dependencies = [ 380 | "once_cell", 381 | "wasm-bindgen", 382 | ] 383 | 384 | [[package]] 385 | name = "lazy_static" 386 | version = "1.5.0" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 389 | 390 | [[package]] 391 | name = "libc" 392 | version = "0.2.169" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 395 | 396 | [[package]] 397 | name = "log" 398 | version = "0.4.22" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 401 | 402 | [[package]] 403 | name = "memchr" 404 | version = "2.7.4" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 407 | 408 | [[package]] 409 | name = "num-traits" 410 | version = "0.2.19" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 413 | dependencies = [ 414 | "autocfg", 415 | ] 416 | 417 | [[package]] 418 | name = "num_enum" 419 | version = "0.5.11" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" 422 | dependencies = [ 423 | "num_enum_derive", 424 | ] 425 | 426 | [[package]] 427 | name = "num_enum_derive" 428 | version = "0.5.11" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" 431 | dependencies = [ 432 | "proc-macro-crate", 433 | "proc-macro2", 434 | "quote", 435 | "syn 1.0.109", 436 | ] 437 | 438 | [[package]] 439 | name = "once_cell" 440 | version = "1.20.2" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 443 | 444 | [[package]] 445 | name = "oorandom" 446 | version = "11.1.4" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" 449 | 450 | [[package]] 451 | name = "os_str_bytes" 452 | version = "6.6.1" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" 455 | 456 | [[package]] 457 | name = "petgraph" 458 | version = "0.6.5" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" 461 | dependencies = [ 462 | "fixedbitset", 463 | "indexmap 2.7.0", 464 | ] 465 | 466 | [[package]] 467 | name = "plotters" 468 | version = "0.3.7" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" 471 | dependencies = [ 472 | "num-traits", 473 | "plotters-backend", 474 | "plotters-svg", 475 | "wasm-bindgen", 476 | "web-sys", 477 | ] 478 | 479 | [[package]] 480 | name = "plotters-backend" 481 | version = "0.3.7" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" 484 | 485 | [[package]] 486 | name = "plotters-svg" 487 | version = "0.3.7" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" 490 | dependencies = [ 491 | "plotters-backend", 492 | ] 493 | 494 | [[package]] 495 | name = "ppv-lite86" 496 | version = "0.2.20" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 499 | dependencies = [ 500 | "zerocopy", 501 | ] 502 | 503 | [[package]] 504 | name = "proc-macro-crate" 505 | version = "1.3.1" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" 508 | dependencies = [ 509 | "once_cell", 510 | "toml_edit", 511 | ] 512 | 513 | [[package]] 514 | name = "proc-macro2" 515 | version = "1.0.92" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 518 | dependencies = [ 519 | "unicode-ident", 520 | ] 521 | 522 | [[package]] 523 | name = "proc_macro2_helper" 524 | version = "0.2.10" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "79528bef70da112116feb5ecb6b64f1394e5360660d6474a760789ea07885501" 527 | dependencies = [ 528 | "proc-macro2", 529 | "syn 2.0.94", 530 | ] 531 | 532 | [[package]] 533 | name = "quote" 534 | version = "1.0.38" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 537 | dependencies = [ 538 | "proc-macro2", 539 | ] 540 | 541 | [[package]] 542 | name = "rand" 543 | version = "0.8.5" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 546 | dependencies = [ 547 | "libc", 548 | "rand_chacha", 549 | "rand_core", 550 | ] 551 | 552 | [[package]] 553 | name = "rand_chacha" 554 | version = "0.3.1" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 557 | dependencies = [ 558 | "ppv-lite86", 559 | "rand_core", 560 | ] 561 | 562 | [[package]] 563 | name = "rand_core" 564 | version = "0.6.4" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 567 | dependencies = [ 568 | "getrandom", 569 | ] 570 | 571 | [[package]] 572 | name = "rand_derive2" 573 | version = "0.1.21" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "7e07d80c051ce2007c5cbb87ae0a6be7c5e5345cb03e06717b64ed5c426cbfb3" 576 | dependencies = [ 577 | "proc-macro2", 578 | "proc_macro2_helper", 579 | "quote", 580 | "syn 2.0.94", 581 | ] 582 | 583 | [[package]] 584 | name = "rayon" 585 | version = "1.10.0" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" 588 | dependencies = [ 589 | "either", 590 | "rayon-core", 591 | ] 592 | 593 | [[package]] 594 | name = "rayon-core" 595 | version = "1.12.1" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" 598 | dependencies = [ 599 | "crossbeam-deque", 600 | "crossbeam-utils", 601 | ] 602 | 603 | [[package]] 604 | name = "regex" 605 | version = "1.11.1" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 608 | dependencies = [ 609 | "aho-corasick", 610 | "memchr", 611 | "regex-automata", 612 | "regex-syntax", 613 | ] 614 | 615 | [[package]] 616 | name = "regex-automata" 617 | version = "0.4.9" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 620 | dependencies = [ 621 | "aho-corasick", 622 | "memchr", 623 | "regex-syntax", 624 | ] 625 | 626 | [[package]] 627 | name = "regex-syntax" 628 | version = "0.8.5" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 631 | 632 | [[package]] 633 | name = "ron" 634 | version = "0.6.6" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "86018df177b1beef6c7c8ef949969c4f7cb9a9344181b92486b23c79995bdaa4" 637 | dependencies = [ 638 | "base64", 639 | "bitflags", 640 | "serde", 641 | ] 642 | 643 | [[package]] 644 | name = "ryu" 645 | version = "1.0.18" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 648 | 649 | [[package]] 650 | name = "same-file" 651 | version = "1.0.6" 652 | source = "registry+https://github.com/rust-lang/crates.io-index" 653 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 654 | dependencies = [ 655 | "winapi-util", 656 | ] 657 | 658 | [[package]] 659 | name = "serde" 660 | version = "1.0.217" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 663 | dependencies = [ 664 | "serde_derive", 665 | ] 666 | 667 | [[package]] 668 | name = "serde_derive" 669 | version = "1.0.217" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 672 | dependencies = [ 673 | "proc-macro2", 674 | "quote", 675 | "syn 2.0.94", 676 | ] 677 | 678 | [[package]] 679 | name = "serde_json" 680 | version = "1.0.134" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" 683 | dependencies = [ 684 | "itoa", 685 | "memchr", 686 | "ryu", 687 | "serde", 688 | ] 689 | 690 | [[package]] 691 | name = "smallvec" 692 | version = "1.13.2" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 695 | 696 | [[package]] 697 | name = "strum" 698 | version = "0.20.0" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c" 701 | 702 | [[package]] 703 | name = "strum_macros" 704 | version = "0.20.1" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149" 707 | dependencies = [ 708 | "heck", 709 | "proc-macro2", 710 | "quote", 711 | "syn 1.0.109", 712 | ] 713 | 714 | [[package]] 715 | name = "syn" 716 | version = "1.0.109" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 719 | dependencies = [ 720 | "proc-macro2", 721 | "quote", 722 | "unicode-ident", 723 | ] 724 | 725 | [[package]] 726 | name = "syn" 727 | version = "2.0.94" 728 | source = "registry+https://github.com/rust-lang/crates.io-index" 729 | checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" 730 | dependencies = [ 731 | "proc-macro2", 732 | "quote", 733 | "unicode-ident", 734 | ] 735 | 736 | [[package]] 737 | name = "textwrap" 738 | version = "0.16.1" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" 741 | 742 | [[package]] 743 | name = "tinytemplate" 744 | version = "1.2.1" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 747 | dependencies = [ 748 | "serde", 749 | "serde_json", 750 | ] 751 | 752 | [[package]] 753 | name = "toml_datetime" 754 | version = "0.6.8" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 757 | 758 | [[package]] 759 | name = "toml_edit" 760 | version = "0.19.15" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" 763 | dependencies = [ 764 | "indexmap 2.7.0", 765 | "toml_datetime", 766 | "winnow", 767 | ] 768 | 769 | [[package]] 770 | name = "unicode-ident" 771 | version = "1.0.14" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 774 | 775 | [[package]] 776 | name = "unicode-segmentation" 777 | version = "1.12.0" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 780 | 781 | [[package]] 782 | name = "walkdir" 783 | version = "2.5.0" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 786 | dependencies = [ 787 | "same-file", 788 | "winapi-util", 789 | ] 790 | 791 | [[package]] 792 | name = "wasi" 793 | version = "0.11.0+wasi-snapshot-preview1" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 796 | 797 | [[package]] 798 | name = "wasm-bindgen" 799 | version = "0.2.99" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" 802 | dependencies = [ 803 | "cfg-if", 804 | "once_cell", 805 | "wasm-bindgen-macro", 806 | ] 807 | 808 | [[package]] 809 | name = "wasm-bindgen-backend" 810 | version = "0.2.99" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" 813 | dependencies = [ 814 | "bumpalo", 815 | "log", 816 | "proc-macro2", 817 | "quote", 818 | "syn 2.0.94", 819 | "wasm-bindgen-shared", 820 | ] 821 | 822 | [[package]] 823 | name = "wasm-bindgen-macro" 824 | version = "0.2.99" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" 827 | dependencies = [ 828 | "quote", 829 | "wasm-bindgen-macro-support", 830 | ] 831 | 832 | [[package]] 833 | name = "wasm-bindgen-macro-support" 834 | version = "0.2.99" 835 | source = "registry+https://github.com/rust-lang/crates.io-index" 836 | checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" 837 | dependencies = [ 838 | "proc-macro2", 839 | "quote", 840 | "syn 2.0.94", 841 | "wasm-bindgen-backend", 842 | "wasm-bindgen-shared", 843 | ] 844 | 845 | [[package]] 846 | name = "wasm-bindgen-shared" 847 | version = "0.2.99" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" 850 | 851 | [[package]] 852 | name = "web-sys" 853 | version = "0.3.76" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" 856 | dependencies = [ 857 | "js-sys", 858 | "wasm-bindgen", 859 | ] 860 | 861 | [[package]] 862 | name = "winapi" 863 | version = "0.3.9" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 866 | dependencies = [ 867 | "winapi-i686-pc-windows-gnu", 868 | "winapi-x86_64-pc-windows-gnu", 869 | ] 870 | 871 | [[package]] 872 | name = "winapi-i686-pc-windows-gnu" 873 | version = "0.4.0" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 876 | 877 | [[package]] 878 | name = "winapi-util" 879 | version = "0.1.9" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 882 | dependencies = [ 883 | "windows-sys", 884 | ] 885 | 886 | [[package]] 887 | name = "winapi-x86_64-pc-windows-gnu" 888 | version = "0.4.0" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 891 | 892 | [[package]] 893 | name = "windows-sys" 894 | version = "0.59.0" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 897 | dependencies = [ 898 | "windows-targets", 899 | ] 900 | 901 | [[package]] 902 | name = "windows-targets" 903 | version = "0.52.6" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 906 | dependencies = [ 907 | "windows_aarch64_gnullvm", 908 | "windows_aarch64_msvc", 909 | "windows_i686_gnu", 910 | "windows_i686_gnullvm", 911 | "windows_i686_msvc", 912 | "windows_x86_64_gnu", 913 | "windows_x86_64_gnullvm", 914 | "windows_x86_64_msvc", 915 | ] 916 | 917 | [[package]] 918 | name = "windows_aarch64_gnullvm" 919 | version = "0.52.6" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 922 | 923 | [[package]] 924 | name = "windows_aarch64_msvc" 925 | version = "0.52.6" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 928 | 929 | [[package]] 930 | name = "windows_i686_gnu" 931 | version = "0.52.6" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 934 | 935 | [[package]] 936 | name = "windows_i686_gnullvm" 937 | version = "0.52.6" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 940 | 941 | [[package]] 942 | name = "windows_i686_msvc" 943 | version = "0.52.6" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 946 | 947 | [[package]] 948 | name = "windows_x86_64_gnu" 949 | version = "0.52.6" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 952 | 953 | [[package]] 954 | name = "windows_x86_64_gnullvm" 955 | version = "0.52.6" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 958 | 959 | [[package]] 960 | name = "windows_x86_64_msvc" 961 | version = "0.52.6" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 964 | 965 | [[package]] 966 | name = "winnow" 967 | version = "0.5.40" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" 970 | dependencies = [ 971 | "memchr", 972 | ] 973 | 974 | [[package]] 975 | name = "zerocopy" 976 | version = "0.7.35" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 979 | dependencies = [ 980 | "byteorder", 981 | "zerocopy-derive", 982 | ] 983 | 984 | [[package]] 985 | name = "zerocopy-derive" 986 | version = "0.7.35" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 989 | dependencies = [ 990 | "proc-macro2", 991 | "quote", 992 | "syn 2.0.94", 993 | ] 994 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "grid_pathfinding" 3 | version = "0.2.1" 4 | authors = ["Thom van der Woude "] 5 | edition = "2021" 6 | description = "Pathfinding using JPS and connected components on a grid." 7 | keywords = ["pathfinding","grid","jump","point","JPS"] 8 | categories = ["game-development","simulation","algorithms"] 9 | license = "MIT" 10 | repository = "https://github.com/tbvanderwoude/grid_pathfinding" 11 | readme = "README.md" 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | petgraph = "0.6" 16 | indexmap = "2.3" 17 | fxhash = "0.2" 18 | itertools = "0.13" 19 | num-traits = "0.2" 20 | grid_util = "0.1" 21 | log = "0.4" 22 | smallvec = "1.13.2" 23 | 24 | [lib] 25 | bench = false 26 | 27 | [dev-dependencies] 28 | criterion = { version = "0.4", features = ["html_reports"] } 29 | grid_pathfinding_benchmark = { path = "grid_pathfinding_benchmark" } 30 | rand = "0.8.5" 31 | 32 | [[bench]] 33 | name = "comparison_bench" 34 | harness = false 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Thom van der Woude 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grid_pathfinding 2 | 3 | A grid-based pathfinding system. Implements [Jump Point Search](https://en.wikipedia.org/wiki/Jump_point_search) with 4 | [improved pruning rules](https://www.researchgate.net/publication/287338108_Improving_jump_point_search) for speedy pathfinding. Pre-computes 5 | [connected components](https://en.wikipedia.org/wiki/Component_(graph_theory)) 6 | to avoid flood-filling behavior if no path exists. Both [4-neighborhood](https://en.wikipedia.org/wiki/Von_Neumann_neighborhood) and [8-neighborhood](https://en.wikipedia.org/wiki/Moore_neighborhood) grids are supported and a custom variant of JPS is implemented for the 4-neighborhood. 7 | 8 | ### Example 9 | Below a [simple 8-grid example](examples/simple_8.rs) is given, illustrating how to set a basic problem and find a path. 10 | ```rust,no_run 11 | use grid_pathfinding::PathingGrid; 12 | use grid_util::grid::Grid; 13 | use grid_util::point::Point; 14 | 15 | // In this example a path is found on a 3x3 grid with shape 16 | // ___ 17 | // |S | 18 | // | # | 19 | // | E| 20 | // ___ 21 | // where 22 | // - # marks an obstacle 23 | // - S marks the start 24 | // - E marks the end 25 | 26 | fn main() { 27 | let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); 28 | pathing_grid.set(1, 1, true); 29 | pathing_grid.generate_components(); 30 | println!("{}", pathing_grid); 31 | let start = Point::new(0, 0); 32 | let end = Point::new(2, 2); 33 | let path = pathing_grid 34 | .get_path_single_goal(start, end, false) 35 | .unwrap(); 36 | println!("Path:"); 37 | for p in path { 38 | println!("{:?}", p); 39 | } 40 | } 41 | ``` 42 | This assumes an 8-neighborhood, which is the default grid type. The same problem can be solved for a 4-neighborhood, disallowing diagonal moves, by adding the line 43 | ```rust,no_run 44 | pathing_grid.allow_diagonal_move = false; 45 | ``` 46 | before component generation, which is done in example [simple_4](examples/simple_4.rs). 47 | 48 | 49 | 50 | See [examples](examples/) for finding paths with multiple goals and generating waypoints instead of full paths. 51 | 52 | ### Benchmarks 53 | The system can be benchmarked using scenarios from the [Moving AI 2D pathfinding benchmarks](https://movingai.com/benchmarks/grids.html). The [grid_pathfinding_benchmark](grid_pathfinding_benchmark) utility crate provides general support for loading these files. The default benchmark executed using `cargo bench` runs three scenario sets from the [Dragon Age: Origins](https://movingai.com/benchmarks/dao/index.html): `dao/arena`, `dao/den312` and `dao/arena2` (or `dao/den009d` when using the rectilinear algorithm). Running these requires the corresponding map and scenario files to be saved in folders called `maps/dao` and `scenarios/dao`. 54 | 55 | A baseline can be set using 56 | ```bash 57 | cargo bench -- --save-baseline main 58 | ``` 59 | New runs can be compared to this baseline using 60 | ```bash 61 | cargo bench -- --baseline main 62 | ``` 63 | 64 | ### Performance 65 | Using an i5-6600 quad-core running at 3.3 GHz, running the `dao/arena2` set of 910 scenarios on a 281x209 grid takes 123 ms using JPS allowing diagonals and with improved pruning disabled. Using default neighbor generation as in normal A* (enabled by setting `GRAPH_PRUNING = false`) makes this take 1.26 s, a factor 10 difference. As a rule, the relative difference increases as maps get larger, with the `dao/arena` set of 130 scenarios on a 49x49 grid taking 721 us and 1.01 ms respectively with and without pruning. 66 | 67 | 68 | An existing C++ [JPS implementation](https://github.com/nathansttt/hog2) runs the same scenarios in about 60 ms. The fastest solver known to the author is the [l1-path-finder](https://mikolalysenko.github.io/l1-path-finder/www/) (implemented in Javascript) which can do this in 38 ms using A* with landmarks (for a 4-neighborhood). 69 | 70 | 71 | ### Goal of crate 72 | The long-term goal of this crate is to provide a fast off-the-shelf pathfinding implementation for grids. -------------------------------------------------------------------------------- /benches/comparison_bench.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | use grid_pathfinding::PathingGrid; 3 | use grid_pathfinding_benchmark::*; 4 | use grid_util::grid::Grid; 5 | 6 | fn dao_bench(c: &mut Criterion) { 7 | for (allow_diag, pruning) in [(true, false), (true, true)] { 8 | let bench_set = if allow_diag { 9 | ["dao/arena", "dao/den312d", "dao/arena2"] 10 | } else { 11 | ["dao/arena", "dao/den009d", "dao/den312d"] 12 | }; 13 | for name in bench_set { 14 | let (bool_grid, scenarios) = get_benchmark(name.to_owned()); 15 | let mut pathing_grid: PathingGrid = 16 | PathingGrid::new(bool_grid.width, bool_grid.height, true); 17 | pathing_grid.grid = bool_grid.clone(); 18 | pathing_grid.allow_diagonal_move = allow_diag; 19 | pathing_grid.improved_pruning = pruning; 20 | pathing_grid.update_all_neighbours(); 21 | pathing_grid.generate_components(); 22 | let diag_str = if allow_diag { "8-grid" } else { "4-grid" }; 23 | let improved_str = if pruning { " (improved pruning)" } else { "" }; 24 | 25 | c.bench_function(format!("{name}, {diag_str}{improved_str}").as_str(), |b| { 26 | b.iter(|| { 27 | for (start, end) in &scenarios { 28 | black_box(pathing_grid.get_path_single_goal(*start, *end, false)); 29 | } 30 | }) 31 | }); 32 | } 33 | } 34 | } 35 | 36 | criterion_group!(benches, dao_bench); 37 | criterion_main!(benches); 38 | -------------------------------------------------------------------------------- /benches/single_bench.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | use grid_pathfinding::PathingGrid; 3 | use grid_pathfinding_benchmark::*; 4 | use grid_util::grid::Grid; 5 | 6 | fn dao_bench_single(c: &mut Criterion) { 7 | for (allow_diag, pruning) in [(true, false)] { 8 | let bench_set = ["dao/arena"]; 9 | for name in bench_set { 10 | let (bool_grid, scenarios) = get_benchmark(name.to_owned()); 11 | let mut pathing_grid: PathingGrid = 12 | PathingGrid::new(bool_grid.width, bool_grid.height, true); 13 | pathing_grid.grid = bool_grid.clone(); 14 | pathing_grid.allow_diagonal_move = allow_diag; 15 | pathing_grid.improved_pruning = pruning; 16 | pathing_grid.update_all_neighbours(); 17 | pathing_grid.generate_components(); 18 | let diag_str = if allow_diag { "8-grid" } else { "4-grid" }; 19 | let improved_str = if pruning { " (improved pruning)" } else { "" }; 20 | 21 | c.bench_function(format!("{name}, {diag_str}{improved_str}").as_str(), |b| { 22 | b.iter(|| { 23 | for (start, end) in &scenarios { 24 | black_box(pathing_grid.get_path_single_goal(*start, *end, false)); 25 | } 26 | }) 27 | }); 28 | } 29 | } 30 | } 31 | 32 | criterion_group!(benches, dao_bench_single); 33 | criterion_main!(benches); 34 | -------------------------------------------------------------------------------- /examples/benchmark_runner.rs: -------------------------------------------------------------------------------- 1 | use grid_pathfinding::PathingGrid; 2 | use grid_pathfinding_benchmark::*; 3 | use grid_util::grid::Grid; 4 | use grid_util::point::Point; 5 | use std::time::{Duration, Instant}; 6 | 7 | fn main() { 8 | let benchmark_names = get_benchmark_names(); 9 | let mut total_time = Duration::ZERO; 10 | for name in benchmark_names { 11 | println!("Benchmark name: {}", name); 12 | 13 | let (bool_grid, scenarios) = get_benchmark(name); 14 | // for (allow_diag, pruning) in [(false, false), (true, false), (true, true)] { 15 | for (allow_diag, pruning) in [(true, false)] { 16 | let mut pathing_grid: PathingGrid = 17 | PathingGrid::new(bool_grid.width, bool_grid.height, true); 18 | pathing_grid.grid = bool_grid.clone(); 19 | pathing_grid.allow_diagonal_move = allow_diag; 20 | pathing_grid.improved_pruning = pruning; 21 | pathing_grid.update_all_neighbours(); 22 | pathing_grid.generate_components(); 23 | let number_of_scenarios = scenarios.len() as u32; 24 | let before = Instant::now(); 25 | run_scenarios(&pathing_grid, &scenarios); 26 | let elapsed = before.elapsed(); 27 | println!( 28 | "\tElapsed time: {:.2?}; per scenario: {:.2?}", 29 | elapsed, 30 | elapsed / number_of_scenarios 31 | ); 32 | total_time += elapsed; 33 | } 34 | } 35 | println!("\tTotal benchmark time: {:.2?}", total_time); 36 | } 37 | 38 | pub fn run_scenarios(pathing_grid: &PathingGrid, scenarios: &Vec<(Point, Point)>) { 39 | for (start, goal) in scenarios { 40 | let path: Option> = pathing_grid.get_waypoints_single_goal(*start, *goal, false); 41 | assert!(path.is_some()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/heuristic_factor.rs: -------------------------------------------------------------------------------- 1 | use grid_pathfinding::PathingGrid; 2 | use grid_util::grid::Grid; 3 | use grid_util::point::Point; 4 | use grid_util::Rect; 5 | 6 | // The heuristic_factor can be set to scale the heuristic, causing nodes that are closer to the goal (ignoring obstacles) 7 | // to be evaluated quicker than in normal operation. This is called Weighted A* and it can speed up the algorithm in certain scenarios. 8 | 9 | fn main() { 10 | const N: i32 = 30; 11 | let mut pathing_grid: PathingGrid = PathingGrid::new(N as usize, N as usize, true); 12 | pathing_grid.heuristic_factor = 1.3; 13 | pathing_grid.set_rectangle(&Rect::new(1, 1, N - 2, N - 2), false); 14 | pathing_grid.set_rectangle(&Rect::new(8, 8, 8, 8), true); 15 | pathing_grid.set_rectangle(&Rect::new(0, 3, 6, 6), true); 16 | pathing_grid.set_rectangle(&Rect::new(10, 0, 6, 6), true); 17 | pathing_grid.generate_components(); 18 | println!("{}", pathing_grid); 19 | let start = Point::new(1, 1); 20 | let end = Point::new(N - 3, N - 3); 21 | let path = pathing_grid 22 | .get_path_single_goal(start, end, false) 23 | .unwrap(); 24 | println!("Path:"); 25 | for p in path { 26 | println!("{:?}", p); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/multiple_goals.rs: -------------------------------------------------------------------------------- 1 | use grid_pathfinding::PathingGrid; 2 | use grid_util::grid::Grid; 3 | use grid_util::point::Point; 4 | 5 | // In this example a path is found to one of two goals on a 3x3 grid with shape 6 | // ___ 7 | // |S G| 8 | // | # | 9 | // | G| 10 | // ___ 11 | // where 12 | // - \# marks an obstacle 13 | // - S marks the start 14 | // - G marks a goal 15 | // The found path moves to the closest goal, which is the top one. 16 | 17 | fn main() { 18 | let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); 19 | pathing_grid.set(1, 1, true); 20 | pathing_grid.generate_components(); 21 | println!("{}", pathing_grid); 22 | let start = Point::new(0, 0); 23 | let goal_1 = Point::new(2, 0); 24 | let goal_2 = Point::new(2, 2); 25 | let goals = vec![&goal_1, &goal_2]; 26 | let (selected_goal, path) = pathing_grid.get_path_multiple_goals(start, goals).unwrap(); 27 | println!("Selected goal: {:?}\n", selected_goal); 28 | println!("Path:"); 29 | for p in path { 30 | println!("{:?}", p); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/paths_and_waypoints.rs: -------------------------------------------------------------------------------- 1 | use grid_pathfinding::{waypoints_to_path, PathingGrid}; 2 | use grid_util::grid::Grid; 3 | use grid_util::point::Point; 4 | 5 | // This example illustrates the difference between waypoints and paths. 6 | // A path is found on a 5x5 grid with shape 7 | // ----- 8 | // |S | 9 | // | # | 10 | // | | 11 | // | | 12 | // | E| 13 | // ----- 14 | // where 15 | // - S marks the start 16 | // - E marks the end 17 | // First the waypoints are found using [get_waypoints_single_goal](PathingGrid::get_waypoints_single_goal). 18 | // These are then expanded using [get_waypoints_single_goal](PathingGrid::get_waypoints_single_goal). 19 | // Lastly, [get_path_single_goal](PathingGrid::get_path_single_goal) is used to directly get the 20 | // path, as a shorthand for the two previous calls. 21 | 22 | fn main() { 23 | let mut pathing_grid: PathingGrid = PathingGrid::new(5, 5, false); 24 | pathing_grid.set(1, 1, true); 25 | pathing_grid.generate_components(); 26 | println!("{}", pathing_grid); 27 | let start = Point::new(0, 0); 28 | let end = Point::new(4, 4); 29 | if let Some(path) = pathing_grid.get_waypoints_single_goal(start, end, false) { 30 | println!("Waypoints:"); 31 | for p in &path { 32 | println!("{:?}", p); 33 | } 34 | println!("\nPath generated from waypoints:"); 35 | for p in waypoints_to_path(path) { 36 | println!("{:?}", p); 37 | } 38 | } 39 | println!("\nDirectly computed path"); 40 | let expanded_path = pathing_grid 41 | .get_path_single_goal(start, end, false) 42 | .unwrap(); 43 | for p in expanded_path { 44 | println!("{:?}", p); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/simple_4.rs: -------------------------------------------------------------------------------- 1 | use grid_pathfinding::PathingGrid; 2 | use grid_util::grid::Grid; 3 | use grid_util::point::Point; 4 | 5 | // In this example a path is found on a 3x3 grid with shape 6 | // ___ 7 | // |S | 8 | // | # | 9 | // | E| 10 | // ___ 11 | // where 12 | // - # marks an obstacle 13 | // - S marks the start 14 | // - E marks the end 15 | // 16 | // Nodes have a 4-neighborhood 17 | 18 | fn main() { 19 | let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); 20 | pathing_grid.allow_diagonal_move = false; 21 | pathing_grid.set(1, 1, true); 22 | pathing_grid.generate_components(); 23 | println!("{}", pathing_grid); 24 | let start = Point::new(0, 0); 25 | let end = Point::new(2, 2); 26 | let path = pathing_grid 27 | .get_path_single_goal(start, end, false) 28 | .unwrap(); 29 | println!("Path:"); 30 | for p in path { 31 | println!("{:?}", p); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/simple_8.rs: -------------------------------------------------------------------------------- 1 | use grid_pathfinding::PathingGrid; 2 | use grid_util::grid::Grid; 3 | use grid_util::point::Point; 4 | 5 | // In this example a path is found on a 3x3 grid with shape 6 | // ___ 7 | // |S | 8 | // | # | 9 | // | E| 10 | // ___ 11 | // where 12 | // - # marks an obstacle 13 | // - S marks the start 14 | // - E marks the end 15 | // 16 | // Nodes have an 8-neighborhood 17 | 18 | fn main() { 19 | let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); 20 | pathing_grid.set(1, 1, true); 21 | pathing_grid.generate_components(); 22 | println!("{}", pathing_grid); 23 | let start = Point::new(0, 0); 24 | let end = Point::new(2, 2); 25 | let path = pathing_grid 26 | .get_path_single_goal(start, end, false) 27 | .unwrap(); 28 | println!("Path:"); 29 | for p in path { 30 | println!("{:?}", p); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /grid_pathfinding_benchmark/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "grid_pathfinding_benchmark" 3 | version = "0.1.0" 4 | authors = ["Thom van der Woude "] 5 | edition = "2021" 6 | description = "Helper crate for loading Moving AI pathfinding benchmarks" 7 | keywords = ["pathfinding","grid","benchmark"] 8 | categories = ["game-development","simulation","algorithms"] 9 | license = "MIT" 10 | repository = "https://github.com/tbvanderwoude/grid_pathfinding" 11 | readme = "README.md" 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | grid_util = "0.1.1" 16 | criterion = { version = "0.4", features = ["html_reports"] } 17 | csv = "1.3.0" 18 | serde = "1.0.204" 19 | walkdir = "2.5.0" 20 | -------------------------------------------------------------------------------- /grid_pathfinding_benchmark/README.md: -------------------------------------------------------------------------------- 1 | # grid_pathfinding_benchmark 2 | Helper crate for loading Moving AI pathfinding benchmarks. -------------------------------------------------------------------------------- /grid_pathfinding_benchmark/src/lib.rs: -------------------------------------------------------------------------------- 1 | use csv::ReaderBuilder; 2 | use grid_util::grid::Grid; 3 | use grid_util::point::Point; 4 | use grid_util::BoolGrid; 5 | use serde::Deserialize; 6 | use std::fs::{self, File}; 7 | use std::io::{self, BufRead}; 8 | use std::path::Path; 9 | use walkdir::WalkDir; 10 | 11 | #[allow(unused)] 12 | #[derive(Debug, Deserialize)] 13 | pub struct Scenario { 14 | id: u32, 15 | file_name: String, 16 | w: u32, 17 | h: u32, 18 | x1: u32, 19 | y1: u32, 20 | x2: u32, 21 | y2: u32, 22 | distance: f64, 23 | } 24 | 25 | fn load_benchmark(name: &str) -> (BoolGrid, Vec<(Point, Point)>) { 26 | let map_str = fs::read_to_string(Path::new(&format!("./maps/{}.map", name))) 27 | .expect("Could not read scenario file"); 28 | 29 | let file = File::open(Path::new(&format!("./scenarios/{}.map.scen", name))) 30 | .expect("Could not open scenario file"); 31 | 32 | // Create a buffer reader to read lines 33 | let reader = io::BufReader::new(file); 34 | let mut lines = reader.lines(); 35 | 36 | // Skip the first line 37 | lines.next(); 38 | 39 | // Create a CSV reader with tab delimiter from remaining lines 40 | let remaining_data = lines.collect::, _>>().unwrap().join("\n"); 41 | 42 | let mut csv_reader = ReaderBuilder::new() 43 | .delimiter(b'\t') 44 | .has_headers(false) 45 | // .flexible(true) 46 | .from_reader(remaining_data.as_bytes()); 47 | // Initialize an empty vector to store the parsed data 48 | let mut data_array: Vec<(Point, Point)> = Vec::new(); 49 | 50 | // Iterate over the records in the file 51 | for result in csv_reader.deserialize() { 52 | let record: Scenario = result.expect("Could not parse scenario record"); 53 | let start = Point::new(record.y1 as i32, record.x1 as i32); 54 | let goal = Point::new(record.y2 as i32, record.x2 as i32); 55 | data_array.push((start, goal)); 56 | } 57 | 58 | let lines: Vec<&str> = map_str.lines().collect(); 59 | let parse_line = |line: &str| -> usize { 60 | line.split_once(' ') 61 | .unwrap() 62 | .1 63 | .parse::() 64 | .expect("Could not parse value") 65 | }; 66 | 67 | let w = parse_line(&lines[1]); 68 | let h = parse_line(&lines[2]); 69 | 70 | let offset = 4; 71 | let mut bool_grid: BoolGrid = BoolGrid::new(w, h, false); 72 | for y in 0..bool_grid.height() { 73 | for x in 0..bool_grid.width() { 74 | // Not sure why x, y have to be swapped here... 75 | let tile_val = lines[offset + x].as_bytes()[y]; 76 | let val = ![b'.', b'G'].contains(&tile_val); 77 | bool_grid.set(x, y, val); 78 | } 79 | } 80 | (bool_grid, data_array) 81 | } 82 | 83 | pub fn get_benchmark_names() -> Vec { 84 | let root = Path::new("maps/"); 85 | let root = root 86 | .canonicalize() 87 | .expect("Failed to canonicalize root path"); 88 | let mut names = Vec::new(); 89 | for entry in WalkDir::new(&root).into_iter() { 90 | let path_str = entry.expect("Could not get dir entry"); 91 | let rel_path = path_str 92 | .path() 93 | .strip_prefix(&root) 94 | .unwrap(); 95 | if rel_path.components().count() >= 2{ 96 | let name = rel_path 97 | .to_str() 98 | .unwrap() 99 | .split_once('.') 100 | .unwrap() 101 | .0; 102 | names.push(name.to_owned()); 103 | } 104 | } 105 | names 106 | } 107 | 108 | pub fn get_benchmark(name: String) -> (BoolGrid, Vec<(Point, Point)>) { 109 | let benchmark_names = get_benchmark_names(); 110 | if benchmark_names.contains(&name) { 111 | load_benchmark(name.as_str()) 112 | } else { 113 | panic!("Could not load benchmark!"); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/astar_jps.rs: -------------------------------------------------------------------------------- 1 | use fxhash::FxBuildHasher; 2 | /// This module implements a variant of 3 | /// [pathfinding's astar function](https://docs.rs/pathfinding/latest/pathfinding/directed/astar/index.html) 4 | /// which enables the JPS implementation to generate successors based on the parent if there is one 5 | /// as it should. 6 | use indexmap::map::Entry::{Occupied, Vacant}; 7 | use indexmap::IndexMap; 8 | use num_traits::Zero; 9 | 10 | type FxIndexMap = IndexMap; 11 | 12 | use log::warn; 13 | use std::cmp::Ordering; 14 | use std::collections::BinaryHeap; 15 | 16 | use std::hash::Hash; 17 | 18 | #[derive(Clone, Debug)] 19 | 20 | struct SearchNode { 21 | estimated_cost: K, 22 | cost: K, 23 | index: usize, 24 | } 25 | 26 | impl Eq for SearchNode {} 27 | 28 | impl PartialEq for SearchNode { 29 | fn eq(&self, other: &Self) -> bool { 30 | self.estimated_cost.eq(&other.estimated_cost) && self.cost.eq(&other.cost) 31 | } 32 | } 33 | 34 | impl PartialOrd for SearchNode { 35 | fn partial_cmp(&self, other: &Self) -> Option { 36 | Some(self.cmp(other)) 37 | } 38 | } 39 | 40 | impl Ord for SearchNode { 41 | fn cmp(&self, other: &Self) -> Ordering { 42 | // First orders per estimated cost, then creates subordering 43 | // based on cost, favoring exploration of smallest cost nodes first 44 | match other.estimated_cost.cmp(&self.estimated_cost) { 45 | Ordering::Equal => self.cost.cmp(&other.cost), 46 | // Uncommenting this gives the opposite tie-breaking effect 47 | // Ordering::Equal => other.cost.cmp(&self.cost), 48 | s => s, 49 | } 50 | } 51 | } 52 | 53 | fn reverse_path(parents: &FxIndexMap, mut parent: F, start: usize) -> Vec 54 | where 55 | N: Eq + Hash + Clone, 56 | F: FnMut(&V) -> usize, 57 | { 58 | let mut path: Vec = itertools::unfold(start, |i| { 59 | parents.get_index(*i).map(|(node, value)| { 60 | *i = parent(value); 61 | node.clone() 62 | }) 63 | }) 64 | .collect(); 65 | path.reverse(); 66 | path 67 | } 68 | 69 | /// [AstarContext] represents the search fringe and node parent map, facilitating reuse of memory allocations. 70 | #[derive(Clone, Debug)] 71 | pub struct AstarContext { 72 | fringe: BinaryHeap>, 73 | parents: FxIndexMap, 74 | } 75 | 76 | impl AstarContext 77 | where 78 | N: Eq + Hash + Clone, 79 | C: Zero + Ord + Copy, 80 | { 81 | pub fn new() -> AstarContext { 82 | AstarContext { 83 | fringe: BinaryHeap::new(), 84 | parents: FxIndexMap::default(), 85 | } 86 | } 87 | pub fn astar_jps( 88 | &mut self, 89 | start: &N, 90 | mut successors: FN, 91 | mut heuristic: FH, 92 | mut success: FS, 93 | ) -> Option<(Vec, C)> 94 | where 95 | FN: FnMut(&Option<&N>, &N) -> IN, 96 | IN: IntoIterator, 97 | FH: FnMut(&N) -> C, 98 | FS: FnMut(&N) -> bool, 99 | { 100 | self.fringe.clear(); 101 | self.parents.clear(); 102 | self.fringe.push(SearchNode { 103 | estimated_cost: Zero::zero(), 104 | cost: Zero::zero(), 105 | index: 0, 106 | }); 107 | self.parents 108 | .insert(start.clone(), (usize::MAX, Zero::zero())); 109 | while let Some(SearchNode { cost, index, .. }) = self.fringe.pop() { 110 | let successors = { 111 | let (node, &(parent_index, c)) = self.parents.get_index(index).unwrap(); 112 | if success(node) { 113 | let path = reverse_path(&self.parents, |&(p, _)| p, index); 114 | return Some((path, cost)); 115 | } 116 | // We may have inserted a node several time into the binary heap if we found 117 | // a better way to access it. Ensure that we are currently dealing with the 118 | // best path and discard the others. 119 | if cost > c { 120 | continue; 121 | } 122 | let optional_parent_node = self.parents.get_index(parent_index).map(|x| x.0); 123 | 124 | successors(&optional_parent_node, node) 125 | }; 126 | for (successor, move_cost) in successors { 127 | let new_cost = cost + move_cost; 128 | let h; // heuristic(&successor) 129 | let n; // index for successor 130 | match self.parents.entry(successor) { 131 | Vacant(e) => { 132 | h = heuristic(e.key()); 133 | n = e.index(); 134 | e.insert((index, new_cost)); 135 | } 136 | Occupied(mut e) => { 137 | if e.get().1 > new_cost { 138 | h = heuristic(e.key()); 139 | n = e.index(); 140 | e.insert((index, new_cost)); 141 | } else { 142 | continue; 143 | } 144 | } 145 | } 146 | 147 | self.fringe.push(SearchNode { 148 | estimated_cost: new_cost + h, 149 | cost: new_cost, 150 | index: n, 151 | }); 152 | } 153 | } 154 | warn!("Reachable goal could not be pathed to, is reachable graph correct?"); 155 | None 156 | } 157 | } 158 | 159 | /// Standalone astar_jps function that creates an [AstarContext] object for backward-compatibility. 160 | pub fn astar_jps( 161 | start: &N, 162 | successors: FN, 163 | heuristic: FH, 164 | success: FS, 165 | ) -> Option<(Vec, C)> 166 | where 167 | N: Eq + Hash + Clone, 168 | C: Zero + Ord + Copy, 169 | FN: FnMut(&Option<&N>, &N) -> IN, 170 | IN: IntoIterator, 171 | FH: FnMut(&N) -> C, 172 | FS: FnMut(&N) -> bool, 173 | { 174 | let mut search = AstarContext::new(); 175 | search.astar_jps(start, successors, heuristic, success) 176 | } 177 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # grid_pathfinding 2 | //! 3 | //! A grid-based pathfinding system. Implements 4 | //! [Jump Point Search](https://en.wikipedia.org/wiki/Jump_point_search) with 5 | //! [improved pruning rules](https://www.researchgate.net/publication/287338108_Improving_jump_point_search) 6 | //! for speedy 7 | //! pathfinding. Note that this assumes a uniform-cost grid. Pre-computes 8 | //! [connected components](https://en.wikipedia.org/wiki/Component_(graph_theory)) 9 | //! to avoid flood-filling behaviour if no path exists. 10 | mod astar_jps; 11 | use astar_jps::AstarContext; 12 | use core::fmt; 13 | use grid_util::direction::Direction; 14 | use grid_util::grid::{BoolGrid, Grid, SimpleGrid}; 15 | use grid_util::point::Point; 16 | use petgraph::unionfind::UnionFind; 17 | use smallvec::SmallVec; 18 | use std::collections::VecDeque; 19 | use std::sync::{Arc, Mutex}; 20 | 21 | const EQUAL_EDGE_COST: bool = false; 22 | const GRAPH_PRUNING: bool = true; 23 | const N_SMALLVEC_SIZE: usize = 8; 24 | 25 | // Costs for diagonal and cardinal moves. 26 | // Values for unequal costs approximating a ratio D/C of sqrt(2) are from 27 | // https://github.com/riscy/a_star_on_grids 28 | const D: i32 = if EQUAL_EDGE_COST { 1 } else { 99 }; 29 | const C: i32 = if EQUAL_EDGE_COST { 1 } else { 70 }; 30 | const E: i32 = 2 * C - D; 31 | 32 | /// Helper function for debugging binary representations of neighborhoods. 33 | pub fn explain_bin_neighborhood(nn: u8) { 34 | for i in 0..8_i32 { 35 | let x = nn & (1 << i) != 0; 36 | let dir = Direction::try_from(i.rem_euclid(8)).unwrap(); 37 | if x { 38 | println!("\t\t {dir:?}"); 39 | } 40 | } 41 | } 42 | 43 | /// Turns waypoints into a path on the grid which can be followed step by step. Due to symmetry this 44 | /// is typically one of many ways to follow the waypoints. 45 | pub fn waypoints_to_path(waypoints: Vec) -> Vec { 46 | let mut waypoint_queue = waypoints.into_iter().collect::>(); 47 | let mut path: Vec = Vec::new(); 48 | let mut current = waypoint_queue.pop_front().unwrap(); 49 | path.push(current); 50 | for next in waypoint_queue { 51 | while current.move_distance(&next) >= 1 { 52 | let delta = current.dir(&next); 53 | current = current + delta; 54 | path.push(current); 55 | } 56 | } 57 | path 58 | } 59 | 60 | /// [PathingGrid] maintains information about components using a [UnionFind] structure in addition to the raw 61 | /// [bool] grid values in the [BoolGrid] that determine whether a space is occupied ([true]) or 62 | /// empty ([false]). It also records neighbours in [u8] format for fast lookups during search. 63 | /// Implements [Grid] by building on [BoolGrid]. 64 | #[derive(Clone, Debug)] 65 | pub struct PathingGrid { 66 | pub grid: BoolGrid, 67 | pub neighbours: SimpleGrid, 68 | pub components: UnionFind, 69 | pub components_dirty: bool, 70 | pub heuristic_factor: f32, 71 | pub improved_pruning: bool, 72 | pub allow_diagonal_move: bool, 73 | context: Arc>>, 74 | } 75 | 76 | impl Default for PathingGrid { 77 | fn default() -> PathingGrid { 78 | PathingGrid { 79 | grid: BoolGrid::default(), 80 | neighbours: SimpleGrid::default(), 81 | components: UnionFind::new(0), 82 | components_dirty: false, 83 | improved_pruning: true, 84 | heuristic_factor: 1.0, 85 | allow_diagonal_move: true, 86 | context: Arc::new(Mutex::new(AstarContext::new())), 87 | } 88 | } 89 | } 90 | impl PathingGrid { 91 | fn neighborhood_points(&self, point: &Point) -> Vec { 92 | if self.allow_diagonal_move { 93 | point.moore_neighborhood() 94 | } else { 95 | point.neumann_neighborhood() 96 | } 97 | } 98 | fn neighborhood_points_and_cost( 99 | &self, 100 | pos: &Point, 101 | ) -> SmallVec<[(Point, i32); N_SMALLVEC_SIZE]> { 102 | self.neighborhood_points(pos) 103 | .into_iter() 104 | .filter(|p| self.can_move_to(*p)) 105 | // See comment in pruned_neighborhood about cost calculation 106 | .map(move |p| (p, (pos.dir_obj(&p).num() % 2) * (D - C) + C)) 107 | .collect::>() 108 | } 109 | /// Uses C as cost for cardinal (straight) moves and D for diagonal moves. 110 | pub fn heuristic(&self, p1: &Point, p2: &Point) -> i32 { 111 | if self.allow_diagonal_move { 112 | let delta_x = (p1.x - p2.x).abs(); 113 | let delta_y = (p1.y - p2.y).abs(); 114 | // Formula from https://github.com/riscy/a_star_on_grids 115 | // to efficiently compute the cost of a path taking the maximal amount 116 | // of diagonal steps before going straight 117 | (E * (delta_x - delta_y).abs() + D * (delta_x + delta_y)) / 2 118 | } else { 119 | p1.manhattan_distance(p2) * C 120 | } 121 | } 122 | fn can_move_to(&self, pos: Point) -> bool { 123 | self.in_bounds(pos.x, pos.y) && !self.grid.get(pos.x as usize, pos.y as usize) 124 | } 125 | fn in_bounds(&self, x: i32, y: i32) -> bool { 126 | x >= 0 && y >= 0 && self.grid.index_in_bounds(x as usize, y as usize) 127 | } 128 | /// The neighbour indexing used here corresponds to that used in [grid_util::Direction]. 129 | fn indexed_neighbor(&self, node: &Point, index: i32) -> bool { 130 | (self.neighbours.get_point(*node) & 1 << (index.rem_euclid(8))) != 0 131 | } 132 | fn is_forced(&self, dir: Direction, node: &Point) -> bool { 133 | let dir_num = dir.num(); 134 | if dir.diagonal() { 135 | !self.indexed_neighbor(node, 3 + dir_num) || !self.indexed_neighbor(node, 5 + dir_num) 136 | } else { 137 | !self.indexed_neighbor(node, 2 + dir_num) || !self.indexed_neighbor(node, 6 + dir_num) 138 | } 139 | } 140 | 141 | fn pruned_neighborhood<'a>( 142 | &self, 143 | dir: Direction, 144 | node: &'a Point, 145 | ) -> impl Iterator + 'a { 146 | let dir_num = dir.num(); 147 | let mut n_mask: u8; 148 | let mut neighbours = self.neighbours.get_point(*node); 149 | if !self.allow_diagonal_move { 150 | neighbours &= 0b01010101; 151 | n_mask = 0b01000101_u8.rotate_left(dir_num as u32); 152 | } else if dir.diagonal() { 153 | n_mask = 0b10000011_u8.rotate_left(dir_num as u32); 154 | if !self.indexed_neighbor(node, 3 + dir_num) { 155 | n_mask |= 1 << ((dir_num + 2) % 8); 156 | } 157 | if !self.indexed_neighbor(node, 5 + dir_num) { 158 | n_mask |= 1 << ((dir_num + 6) % 8); 159 | } 160 | } else { 161 | n_mask = 0b00000001 << dir_num; 162 | if !self.indexed_neighbor(node, 2 + dir_num) { 163 | n_mask |= 1 << ((dir_num + 1) % 8); 164 | } 165 | if !self.indexed_neighbor(node, 6 + dir_num) { 166 | n_mask |= 1 << ((dir_num + 7) % 8); 167 | } 168 | } 169 | let comb_mask = neighbours & n_mask; 170 | (0..8) 171 | .step_by(if self.allow_diagonal_move { 1 } else { 2 }) 172 | .filter(move |x| comb_mask & (1 << *x) != 0) 173 | // (dir_num % 2) * (D-C) + C) 174 | // is an optimized version without a conditional of 175 | // if dir.diagonal() {D} else {C} 176 | .map(move |d| (node.moore_neighbor(d), (dir_num % 2) * (D - C) + C)) 177 | } 178 | 179 | /// Straight jump in a cardinal direction. 180 | fn jump_straight( 181 | &self, 182 | mut initial: Point, 183 | mut cost: i32, 184 | direction: Direction, 185 | goal: &F, 186 | ) -> Option<(Point, i32)> 187 | where 188 | F: Fn(&Point) -> bool, 189 | { 190 | debug_assert!(!direction.diagonal()); 191 | loop { 192 | initial = initial + direction; 193 | if !self.can_move_to(initial) { 194 | return None; 195 | } 196 | 197 | if goal(&initial) || self.is_forced(direction, &initial) { 198 | return Some((initial, cost)); 199 | } 200 | 201 | // Straight jumps always take cardinal cost 202 | cost += C; 203 | } 204 | } 205 | 206 | /// Performs the jumping of node neighbours, skipping over unnecessary nodes until a goal or a forced node is found. 207 | fn jump( 208 | &self, 209 | mut initial: Point, 210 | mut cost: i32, 211 | direction: Direction, 212 | goal: &F, 213 | ) -> Option<(Point, i32)> 214 | where 215 | F: Fn(&Point) -> bool, 216 | { 217 | loop { 218 | initial = initial + direction; 219 | if !self.can_move_to(initial) { 220 | return None; 221 | } 222 | 223 | if goal(&initial) || self.is_forced(direction, &initial) { 224 | return Some((initial, cost)); 225 | } 226 | if direction.diagonal() 227 | && (self 228 | .jump_straight(initial, 1, direction.x_dir(), goal) 229 | .is_some() 230 | || self 231 | .jump_straight(initial, 1, direction.y_dir(), goal) 232 | .is_some()) 233 | { 234 | return Some((initial, cost)); 235 | } 236 | 237 | // When using a 4-neighborhood (specified by setting allow_diagonal_move to false), 238 | // jumps perpendicular to the direction are performed. This is necessary to not miss the 239 | // goal when passing by. 240 | if !self.allow_diagonal_move { 241 | let perp_1 = direction.rotate_ccw(2); 242 | let perp_2 = direction.rotate_cw(2); 243 | if self.jump_straight(initial, 1, perp_1, goal).is_some() 244 | || self.jump_straight(initial, 1, perp_2, goal).is_some() 245 | { 246 | return Some((initial, cost)); 247 | } 248 | } 249 | 250 | // See comment in pruned_neighborhood about cost calculation 251 | cost += (direction.num() % 2) * (D - C) + C; 252 | } 253 | } 254 | 255 | /// Updates the neighbours grid after changing the grid. 256 | fn update_neighbours(&mut self, x: i32, y: i32, blocked: bool) { 257 | let p = Point::new(x, y); 258 | for i in 0..8 { 259 | let neighbor = p.moore_neighbor(i); 260 | if self.in_bounds(neighbor.x, neighbor.y) { 261 | let ix = (i + 4) % 8; 262 | let mut n_mask = self.neighbours.get_point(neighbor); 263 | if blocked { 264 | n_mask &= !(1 << ix); 265 | } else { 266 | n_mask |= 1 << ix; 267 | } 268 | self.neighbours.set_point(neighbor, n_mask); 269 | } 270 | } 271 | } 272 | fn jps_neighbours( 273 | &self, 274 | parent: Option<&Point>, 275 | node: &Point, 276 | goal: &F, 277 | ) -> SmallVec<[(Point, i32); N_SMALLVEC_SIZE]> 278 | where 279 | F: Fn(&Point) -> bool, 280 | { 281 | match parent { 282 | Some(parent_node) => { 283 | let mut succ = SmallVec::new(); 284 | let dir = parent_node.dir_obj(node); 285 | for (n, c) in self.pruned_neighborhood(dir, node) { 286 | let dir = node.dir_obj(&n); 287 | // Jumps the neighbor, skipping over unnecessary nodes. 288 | if let Some((jumped_node, cost)) = self.jump(*node, c, dir, goal) { 289 | // If improved pruning is enabled, expand any diagonal unforced nodes 290 | if self.improved_pruning 291 | && dir.diagonal() 292 | && !goal(&jumped_node) 293 | && !self.is_forced(dir, &jumped_node) 294 | { 295 | // Recursively expand the unforced diagonal node 296 | let jump_points = self.jps_neighbours(parent, &jumped_node, goal); 297 | 298 | // Extend the successors with the neighbours of the unforced node, correcting the 299 | // cost to include the cost from parent_node to jumped_node 300 | succ.extend(jump_points.into_iter().map(|(p, c)| (p, c + cost))); 301 | } else { 302 | succ.push((jumped_node, cost)); 303 | } 304 | } 305 | } 306 | succ 307 | } 308 | None => { 309 | // For the starting node, just generate the full normal neighborhood without any pruning or jumping. 310 | self.neighborhood_points_and_cost(node) 311 | } 312 | } 313 | } 314 | /// Retrieves the component id a given [Point] belongs to. 315 | pub fn get_component(&self, point: &Point) -> usize { 316 | self.components.find(self.get_ix_point(point)) 317 | } 318 | /// Checks if start and goal are on the same component. 319 | pub fn reachable(&self, start: &Point, goal: &Point) -> bool { 320 | !self.unreachable(start, goal) 321 | } 322 | 323 | /// Checks if start and goal are not on the same component. 324 | pub fn unreachable(&self, start: &Point, goal: &Point) -> bool { 325 | if self.in_bounds(start.x, start.y) && self.in_bounds(goal.x, goal.y) { 326 | let start_ix = self.get_ix_point(start); 327 | let goal_ix = self.get_ix_point(goal); 328 | !self.components.equiv(start_ix, goal_ix) 329 | } else { 330 | true 331 | } 332 | } 333 | 334 | /// Checks if any neighbour of the goal is on the same component as the start. 335 | pub fn neighbours_reachable(&self, start: &Point, goal: &Point) -> bool { 336 | if self.in_bounds(start.x, start.y) && self.in_bounds(goal.x, goal.y) { 337 | let start_ix = self.get_ix_point(start); 338 | let neighborhood = self.neighborhood_points(goal); 339 | neighborhood.iter().any(|p| { 340 | self.in_bounds(p.x, p.y) && self.components.equiv(start_ix, self.get_ix_point(p)) 341 | }) 342 | } else { 343 | true 344 | } 345 | } 346 | 347 | /// Checks if every neighbour of the goal is on a different component as the start. 348 | pub fn neighbours_unreachable(&self, start: &Point, goal: &Point) -> bool { 349 | if self.in_bounds(start.x, start.y) && self.in_bounds(goal.x, goal.y) { 350 | let start_ix = self.get_ix_point(start); 351 | let neighborhood = self.neighborhood_points(goal); 352 | neighborhood.iter().all(|p| { 353 | !self.in_bounds(p.x, p.y) || !self.components.equiv(start_ix, self.get_ix_point(p)) 354 | }) 355 | } else { 356 | true 357 | } 358 | } 359 | /// Computes a path from start to goal using JPS. If approximate is [true], then it will 360 | /// path to one of the neighbours of the goal, which is useful if the goal itself is 361 | /// blocked. If diagonals are allowed, the heuristic used computes the path cost 362 | /// of taking the maximal number of diagonal moves before continuing straight. If diagonals are not allowed, the [Manhattan distance](https://en.wikipedia.org/wiki/Taxicab_geometry) 363 | /// is used instead (see [heuristic](Self::heuristic)). This can be 364 | /// specified by setting [allow_diagonal_move](Self::allow_diagonal_move). 365 | /// The heuristic will be scaled by [heuristic_factor](Self::heuristic_factor) which can be used to trade optimality for faster solving for many practical problems, a technique 366 | /// called Weighted A*. In pathfinding language, a factor greater than 367 | /// 1.0 will make the heuristic [inadmissible](https://en.wikipedia.org/wiki/Admissible_heuristic), a requirement for solution optimality. By default, 368 | /// the [heuristic_factor](Self::heuristic_factor) is 1.0 which gives optimal solutions. 369 | pub fn get_path_single_goal( 370 | &self, 371 | start: Point, 372 | goal: Point, 373 | approximate: bool, 374 | ) -> Option> { 375 | self.get_waypoints_single_goal(start, goal, approximate) 376 | .map(waypoints_to_path) 377 | } 378 | 379 | /// Computes a path from the start to one of the given goals and returns the selected goal in addition to the found path. Otherwise behaves similar to [get_path_single_goal](Self::get_path_single_goal). 380 | pub fn get_path_multiple_goals( 381 | &self, 382 | start: Point, 383 | goals: Vec<&Point>, 384 | ) -> Option<(Point, Vec)> { 385 | self.get_waypoints_multiple_goals(start, goals) 386 | .map(|(x, y)| (x, waypoints_to_path(y))) 387 | } 388 | /// The raw waypoints (jump points) from which [get_path_multiple_goals](Self::get_path_multiple_goals) makes a path. 389 | pub fn get_waypoints_multiple_goals( 390 | &self, 391 | start: Point, 392 | goals: Vec<&Point>, 393 | ) -> Option<(Point, Vec)> { 394 | if goals.is_empty() { 395 | return None; 396 | } 397 | let mut ct = self.context.lock().unwrap(); 398 | let result = ct.astar_jps( 399 | &start, 400 | |parent, node| { 401 | if GRAPH_PRUNING { 402 | self.jps_neighbours(*parent, node, &|node_pos| goals.contains(&node_pos)) 403 | } else { 404 | self.neighborhood_points_and_cost(node) 405 | } 406 | }, 407 | |point| { 408 | (goals 409 | .iter() 410 | .map(|x| self.heuristic(point, x)) 411 | .min() 412 | .unwrap() as f32 413 | * self.heuristic_factor) as i32 414 | }, 415 | |point| goals.contains(&point), 416 | ); 417 | result.map(|(v, _c)| (*v.last().unwrap(), v)) 418 | } 419 | /// The raw waypoints (jump points) from which [get_path_single_goal](Self::get_path_single_goal) makes a path. 420 | pub fn get_waypoints_single_goal( 421 | &self, 422 | start: Point, 423 | goal: Point, 424 | approximate: bool, 425 | ) -> Option> { 426 | if approximate { 427 | // Check if start and one of the goal neighbours are on the same connected component. 428 | if self.neighbours_unreachable(&start, &goal) { 429 | // No neigbhours of the goal are reachable from the start 430 | return None; 431 | } 432 | // A neighbour of the goal can be reached, compute a path 433 | let mut ct = self.context.lock().unwrap(); 434 | ct.astar_jps( 435 | &start, 436 | |parent, node| { 437 | if GRAPH_PRUNING { 438 | self.jps_neighbours(*parent, node, &|node_pos| { 439 | self.heuristic(node_pos, &goal) <= if EQUAL_EDGE_COST { 1 } else { 99 } 440 | }) 441 | } else { 442 | self.neighborhood_points_and_cost(node) 443 | } 444 | }, 445 | |point| (self.heuristic(point, &goal) as f32 * self.heuristic_factor) as i32, 446 | |point| self.heuristic(point, &goal) <= if EQUAL_EDGE_COST { 1 } else { 99 }, 447 | ) 448 | } else { 449 | // Check if start and goal are on the same connected component. 450 | if self.unreachable(&start, &goal) { 451 | return None; 452 | } 453 | // The goal is reachable from the start, compute a path 454 | let mut ct = self.context.lock().unwrap(); 455 | ct.astar_jps( 456 | &start, 457 | |parent, node| { 458 | if GRAPH_PRUNING { 459 | self.jps_neighbours(*parent, node, &|node_pos| *node_pos == goal) 460 | } else { 461 | self.neighborhood_points_and_cost(node) 462 | } 463 | }, 464 | |point| (self.heuristic(point, &goal) as f32 * self.heuristic_factor) as i32, 465 | |point| *point == goal, 466 | ) 467 | } 468 | .map(|(v, _c)| v) 469 | } 470 | /// Regenerates the components if they are marked as dirty. 471 | pub fn update(&mut self) { 472 | if self.components_dirty { 473 | // The components are dirty, regenerate them 474 | self.generate_components(); 475 | } 476 | } 477 | 478 | pub fn update_all_neighbours(&mut self) { 479 | for x in 0..self.width() { 480 | for y in 0..self.height() { 481 | self.update_neighbours(x as i32, y as i32, self.get(x, y)); 482 | } 483 | } 484 | } 485 | /// Generates a new [UnionFind] structure and links up grid neighbours to the same components. 486 | pub fn generate_components(&mut self) { 487 | let w = self.grid.width; 488 | let h = self.grid.height; 489 | self.components = UnionFind::new(w * h); 490 | self.components_dirty = false; 491 | for x in 0..w { 492 | for y in 0..h { 493 | if !self.grid.get(x, y) { 494 | let parent_ix = self.grid.get_ix(x, y); 495 | let point = Point::new(x as i32, y as i32); 496 | 497 | if self.allow_diagonal_move { 498 | vec![ 499 | Point::new(point.x, point.y + 1), 500 | Point::new(point.x, point.y - 1), 501 | Point::new(point.x + 1, point.y), 502 | Point::new(point.x + 1, point.y - 1), 503 | Point::new(point.x + 1, point.y), 504 | Point::new(point.x + 1, point.y + 1), 505 | ] 506 | } else { 507 | vec![ 508 | Point::new(point.x, point.y + 1), 509 | Point::new(point.x, point.y - 1), 510 | Point::new(point.x + 1, point.y), 511 | ] 512 | } 513 | .into_iter() 514 | .filter(|p| self.grid.point_in_bounds(*p) && !self.grid.get_point(*p)) 515 | .for_each(|p| { 516 | let ix = self.grid.get_ix(p.x as usize, p.y as usize); 517 | self.components.union(parent_ix, ix); 518 | }); 519 | } 520 | } 521 | } 522 | } 523 | } 524 | impl fmt::Display for PathingGrid { 525 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 526 | writeln!(f, "Grid:")?; 527 | for y in 0..self.grid.height { 528 | let values = (0..self.grid.width) 529 | .map(|x| self.grid.get(x, y) as i32) 530 | .collect::>(); 531 | writeln!(f, "{:?}", values)?; 532 | } 533 | writeln!(f, "\nNeighbours:")?; 534 | for y in 0..self.neighbours.height { 535 | let values = (0..self.neighbours.width) 536 | .map(|x| self.neighbours.get(x, y) as i32) 537 | .collect::>(); 538 | writeln!(f, "{:?}", values)?; 539 | } 540 | Ok(()) 541 | } 542 | } 543 | 544 | impl Grid for PathingGrid { 545 | fn new(width: usize, height: usize, default_value: bool) -> Self { 546 | let mut base_grid = PathingGrid { 547 | grid: BoolGrid::new(width, height, default_value), 548 | neighbours: SimpleGrid::new(width, height, 0b11111111), 549 | components: UnionFind::new(width * height), 550 | components_dirty: false, 551 | improved_pruning: true, 552 | heuristic_factor: 1.0, 553 | allow_diagonal_move: true, 554 | context: Arc::new(Mutex::new(AstarContext::new())), 555 | }; 556 | // Emulates 'placing' of blocked tile around map border to correctly initialize neighbours 557 | // and make behaviour of a map bordered by tiles the same as a borderless map. 558 | for i in -1..=(width as i32) { 559 | base_grid.update_neighbours(i, -1, true); 560 | base_grid.update_neighbours(i, height as i32, true); 561 | } 562 | for j in -1..=(height as i32) { 563 | base_grid.update_neighbours(-1, j, true); 564 | base_grid.update_neighbours(width as i32, j, true); 565 | } 566 | base_grid 567 | } 568 | fn get(&self, x: usize, y: usize) -> bool { 569 | self.grid.get(x, y) 570 | } 571 | /// Updates a position on the grid. Joins newly connected components and flags the components 572 | /// as dirty if components are (potentially) broken apart into multiple. 573 | fn set(&mut self, x: usize, y: usize, blocked: bool) { 574 | let p = Point::new(x as i32, y as i32); 575 | if self.grid.get(x, y) != blocked && blocked { 576 | self.components_dirty = true; 577 | } else { 578 | let p_ix = self.grid.get_ix(x, y); 579 | for p in self.neighborhood_points(&p) { 580 | if self.can_move_to(p) { 581 | self.components 582 | .union(p_ix, self.grid.get_ix(p.x as usize, p.y as usize)); 583 | } 584 | } 585 | } 586 | self.update_neighbours(p.x, p.y, blocked); 587 | self.grid.set(x, y, blocked); 588 | } 589 | fn width(&self) -> usize { 590 | self.grid.width() 591 | } 592 | fn height(&self) -> usize { 593 | self.grid.height() 594 | } 595 | } 596 | 597 | #[cfg(test)] 598 | mod tests { 599 | use grid_util::Rect; 600 | 601 | use super::*; 602 | 603 | /// Tests whether points are correctly mapped to different connected components 604 | #[test] 605 | fn test_component_generation() { 606 | // Corresponds to the following 3x3 grid: 607 | // ___ 608 | // | # | 609 | // | # | 610 | // ___ 611 | let mut path_graph = PathingGrid::new(3, 2, false); 612 | path_graph.grid.set(1, 0, true); 613 | path_graph.grid.set(1, 1, true); 614 | let f_ix = |p| path_graph.get_ix_point(p); 615 | let p1 = Point::new(0, 0); 616 | let p2 = Point::new(1, 1); 617 | let p3 = Point::new(0, 1); 618 | let p4 = Point::new(2, 0); 619 | let p1_ix = f_ix(&p1); 620 | let p2_ix = f_ix(&p2); 621 | let p3_ix = f_ix(&p3); 622 | let p4_ix = f_ix(&p4); 623 | path_graph.generate_components(); 624 | assert!(!path_graph.components.equiv(p1_ix, p2_ix)); 625 | assert!(path_graph.components.equiv(p1_ix, p3_ix)); 626 | assert!(!path_graph.components.equiv(p1_ix, p4_ix)); 627 | } 628 | 629 | #[test] 630 | fn reachable_with_diagonals() { 631 | let mut path_graph = PathingGrid::new(3, 2, false); 632 | path_graph.grid.set(1, 0, true); 633 | path_graph.grid.set(1, 1, true); 634 | let p1 = Point::new(0, 0); 635 | let p2 = Point::new(1, 0); 636 | let p3 = Point::new(0, 1); 637 | let p4 = Point::new(2, 0); 638 | path_graph.generate_components(); 639 | assert!(path_graph.unreachable(&p1, &p2)); 640 | assert!(!path_graph.unreachable(&p1, &p3)); 641 | assert!(path_graph.unreachable(&p1, &p4)); 642 | assert!(!path_graph.neighbours_unreachable(&p1, &p2)); 643 | assert!(path_graph.neighbours_unreachable(&p1, &p4)); 644 | } 645 | 646 | /// Asserts that the two corners are connected on a 4-grid. 647 | #[test] 648 | fn reachable_without_diagonals() { 649 | // |S | 650 | // | # | 651 | // | G| 652 | // ___ 653 | let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); 654 | pathing_grid.improved_pruning = false; 655 | pathing_grid.allow_diagonal_move = false; 656 | pathing_grid.set(1, 1, true); 657 | pathing_grid.generate_components(); 658 | let start = Point::new(0, 0); 659 | let end = Point::new(2, 2); 660 | assert!(pathing_grid.reachable(&start, &end)); 661 | } 662 | 663 | /// Asserts that the case in which start and goal are equal is handled correctly. 664 | #[test] 665 | fn equal_start_goal() { 666 | for (allow_diag, pruning) in [(false, false), (true, false), (true, true)] { 667 | let mut pathing_grid: PathingGrid = PathingGrid::new(1, 1, false); 668 | pathing_grid.allow_diagonal_move = allow_diag; 669 | pathing_grid.improved_pruning = pruning; 670 | pathing_grid.generate_components(); 671 | let start = Point::new(0, 0); 672 | let path = pathing_grid 673 | .get_path_single_goal(start, start, false) 674 | .unwrap(); 675 | assert!(path.len() == 1); 676 | } 677 | } 678 | 679 | /// Asserts that the optimal 4 step solution is found. 680 | #[test] 681 | fn solve_simple_problem() { 682 | for (allow_diag, pruning, expected) in 683 | [(false, false, 5), (true, false, 4), (true, true, 4)] 684 | { 685 | let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); 686 | pathing_grid.allow_diagonal_move = allow_diag; 687 | pathing_grid.improved_pruning = pruning; 688 | pathing_grid.set(1, 1, true); 689 | pathing_grid.generate_components(); 690 | let start = Point::new(0, 0); 691 | let end = Point::new(2, 2); 692 | let path = pathing_grid 693 | .get_path_single_goal(start, end, false) 694 | .unwrap(); 695 | assert!(path.len() == expected); 696 | } 697 | } 698 | 699 | #[test] 700 | fn test_multiple_goals() { 701 | for (allow_diag, pruning, expected) in 702 | [(false, false, 7), (true, false, 5), (true, true, 5)] 703 | { 704 | let mut pathing_grid: PathingGrid = PathingGrid::new(5, 5, false); 705 | pathing_grid.allow_diagonal_move = allow_diag; 706 | pathing_grid.improved_pruning = pruning; 707 | pathing_grid.set(1, 1, true); 708 | pathing_grid.generate_components(); 709 | let start = Point::new(0, 0); 710 | let goal_1 = Point::new(4, 4); 711 | let goal_2 = Point::new(3, 3); 712 | let goals = vec![&goal_1, &goal_2]; 713 | let (selected_goal, path) = pathing_grid.get_path_multiple_goals(start, goals).unwrap(); 714 | assert_eq!(selected_goal, Point::new(3, 3)); 715 | assert!(path.len() == expected); 716 | } 717 | } 718 | 719 | #[test] 720 | fn test_complex() { 721 | for (allow_diag, pruning, expected) in 722 | [(false, false, 15), (true, false, 10), (true, true, 10)] 723 | { 724 | let mut pathing_grid: PathingGrid = PathingGrid::new(10, 10, false); 725 | pathing_grid.set_rectangle(&Rect::new(1, 1, 2, 2), true); 726 | pathing_grid.set_rectangle(&Rect::new(5, 0, 2, 2), true); 727 | pathing_grid.set_rectangle(&Rect::new(0, 5, 2, 2), true); 728 | pathing_grid.set_rectangle(&Rect::new(8, 8, 2, 2), true); 729 | // pathing_grid.improved_pruning = false; 730 | pathing_grid.allow_diagonal_move = allow_diag; 731 | pathing_grid.improved_pruning = pruning; 732 | pathing_grid.generate_components(); 733 | let start = Point::new(0, 0); 734 | let end = Point::new(7, 7); 735 | let path = pathing_grid 736 | .get_path_single_goal(start, end, false) 737 | .unwrap(); 738 | assert!(path.len() == expected); 739 | } 740 | } 741 | #[test] 742 | fn test_complex_waypoints() { 743 | for (allow_diag, pruning, expected) in 744 | [(false, false, 11), (true, false, 7), (true, true, 5)] 745 | { 746 | let mut pathing_grid: PathingGrid = PathingGrid::new(10, 10, false); 747 | pathing_grid.set_rectangle(&Rect::new(1, 1, 2, 2), true); 748 | pathing_grid.set_rectangle(&Rect::new(5, 0, 2, 2), true); 749 | pathing_grid.set_rectangle(&Rect::new(0, 5, 2, 2), true); 750 | pathing_grid.set_rectangle(&Rect::new(8, 8, 2, 2), true); 751 | // pathing_grid.improved_pruning = false; 752 | pathing_grid.allow_diagonal_move = allow_diag; 753 | pathing_grid.improved_pruning = pruning; 754 | pathing_grid.generate_components(); 755 | let start = Point::new(0, 0); 756 | let end = Point::new(7, 7); 757 | let path = pathing_grid 758 | .get_waypoints_single_goal(start, end, false) 759 | .unwrap(); 760 | assert!(path.len() == expected); 761 | } 762 | } 763 | 764 | // Tests whether allowing diagonals has the expected effect on diagonal reachability in a minimal setting. 765 | #[test] 766 | fn test_diagonal_switch_reachable() { 767 | // ___ 768 | // | #| 769 | // |# | 770 | // __ 771 | let mut pathing_grid: PathingGrid = PathingGrid::new(2, 2, true); 772 | pathing_grid.allow_diagonal_move = false; 773 | let mut pathing_grid_diag: PathingGrid = PathingGrid::new(2, 2, true); 774 | for pathing_grid in [&mut pathing_grid, &mut pathing_grid_diag] { 775 | pathing_grid.set(0, 0, false); 776 | pathing_grid.set(1, 1, false); 777 | pathing_grid.generate_components(); 778 | } 779 | let start = Point::new(0, 0); 780 | let end = Point::new(1, 1); 781 | assert!(pathing_grid.unreachable(&start, &end)); 782 | assert!(pathing_grid_diag.reachable(&start, &end)); 783 | } 784 | 785 | // Tests whether allowing diagonals has the expected effect on path existence in a minimal setting. 786 | #[test] 787 | fn test_diagonal_switch_path() { 788 | // ___ 789 | // | #| 790 | // |# | 791 | // __ 792 | let mut pathing_grid: PathingGrid = PathingGrid::new(2, 2, true); 793 | pathing_grid.allow_diagonal_move = false; 794 | let mut pathing_grid_diag: PathingGrid = PathingGrid::new(2, 2, true); 795 | for pathing_grid in [&mut pathing_grid, &mut pathing_grid_diag] { 796 | pathing_grid.set(0, 0, false); 797 | pathing_grid.set(1, 1, false); 798 | pathing_grid.generate_components(); 799 | } 800 | let start = Point::new(0, 0); 801 | let goal = Point::new(1, 1); 802 | let path = pathing_grid.get_path_single_goal(start, goal, false); 803 | let path_diag = pathing_grid_diag.get_path_single_goal(start, goal, false); 804 | assert!(path.is_none()); 805 | assert!(path_diag.is_some()); 806 | } 807 | } 808 | -------------------------------------------------------------------------------- /tests/fuzz_test.rs: -------------------------------------------------------------------------------- 1 | /// Fuzzes pathfinding system by checking for many random grids that a path is always found if the goal is reachable 2 | /// by being part of the same connected component. All system settings (diagonals, improved pruning) are tested. 3 | use grid_pathfinding::*; 4 | use grid_util::*; 5 | use rand::prelude::*; 6 | 7 | fn random_grid(n: usize, rng: &mut StdRng, diagonal: bool, improved_pruning: bool) -> PathingGrid { 8 | let mut pathing_grid: PathingGrid = PathingGrid::new(n, n, false); 9 | pathing_grid.allow_diagonal_move = diagonal; 10 | pathing_grid.improved_pruning = improved_pruning; 11 | for x in 0..pathing_grid.width() { 12 | for y in 0..pathing_grid.height() { 13 | pathing_grid.set(x, y, rng.gen_bool(0.4)) 14 | } 15 | } 16 | pathing_grid.generate_components(); 17 | pathing_grid 18 | } 19 | 20 | fn visualize_grid(grid: &PathingGrid, start: &Point, end: &Point) { 21 | let grid = &grid.grid; 22 | for y in (0..grid.height).rev() { 23 | for x in 0..grid.width { 24 | let p = Point::new(x as i32, y as i32); 25 | if *start == p { 26 | print!("S"); 27 | } else if *end == p { 28 | print!("G"); 29 | } else if grid.get(x, y) { 30 | print!("#"); 31 | } else { 32 | print!("."); 33 | } 34 | } 35 | println!(); 36 | } 37 | } 38 | 39 | #[test] 40 | fn fuzz() { 41 | const N: usize = 10; 42 | const N_GRIDS: usize = 10000; 43 | let mut rng = StdRng::seed_from_u64(0); 44 | for (diagonal, improved_pruning) in [(false, false), (true, false), (true, true)] { 45 | let mut random_grids: Vec = Vec::new(); 46 | for _ in 0..N_GRIDS { 47 | random_grids.push(random_grid(N, &mut rng, diagonal, improved_pruning)) 48 | } 49 | 50 | let start = Point::new(0, 0); 51 | let end = Point::new(N as i32 - 1, N as i32 - 1); 52 | for mut random_grid in random_grids { 53 | random_grid.set_point(start, false); 54 | random_grid.set_point(end, false); 55 | let reachable = random_grid.reachable(&start, &end); 56 | let path = random_grid.get_path_single_goal(start, end, false); 57 | // Show the grid if a path is not found 58 | if path.is_some() != reachable { 59 | visualize_grid(&random_grid, &start, &end); 60 | } 61 | assert!(path.is_some() == reachable); 62 | } 63 | } 64 | } 65 | --------------------------------------------------------------------------------