├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── bacon.toml ├── compile-all-targets.sh ├── fmt.sh ├── rustfmt.toml ├── src ├── achievements.rs ├── args.rs ├── cell_draw.rs ├── dim.rs ├── display.rs ├── events.rs ├── hof.rs ├── layout.rs ├── main.rs ├── maze.rs ├── nature.rs ├── path.rs ├── pos.rs ├── pos_map.rs ├── renderer.rs ├── run.rs ├── skin.rs └── specs.rs └── website ├── build.png ├── deploy.sh ├── hof.png ├── index.html ├── level-40-transparent.png ├── level-40-white.png ├── level-45.png ├── level-46.png └── level-9.png /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /deploy.sh 3 | /run.sh 4 | /release.sh 5 | /releases 6 | /mazter*.zip 7 | /build 8 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 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 = "android-tzdata" 16 | version = "0.1.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 19 | 20 | [[package]] 21 | name = "android_system_properties" 22 | version = "0.1.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 25 | dependencies = [ 26 | "libc", 27 | ] 28 | 29 | [[package]] 30 | name = "anyhow" 31 | version = "1.0.91" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" 34 | 35 | [[package]] 36 | name = "atty" 37 | version = "0.2.14" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 40 | dependencies = [ 41 | "hermit-abi 0.1.19", 42 | "libc", 43 | "winapi", 44 | ] 45 | 46 | [[package]] 47 | name = "autocfg" 48 | version = "1.4.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 51 | 52 | [[package]] 53 | name = "bitflags" 54 | version = "1.3.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 57 | 58 | [[package]] 59 | name = "bitflags" 60 | version = "2.6.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 63 | 64 | [[package]] 65 | name = "bumpalo" 66 | version = "3.16.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 69 | 70 | [[package]] 71 | name = "byteorder" 72 | version = "1.5.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 75 | 76 | [[package]] 77 | name = "cc" 78 | version = "1.1.31" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" 81 | dependencies = [ 82 | "shlex", 83 | ] 84 | 85 | [[package]] 86 | name = "cfg-if" 87 | version = "1.0.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 90 | 91 | [[package]] 92 | name = "cfg_aliases" 93 | version = "0.2.1" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 96 | 97 | [[package]] 98 | name = "chrono" 99 | version = "0.4.38" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 102 | dependencies = [ 103 | "android-tzdata", 104 | "iana-time-zone", 105 | "js-sys", 106 | "num-traits", 107 | "wasm-bindgen", 108 | "windows-targets", 109 | ] 110 | 111 | [[package]] 112 | name = "clap" 113 | version = "3.2.25" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" 116 | dependencies = [ 117 | "atty", 118 | "bitflags 1.3.2", 119 | "clap_derive", 120 | "clap_lex", 121 | "indexmap", 122 | "once_cell", 123 | "strsim", 124 | "termcolor", 125 | "textwrap", 126 | ] 127 | 128 | [[package]] 129 | name = "clap_derive" 130 | version = "3.2.25" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" 133 | dependencies = [ 134 | "heck", 135 | "proc-macro-error", 136 | "proc-macro2", 137 | "quote", 138 | "syn 1.0.109", 139 | ] 140 | 141 | [[package]] 142 | name = "clap_lex" 143 | version = "0.2.4" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 146 | dependencies = [ 147 | "os_str_bytes", 148 | ] 149 | 150 | [[package]] 151 | name = "cli-log" 152 | version = "2.1.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "e220aa46e5395cd473a054f8e7e52403108ce147a4eb68c001afb01672a4e046" 155 | dependencies = [ 156 | "chrono", 157 | "file-size", 158 | "log", 159 | "proc-status", 160 | ] 161 | 162 | [[package]] 163 | name = "coolor" 164 | version = "1.0.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "691defa50318376447a73ced869862baecfab35f6aabaa91a4cd726b315bfe1a" 167 | dependencies = [ 168 | "crossterm", 169 | ] 170 | 171 | [[package]] 172 | name = "core-foundation-sys" 173 | version = "0.8.7" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 176 | 177 | [[package]] 178 | name = "crokey" 179 | version = "1.1.0" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "520e83558f4c008ac06fa6a86e5c1d4357be6f994cce7434463ebcdaadf47bb1" 182 | dependencies = [ 183 | "crokey-proc_macros", 184 | "crossterm", 185 | "once_cell", 186 | "serde", 187 | "strict", 188 | ] 189 | 190 | [[package]] 191 | name = "crokey-proc_macros" 192 | version = "1.1.0" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "370956e708a1ce65fe4ac5bb7185791e0ece7485087f17736d54a23a0895049f" 195 | dependencies = [ 196 | "crossterm", 197 | "proc-macro2", 198 | "quote", 199 | "strict", 200 | "syn 1.0.109", 201 | ] 202 | 203 | [[package]] 204 | name = "crossbeam" 205 | version = "0.8.4" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" 208 | dependencies = [ 209 | "crossbeam-channel", 210 | "crossbeam-deque", 211 | "crossbeam-epoch", 212 | "crossbeam-queue", 213 | "crossbeam-utils", 214 | ] 215 | 216 | [[package]] 217 | name = "crossbeam-channel" 218 | version = "0.5.13" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" 221 | dependencies = [ 222 | "crossbeam-utils", 223 | ] 224 | 225 | [[package]] 226 | name = "crossbeam-deque" 227 | version = "0.8.5" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" 230 | dependencies = [ 231 | "crossbeam-epoch", 232 | "crossbeam-utils", 233 | ] 234 | 235 | [[package]] 236 | name = "crossbeam-epoch" 237 | version = "0.9.18" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 240 | dependencies = [ 241 | "crossbeam-utils", 242 | ] 243 | 244 | [[package]] 245 | name = "crossbeam-queue" 246 | version = "0.3.11" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" 249 | dependencies = [ 250 | "crossbeam-utils", 251 | ] 252 | 253 | [[package]] 254 | name = "crossbeam-utils" 255 | version = "0.8.20" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 258 | 259 | [[package]] 260 | name = "crossterm" 261 | version = "0.28.1" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 264 | dependencies = [ 265 | "bitflags 2.6.0", 266 | "crossterm_winapi", 267 | "mio", 268 | "parking_lot", 269 | "rustix", 270 | "signal-hook", 271 | "signal-hook-mio", 272 | "winapi", 273 | ] 274 | 275 | [[package]] 276 | name = "crossterm_winapi" 277 | version = "0.9.1" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 280 | dependencies = [ 281 | "winapi", 282 | ] 283 | 284 | [[package]] 285 | name = "csv" 286 | version = "1.3.0" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" 289 | dependencies = [ 290 | "csv-core", 291 | "itoa", 292 | "ryu", 293 | "serde", 294 | ] 295 | 296 | [[package]] 297 | name = "csv-core" 298 | version = "0.1.11" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" 301 | dependencies = [ 302 | "memchr", 303 | ] 304 | 305 | [[package]] 306 | name = "directories" 307 | version = "4.0.1" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" 310 | dependencies = [ 311 | "dirs-sys", 312 | ] 313 | 314 | [[package]] 315 | name = "dirs-sys" 316 | version = "0.3.7" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" 319 | dependencies = [ 320 | "libc", 321 | "redox_users", 322 | "winapi", 323 | ] 324 | 325 | [[package]] 326 | name = "errno" 327 | version = "0.3.9" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 330 | dependencies = [ 331 | "libc", 332 | "windows-sys 0.52.0", 333 | ] 334 | 335 | [[package]] 336 | name = "file-size" 337 | version = "1.0.3" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "9544f10105d33957765016b8a9baea7e689bf1f0f2f32c2fa2f568770c38d2b3" 340 | 341 | [[package]] 342 | name = "fnv" 343 | version = "1.0.7" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 346 | 347 | [[package]] 348 | name = "getrandom" 349 | version = "0.2.15" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 352 | dependencies = [ 353 | "cfg-if", 354 | "libc", 355 | "wasi", 356 | ] 357 | 358 | [[package]] 359 | name = "hashbrown" 360 | version = "0.12.3" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 363 | 364 | [[package]] 365 | name = "heck" 366 | version = "0.4.1" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 369 | 370 | [[package]] 371 | name = "hermit-abi" 372 | version = "0.1.19" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 375 | dependencies = [ 376 | "libc", 377 | ] 378 | 379 | [[package]] 380 | name = "hermit-abi" 381 | version = "0.3.9" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 384 | 385 | [[package]] 386 | name = "iana-time-zone" 387 | version = "0.1.61" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 390 | dependencies = [ 391 | "android_system_properties", 392 | "core-foundation-sys", 393 | "iana-time-zone-haiku", 394 | "js-sys", 395 | "wasm-bindgen", 396 | "windows-core", 397 | ] 398 | 399 | [[package]] 400 | name = "iana-time-zone-haiku" 401 | version = "0.1.2" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 404 | dependencies = [ 405 | "cc", 406 | ] 407 | 408 | [[package]] 409 | name = "indexmap" 410 | version = "1.9.3" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 413 | dependencies = [ 414 | "autocfg", 415 | "hashbrown", 416 | ] 417 | 418 | [[package]] 419 | name = "itoa" 420 | version = "1.0.11" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 423 | 424 | [[package]] 425 | name = "js-sys" 426 | version = "0.3.72" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" 429 | dependencies = [ 430 | "wasm-bindgen", 431 | ] 432 | 433 | [[package]] 434 | name = "lazy-regex" 435 | version = "3.3.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "8d8e41c97e6bc7ecb552016274b99fbb5d035e8de288c582d9b933af6677bfda" 438 | dependencies = [ 439 | "lazy-regex-proc_macros", 440 | "once_cell", 441 | "regex", 442 | ] 443 | 444 | [[package]] 445 | name = "lazy-regex-proc_macros" 446 | version = "3.3.0" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "76e1d8b05d672c53cb9c7b920bbba8783845ae4f0b076e02a3db1d02c81b4163" 449 | dependencies = [ 450 | "proc-macro2", 451 | "quote", 452 | "regex", 453 | "syn 2.0.85", 454 | ] 455 | 456 | [[package]] 457 | name = "libc" 458 | version = "0.2.161" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" 461 | 462 | [[package]] 463 | name = "libredox" 464 | version = "0.1.3" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 467 | dependencies = [ 468 | "bitflags 2.6.0", 469 | "libc", 470 | ] 471 | 472 | [[package]] 473 | name = "linux-raw-sys" 474 | version = "0.4.14" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 477 | 478 | [[package]] 479 | name = "lock_api" 480 | version = "0.4.12" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 483 | dependencies = [ 484 | "autocfg", 485 | "scopeguard", 486 | ] 487 | 488 | [[package]] 489 | name = "log" 490 | version = "0.4.22" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 493 | 494 | [[package]] 495 | name = "mazter" 496 | version = "2.1.1" 497 | dependencies = [ 498 | "anyhow", 499 | "clap", 500 | "cli-log", 501 | "crokey", 502 | "csv", 503 | "directories", 504 | "fnv", 505 | "rand", 506 | "serde", 507 | "smallvec", 508 | "termimad", 509 | "terminal-light", 510 | "whoami", 511 | ] 512 | 513 | [[package]] 514 | name = "memchr" 515 | version = "2.7.4" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 518 | 519 | [[package]] 520 | name = "minimad" 521 | version = "0.13.1" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "a9c5d708226d186590a7b6d4a9780e2bdda5f689e0d58cd17012a298efd745d2" 524 | dependencies = [ 525 | "once_cell", 526 | ] 527 | 528 | [[package]] 529 | name = "mio" 530 | version = "1.0.2" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 533 | dependencies = [ 534 | "hermit-abi 0.3.9", 535 | "libc", 536 | "log", 537 | "wasi", 538 | "windows-sys 0.52.0", 539 | ] 540 | 541 | [[package]] 542 | name = "nix" 543 | version = "0.29.0" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 546 | dependencies = [ 547 | "bitflags 2.6.0", 548 | "cfg-if", 549 | "cfg_aliases", 550 | "libc", 551 | ] 552 | 553 | [[package]] 554 | name = "num-traits" 555 | version = "0.2.19" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 558 | dependencies = [ 559 | "autocfg", 560 | ] 561 | 562 | [[package]] 563 | name = "once_cell" 564 | version = "1.20.2" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 567 | 568 | [[package]] 569 | name = "os_str_bytes" 570 | version = "6.6.1" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" 573 | 574 | [[package]] 575 | name = "parking_lot" 576 | version = "0.12.3" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 579 | dependencies = [ 580 | "lock_api", 581 | "parking_lot_core", 582 | ] 583 | 584 | [[package]] 585 | name = "parking_lot_core" 586 | version = "0.9.10" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 589 | dependencies = [ 590 | "cfg-if", 591 | "libc", 592 | "redox_syscall", 593 | "smallvec", 594 | "windows-targets", 595 | ] 596 | 597 | [[package]] 598 | name = "ppv-lite86" 599 | version = "0.2.20" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 602 | dependencies = [ 603 | "zerocopy", 604 | ] 605 | 606 | [[package]] 607 | name = "proc-macro-error" 608 | version = "1.0.4" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 611 | dependencies = [ 612 | "proc-macro-error-attr", 613 | "proc-macro2", 614 | "quote", 615 | "syn 1.0.109", 616 | "version_check", 617 | ] 618 | 619 | [[package]] 620 | name = "proc-macro-error-attr" 621 | version = "1.0.4" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 624 | dependencies = [ 625 | "proc-macro2", 626 | "quote", 627 | "version_check", 628 | ] 629 | 630 | [[package]] 631 | name = "proc-macro2" 632 | version = "1.0.89" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 635 | dependencies = [ 636 | "unicode-ident", 637 | ] 638 | 639 | [[package]] 640 | name = "proc-status" 641 | version = "0.1.1" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "f0e0c0ac915e7b76b47850ba4ffc377abde6c6ff9eeace61d0a89623db449712" 644 | dependencies = [ 645 | "thiserror", 646 | ] 647 | 648 | [[package]] 649 | name = "quote" 650 | version = "1.0.37" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 653 | dependencies = [ 654 | "proc-macro2", 655 | ] 656 | 657 | [[package]] 658 | name = "rand" 659 | version = "0.8.5" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 662 | dependencies = [ 663 | "libc", 664 | "rand_chacha", 665 | "rand_core", 666 | ] 667 | 668 | [[package]] 669 | name = "rand_chacha" 670 | version = "0.3.1" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 673 | dependencies = [ 674 | "ppv-lite86", 675 | "rand_core", 676 | ] 677 | 678 | [[package]] 679 | name = "rand_core" 680 | version = "0.6.4" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 683 | dependencies = [ 684 | "getrandom", 685 | ] 686 | 687 | [[package]] 688 | name = "redox_syscall" 689 | version = "0.5.7" 690 | source = "registry+https://github.com/rust-lang/crates.io-index" 691 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 692 | dependencies = [ 693 | "bitflags 2.6.0", 694 | ] 695 | 696 | [[package]] 697 | name = "redox_users" 698 | version = "0.4.6" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" 701 | dependencies = [ 702 | "getrandom", 703 | "libredox", 704 | "thiserror", 705 | ] 706 | 707 | [[package]] 708 | name = "regex" 709 | version = "1.11.1" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 712 | dependencies = [ 713 | "aho-corasick", 714 | "memchr", 715 | "regex-automata", 716 | "regex-syntax", 717 | ] 718 | 719 | [[package]] 720 | name = "regex-automata" 721 | version = "0.4.8" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" 724 | dependencies = [ 725 | "aho-corasick", 726 | "memchr", 727 | "regex-syntax", 728 | ] 729 | 730 | [[package]] 731 | name = "regex-syntax" 732 | version = "0.8.5" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 735 | 736 | [[package]] 737 | name = "rustix" 738 | version = "0.38.38" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" 741 | dependencies = [ 742 | "bitflags 2.6.0", 743 | "errno", 744 | "libc", 745 | "linux-raw-sys", 746 | "windows-sys 0.52.0", 747 | ] 748 | 749 | [[package]] 750 | name = "ryu" 751 | version = "1.0.18" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 754 | 755 | [[package]] 756 | name = "scopeguard" 757 | version = "1.2.0" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 760 | 761 | [[package]] 762 | name = "serde" 763 | version = "1.0.214" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" 766 | dependencies = [ 767 | "serde_derive", 768 | ] 769 | 770 | [[package]] 771 | name = "serde_derive" 772 | version = "1.0.214" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" 775 | dependencies = [ 776 | "proc-macro2", 777 | "quote", 778 | "syn 2.0.85", 779 | ] 780 | 781 | [[package]] 782 | name = "shlex" 783 | version = "1.3.0" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 786 | 787 | [[package]] 788 | name = "signal-hook" 789 | version = "0.3.17" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 792 | dependencies = [ 793 | "libc", 794 | "signal-hook-registry", 795 | ] 796 | 797 | [[package]] 798 | name = "signal-hook-mio" 799 | version = "0.2.4" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 802 | dependencies = [ 803 | "libc", 804 | "mio", 805 | "signal-hook", 806 | ] 807 | 808 | [[package]] 809 | name = "signal-hook-registry" 810 | version = "1.4.2" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 813 | dependencies = [ 814 | "libc", 815 | ] 816 | 817 | [[package]] 818 | name = "smallvec" 819 | version = "1.13.2" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 822 | 823 | [[package]] 824 | name = "strict" 825 | version = "0.2.0" 826 | source = "registry+https://github.com/rust-lang/crates.io-index" 827 | checksum = "f42444fea5b87a39db4218d9422087e66a85d0e7a0963a439b07bcdf91804006" 828 | 829 | [[package]] 830 | name = "strsim" 831 | version = "0.10.0" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 834 | 835 | [[package]] 836 | name = "syn" 837 | version = "1.0.109" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 840 | dependencies = [ 841 | "proc-macro2", 842 | "quote", 843 | "unicode-ident", 844 | ] 845 | 846 | [[package]] 847 | name = "syn" 848 | version = "2.0.85" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" 851 | dependencies = [ 852 | "proc-macro2", 853 | "quote", 854 | "unicode-ident", 855 | ] 856 | 857 | [[package]] 858 | name = "termcolor" 859 | version = "1.4.1" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 862 | dependencies = [ 863 | "winapi-util", 864 | ] 865 | 866 | [[package]] 867 | name = "termimad" 868 | version = "0.31.0" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "9cda3a7471f9978706978454c45ef8dda67e9f8f3cdb9319eb2e9323deb6ae62" 871 | dependencies = [ 872 | "coolor", 873 | "crokey", 874 | "crossbeam", 875 | "lazy-regex", 876 | "minimad", 877 | "serde", 878 | "thiserror", 879 | "unicode-width", 880 | ] 881 | 882 | [[package]] 883 | name = "terminal-light" 884 | version = "1.7.0" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "4a9474484d1a0c031cd7d065c6f027a376859c9fedb32c94df3d7a797218bbb7" 887 | dependencies = [ 888 | "coolor", 889 | "crossterm", 890 | "thiserror", 891 | "xterm-query", 892 | ] 893 | 894 | [[package]] 895 | name = "textwrap" 896 | version = "0.16.1" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" 899 | 900 | [[package]] 901 | name = "thiserror" 902 | version = "1.0.65" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" 905 | dependencies = [ 906 | "thiserror-impl", 907 | ] 908 | 909 | [[package]] 910 | name = "thiserror-impl" 911 | version = "1.0.65" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" 914 | dependencies = [ 915 | "proc-macro2", 916 | "quote", 917 | "syn 2.0.85", 918 | ] 919 | 920 | [[package]] 921 | name = "unicode-ident" 922 | version = "1.0.13" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 925 | 926 | [[package]] 927 | name = "unicode-width" 928 | version = "0.1.14" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 931 | 932 | [[package]] 933 | name = "version_check" 934 | version = "0.9.5" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 937 | 938 | [[package]] 939 | name = "wasi" 940 | version = "0.11.0+wasi-snapshot-preview1" 941 | source = "registry+https://github.com/rust-lang/crates.io-index" 942 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 943 | 944 | [[package]] 945 | name = "wasite" 946 | version = "0.1.0" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 949 | 950 | [[package]] 951 | name = "wasm-bindgen" 952 | version = "0.2.95" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" 955 | dependencies = [ 956 | "cfg-if", 957 | "once_cell", 958 | "wasm-bindgen-macro", 959 | ] 960 | 961 | [[package]] 962 | name = "wasm-bindgen-backend" 963 | version = "0.2.95" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" 966 | dependencies = [ 967 | "bumpalo", 968 | "log", 969 | "once_cell", 970 | "proc-macro2", 971 | "quote", 972 | "syn 2.0.85", 973 | "wasm-bindgen-shared", 974 | ] 975 | 976 | [[package]] 977 | name = "wasm-bindgen-macro" 978 | version = "0.2.95" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" 981 | dependencies = [ 982 | "quote", 983 | "wasm-bindgen-macro-support", 984 | ] 985 | 986 | [[package]] 987 | name = "wasm-bindgen-macro-support" 988 | version = "0.2.95" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" 991 | dependencies = [ 992 | "proc-macro2", 993 | "quote", 994 | "syn 2.0.85", 995 | "wasm-bindgen-backend", 996 | "wasm-bindgen-shared", 997 | ] 998 | 999 | [[package]] 1000 | name = "wasm-bindgen-shared" 1001 | version = "0.2.95" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" 1004 | 1005 | [[package]] 1006 | name = "web-sys" 1007 | version = "0.3.72" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" 1010 | dependencies = [ 1011 | "js-sys", 1012 | "wasm-bindgen", 1013 | ] 1014 | 1015 | [[package]] 1016 | name = "whoami" 1017 | version = "1.5.2" 1018 | source = "registry+https://github.com/rust-lang/crates.io-index" 1019 | checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" 1020 | dependencies = [ 1021 | "redox_syscall", 1022 | "wasite", 1023 | "web-sys", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "winapi" 1028 | version = "0.3.9" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1031 | dependencies = [ 1032 | "winapi-i686-pc-windows-gnu", 1033 | "winapi-x86_64-pc-windows-gnu", 1034 | ] 1035 | 1036 | [[package]] 1037 | name = "winapi-i686-pc-windows-gnu" 1038 | version = "0.4.0" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1041 | 1042 | [[package]] 1043 | name = "winapi-util" 1044 | version = "0.1.9" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 1047 | dependencies = [ 1048 | "windows-sys 0.59.0", 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "winapi-x86_64-pc-windows-gnu" 1053 | version = "0.4.0" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1056 | 1057 | [[package]] 1058 | name = "windows-core" 1059 | version = "0.52.0" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1062 | dependencies = [ 1063 | "windows-targets", 1064 | ] 1065 | 1066 | [[package]] 1067 | name = "windows-sys" 1068 | version = "0.52.0" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1071 | dependencies = [ 1072 | "windows-targets", 1073 | ] 1074 | 1075 | [[package]] 1076 | name = "windows-sys" 1077 | version = "0.59.0" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1080 | dependencies = [ 1081 | "windows-targets", 1082 | ] 1083 | 1084 | [[package]] 1085 | name = "windows-targets" 1086 | version = "0.52.6" 1087 | source = "registry+https://github.com/rust-lang/crates.io-index" 1088 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1089 | dependencies = [ 1090 | "windows_aarch64_gnullvm", 1091 | "windows_aarch64_msvc", 1092 | "windows_i686_gnu", 1093 | "windows_i686_gnullvm", 1094 | "windows_i686_msvc", 1095 | "windows_x86_64_gnu", 1096 | "windows_x86_64_gnullvm", 1097 | "windows_x86_64_msvc", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "windows_aarch64_gnullvm" 1102 | version = "0.52.6" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1105 | 1106 | [[package]] 1107 | name = "windows_aarch64_msvc" 1108 | version = "0.52.6" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1111 | 1112 | [[package]] 1113 | name = "windows_i686_gnu" 1114 | version = "0.52.6" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1117 | 1118 | [[package]] 1119 | name = "windows_i686_gnullvm" 1120 | version = "0.52.6" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1123 | 1124 | [[package]] 1125 | name = "windows_i686_msvc" 1126 | version = "0.52.6" 1127 | source = "registry+https://github.com/rust-lang/crates.io-index" 1128 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1129 | 1130 | [[package]] 1131 | name = "windows_x86_64_gnu" 1132 | version = "0.52.6" 1133 | source = "registry+https://github.com/rust-lang/crates.io-index" 1134 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1135 | 1136 | [[package]] 1137 | name = "windows_x86_64_gnullvm" 1138 | version = "0.52.6" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1141 | 1142 | [[package]] 1143 | name = "windows_x86_64_msvc" 1144 | version = "0.52.6" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1147 | 1148 | [[package]] 1149 | name = "xterm-query" 1150 | version = "0.5.0" 1151 | source = "registry+https://github.com/rust-lang/crates.io-index" 1152 | checksum = "775333f1c03ec9f81f7f295de5a8dac7350b91f7e03e2dd730593ba4ec3515d1" 1153 | dependencies = [ 1154 | "nix", 1155 | "thiserror", 1156 | ] 1157 | 1158 | [[package]] 1159 | name = "zerocopy" 1160 | version = "0.7.35" 1161 | source = "registry+https://github.com/rust-lang/crates.io-index" 1162 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1163 | dependencies = [ 1164 | "byteorder", 1165 | "zerocopy-derive", 1166 | ] 1167 | 1168 | [[package]] 1169 | name = "zerocopy-derive" 1170 | version = "0.7.35" 1171 | source = "registry+https://github.com/rust-lang/crates.io-index" 1172 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1173 | dependencies = [ 1174 | "proc-macro2", 1175 | "quote", 1176 | "syn 2.0.85", 1177 | ] 1178 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mazter" 3 | version = "2.1.1" 4 | authors = ["dystroy "] 5 | documentation = "https://dystroy.org/mazter" 6 | homepage = "https://dystroy.org/mazter" 7 | repository = "https://github.com/Canop/mazter" 8 | edition = "2021" 9 | keywords = ["key", "parse"] 10 | license = "MIT" 11 | categories = ["games"] 12 | description = "Mazes in your terminal" 13 | readme = "README.md" 14 | rust-version = "1.56" 15 | 16 | [dependencies] 17 | anyhow = "1.0" 18 | clap = { version = "3.2.1", features = ["derive"] } 19 | cli-log = "2.0" 20 | crokey = "1.1" 21 | csv = "1.2" 22 | directories = "4.0" 23 | fnv = "1.0" 24 | rand = "0.8" 25 | serde = { version = "1.0", features = ["derive"] } 26 | smallvec = "1.13" 27 | termimad = "0.31" 28 | terminal-light = "1.7" 29 | whoami = "1.2" 30 | 31 | [patch.crates-io] 32 | # crokey = { path = "../crokey" } 33 | # termimad = { path = "../termimad" } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Denys Séguret 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 | 2 | Mazes in your terminal. 3 | 4 | [![MIT][s2]][l2] [![Latest Version][s1]][l1] [![Chat on Miaou][s4]][l4] 5 | 6 | [s1]: https://img.shields.io/crates/v/mazter.svg 7 | [l1]: https://crates.io/crates/mazter 8 | 9 | [s2]: https://img.shields.io/badge/license-MIT-blue.svg 10 | [l2]: LICENSE 11 | 12 | [s4]: https://miaou.dystroy.org/static/shields/room.svg 13 | [l4]: https://miaou.dystroy.org/3490?mazter 14 | 15 | Mazter is a maze game and generator, with fluid moves and an adaptative rendering dynamically finding the resolution which best suits your terminal's size. 16 | 17 | 18 | [![asciicast](https://asciinema.org/a/Ip2uIlWMZhjpEotKK5I9Vll65.svg)](https://asciinema.org/a/Ip2uIlWMZhjpEotKK5I9Vll65?autoplay=true&loop=true) 19 | 20 | 21 | ## Install 22 | 23 | Either 24 | 25 | * download the precompiled binaries from the [official site](https://dystroy.org/mazter), 26 | * or [install the Rust development environment](https://rustup.rs/) then run `cargo install mazter` 27 | 28 | ## See options 29 | 30 | ```bash 31 | mazter --help 32 | ``` 33 | 34 | ## Play 35 | 36 | Simply run 37 | 38 | ```bash 39 | mazter 40 | ``` 41 | 42 | Move with the arrow keys to exit the maze. 43 | 44 | An encounter with a red monster teleports you a short distance, and removes one life. 45 | 46 | You get more lives on green squares. 47 | 48 | ![screenshot](website/level-40-white.png) 49 | 50 | As your accomplishments are saved, you'll start at the first level you didn't already win. 51 | 52 | But you may replay a previous level with `mazter --level 3`. 53 | 54 | If you're several players on the same account, you should specify who's playing: 55 | 56 | 57 | ```bash 58 | mazter --user gael 59 | ``` 60 | 61 | You can see the Hall of Fame with `mazter --hof`: 62 | 63 | ![screenshot](website/hof.png) 64 | 65 | ## Just generate a maze 66 | 67 | ### build a random maze 68 | 69 | ```bash 70 | mazter --build 71 | ``` 72 | 73 | ![screenshot](website/build.png) 74 | 75 | ### build a maze for a given level 76 | 77 | ```bash 78 | mazter --build --level 20 79 | ``` 80 | 81 | ## Just gaze 82 | 83 | With the screen-saver mode, mazter plays by himself, even taking a place in the hall of fame. 84 | 85 | ```bash 86 | mazter --screen-saver 87 | ``` 88 | 89 | 90 | -------------------------------------------------------------------------------- /bacon.toml: -------------------------------------------------------------------------------- 1 | # This is a configuration file for the bacon tool 2 | # 3 | # Bacon repository: https://github.com/Canop/bacon 4 | # Complete help on configuration: https://dystroy.org/bacon/config/ 5 | # You can also check bacon's own bacon.toml file 6 | # as an example: https://github.com/Canop/bacon/blob/main/bacon.toml 7 | 8 | default_job = "check" 9 | 10 | [jobs.check] 11 | command = ["cargo", "check", "--color", "always"] 12 | need_stdout = false 13 | 14 | [jobs.check-all] 15 | command = ["cargo", "check", "--all-targets", "--color", "always"] 16 | need_stdout = false 17 | 18 | [jobs.clippy-all] 19 | command = [ 20 | "cargo", "clippy", 21 | "--all-targets", 22 | "--color", "always", 23 | "--", 24 | "-A", "clippy::collapsible_else_if", 25 | "-A", "clippy::nonminimal_bool", 26 | ] 27 | need_stdout = false 28 | 29 | [jobs.test] 30 | command = [ 31 | "cargo", "test", "--color", "always", 32 | "--", "--color", "always", # see https://github.com/Canop/bacon/issues/124 33 | ] 34 | need_stdout = true 35 | 36 | [jobs.doc] 37 | command = ["cargo", "doc", "--color", "always", "--no-deps"] 38 | need_stdout = false 39 | 40 | # If the doc compiles, then it opens in your browser and bacon switches 41 | # to the previous job 42 | [jobs.doc-open] 43 | command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] 44 | need_stdout = false 45 | on_success = "back" # so that we don't open the browser at each change 46 | 47 | # You can run your application and have the result displayed in bacon, 48 | # *if* it makes sense for this crate. You can run an example the same 49 | # way. Don't forget the `--color always` part or the errors won't be 50 | # properly parsed. 51 | [jobs.run] 52 | command = [ 53 | "cargo", "run", 54 | "--color", "always", 55 | # put launch parameters for your program behind a `--` separator 56 | ] 57 | need_stdout = true 58 | allow_warnings = true 59 | 60 | # You may define here keybindings that would be specific to 61 | # a project, for example a shortcut to launch a specific job. 62 | # Shortcuts to internal functions (scrolling, toggling, etc.) 63 | # should go in your personal global prefs.toml file instead. 64 | [keybindings] 65 | # alt-m = "job:my-job" 66 | -------------------------------------------------------------------------------- /compile-all-targets.sh: -------------------------------------------------------------------------------- 1 | # WARNING: This script is NOT meant for normal installation, it's dedicated 2 | # to the compilation of all supported targets, from a linux machine. 3 | # There's no Mac version here because I don't know how to legally compile 4 | # for Mac on Linux 5 | 6 | H1="\n\e[30;104;1m\e[2K\n\e[A" # style first header 7 | H2="\n\e[30;104m\e[1K\n\e[A" # style second header 8 | EH="\e[00m\n\e[2K" # end header 9 | 10 | version=$(sed 's/version = "\([0-9.]\{1,\}\(-[a-z]\+\)\?\)"/\1/;t;d' Cargo.toml | head -1) 11 | echo -e "${H1}Compilation of all targets for mazter $version${EH}" 12 | 13 | # clean previous build 14 | rm -rf build 15 | mkdir build 16 | echo " build cleaned" 17 | 18 | # build the windows version 19 | # use cargo cross 20 | echo -e "${H2}Compiling the Windows version${EH}" 21 | cargo clean 22 | cross build --target x86_64-pc-windows-gnu --release 23 | mkdir build/x86_64-pc-windows-gnu 24 | cp target/x86_64-pc-windows-gnu/release/mazter.exe build/x86_64-pc-windows-gnu/ 25 | 26 | # build a musl version 27 | echo -e "${H2}Compiling the MUSL version${EH}" 28 | cross build --release --target x86_64-unknown-linux-musl 29 | mkdir build/x86_64-unknown-linux-musl 30 | cp target/x86_64-unknown-linux-musl/release/mazter build/x86_64-unknown-linux-musl 31 | 32 | # build the linux version 33 | echo -e "${H2}Compiling the linux version${EH}" 34 | cargo build --release 35 | strip target/release/mazter 36 | mkdir build/x86_64-linux/ 37 | cp target/release/mazter build/x86_64-linux/ 38 | -------------------------------------------------------------------------------- /fmt.sh: -------------------------------------------------------------------------------- 1 | cargo +nightly fmt 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | style_edition = "2024" 3 | imports_granularity = "one" 4 | imports_layout = "Vertical" 5 | fn_params_layout = "Vertical" 6 | -------------------------------------------------------------------------------- /src/achievements.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::*, 3 | anyhow::anyhow, 4 | fnv::FnvHasher, 5 | std::{ 6 | cmp::Reverse, 7 | fs, 8 | hash::{ 9 | Hash, 10 | Hasher, 11 | }, 12 | path::PathBuf, 13 | }, 14 | }; 15 | 16 | /// Must be changed when the rules change so that all levels are considered 17 | /// not done. It's not necessary to change it when specs changed because they're 18 | /// hashed in the record. 19 | const SALT: u64 = 20220722; 20 | 21 | #[derive(Debug, Clone, Copy, PartialEq)] 22 | pub struct Achievement<'s> { 23 | user: &'s str, 24 | level: usize, 25 | } 26 | 27 | impl<'s> Achievement<'s> { 28 | pub fn new( 29 | user: &'s str, 30 | level: usize, 31 | ) -> Self { 32 | Self { user, level } 33 | } 34 | /// get the hash according to FNV 35 | pub fn hash(self) -> u64 { 36 | let mut hasher = FnvHasher::with_key(SALT); 37 | self.user.hash(&mut hasher); 38 | let specs = Specs::for_level(self.level); 39 | specs.hash(&mut hasher); 40 | hasher.finish() 41 | } 42 | } 43 | 44 | #[derive(Debug, serde::Serialize, serde::Deserialize)] 45 | pub struct Record { 46 | pub user: String, 47 | pub level: usize, 48 | hash: u64, 49 | } 50 | 51 | impl<'s> From> for Record { 52 | fn from(ach: Achievement<'s>) -> Self { 53 | let level = ach.level; 54 | let user = ach.user.to_string(); 55 | let hash = ach.hash(); 56 | Self { user, level, hash } 57 | } 58 | } 59 | 60 | impl Record { 61 | pub fn achievement(&self) -> Achievement<'_> { 62 | Achievement::new(&self.user, self.level) 63 | } 64 | pub fn is_valid(&self) -> bool { 65 | self.hash == self.achievement().hash() 66 | } 67 | } 68 | 69 | /// Achievement Database 70 | /// 71 | /// It's designed to ensure a level you achieved stays achieved 72 | /// on upgrading mazter unless the level specifications changed 73 | /// with the upgrade 74 | pub struct Database { 75 | file_path: PathBuf, 76 | records: Vec, 77 | } 78 | 79 | impl Database { 80 | fn new() -> anyhow::Result { 81 | let project_dirs = directories::ProjectDirs::from("org", "dystroy", "mazter") 82 | .ok_or_else(|| anyhow!("no conf directory"))?; 83 | let file_path = project_dirs.data_dir().join("achievements.csv"); 84 | debug!("file_path: {:?}", &file_path); 85 | let mut records = Vec::new(); 86 | if file_path.exists() { 87 | let mut csv_reader = csv::Reader::from_path(&file_path)?; 88 | for res in csv_reader.deserialize::() { 89 | let Ok(record) = res else { continue }; 90 | if record.is_valid() { 91 | records.push(record); 92 | } else { 93 | // the most normal cause is the level spec having changed 94 | // since the user won it 95 | info!("invalid record: {:#?}", &record); 96 | } 97 | } 98 | } 99 | Ok(Self { file_path, records }) 100 | } 101 | fn add( 102 | &mut self, 103 | ach: Achievement, 104 | ) { 105 | self.records.push(ach.into()); 106 | } 107 | fn write(&self) -> anyhow::Result<()> { 108 | fs::create_dir_all( 109 | self.file_path 110 | .parent() 111 | .expect("conf file parent should be defined"), 112 | )?; 113 | let mut writer = csv::Writer::from_path(&self.file_path)?; 114 | for record in &self.records { 115 | writer.serialize(record)?; 116 | } 117 | writer.flush()?; 118 | Ok(()) 119 | } 120 | fn contains( 121 | &self, 122 | ach: Achievement, 123 | ) -> bool { 124 | self.records 125 | .iter() 126 | .any(|record| record.achievement() == ach) 127 | } 128 | 129 | pub fn save(ach: Achievement) -> anyhow::Result<()> { 130 | let mut db = Self::new()?; 131 | db.add(ach); 132 | db.write()?; 133 | Ok(()) 134 | } 135 | /// save the achievement and return the first following 136 | /// level not won 137 | pub fn advance(ach: Achievement) -> anyhow::Result { 138 | let mut db = Self::new()?; 139 | db.add(ach); 140 | db.write()?; 141 | let mut level = ach.level + 1; 142 | loop { 143 | if !db.contains(Achievement::new(ach.user, level)) { 144 | return Ok(level); 145 | } 146 | level += 1; 147 | } 148 | } 149 | pub fn first_not_won(user: &str) -> anyhow::Result { 150 | let db = Self::new()?; 151 | let mut level = 1; 152 | loop { 153 | if !db.contains(Achievement::new(user, level)) { 154 | return Ok(level); 155 | } 156 | level += 1; 157 | } 158 | } 159 | pub fn can_play( 160 | user: &str, 161 | target: usize, 162 | ) -> anyhow::Result { 163 | if target == 0 { 164 | return Ok(true); 165 | } 166 | let db = Self::new()?; 167 | let mut level = 1; 168 | loop { 169 | if level == target { 170 | return Ok(true); 171 | } 172 | if !db.contains(Achievement::new(user, level)) { 173 | return Ok(false); 174 | } 175 | level += 1; 176 | } 177 | } 178 | pub fn reset( 179 | user: &str, 180 | print: bool, 181 | ) -> anyhow::Result<()> { 182 | let mut db = Self::new()?; 183 | if db.records.iter().any(|record| record.user == user) { 184 | if print { 185 | println!("Removing achievements of user {user:?}"); 186 | println!( 187 | "If you change your mind, you can put the folowwing lines back in {:?}", 188 | db.file_path 189 | ); 190 | } 191 | let mut records = Vec::new(); 192 | let mut printer = csv::Writer::from_writer(std::io::stdout()); 193 | for record in db.records.drain(..) { 194 | if record.user == user { 195 | if print { 196 | printer.serialize(record)?; 197 | } 198 | } else { 199 | records.push(record); 200 | } 201 | } 202 | db.records = records; 203 | db.write()?; 204 | } else if print { 205 | println!("No achievement were found for user {user:?}"); 206 | } 207 | Ok(()) 208 | } 209 | pub fn hof() -> anyhow::Result> { 210 | let mut db = Self::new()?; 211 | let mut hof: Vec = Vec::new(); 212 | for record in db.records.drain(..) { 213 | if let Some(idx) = hof.iter().position(|hr| hr.user == record.user) { 214 | if hof[idx].level < record.level { 215 | hof[idx] = record; 216 | } 217 | } else { 218 | hof.push(record); 219 | } 220 | } 221 | hof.sort_by_key(|r| Reverse(r.level)); 222 | Ok(hof) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, clap::Parser)] 2 | #[clap( 3 | author, 4 | version, 5 | about = "Mazes in your terminal - doc at https://dystroy.org/mazter" 6 | )] 7 | pub struct Args { 8 | /// don't play, just print a random maze 9 | #[clap(long, value_parser)] 10 | pub build: bool, 11 | 12 | /// forget all achievements of the user 13 | #[clap(long, value_parser)] 14 | pub reset: bool, 15 | 16 | /// print the Hall of Fame 17 | #[clap(long, value_parser)] 18 | pub hof: bool, 19 | 20 | /// level to play or print - default is the first not won 21 | #[clap(long, value_parser)] 22 | pub level: Option, 23 | 24 | /// number of levels to play 25 | #[clap(long, value_parser)] 26 | pub levels: Option, 27 | 28 | /// user playing 29 | #[clap(short, long, value_parser, default_value_t = whoami::username())] 30 | pub user: String, 31 | 32 | /// let mazter play alone 33 | #[clap(long, value_parser)] 34 | pub screen_saver: bool, 35 | } 36 | -------------------------------------------------------------------------------- /src/cell_draw.rs: -------------------------------------------------------------------------------- 1 | //! fonctions dedicated to drawing cells (i.e. char positions in the terminal) 2 | use { 3 | std::io::Write, 4 | termimad::crossterm::{ 5 | QueueableCommand, 6 | cursor, 7 | style::{ 8 | Color, 9 | Colors, 10 | Print, 11 | SetColors, 12 | }, 13 | }, 14 | }; 15 | 16 | // block characterss 17 | static HORIZONTAL_BC: [char; 9] = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']; 18 | static VERTICAL_BC: [char; 9] = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; 19 | 20 | pub struct CellPos { 21 | pub x: u16, 22 | pub y: u16, 23 | } 24 | 25 | /// Draw a cell with two colors, one for the left part, one for the right part 26 | pub fn draw_bicolor_horizontal( 27 | w: &mut W, 28 | x: u16, 29 | y: u16, 30 | left_color: Color, 31 | right_color: Color, 32 | av: usize, // part of the left color, in [0, 8] (0 is 100% right_color) 33 | ) -> anyhow::Result<()> { 34 | w.queue(cursor::MoveTo(x, y))?; 35 | w.queue(SetColors(Colors { 36 | foreground: Some(left_color), 37 | background: Some(right_color), 38 | }))?; 39 | w.queue(Print(HORIZONTAL_BC[av]))?; 40 | Ok(()) 41 | } 42 | 43 | /// Draw a cell with two colors, one for the top part, one for the bottom part 44 | pub fn draw_bicolor_vertical( 45 | w: &mut W, 46 | x: u16, 47 | y: u16, 48 | top_color: Color, 49 | bottom_color: Color, 50 | av: usize, // part of the top color, in [0, 8] (0 is 100% bottom_color) 51 | ) -> anyhow::Result<()> { 52 | w.queue(cursor::MoveTo(x, y))?; 53 | w.queue(SetColors(Colors { 54 | foreground: Some(bottom_color), 55 | background: Some(top_color), 56 | }))?; 57 | w.queue(Print(VERTICAL_BC[av]))?; 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /src/dim.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::*, 3 | std::io, 4 | termimad::crossterm::terminal, 5 | }; 6 | 7 | /// a couple of usize intended as dimensions 8 | /// (screen, maze, etc.) 9 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 10 | pub struct Dim { 11 | pub w: usize, 12 | pub h: usize, 13 | } 14 | impl Dim { 15 | pub fn new( 16 | w: usize, 17 | h: usize, 18 | ) -> Self { 19 | Self { w, h } 20 | } 21 | pub fn terminal() -> io::Result { 22 | #[allow(unused_mut)] 23 | let (mut width, mut height) = terminal::size()?; 24 | #[cfg(windows)] 25 | { 26 | width -= 1; 27 | height -= 1; 28 | } 29 | Ok(Self::new(width as usize, height as usize)) 30 | } 31 | pub fn idx( 32 | self, 33 | p: Pos, 34 | ) -> usize { 35 | p.x + self.w * p.y 36 | } 37 | pub fn verticalize(&mut self) { 38 | let w = self.h; 39 | self.h += self.w; 40 | self.w = (w / 2).max(MIN_DIM + 1); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/display.rs: -------------------------------------------------------------------------------- 1 | use crate::Dim; 2 | 3 | /// A maze rendering target 4 | #[derive(Debug, Clone, Copy)] 5 | pub enum Display { 6 | Alternate(Dim), 7 | Standard, 8 | } 9 | -------------------------------------------------------------------------------- /src/events.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(Debug, Clone, Default)] 4 | pub struct EventList { 5 | pub events: Vec, 6 | } 7 | 8 | /// An event that can happen in the game and that we want to animate for clarity. 9 | #[derive(Debug, Clone)] 10 | pub enum Event { 11 | Move(PosMove), 12 | Teleport(Teleport), 13 | } 14 | 15 | #[derive(Debug, Clone, Copy)] 16 | pub struct PosMove { 17 | pub start: Pos, 18 | pub dir: Dir, 19 | pub moving_nature: Nature, 20 | pub start_background_nature: Nature, 21 | pub dest_background_nature: Nature, 22 | } 23 | 24 | #[derive(Debug, Clone)] 25 | pub struct Teleport { 26 | pub start: Pos, 27 | pub possible_jumps: Vec, 28 | pub arrival: Pos, 29 | } 30 | 31 | impl EventList { 32 | pub fn add_teleport( 33 | &mut self, 34 | start: Pos, 35 | possible_jumps: Vec, 36 | arrival: Pos, 37 | ) { 38 | self.events.push(Event::Teleport(Teleport { 39 | start, 40 | possible_jumps, 41 | arrival, 42 | })); 43 | } 44 | pub fn add_player_move( 45 | &mut self, 46 | start: Pos, 47 | dir: Dir, 48 | dest_background_nature: Nature, 49 | ) { 50 | let moving_nature = Nature::Player; 51 | let start_background_nature = Nature::Room; 52 | self.events.push(Event::Move(PosMove { 53 | start, 54 | dir, 55 | moving_nature, 56 | start_background_nature, 57 | dest_background_nature, 58 | })); 59 | } 60 | pub fn add_monster_move( 61 | &mut self, 62 | start: Pos, 63 | dir: Dir, 64 | mut dest_background_nature: Nature, 65 | ) { 66 | let Some(dest) = start.in_dir(dir) else { 67 | return; 68 | }; 69 | // for a better rendering, we set the dest_background_nature to the 70 | // one of a player/move leaving the destination 71 | for event in &self.events { 72 | if let Event::Move(pos_move) = event { 73 | if pos_move.start == dest { 74 | dest_background_nature = pos_move.moving_nature; 75 | break; 76 | } 77 | } 78 | } 79 | let moving_nature = Nature::Monster; 80 | let start_background_nature = Nature::Room; 81 | self.events.push(Event::Move(PosMove { 82 | start, 83 | dir, 84 | moving_nature, 85 | start_background_nature, 86 | dest_background_nature, 87 | })); 88 | } 89 | pub fn is_empty(&self) -> bool { 90 | self.events.is_empty() 91 | } 92 | pub fn clear(&mut self) { 93 | self.events.clear(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/hof.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::*, 3 | termimad::{ 4 | MadSkin, 5 | minimad::{ 6 | Alignment, 7 | Col, 8 | OwningTemplateExpander, 9 | TableBuilder, 10 | }, 11 | }, 12 | }; 13 | 14 | // display the hall of fame 15 | pub fn print() -> anyhow::Result<()> { 16 | let hof = Database::hof()?; 17 | if hof.is_empty() { 18 | println!("The Hall of Fame is empty"); 19 | return Ok(()); 20 | } 21 | let mut expander = OwningTemplateExpander::new(); 22 | for entry in &hof { 23 | expander 24 | .sub("rows") 25 | .set("user", &entry.user) 26 | .set("level", entry.level); 27 | } 28 | let mut tbl = TableBuilder::default(); 29 | tbl.col(Col::new("**User**", "${user}")); 30 | tbl.col(Col::new("**Level**", "${level}").align_content(Alignment::Right)); 31 | let skin = MadSkin::default(); 32 | skin.print_owning_expander_md(&expander, &tbl); 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /src/layout.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct Layout { 5 | pub content: Dim, 6 | pub margin: Dim, // top and left margin for the whole (including texts) 7 | pub trim: Dim, // what part of the maze, left and top, is out of screen 8 | pub double_sizes: bool, 9 | } 10 | 11 | impl Layout { 12 | /// return the canonical cell position from a maze position. 13 | /// 14 | /// If the layout is double_sizes, this is the leftest one of the two 15 | /// cells which make the position. 16 | /// If the layout is half size, only one half of the cell belongs to 17 | /// the provided maze position. 18 | /// Return None if the position is out of the maze rendering area (can't 19 | /// happen in double_sizes mode) 20 | pub fn maze_to_screen( 21 | &self, 22 | pos: Pos, 23 | ) -> Option<(u16, u16)> { 24 | if self.double_sizes { 25 | Some(self.maze_to_screen_double_size(pos)) 26 | } else { 27 | if pos.x < self.trim.w || pos.y < self.trim.h { 28 | None 29 | } else { 30 | let x = pos.x - self.trim.w; 31 | let y = pos.y - self.trim.h; 32 | if x >= self.content.w || y >= self.content.h { 33 | None 34 | } else { 35 | let x = self.margin.w + x; 36 | let y = self.margin.h + 1 + y / 2; // 1 for the top texts 37 | Some((x as u16, y as u16)) 38 | } 39 | } 40 | } 41 | } 42 | /// Assuming the layout is in double_sizes mode, return the leftest 43 | /// cell for tye maze position 44 | pub fn maze_to_screen_double_size( 45 | &self, 46 | pos: Pos, 47 | ) -> (u16, u16) { 48 | let x = self.margin.w + 2 * pos.x; 49 | let y = self.margin.h + pos.y + 1; // 1 for the top texts 50 | (x as u16, y as u16) 51 | } 52 | pub fn compute( 53 | maze_dim: Dim, 54 | player_pos: Option, 55 | display: Display, 56 | ) -> Self { 57 | let content_width; 58 | let content_height; 59 | let mut left_trim = 0; 60 | let mut top_trim = 0; 61 | let left_margin; 62 | let top_margin; 63 | let double_sizes; 64 | // we assume maze_dim.h is fair (it must be) 65 | match display { 66 | Display::Alternate(Dim { w, h }) => { 67 | let available_width = w; 68 | let available_height = h - 3; 69 | double_sizes = 2 * maze_dim.w < available_width && maze_dim.h < available_height; 70 | if double_sizes { 71 | content_width = 2 * maze_dim.w; 72 | content_height = maze_dim.h; 73 | } else { 74 | if maze_dim.w > available_width { 75 | content_width = available_width; 76 | if let Some(player) = player_pos { 77 | if player.x > available_width / 2 { 78 | left_trim = (player.x - available_width / 2) 79 | .min(maze_dim.w - content_width); 80 | } 81 | } 82 | } else { 83 | content_width = maze_dim.w; 84 | } 85 | if maze_dim.h / 2 > available_height { 86 | content_height = available_height; 87 | if let Some(player) = player_pos { 88 | if player.y > available_height { 89 | top_trim = (player.y - available_height) 90 | .min(maze_dim.h - 2 * content_height); 91 | } 92 | } 93 | } else { 94 | content_height = maze_dim.h / 2; 95 | } 96 | }; 97 | left_margin = (available_width - content_width) / 2; 98 | top_margin = (available_height - content_height) / 2; 99 | } 100 | Display::Standard => { 101 | left_margin = 1; 102 | top_margin = 0; 103 | double_sizes = maze_dim.w < 20 && maze_dim.h < 30; 104 | if double_sizes { 105 | content_width = 2 * maze_dim.w; 106 | content_height = maze_dim.h; 107 | } else { 108 | content_width = maze_dim.w; 109 | content_height = maze_dim.h / 2; 110 | }; 111 | } 112 | } 113 | Self { 114 | content: Dim::new(content_width, content_height), 115 | margin: Dim::new(left_margin, top_margin), 116 | trim: Dim::new(left_trim, top_trim), 117 | double_sizes, 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate cli_log; 3 | 4 | mod achievements; 5 | mod args; 6 | mod cell_draw; 7 | mod dim; 8 | mod display; 9 | mod events; 10 | mod hof; 11 | mod layout; 12 | mod maze; 13 | mod nature; 14 | mod path; 15 | mod pos; 16 | mod pos_map; 17 | mod renderer; 18 | mod run; 19 | mod skin; 20 | mod specs; 21 | 22 | use { 23 | clap::Parser, 24 | std::io::{ 25 | self, 26 | Write, 27 | }, 28 | termimad::crossterm::{ 29 | QueueableCommand, 30 | cursor, 31 | event::{ 32 | DisableMouseCapture, 33 | EnableMouseCapture, 34 | }, 35 | terminal::{ 36 | self, 37 | EnterAlternateScreen, 38 | LeaveAlternateScreen, 39 | }, 40 | }, 41 | }; 42 | 43 | pub use { 44 | achievements::*, 45 | args::*, 46 | cell_draw::*, 47 | dim::*, 48 | display::*, 49 | events::*, 50 | layout::*, 51 | maze::*, 52 | nature::*, 53 | pos::*, 54 | pos_map::*, 55 | renderer::*, 56 | run::*, 57 | skin::*, 58 | specs::*, 59 | }; 60 | 61 | /// play the game, runing level after level, 62 | /// in an alternate terminal 63 | fn play(args: &Args) -> anyhow::Result<()> { 64 | let skin = Skin::build(); 65 | let mut w = std::io::BufWriter::new(std::io::stderr()); 66 | w.queue(EnterAlternateScreen)?; 67 | w.queue(cursor::Hide)?; 68 | w.queue(EnableMouseCapture)?; 69 | terminal::enable_raw_mode()?; 70 | let r = run(&mut w, &skin, args); 71 | w.flush()?; 72 | terminal::disable_raw_mode()?; 73 | w.queue(DisableMouseCapture)?; 74 | w.queue(cursor::Show)?; 75 | w.queue(LeaveAlternateScreen)?; 76 | w.flush()?; 77 | r 78 | } 79 | 80 | /// build a maze and print it on stdout 81 | fn build(args: &Args) -> anyhow::Result<()> { 82 | let specs = if let Some(level) = args.level { 83 | let user = &args.user; 84 | if Database::can_play(user, level)? { 85 | Specs::for_level(level) 86 | } else { 87 | anyhow::bail!( 88 | "User {user:?} must win the previous levels before printing level {level}" 89 | ) 90 | } 91 | } else { 92 | Specs::for_terminal_build()? 93 | }; 94 | debug!("specs: {:#?}", &specs); 95 | let skin = Skin::build(); 96 | let maze: Maze = specs.into(); 97 | let renderer = Renderer { 98 | display: Display::Standard, 99 | skin: &skin, 100 | }; 101 | renderer.write(&mut io::stdout(), &maze) 102 | } 103 | 104 | fn main() -> anyhow::Result<()> { 105 | init_cli_log!(); 106 | let args = Args::parse(); 107 | info!("launch args: {:#?}", &args); 108 | if args.hof { 109 | hof::print() 110 | } else if args.reset { 111 | Database::reset(&args.user, true) 112 | } else if args.build { 113 | build(&args) 114 | } else { 115 | play(&args) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/maze.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::*, 3 | rand::{ 4 | Rng, 5 | thread_rng, 6 | }, 7 | smallvec::SmallVec, 8 | }; 9 | 10 | pub const MIN_JUMP: usize = 2; 11 | pub const BLAST_RADIUS: usize = 4; // must be greater than MIN_JUMP 12 | pub const MIN_DIM: usize = 7; // must be greater than BLAST_RADIUS + 2 13 | 14 | /// A maze and the state of the game (player 15 | /// and monster positions, etc.) 16 | pub struct Maze { 17 | pub name: String, 18 | pub dim: Dim, 19 | rooms: PosSet, 20 | invisible_walls: PosSet, // look like rooms, but can't teleport to them 21 | openings: Vec, // used in growth: where it's possible to dig a new cell 22 | exit: Option, 23 | start: Option, 24 | player: Option, 25 | cuts: Vec, 26 | highlights: PosSet, 27 | monsters: Vec, 28 | turn: usize, // a counter incremented at every end_turn 29 | next_monster: usize, // turn at which a new monster should appear 30 | pub lives: i32, 31 | monsters_period: usize, 32 | potions: PosSet, 33 | max_monsters: usize, 34 | pub default_status: &'static str, 35 | squared_radius: Option, 36 | } 37 | impl Maze { 38 | pub fn new>( 39 | name: S, 40 | dim: Dim, 41 | ) -> Self { 42 | let width = dim.w; 43 | let height = (dim.h / 2) * 2; 44 | Self { 45 | name: name.into(), 46 | dim: Dim::new(width, height), 47 | rooms: PosSet::new(dim, false), 48 | invisible_walls: PosSet::new(dim, false), 49 | openings: Vec::new(), 50 | start: None, 51 | exit: None, 52 | player: None, 53 | cuts: Vec::new(), 54 | monsters: Vec::new(), 55 | highlights: PosSet::new(dim, false), 56 | turn: 0, 57 | next_monster: 50.min((width + height) / 3), 58 | lives: 1, 59 | monsters_period: width + height - 3, 60 | potions: PosSet::new(dim, false), 61 | max_monsters: 10, 62 | default_status: "", 63 | squared_radius: None, 64 | } 65 | } 66 | pub fn start(&self) -> Option { 67 | self.start 68 | } 69 | pub fn set_start( 70 | &mut self, 71 | player: Pos, 72 | ) { 73 | self.start = Some(player); 74 | self.player = Some(player); 75 | self.open(player); 76 | } 77 | pub fn status(&self) -> &'static str { 78 | if self.is_won() { 79 | "You win. Hit any key for next level" 80 | } else if self.is_lost() { 81 | "You lost. Hit any key to try again" 82 | } else { 83 | self.default_status 84 | } 85 | } 86 | pub fn player(&self) -> Option { 87 | self.player 88 | } 89 | pub fn is_won(&self) -> bool { 90 | self.player == self.exit 91 | } 92 | pub fn is_lost(&self) -> bool { 93 | self.lives < 1 94 | } 95 | pub fn is_wall( 96 | &self, 97 | p: Pos, 98 | ) -> bool { 99 | !self.rooms.get(p) 100 | } 101 | pub fn is_room( 102 | &self, 103 | p: Pos, 104 | ) -> bool { 105 | self.rooms.get(p) 106 | } 107 | pub fn give_up(&mut self) { 108 | self.lives = 0; 109 | } 110 | /// While a cell can contain several "things", one of them is 111 | /// more visible and determines how it looks 112 | pub fn visible_nature( 113 | &self, 114 | p: Pos, 115 | ) -> Nature { 116 | if !self.rooms.get(p) { 117 | if self.invisible_walls.get(p) { 118 | Nature::InvisibleWall 119 | } else { 120 | Nature::Wall 121 | } 122 | } else if self.monsters.contains(&p) { 123 | Nature::Monster 124 | } else if Some(p) == self.player { 125 | Nature::Player 126 | } else if self.potions.get(p) { 127 | Nature::Potion 128 | } else if self.highlights.get(p) { 129 | Nature::Highlight 130 | } else { 131 | Nature::Room 132 | } 133 | } 134 | fn center(&self) -> Pos { 135 | Pos::new(self.dim.w / 2, self.dim.h / 2) 136 | } 137 | fn open( 138 | &mut self, 139 | p: Pos, 140 | ) { 141 | self.rooms.set(p, true); 142 | let neighbours = self.inside_neighbours(p); 143 | for p in neighbours { 144 | if self.is_wall(p) { 145 | if let Some(squared_radius) = self.squared_radius { 146 | if Pos::sq_euclidian_distance(p, self.center()) > squared_radius { 147 | continue; 148 | } 149 | } 150 | self.openings.push(p); 151 | } 152 | } 153 | } 154 | pub fn pos_in_dir( 155 | &self, 156 | pos: Pos, 157 | dir: Dir, 158 | ) -> Option { 159 | match dir { 160 | Dir::Up => { 161 | if pos.y == 0 { 162 | None 163 | } else { 164 | Some(Pos::new(pos.x, pos.y - 1)) 165 | } 166 | } 167 | Dir::Right => { 168 | if pos.x == self.dim.w - 1 { 169 | None 170 | } else { 171 | Some(Pos::new(pos.x + 1, pos.y)) 172 | } 173 | } 174 | Dir::Down => { 175 | if pos.y == self.dim.h - 1 { 176 | None 177 | } else { 178 | Some(Pos::new(pos.x, pos.y + 1)) 179 | } 180 | } 181 | Dir::Left => { 182 | if pos.x == 0 { 183 | None 184 | } else { 185 | Some(Pos::new(pos.x - 1, pos.y)) 186 | } 187 | } 188 | } 189 | } 190 | /// Try moving the player in the given direction, adding both this move 191 | /// and the monster move to the provided moves vector. 192 | pub fn try_move( 193 | &mut self, 194 | dir: Dir, 195 | events: &mut EventList, 196 | ) { 197 | let Some(p) = self.player else { 198 | return; 199 | }; 200 | let Some(dest) = self.pos_in_dir(p, dir) else { 201 | return; 202 | }; 203 | if !self.is_room(dest) { 204 | return; 205 | } 206 | events.add_player_move(p, dir, self.visible_nature(dest)); 207 | self.player = Some(dest); 208 | self.player_moved(events); 209 | } 210 | fn seek_open(&mut self) -> bool { 211 | let mut rng = thread_rng(); 212 | loop { 213 | if self.openings.is_empty() { 214 | return false; 215 | } 216 | let len = self.openings.len(); 217 | let tail = match len % 35 { 218 | 0 => len, 219 | 1 => len.min(15), 220 | _ => len.min(4), 221 | }; 222 | let idx: usize = rng.gen_range(0..tail); 223 | let opening = self.openings.swap_remove(len - idx - 1); 224 | let neighbours = self.inside_neighbours(opening); 225 | let room_count = neighbours.iter().filter(|&p| self.is_room(*p)).count(); 226 | if room_count != 1 { 227 | continue; 228 | } 229 | self.open(opening); 230 | return true; 231 | } 232 | } 233 | fn can_place_exit(&self) -> bool { 234 | for x in 1..self.dim.w - 1 { 235 | if self.is_room(Pos::new(x, 1)) { 236 | return true; 237 | } 238 | if self.is_room(Pos::new(x, self.dim.h - 2)) { 239 | return true; 240 | } 241 | } 242 | for y in 1..self.dim.h - 1 { 243 | if self.is_room(Pos::new(1, y)) { 244 | return true; 245 | } 246 | if self.is_room(Pos::new(self.dim.w - 2, y)) { 247 | return true; 248 | } 249 | } 250 | false 251 | } 252 | // border walls which, when open, make an exit 253 | fn possible_exits(&self) -> Vec { 254 | let mut possible_exits = Vec::new(); 255 | for x in 1..self.dim.w - 1 { 256 | if self.is_room(Pos::new(x, 1)) { 257 | possible_exits.push(Pos::new(x, 0)); 258 | } 259 | if self.is_room(Pos::new(x, self.dim.h - 2)) { 260 | possible_exits.push(Pos::new(x, self.dim.h - 1)); 261 | } 262 | } 263 | for y in 1..self.dim.h - 1 { 264 | if self.is_room(Pos::new(1, y)) { 265 | possible_exits.push(Pos::new(0, y)); 266 | } 267 | if self.is_room(Pos::new(self.dim.w - 2, y)) { 268 | possible_exits.push(Pos::new(self.dim.w - 1, y)); 269 | } 270 | } 271 | possible_exits 272 | } 273 | pub fn possible_jumps( 274 | &self, 275 | p: Pos, 276 | ) -> Vec { 277 | let mut possible_jumps = Vec::new(); 278 | let r = BLAST_RADIUS 279 | .min(self.dim.w / 2 - 3) 280 | .min(self.dim.h / 2 - 3) 281 | .max(1); 282 | let c = Pos::new( 283 | p.x.max(r + 1).min(self.dim.w - r - 1), 284 | p.y.max(r + 1).min(self.dim.h - r - 1), 285 | ); 286 | for x in c.x - r..=c.x + r { 287 | for y in c.y - r..=c.y + r { 288 | let d = Pos::new(x, y); 289 | if self.is_wall(d) || self.monsters.contains(&d) { 290 | continue; 291 | } 292 | if Pos::manhattan_distance(p, d) >= MIN_JUMP { 293 | possible_jumps.push(d); 294 | } 295 | } 296 | } 297 | possible_jumps 298 | } 299 | fn len_to_player( 300 | &self, 301 | p: Pos, 302 | ) -> Option { 303 | self.player 304 | .and_then(|player| path::find_astar(self, player, p)) 305 | .map(|path| path.len()) 306 | } 307 | fn try_make_exit(&mut self) { 308 | if let Some(exit) = self.exit { 309 | self.rooms.set(exit, true); 310 | } 311 | self.exit = self 312 | .possible_exits() 313 | .drain(..) 314 | .max_by_key(|p| self.len_to_player(*p).unwrap_or(0)); 315 | if let Some(exit) = self.exit { 316 | self.rooms.set(exit, true); 317 | } 318 | } 319 | fn empty_rooms(&self) -> Vec { 320 | let mut empty_rooms = Vec::new(); 321 | for x in 1..self.dim.w - 1 { 322 | for y in 1..self.dim.h - 1 { 323 | let p = Pos::new(x, y); 324 | if self.is_room(p) 325 | && Some(p) != self.player 326 | && !self.potions.get(p) 327 | && !self.monsters.contains(&p) 328 | { 329 | empty_rooms.push(p); 330 | } 331 | } 332 | } 333 | empty_rooms 334 | } 335 | fn possible_cuts(&self) -> Vec { 336 | let mut possible_cuts = Vec::new(); 337 | for x in 1..self.dim.w - 1 { 338 | for y in 1..self.dim.h - 1 { 339 | let p = Pos::new(x, y); 340 | if let Some(squared_radius) = self.squared_radius { 341 | if Pos::sq_euclidian_distance(p, self.center()) > squared_radius { 342 | continue; 343 | } 344 | } 345 | if !self.is_wall(p) || Some(p) == self.player { 346 | continue; 347 | } 348 | // we check the position isn't surrounded by walls 349 | let only_walls = self 350 | .neighbours_8(p) 351 | .iter() 352 | .all(|&n| self.is_wall(n) || self.is_wall(n)); 353 | if only_walls { 354 | continue; 355 | } 356 | possible_cuts.push(p); 357 | } 358 | } 359 | possible_cuts 360 | } 361 | fn add_cuts( 362 | &mut self, 363 | n: usize, 364 | ) { 365 | debug!("adding {n} cuts"); 366 | let mut possible_cuts = self.possible_cuts(); 367 | let mut rng = thread_rng(); 368 | let mut added = 0; 369 | while added < n && !possible_cuts.is_empty() { 370 | let idx: usize = rng.gen_range(0..possible_cuts.len()); 371 | let cut = possible_cuts.swap_remove(idx); 372 | self.cuts.push(cut); 373 | self.rooms.set(cut, true); 374 | added += 1; 375 | } 376 | } 377 | fn add_potions( 378 | &mut self, 379 | n: usize, 380 | ) { 381 | debug!("adding {n} potions"); 382 | let mut empty_rooms = self.empty_rooms(); 383 | let mut rng = thread_rng(); 384 | let mut added = 0; 385 | while added < n && !empty_rooms.is_empty() { 386 | let idx: usize = rng.gen_range(0..empty_rooms.len()); 387 | let potion = empty_rooms.swap_remove(idx); 388 | self.potions.set(potion, true); 389 | added += 1; 390 | } 391 | } 392 | #[allow(dead_code)] 393 | fn neighbours( 394 | &self, 395 | p: Pos, 396 | ) -> SmallVec<[Pos; 4]> { 397 | let mut list = SmallVec::new(); 398 | if p.y > 0 { 399 | list.push(Pos::new(p.x, p.y - 1)); 400 | } 401 | if p.x < self.dim.w - 1 { 402 | list.push(Pos::new(p.x + 1, p.y)); 403 | } 404 | if p.y < self.dim.h - 1 { 405 | list.push(Pos::new(p.x, p.y + 1)); 406 | } 407 | if p.x > 0 { 408 | list.push(Pos::new(p.x - 1, p.y)); 409 | } 410 | list 411 | } 412 | // neighbour cells, including diagonals 413 | fn neighbours_8( 414 | &self, 415 | p: Pos, 416 | ) -> SmallVec<[Pos; 8]> { 417 | let mut list = SmallVec::new(); 418 | if p.x > 0 && p.y > 0 { 419 | list.push(Pos::new(p.x - 1, p.y - 1)); 420 | } 421 | if p.y > 0 { 422 | list.push(Pos::new(p.x, p.y - 1)); 423 | } 424 | if p.x < self.dim.w - 1 && p.y > 0 { 425 | list.push(Pos::new(p.x + 1, p.y - 1)); 426 | } 427 | if p.x < self.dim.w - 1 { 428 | list.push(Pos::new(p.x + 1, p.y)); 429 | } 430 | if p.x < self.dim.w - 1 && p.y < self.dim.h - 1 { 431 | list.push(Pos::new(p.x + 1, p.y + 1)); 432 | } 433 | if p.y < self.dim.h - 1 { 434 | list.push(Pos::new(p.x, p.y + 1)); 435 | } 436 | if p.x > 0 && p.y < self.dim.h - 1 { 437 | list.push(Pos::new(p.x - 1, p.y + 1)); 438 | } 439 | if p.x > 0 { 440 | list.push(Pos::new(p.x - 1, p.y)); 441 | } 442 | list 443 | } 444 | // (not counting the border) 445 | fn inside_neighbours( 446 | &self, 447 | p: Pos, 448 | ) -> SmallVec<[Pos; 4]> { 449 | let mut list = SmallVec::new(); 450 | if p.y > 1 { 451 | list.push(Pos::new(p.x, p.y - 1)); 452 | } 453 | if p.x < self.dim.w - 2 { 454 | list.push(Pos::new(p.x + 1, p.y)); 455 | } 456 | if p.y < self.dim.h - 2 { 457 | list.push(Pos::new(p.x, p.y + 1)); 458 | } 459 | if p.x > 1 { 460 | list.push(Pos::new(p.x - 1, p.y)); 461 | } 462 | list 463 | } 464 | pub fn enterable_neighbours( 465 | &self, 466 | p: Pos, 467 | ) -> SmallVec<[Pos; 4]> { 468 | let mut list = SmallVec::new(); 469 | if p.y > 0 { 470 | let e = Pos::new(p.x, p.y - 1); 471 | if self.is_room(e) { 472 | list.push(e); 473 | } 474 | } 475 | if p.x < self.dim.w - 1 { 476 | let e = Pos::new(p.x + 1, p.y); 477 | if self.is_room(e) { 478 | list.push(e); 479 | } 480 | } 481 | if p.y < self.dim.h - 1 { 482 | let e = Pos::new(p.x, p.y + 1); 483 | if self.is_room(e) { 484 | list.push(e); 485 | } 486 | } 487 | if p.x > 0 { 488 | let e = Pos::new(p.x - 1, p.y); 489 | if self.is_room(e) { 490 | list.push(e); 491 | } 492 | } 493 | list 494 | } 495 | fn grow( 496 | &mut self, 497 | max: usize, 498 | ) -> usize { 499 | for n in 0..max { 500 | let open = self.seek_open(); 501 | if !open { 502 | return n; 503 | } 504 | } 505 | max 506 | } 507 | /// Due to cuts added after growing, some rooms may be unreachable 508 | /// in case of interrupted growing. This function makes them 509 | /// invisble walls to ensure we can't teleport to them. 510 | fn change_unreachable_rooms_into_invisible_walls(&mut self) { 511 | let Some(exit) = self.exit else { 512 | return; 513 | }; 514 | for x in 0..self.dim.w { 515 | for y in 0..self.dim.h { 516 | let pos = Pos::new(x, y); 517 | if !self.rooms.get(pos) { 518 | continue; 519 | } 520 | let path = path::find_astar(self, pos, exit); 521 | if path.is_none() { 522 | self.rooms.set(pos, false); 523 | self.invisible_walls.set(pos, true); 524 | } 525 | } 526 | } 527 | } 528 | /// Make some walls invisible, for cosmetic reasons 529 | /// 530 | /// Warning: don't call this before the maze is fully grown and 531 | /// the exit has been set. 532 | fn grow_invisible_walls(&mut self) { 533 | let mut candidates = Vec::new(); 534 | let mut seen = PosSet::new(self.dim, false); 535 | for x in 0..self.dim.w { 536 | for y in 0..self.dim.h { 537 | let pos = Pos::new(x, y); 538 | if !self.rooms.get(pos) { 539 | candidates.push(pos); 540 | seen.set(pos, true); 541 | } 542 | } 543 | } 544 | while let Some(candidate) = candidates.pop() { 545 | let mut all_walls = true; 546 | for neighbour in self.neighbours_8(candidate) { 547 | if self.rooms.get(neighbour) { 548 | all_walls = false; 549 | } else if !seen.get(neighbour) { 550 | candidates.push(neighbour); 551 | seen.set(neighbour, true); 552 | } 553 | } 554 | if all_walls { 555 | self.invisible_walls.set(candidate, true); 556 | } 557 | } 558 | } 559 | pub fn set_highlights( 560 | &mut self, 561 | arr: &[Pos], 562 | ) { 563 | self.highlights.clear(); 564 | for p in arr { 565 | self.highlights.set(*p, true); 566 | } 567 | } 568 | pub fn clear_highlight(&mut self) -> bool { 569 | if self.highlights.is_empty() { 570 | // slow 571 | false 572 | } else { 573 | self.highlights.clear(); 574 | true 575 | } 576 | } 577 | pub fn highlight_start(&mut self) { 578 | if let Some(start) = self.start { 579 | self.highlights.set(start, true); 580 | } 581 | } 582 | pub fn highlight_path_to_exit( 583 | &mut self, 584 | from: Option, 585 | ) { 586 | if let (Some(start), Some(exit)) = (from, self.exit) { 587 | let path = time!(path::find_astar(self, start, exit)); 588 | if let Some(path) = path { 589 | for p in &path { 590 | self.highlights.set(*p, true); 591 | } 592 | } 593 | self.highlights.set(exit, true); 594 | } 595 | } 596 | // remove a life and teleport the player 597 | pub fn kill_player( 598 | &mut self, 599 | events: &mut EventList, 600 | ) { 601 | self.lives -= 1; 602 | if let Some(player) = self.player { 603 | if self.lives > 0 { 604 | // random jump on collision 605 | let possible_jumps = self.possible_jumps(player); 606 | if possible_jumps.is_empty() { 607 | self.lives = 0; 608 | } else { 609 | let mut rng = thread_rng(); 610 | let idx = rng.gen_range(0..possible_jumps.len()); 611 | let dest = possible_jumps[idx]; 612 | events.add_teleport(player, possible_jumps, dest); 613 | self.player = Some(dest); 614 | if self.potions.remove(dest) { 615 | self.lives += 1; 616 | } 617 | } 618 | } 619 | } else { 620 | self.lives = 0; // there's no player anyway... 621 | } 622 | debug!("Remaining lives: {}", self.lives); 623 | } 624 | pub fn player_moved( 625 | &mut self, 626 | events: &mut EventList, 627 | ) { 628 | if let Some(player) = self.player { 629 | if self.monsters.contains(&player) { 630 | self.kill_player(events); 631 | } else if self.potions.remove(player) { 632 | self.lives += 1; 633 | } 634 | } 635 | self.end_player_turn(events); 636 | } 637 | pub fn move_player_auto( 638 | &mut self, 639 | events: &mut EventList, 640 | ) { 641 | if let (Some(player), Some(exit)) = (self.player, self.exit) { 642 | // first we look for an adjacent life 643 | for dir in [Dir::Up, Dir::Right, Dir::Down, Dir::Left].iter() { 644 | if let Some(dest) = self.pos_in_dir(player, *dir) { 645 | if self.potions.get(dest) { 646 | self.try_move(*dir, events); 647 | return; 648 | } 649 | } 650 | } 651 | // then we just go towards the exit 652 | if let Some(path) = path::find_astar(self, player, exit) { 653 | let dest = path[0]; 654 | if self.monsters.contains(&dest) { 655 | self.end_player_turn(events); 656 | } else { 657 | self.try_move(player.dir_to(dest), events); 658 | } 659 | } else { 660 | // workaround for some invalid mazes I observed 661 | self.kill_player(events); 662 | self.end_player_turn(events); 663 | } 664 | } 665 | } 666 | /// move the world 667 | pub fn end_player_turn( 668 | &mut self, 669 | events: &mut EventList, 670 | ) { 671 | self.turn += 1; 672 | if let (Some(player), Some(exit)) = (self.player, self.exit) { 673 | for i in 0..self.monsters.len() { 674 | if let Some(dir) = self.monsters[i].step_dir_to(player) { 675 | events.add_monster_move(self.monsters[i], dir, Nature::Player); 676 | self.monsters[i] = player; // monster takes the player's place 677 | self.kill_player(events); 678 | break; // other monsters don't move 679 | } 680 | if let Some(path) = path::find_astar(self, self.monsters[i], player) { 681 | let dest = path[0]; 682 | if self.monsters.contains(&dest) { 683 | continue; 684 | } 685 | events.add_monster_move( 686 | self.monsters[i], 687 | self.monsters[i].dir_to(dest), 688 | self.visible_nature(dest), 689 | ); 690 | self.monsters[i] = dest; 691 | self.potions.set(dest, false); 692 | if dest == player { 693 | self.kill_player(events); 694 | break; // other monsters don't move 695 | } 696 | } 697 | } 698 | if self.monsters.len() < self.max_monsters && self.turn == self.next_monster { 699 | let can_appear = exit != player && !self.monsters.contains(&exit); 700 | if can_appear { 701 | self.monsters.push(exit); 702 | } else { 703 | self.next_monster += 1; 704 | } 705 | if self.monsters.len() < 10 { 706 | self.next_monster = self.turn + self.monsters_period; 707 | self.monsters_period += match self.monsters.len() { 708 | 1 => 105, 709 | 2 => 60, 710 | _ => 35, 711 | }; 712 | } 713 | } 714 | } 715 | } 716 | } 717 | 718 | impl From for Maze { 719 | fn from(specs: Specs) -> Self { 720 | let width = specs.dim.w; 721 | let height = specs.dim.h; 722 | let mut maze = Self::new(&specs.name, specs.dim); 723 | if specs.disk { 724 | let d = width.min(height) / 2; 725 | if d > 10 { 726 | maze.squared_radius = Some((d + 1) * (d + 1)); 727 | } 728 | } 729 | maze.lives = specs.lives; 730 | let mut rng = thread_rng(); 731 | loop { 732 | let start = Pos::new( 733 | rng.gen_range(width / 6..width * 5 / 6), 734 | rng.gen_range(height / 6..height * 5 / 6), 735 | ); 736 | if let Some(squared_radius) = maze.squared_radius { 737 | if Pos::sq_euclidian_distance(start, maze.center()) + 2 > squared_radius { 738 | // this isn't a valid starting position: we might be unable to grow 739 | // from there 740 | continue; 741 | } 742 | } 743 | maze.set_start(start); 744 | break; 745 | } 746 | if specs.fill { 747 | while maze.grow(10) > 0 {} 748 | } else { 749 | let n = (width * height) / 3; 750 | loop { 751 | info!("growing 1"); 752 | if maze.grow(n) == 0 { 753 | break; 754 | } 755 | if maze.can_place_exit() { 756 | break; 757 | } 758 | } 759 | } 760 | maze.add_cuts(specs.cuts); 761 | maze.add_potions(specs.potions); 762 | maze.try_make_exit(); 763 | maze.grow_invisible_walls(); 764 | maze.change_unreachable_rooms_into_invisible_walls(); 765 | maze.default_status = specs.status; 766 | debug!("squared_radius: {:?}", maze.squared_radius); 767 | maze.max_monsters = specs.monsters; 768 | maze.monsters_period = 2 * (width * height) / (width + height); 769 | maze.monsters_period -= (maze.max_monsters * 7).min(maze.monsters_period); 770 | maze.monsters_period = maze.monsters_period.max(10); 771 | maze 772 | } 773 | } 774 | 775 | #[test] 776 | fn show() { 777 | for i in 1..500 { 778 | let specs = Specs::for_level(i); 779 | let maze = Maze::from(specs); 780 | println!( 781 | "{i} : {}x{} -> period: {}", 782 | maze.dim.w, maze.dim.h, maze.monsters_period 783 | ); 784 | } 785 | todo!(); 786 | } 787 | -------------------------------------------------------------------------------- /src/nature.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 2 | pub enum Nature { 3 | Room, 4 | Wall, 5 | InvisibleWall, 6 | Player, 7 | Monster, 8 | Potion, 9 | Highlight, 10 | } 11 | -------------------------------------------------------------------------------- /src/path.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::*, 3 | std::collections::BinaryHeap, 4 | }; 5 | 6 | /// Find a short path between start and goal using A*. 7 | /// 8 | /// The returned path contains the goal but not the start. 9 | pub fn find_astar( 10 | maze: &Maze, 11 | start: Pos, 12 | goal: Pos, 13 | ) -> Option> { 14 | let dim = maze.dim; 15 | 16 | // nodes already evaluated, we know they're not interesting 17 | let mut closed_set = PosSet::new(dim, false); 18 | 19 | // node immediately preceding on the cheapest known path from start 20 | let mut came_from: PosMap = PosMap::new(dim, Pos::new(0, 0)); 21 | 22 | // g_score is the cost of the cheapest path from start to a pos 23 | let mut g_score: PosMap = PosMap::new(dim, i32::MAX); 24 | g_score.set(start, 0); 25 | 26 | // the nodes from which we may expand 27 | let mut open_set: BinaryHeap = BinaryHeap::new(); 28 | open_set.push(ValuedPos::from(start, 0)); 29 | 30 | while let Some(mut current) = open_set.pop().map(|vp| vp.pos) { 31 | closed_set.set(current, true); 32 | let neighbours = maze.enterable_neighbours(current); 33 | for neighbour in &neighbours { 34 | if Pos::sides(*neighbour, goal) { 35 | let mut path = vec![*neighbour]; 36 | while current != start { 37 | path.push(current); 38 | current = came_from.get(current); 39 | } 40 | path.reverse(); 41 | return Some(path); 42 | } 43 | if closed_set.get(*neighbour) { 44 | continue; 45 | } 46 | let tentative_g_score = g_score.get(current) + 1; 47 | let previous_g_score = g_score.get(*neighbour); 48 | if tentative_g_score < previous_g_score { 49 | came_from.set(*neighbour, current); 50 | g_score.set(*neighbour, tentative_g_score); 51 | let new_f_score = 52 | tentative_g_score + 2 * Pos::euclidian_distance(*neighbour, goal) as i32; 53 | open_set.push(ValuedPos::from(*neighbour, new_f_score)); 54 | } 55 | } 56 | } 57 | 58 | // open_set is empty, there's no path 59 | None 60 | } 61 | -------------------------------------------------------------------------------- /src/pos.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::*, 3 | std::cmp::Ordering, 4 | }; 5 | 6 | /// A position 7 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 8 | pub struct Pos { 9 | pub x: usize, 10 | pub y: usize, 11 | } 12 | impl Pos { 13 | pub fn new( 14 | x: usize, 15 | y: usize, 16 | ) -> Self { 17 | Self { x, y } 18 | } 19 | fn dim( 20 | a: Pos, 21 | b: Pos, 22 | ) -> Dim { 23 | let w = if a.x > b.x { a.x - b.x } else { b.x - a.x }; 24 | let h = if a.y > b.y { a.y - b.y } else { b.y - a.y }; 25 | Dim::new(w, h) 26 | } 27 | pub fn sq_euclidian_distance( 28 | a: Pos, 29 | b: Pos, 30 | ) -> usize { 31 | let Dim { w, h } = Self::dim(a, b); 32 | w * w + h * h 33 | } 34 | pub fn euclidian_distance( 35 | a: Pos, 36 | b: Pos, 37 | ) -> f32 { 38 | (Pos::sq_euclidian_distance(a, b) as f32).sqrt() 39 | } 40 | pub fn manhattan_distance( 41 | a: Pos, 42 | b: Pos, 43 | ) -> usize { 44 | let Dim { w, h } = Self::dim(a, b); 45 | w + h 46 | } 47 | pub fn sides( 48 | a: Pos, 49 | b: Pos, 50 | ) -> bool { 51 | Self::manhattan_distance(a, b) == 1 52 | } 53 | /// Return one of the 4 directions if the two positions are just one step away 54 | pub fn step_dir_to( 55 | self, 56 | other: Pos, 57 | ) -> Option { 58 | if other.y == self.y { 59 | if other.x == self.x + 1 { 60 | Some(Dir::Right) 61 | } else if other.x + 1 == self.x { 62 | Some(Dir::Left) 63 | } else { 64 | None 65 | } 66 | } else if other.x == self.x { 67 | if other.y == self.y + 1 { 68 | Some(Dir::Down) 69 | } else if other.y + 1 == self.y { 70 | Some(Dir::Up) 71 | } else { 72 | None 73 | } 74 | } else { 75 | None 76 | } 77 | } 78 | /// Assuming that other is in one of the 4 directions, return the direction 79 | /// (will return something bad if it's eg the same pos) 80 | pub fn dir_to( 81 | self, 82 | other: Pos, 83 | ) -> Dir { 84 | if other.x > self.x { 85 | Dir::Right 86 | } else if other.x < self.x { 87 | Dir::Left 88 | } else if other.y > self.y { 89 | Dir::Down 90 | } else { 91 | Dir::Up 92 | } 93 | } 94 | pub fn in_dir( 95 | self, 96 | dir: Dir, 97 | ) -> Option { 98 | match dir { 99 | Dir::Up if self.y > 0 => Some(Pos::new(self.x, self.y - 1)), 100 | Dir::Right => Some(Pos::new(self.x + 1, self.y)), 101 | Dir::Down => Some(Pos::new(self.x, self.y + 1)), 102 | Dir::Left if self.x > 0 => Some(Pos::new(self.x - 1, self.y)), 103 | _ => None, 104 | } 105 | } 106 | } 107 | 108 | #[derive(Debug, Clone, Copy)] 109 | pub struct ValuedPos { 110 | pub pos: Pos, 111 | pub score: i32, 112 | } 113 | impl ValuedPos { 114 | pub fn from( 115 | pos: Pos, 116 | score: i32, 117 | ) -> Self { 118 | ValuedPos { pos, score } 119 | } 120 | } 121 | impl Eq for ValuedPos {} 122 | impl PartialEq for ValuedPos { 123 | fn eq( 124 | &self, 125 | other: &ValuedPos, 126 | ) -> bool { 127 | self.score == other.score 128 | } 129 | } 130 | // we order in reverse from score 131 | impl Ord for ValuedPos { 132 | fn cmp( 133 | &self, 134 | other: &ValuedPos, 135 | ) -> Ordering { 136 | other.score.cmp(&self.score) 137 | } 138 | } 139 | impl PartialOrd for ValuedPos { 140 | fn partial_cmp( 141 | &self, 142 | other: &ValuedPos, 143 | ) -> Option { 144 | Some(self.cmp(other)) 145 | } 146 | } 147 | 148 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 149 | pub enum Dir { 150 | Up, 151 | Right, 152 | Down, 153 | Left, 154 | } 155 | -------------------------------------------------------------------------------- /src/pos_map.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | /// A mapping between positions in a rect and some 4 | /// values, with a default value on positions you 5 | /// didn't explicitly set 6 | pub struct PosMap { 7 | dim: Dim, 8 | values: Box<[T]>, 9 | default_value: T, 10 | } 11 | 12 | impl PosMap { 13 | pub fn new( 14 | dim: Dim, 15 | default_value: T, 16 | ) -> Self { 17 | let values = vec![default_value; dim.w * dim.h].into_boxed_slice(); 18 | Self { 19 | dim, 20 | values, 21 | default_value, 22 | } 23 | } 24 | pub fn get( 25 | &self, 26 | p: Pos, 27 | ) -> T { 28 | self.values[self.dim.idx(p)] 29 | } 30 | pub fn set( 31 | &mut self, 32 | p: Pos, 33 | value: T, 34 | ) { 35 | self.values[self.dim.idx(p)] = value; 36 | } 37 | pub fn clear(&mut self) { 38 | self.values.fill(self.default_value); 39 | } 40 | pub fn remove( 41 | &mut self, 42 | p: Pos, 43 | ) -> T { 44 | let idx = self.dim.idx(p); 45 | let old = self.values[idx]; 46 | self.values[idx] = self.default_value; 47 | old 48 | } 49 | } 50 | 51 | impl PosMap { 52 | /// Warning: this function is slow 53 | pub fn is_empty(&self) -> bool { 54 | !self.is_not_empty() 55 | } 56 | /// tells whether there are not default values 57 | /// 58 | /// Warning: this function is slow 59 | /// (it could be optimized with a counter but 60 | /// there's no need today in mazter) 61 | pub fn is_not_empty(&self) -> bool { 62 | self.values.iter().any(|&v| v != self.default_value) 63 | } 64 | } 65 | pub type PosSet = PosMap; 66 | -------------------------------------------------------------------------------- /src/renderer.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::*, 3 | std::{ 4 | io::Write, 5 | thread, 6 | time::Duration, 7 | }, 8 | termimad::crossterm::{ 9 | QueueableCommand, 10 | cursor, 11 | style::{ 12 | Colors, 13 | Print, 14 | ResetColor, 15 | SetColors, 16 | SetForegroundColor, 17 | }, 18 | terminal::{ 19 | Clear, 20 | ClearType, 21 | }, 22 | }, 23 | }; 24 | 25 | /// Renders mazes on the set display 26 | pub struct Renderer<'s> { 27 | pub skin: &'s Skin, 28 | pub display: Display, 29 | } 30 | 31 | impl<'s> Renderer<'s> { 32 | fn is_alternate(&self) -> bool { 33 | matches!(self.display, Display::Alternate { .. }) 34 | } 35 | 36 | fn layout( 37 | &self, 38 | maze: &Maze, 39 | ) -> Layout { 40 | Layout::compute(maze.dim, maze.player(), self.display) 41 | } 42 | 43 | fn write_game_header( 44 | &self, 45 | w: &mut W, 46 | layout: &Layout, 47 | maze: &Maze, 48 | ) -> anyhow::Result<()> { 49 | w.queue(cursor::MoveTo(0, layout.margin.h as u16))?; 50 | self.spaces(w, layout.margin.w)?; 51 | w.queue(Print(&maze.name))?; 52 | let lives = if maze.lives > 3 { 53 | format!(" {} ■", maze.lives) 54 | } else { 55 | " ■".repeat(maze.lives as usize) 56 | }; 57 | if layout.content.w > maze.name.len() + 6 { 58 | self.spaces(w, layout.content.w - maze.name.len() - 6)?; 59 | } 60 | w.queue(SetColors(Colors { 61 | foreground: Some(self.skin.potion), 62 | background: None, 63 | }))?; 64 | w.queue(Print(lives))?; 65 | w.queue(Clear(ClearType::UntilNewLine))?; 66 | Ok(()) 67 | } 68 | 69 | fn write_game_status( 70 | &self, 71 | w: &mut W, 72 | layout: &Layout, 73 | maze: &Maze, 74 | ) -> anyhow::Result<()> { 75 | w.queue(cursor::MoveTo( 76 | 0, 77 | (1 + layout.margin.h + layout.content.h) as u16, 78 | ))?; 79 | self.spaces(w, layout.margin.w)?; 80 | w.queue(Print(maze.status()))?; 81 | w.queue(Clear(ClearType::UntilNewLine))?; 82 | Ok(()) 83 | } 84 | 85 | fn draw_teleport_half_size( 86 | &self, 87 | w: &mut W, 88 | layout: &Layout, 89 | maze: &Maze, 90 | teleport: &Teleport, 91 | ) -> anyhow::Result<()> { 92 | for &pos in &teleport.possible_jumps { 93 | let Some((x, y)) = layout.maze_to_screen(pos) else { 94 | continue; 95 | }; 96 | let colors = if pos.y % 2 == 0 { 97 | let bottom_is_jump = teleport 98 | .possible_jumps 99 | .iter() 100 | .any(|p| p.x == pos.x && p.y == pos.y + 1); 101 | let bottom_nature = if bottom_is_jump { 102 | Nature::Monster 103 | } else { 104 | maze.visible_nature(Pos::new(pos.x, pos.y + 1)) 105 | }; 106 | Colors { 107 | foreground: self.skin.color(Nature::Monster), 108 | background: self.skin.color(bottom_nature), 109 | } 110 | } else { 111 | let top_nature = if pos.y > 0 { 112 | let top_is_jump = teleport 113 | .possible_jumps 114 | .iter() 115 | .any(|p| p.x == pos.x && p.y == pos.y - 1); 116 | if top_is_jump { 117 | Nature::Monster 118 | } else { 119 | maze.visible_nature(Pos::new(pos.x, pos.y - 1)) 120 | } 121 | } else { 122 | Nature::Wall 123 | }; 124 | Colors { 125 | foreground: self.skin.color(top_nature), 126 | background: self.skin.color(Nature::Monster), 127 | } 128 | }; 129 | w.queue(cursor::MoveTo(x, y))?; 130 | w.queue(SetColors(colors))?; 131 | w.queue(Print('▀'))?; 132 | } 133 | Ok(()) 134 | } 135 | fn draw_teleport_double_size( 136 | &self, 137 | w: &mut W, 138 | layout: &Layout, 139 | teleport: &Teleport, 140 | ) -> anyhow::Result<()> { 141 | for &pos in &teleport.possible_jumps { 142 | // x and y are for the leftest one of the two cells 143 | let (x, y) = layout.maze_to_screen_double_size(pos); 144 | w.queue(cursor::MoveTo(x, y))?; 145 | w.queue(SetForegroundColor(self.skin.real_color(Nature::Monster)))?; 146 | w.queue(Print("██"))?; 147 | } 148 | Ok(()) 149 | } 150 | 151 | /// Draw one of the step (in [0..16] of the animation) of a moving pos 152 | /// when the maze is rendered in double size. 153 | /// 154 | /// The maze is supposed already rendered, only the moving pos is drawn. 155 | /// The pos_move is also supposed taking place in valid positions. 156 | fn draw_pos_move_step_double_size( 157 | &self, 158 | w: &mut W, 159 | layout: &Layout, 160 | pos_move: PosMove, 161 | av: usize, // in [0..16] 162 | ) -> anyhow::Result<()> { 163 | // x and y are for the leftest one of the two starting cells 164 | let (x, y) = layout.maze_to_screen_double_size(pos_move.start); 165 | let start_bg = self.skin.real_color(pos_move.start_background_nature); 166 | let dest_bg = self.skin.real_color(pos_move.dest_background_nature); 167 | let fg = self.skin.real_color(pos_move.moving_nature); 168 | match pos_move.dir { 169 | Dir::Up => { 170 | let av = av / 2; // 8 real steps 171 | // start pos, leaving 172 | draw_bicolor_vertical(w, x, y, fg, start_bg, av)?; 173 | draw_bicolor_vertical(w, x + 1, y, fg, start_bg, av)?; 174 | 175 | // dest pos, arriving 176 | draw_bicolor_vertical(w, x, y - 1, dest_bg, fg, av)?; 177 | draw_bicolor_vertical(w, x + 1, y - 1, dest_bg, fg, av)?; 178 | } 179 | Dir::Right => { 180 | let av_left = av.min(8); // left cell of each pos 181 | let av_right = if av <= 8 { 0 } else { av - 8 }; 182 | draw_bicolor_horizontal(w, x, y, start_bg, fg, av_left)?; 183 | draw_bicolor_horizontal(w, x + 1, y, start_bg, fg, av_right)?; 184 | draw_bicolor_horizontal(w, x + 2, y, fg, dest_bg, av_left)?; 185 | draw_bicolor_horizontal(w, x + 3, y, fg, dest_bg, av_right)?; 186 | } 187 | Dir::Down => { 188 | let av = av / 2; // 8 real steps 189 | // start pos, leaving 190 | draw_bicolor_vertical(w, x, y, start_bg, fg, 8 - av)?; 191 | draw_bicolor_vertical(w, x + 1, y, start_bg, fg, 8 - av)?; 192 | 193 | ////// dest pos, arriving 194 | draw_bicolor_vertical(w, x, y + 1, fg, dest_bg, 8 - av)?; 195 | draw_bicolor_vertical(w, x + 1, y + 1, fg, dest_bg, 8 - av)?; 196 | } 197 | Dir::Left => { 198 | let av_left = if av <= 8 { 8 } else { 8 - (av - 8) / 2 }; 199 | let av_right = 8 - av.min(8); // right cell of each pos 200 | draw_bicolor_horizontal(w, x, y, fg, start_bg, av_left)?; 201 | draw_bicolor_horizontal(w, x + 1, y, fg, start_bg, av_right)?; 202 | draw_bicolor_horizontal(w, x - 2, y, dest_bg, fg, av_left)?; 203 | draw_bicolor_horizontal(w, x - 1, y, dest_bg, fg, av_right)?; 204 | } 205 | } 206 | Ok(()) 207 | } 208 | 209 | /// Animate moves. Return true when the animation occured, false 210 | /// if it didn't. 211 | /// 212 | /// When there was no animation, this function is instant and the animation 213 | /// may have to be replaced by a wait. 214 | /// 215 | /// The maze is supposed already rendered, only the changes are drawn. 216 | pub fn animate_events( 217 | &mut self, 218 | w: &mut W, 219 | maze: &Maze, 220 | events: &EventList, 221 | ) -> anyhow::Result { 222 | let layout = self.layout(maze); 223 | if !self.is_alternate() { 224 | // a move can only be animated in alternate mode 225 | return Ok(false); 226 | } 227 | if self.skin.room.is_none() { 228 | // block character animation is not possible without a room color 229 | return Ok(false); 230 | } 231 | w.queue(ResetColor)?; 232 | // teleports are drawn only once 233 | let mut teleport_drawn = false; 234 | for event in &events.events { 235 | let Event::Teleport(teleport) = event else { 236 | continue; 237 | }; 238 | if layout.double_sizes { 239 | self.draw_teleport_double_size(w, &layout, teleport)?; 240 | } else { 241 | self.draw_teleport_half_size(w, &layout, maze, teleport)?; 242 | } 243 | teleport_drawn = true; 244 | w.queue(ResetColor)?; 245 | w.flush()?; 246 | } 247 | if layout.double_sizes { 248 | // moves are drawn step by step, but only in double size 249 | for av in 1..=16 { 250 | for event in &events.events { 251 | let Event::Move(pos_move) = event else { 252 | continue; 253 | }; 254 | self.draw_pos_move_step_double_size(w, &layout, *pos_move, av)?; 255 | } 256 | w.queue(ResetColor)?; 257 | w.flush()?; 258 | thread::sleep(Duration::from_millis(8)); 259 | } 260 | } else if teleport_drawn { 261 | // in half size, we just wait a bit after the teleport so that it's 262 | // visible 263 | thread::sleep(Duration::from_millis(120)); 264 | } 265 | Ok(true) 266 | } 267 | 268 | // the rendering when the maze is very small and we can afford using 2 characters 269 | // side by side for each game pos 270 | fn write_maze_double_size( 271 | &self, 272 | w: &mut W, 273 | layout: &Layout, 274 | maze: &Maze, 275 | ) -> anyhow::Result<()> { 276 | for y in 0..maze.dim.h { 277 | if self.is_alternate() { 278 | w.queue(cursor::MoveTo(0, (y + 1 + layout.margin.h) as u16))?; 279 | } 280 | self.spaces(w, layout.margin.w)?; 281 | for x in 0..maze.dim.w { 282 | let pos = Pos::new(x, y); 283 | let color = self.skin.color(maze.visible_nature(pos)); 284 | if color.is_some() { 285 | let colors = Colors { 286 | foreground: color, 287 | background: None, 288 | }; 289 | w.queue(SetColors(colors))?; 290 | w.queue(Print("██"))?; 291 | w.queue(ResetColor)?; 292 | } else { 293 | w.queue(Print(" "))?; 294 | } 295 | } 296 | if self.is_alternate() { 297 | w.queue(Clear(ClearType::UntilNewLine))?; 298 | } else { 299 | writeln!(w)?; 300 | } 301 | } 302 | Ok(()) 303 | } 304 | 305 | fn write_maze_half_size( 306 | &self, 307 | w: &mut W, 308 | layout: &Layout, 309 | maze: &Maze, 310 | ) -> anyhow::Result<()> { 311 | for l in 0..layout.content.h { 312 | if self.is_alternate() { 313 | // a terminal line is two maze rows, and there's a +1 for the top texts 314 | w.queue(cursor::MoveTo(0, (l + 1 + layout.margin.h) as u16))?; 315 | } 316 | self.spaces(w, layout.margin.w)?; 317 | for i in 0..layout.content.w { 318 | let x = i + layout.trim.w; 319 | let top_pos = Pos::new(x, 2 * l + layout.trim.h); 320 | let bot_pos = Pos::new(x, 2 * l + layout.trim.h + 1); 321 | let top = self.skin.color(maze.visible_nature(top_pos)); 322 | let bot = self.skin.color(maze.visible_nature(bot_pos)); 323 | let (shape, colors) = match (top.is_some(), bot.is_some()) { 324 | (true, true) => ('▀', Colors { 325 | foreground: top, 326 | background: bot, 327 | }), 328 | (true, false) => ('▀', Colors { 329 | foreground: top, 330 | background: None, 331 | }), 332 | (false, true) => ('▄', Colors { 333 | foreground: bot, 334 | background: None, 335 | }), 336 | (false, false) => (' ', Colors { 337 | foreground: None, 338 | background: None, 339 | }), 340 | }; 341 | w.queue(SetColors(colors))?; 342 | w.queue(Print(shape))?; 343 | w.queue(ResetColor)?; 344 | } 345 | if self.is_alternate() { 346 | w.queue(Clear(ClearType::UntilNewLine))?; 347 | } else { 348 | writeln!(w)?; 349 | } 350 | } 351 | Ok(()) 352 | } 353 | 354 | fn spaces( 355 | &self, 356 | w: &mut W, 357 | n: usize, 358 | ) -> anyhow::Result<()> { 359 | for _ in 0..n { 360 | w.queue(Print(' '))?; 361 | } 362 | Ok(()) 363 | } 364 | 365 | /// Render the maze (with title and lives count) for the TUI, 366 | /// assuming a buffered writer in an alternate 367 | pub fn write( 368 | &self, 369 | w: &mut W, 370 | maze: &Maze, 371 | ) -> anyhow::Result<()> { 372 | let layout = self.layout(maze); 373 | for i in 0..layout.margin.h { 374 | if self.is_alternate() { 375 | w.queue(cursor::MoveTo(0, i as u16))?; 376 | w.queue(Clear(ClearType::UntilNewLine))?; 377 | } else { 378 | writeln!(w)?; 379 | } 380 | } 381 | if self.is_alternate() { 382 | self.write_game_header(w, &layout, maze)?; 383 | } 384 | if layout.double_sizes { 385 | self.write_maze_double_size(w, &layout, maze)?; 386 | } else { 387 | self.write_maze_half_size(w, &layout, maze)?; 388 | } 389 | if self.is_alternate() { 390 | self.write_game_status(w, &layout, maze)?; 391 | w.queue(Clear(ClearType::FromCursorDown))?; 392 | } 393 | Ok(()) 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /src/run.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::*, 3 | crokey::*, 4 | std::{ 5 | io::Write, 6 | time::Duration, 7 | }, 8 | termimad::{ 9 | EventSource, 10 | EventSourceOptions, 11 | Ticker, 12 | crossbeam::channel::select, 13 | crossterm::event::Event, 14 | }, 15 | }; 16 | 17 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 18 | enum Tick { 19 | PlayerMoveAuto, 20 | Continue, 21 | } 22 | 23 | /// Run the game, assuming the terminal is already in alternate mode 24 | pub fn run( 25 | w: &mut W, 26 | skin: &Skin, 27 | args: &Args, 28 | ) -> anyhow::Result<()> { 29 | let dim = Dim::terminal()?; 30 | debug!("terminal size: {dim:?}"); 31 | let mut renderer = Renderer { 32 | display: Display::Alternate(dim), 33 | skin, 34 | }; 35 | let user = if args.screen_saver { 36 | "screen-saver" 37 | } else { 38 | let user = args.user.as_str().trim(); 39 | if user.is_empty() || user == "screen-saver" { 40 | anyhow::bail!("Invalid user name"); 41 | } 42 | user 43 | }; 44 | let mut levels_won = 0; 45 | let mut level = if let Some(level) = args.level { 46 | if Database::can_play(user, level)? { 47 | level 48 | } else { 49 | anyhow::bail!( 50 | "User {:?} must win the previous levels before trying level {}", 51 | user, 52 | level 53 | ) 54 | } 55 | } else if args.screen_saver { 56 | // by default, the screen saver starts at level 1 57 | 1 58 | } else { 59 | // normal users 60 | Database::first_not_won(user)? 61 | }; 62 | 63 | let mut ticker = Ticker::new(); 64 | let event_source = EventSource::with_options(EventSourceOptions { 65 | combine_keys: false, 66 | ..Default::default() 67 | })?; 68 | let user_events = event_source.receiver(); 69 | 70 | loop { 71 | let specs = Specs::for_level(level); 72 | debug!("maze specs: {:#?}", &specs); 73 | let mut maze: Maze = time!(specs.into()); 74 | let mut screen_saver_beam = if args.screen_saver { 75 | // requesting periodic automatic player moves 76 | Some(ticker.tick_infinitely(Tick::PlayerMoveAuto, Duration::from_millis(140))) 77 | } else { 78 | None 79 | }; 80 | let mut events = EventList::default(); 81 | while !(maze.is_won() || maze.is_lost()) { 82 | renderer.write(w, &maze)?; 83 | w.flush()?; 84 | select! { 85 | recv(user_events) -> user_event => { 86 | match user_event?.event { 87 | Event::Key(key_event) => match key_event.into() { 88 | key!(q) | key!(ctrl-c) | key!(ctrl-q) => { 89 | return Ok(()); 90 | } 91 | key!(up) => maze.try_move(Dir::Up, &mut events), 92 | key!(right) => maze.try_move(Dir::Right, &mut events), 93 | key!(down) => maze.try_move(Dir::Down, &mut events), 94 | key!(left) => maze.try_move(Dir::Left, &mut events), 95 | key!(w) => maze.end_player_turn(&mut events), 96 | key!(a) => maze.give_up(), 97 | _ => {} 98 | }, 99 | Event::Resize(w, h) => { 100 | renderer.display = Display::Alternate(Dim::new(w as usize, h as usize)); 101 | } 102 | _ => {} 103 | } 104 | event_source.unblock(false); 105 | } 106 | recv(ticker.tick_receiver) -> tick => { 107 | if tick? == Tick::PlayerMoveAuto { 108 | maze.move_player_auto(&mut events); 109 | } 110 | } 111 | } 112 | if !events.is_empty() { 113 | renderer.animate_events(w, &maze, &events)?; 114 | events.clear(); 115 | } 116 | } 117 | if let Some(beam) = screen_saver_beam.take() { 118 | ticker.stop_beam(beam); 119 | } 120 | if maze.is_won() { 121 | levels_won += 1; 122 | if let Some(levels) = args.levels { 123 | if levels_won >= levels { 124 | return Ok(()); 125 | } 126 | } 127 | let next_not_won_level = Database::advance(Achievement::new(user, level))?; 128 | level = if args.screen_saver { 129 | level + 1 130 | } else { 131 | next_not_won_level 132 | }; 133 | } else { 134 | maze.highlight_path_to_exit(maze.start()); 135 | } 136 | // waiting while the user is displayed that he won or lost 137 | renderer.write(w, &maze)?; 138 | w.flush()?; 139 | if args.screen_saver { 140 | if maze.is_won() { 141 | continue; // no need to wait 142 | } 143 | ticker.tick_once(Tick::Continue, Duration::from_secs(2)); 144 | } 145 | loop { 146 | select! { 147 | recv(ticker.tick_receiver) -> tick => { 148 | if tick? == Tick::Continue { 149 | break; 150 | } 151 | } 152 | recv(user_events) -> user_event => { 153 | match user_event?.event { 154 | Event::Key(key_event) => match key_event.into() { 155 | key!(ctrl - c) | key!(ctrl - q) => { 156 | return Ok(()); 157 | } 158 | _ => { 159 | } 160 | } 161 | Event::Resize(w, h) => { 162 | renderer.display = Display::Alternate(Dim::new(w as usize, h as usize)); 163 | } 164 | _ => {} 165 | } 166 | event_source.unblock(false); 167 | break; 168 | } 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/skin.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::Nature, 3 | termimad::crossterm::style::Color, 4 | }; 5 | 6 | pub struct Skin { 7 | pub wall: Color, 8 | pub player: Color, 9 | pub highlight: Color, 10 | pub monster: Color, 11 | pub potion: Color, 12 | pub room: Option, 13 | } 14 | impl Skin { 15 | pub fn build() -> Self { 16 | let room = terminal_light::background_color().ok().map(|c| c.into()); 17 | Self { 18 | wall: Color::AnsiValue(102), 19 | player: Color::AnsiValue(214), 20 | highlight: Color::AnsiValue(45), 21 | monster: Color::AnsiValue(196), 22 | potion: Color::AnsiValue(35), 23 | room, 24 | } 25 | } 26 | pub fn color( 27 | &self, 28 | nature: Nature, 29 | ) -> Option { 30 | match nature { 31 | Nature::Wall => Some(self.wall), 32 | Nature::Monster => Some(self.monster), 33 | Nature::Player => Some(self.player), 34 | Nature::Potion => Some(self.potion), 35 | Nature::Highlight => Some(self.highlight), 36 | Nature::Room | Nature::InvisibleWall => self.room, 37 | } 38 | } 39 | pub fn real_color( 40 | &self, 41 | nature: Nature, 42 | ) -> Color { 43 | match nature { 44 | Nature::Wall => self.wall, 45 | Nature::Monster => self.monster, 46 | Nature::Player => self.player, 47 | Nature::Potion => self.potion, 48 | Nature::Highlight => self.highlight, 49 | Nature::Room | Nature::InvisibleWall => self.room.unwrap_or(Color::Black), 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/specs.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::*, 3 | rand::{ 4 | Rng, 5 | thread_rng, 6 | }, 7 | }; 8 | 9 | /// Definition of a maze to build 10 | #[derive(Debug, Clone, Hash)] 11 | pub struct Specs { 12 | pub name: String, 13 | pub dim: Dim, 14 | pub cuts: usize, 15 | pub potions: usize, 16 | pub monsters: usize, 17 | pub lives: i32, 18 | pub status: &'static str, 19 | pub disk: bool, 20 | pub fill: bool, 21 | } 22 | 23 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 24 | enum SizeSpec { 25 | Tiny, 26 | Small, 27 | Normal, 28 | Large, 29 | Huge, 30 | } 31 | impl SizeSpec { 32 | fn dim( 33 | self, 34 | level: usize, 35 | ) -> Dim { 36 | match self { 37 | Self::Tiny => Dim::new(13 + twist(level, 12), MIN_DIM + 1 + twist(level, 7)), 38 | Self::Small => Dim::new(20 + twist(level, level / 10), 18 + twist(level, level / 12)), 39 | Self::Normal => Dim::new(25 + twist(level, level / 9), 20 + twist(level, level / 11)), 40 | Self::Large => Dim::new(30 + twist(level, level / 8), 24 + twist(level, level / 10)), 41 | Self::Huge => Dim::new(40 + twist(level, level / 4), 31 + twist(level, level / 6)), 42 | } 43 | } 44 | } 45 | 46 | /// Return a pseudo-pseudo-random number, capped and reproductible 47 | /// (seed can be eg the level) 48 | fn twist( 49 | seed: usize, 50 | max: usize, 51 | ) -> usize { 52 | if max == 0 { 53 | return 0; 54 | } 55 | (seed * 27 + (max + 173) * 347 + (seed * 293)) % max 56 | } 57 | 58 | impl Specs { 59 | pub fn for_level(level: usize) -> Self { 60 | let name = format!("Level {level}"); 61 | let dim_spec = match level % 11 { 62 | 1 | 4 => SizeSpec::Tiny, 63 | 2 | 6 | 8 => SizeSpec::Small, 64 | 3 | 10 => SizeSpec::Large, 65 | 7 => SizeSpec::Huge, 66 | _ => SizeSpec::Normal, 67 | }; 68 | let mut dim = dim_spec.dim(level); 69 | let disk = level % 7 == 5; 70 | if disk { 71 | dim.w = 24.max(dim.w); 72 | dim.h = 24.max(dim.h); 73 | } else if level % 13 == 7 { 74 | dim.verticalize(); 75 | } 76 | let s = dim.w * dim.h; 77 | let fill = !disk && !(level % 4 == 1 && level > 6); 78 | 79 | let lives; 80 | let potions; 81 | let monsters; 82 | let cuts; 83 | // A cycle of progressively harder levels over 10 turns 84 | match level % 10 { 85 | 1 => { 86 | // simple walk 87 | lives = 1; 88 | monsters = 0; 89 | potions = 0; 90 | cuts = 1 + s / 200; 91 | } 92 | 2 => { 93 | // super easy 94 | lives = 3; 95 | monsters = 1; 96 | potions = 5 + s / (40 + level); 97 | cuts = 1 + s / 100; 98 | } 99 | 3 if level > 10 => { 100 | // 101 | lives = 4; 102 | monsters = 2 + level / 60; 103 | potions = 2 + s / (100 + level); 104 | cuts = 1 + s / 160; 105 | } 106 | 4 if level > 10 => { 107 | // 108 | lives = 2; 109 | monsters = 2; 110 | potions = 5 + s / (100 + level); 111 | cuts = 1 + s / 200; 112 | } 113 | 5 if level > 20 => { 114 | // lot of cuts, few potions and lives 115 | lives = 1; 116 | monsters = 2; 117 | potions = 4 + s / (420 + level); 118 | cuts = 1 + s / 100; 119 | } 120 | 6 if level > 30 => { 121 | lives = 1; 122 | monsters = 5 + level / 90; 123 | potions = 1 + s / 150; 124 | cuts = 1 + s / 150; 125 | } 126 | 7 if level > 30 => { 127 | lives = 2; 128 | monsters = 3 + level / 100; 129 | potions = 5 + s / 100; 130 | cuts = 1 + s / 200; 131 | } 132 | 8 if level > 40 => { 133 | lives = 2; 134 | monsters = 4; 135 | potions = 1 + s / (150 + level); 136 | cuts = 1 + s / 200; 137 | } 138 | 9 if level > 50 => { 139 | // 140 | lives = 2; 141 | monsters = 5 + level / 100; 142 | potions = 1 + s / (200 + 2 * level); 143 | cuts = 1 + s / 100; 144 | } 145 | _ => { 146 | // 147 | lives = 1; 148 | monsters = 2; 149 | potions = 7 + s / (30 + 4 * level); 150 | cuts = 1 + s / (60 + 2 * level); 151 | } 152 | } 153 | let status = match level { 154 | 1 => "Use arrow keys to move and exit the maze", 155 | 2 | 4 => "Red monsters teleport you", 156 | 3 | 6 => "Pick lives on green squares", 157 | 5 | 8 | 12 => "You can abandon with key 'a'", 158 | 10 | 14 | 17 => "Hit 'w' to wait", 159 | 11 => "Sometimes there's no monster, just find the exit", 160 | _ => "", 161 | }; 162 | Self { 163 | name, 164 | dim, 165 | cuts, 166 | potions, 167 | monsters, 168 | lives, 169 | status, 170 | disk, 171 | fill, 172 | } 173 | } 174 | pub fn for_terminal_build() -> std::io::Result { 175 | let mut rng = thread_rng(); 176 | let double = rng.gen_range(0..3) == 0; 177 | let dim = if double { 178 | Dim::new(rng.gen_range(8..35), rng.gen_range(7..20)) 179 | } else { 180 | let mut d = Dim::terminal()?; 181 | d.w -= 2; 182 | d.h = d.h * 2 - 3; 183 | d 184 | }; 185 | let cuts = match rng.gen_range(0..3) { 186 | 0 => (dim.w * dim.h) / 2300, 187 | 1 => (dim.w * dim.h) / 500, 188 | _ => (dim.w * dim.h) / 60, // should be only 2 189 | }; 190 | let fill = rng.gen_range(0..5) < 4; 191 | Ok(Self { 192 | name: "random".to_string(), 193 | dim, 194 | cuts, 195 | potions: 0, 196 | monsters: 0, 197 | lives: 0, 198 | status: "", 199 | disk: rng.gen_range(0..20) == 0, 200 | fill, 201 | }) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /website/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/mazter/186c1b4262c2965959eea57283a3169df49edbf2/website/build.png -------------------------------------------------------------------------------- /website/deploy.sh: -------------------------------------------------------------------------------- 1 | cp -r * ~/dev/www/dystroy/mazter/ 2 | 3 | # deploy on dystroy.org 4 | ~/dev/www/dystroy/deploy.sh 5 | -------------------------------------------------------------------------------- /website/hof.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/mazter/186c1b4262c2965959eea57283a3169df49edbf2/website/hof.png -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mazter 6 | 42 | 43 | 44 |

Mazter

45 |

Mazes in your terminal

46 |

47 | 48 |

49 |

50 | download 51 |

52 |
53 |

Mazter is an open-source terminal game, working on Linux, Mac, Windows, etc.

54 |

Move with the arrow keys to exit the maze.

55 |

An encounter with a red monster teleports you a short distance, and removes one life.

56 |

You get lives on green squares.

57 |

Your achievements are saved, so that next time you'll start at the first level not won.

58 |

If you're several players on the same account, use mazter --user <username>.

59 |

Use mazter --build if you just want to see a random maze.

60 |

And mazter --screen-saver to have mazter play by himself.

61 |

More with mazter --help.

62 |

If you like the game, please tell me. And tell others.

63 |
64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /website/level-40-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/mazter/186c1b4262c2965959eea57283a3169df49edbf2/website/level-40-transparent.png -------------------------------------------------------------------------------- /website/level-40-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/mazter/186c1b4262c2965959eea57283a3169df49edbf2/website/level-40-white.png -------------------------------------------------------------------------------- /website/level-45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/mazter/186c1b4262c2965959eea57283a3169df49edbf2/website/level-45.png -------------------------------------------------------------------------------- /website/level-46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/mazter/186c1b4262c2965959eea57283a3169df49edbf2/website/level-46.png -------------------------------------------------------------------------------- /website/level-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Canop/mazter/186c1b4262c2965959eea57283a3169df49edbf2/website/level-9.png --------------------------------------------------------------------------------