├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── Hack-Regular.ttf ├── mapgen.wasm └── tilesheet_colored.png ├── index.html └── src ├── fundamentals.rs ├── main.rs ├── maptools.rs ├── procgen ├── bsp_tree.rs ├── cellular_automata.rs ├── maze_with_rooms.rs ├── mod.rs ├── room_placement.rs ├── rwalk.rs └── tunneling.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /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 = "adler32" 7 | version = "1.2.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" 10 | 11 | [[package]] 12 | name = "ahash" 13 | version = "0.4.7" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" 16 | 17 | [[package]] 18 | name = "audir-sles" 19 | version = "0.1.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "ea47348666a8edb7ad80cbee3940eb2bccf70df0e6ce09009abe1a836cb779f5" 22 | 23 | [[package]] 24 | name = "audrey" 25 | version = "0.3.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "58b92a84e89497e3cd25d3672cd5d1c288abaac02c18ff21283f17d118b889b8" 28 | dependencies = [ 29 | "dasp_frame", 30 | "dasp_sample", 31 | "hound", 32 | "lewton", 33 | ] 34 | 35 | [[package]] 36 | name = "autocfg" 37 | version = "1.1.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 40 | 41 | [[package]] 42 | name = "bitflags" 43 | version = "1.3.2" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 46 | 47 | [[package]] 48 | name = "bumpalo" 49 | version = "3.9.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" 52 | 53 | [[package]] 54 | name = "bytemuck" 55 | version = "1.9.1" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" 58 | 59 | [[package]] 60 | name = "byteorder" 61 | version = "1.4.3" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 64 | 65 | [[package]] 66 | name = "cc" 67 | version = "1.0.73" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 70 | 71 | [[package]] 72 | name = "cfg-if" 73 | version = "1.0.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 76 | 77 | [[package]] 78 | name = "color_quant" 79 | version = "1.1.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 82 | 83 | [[package]] 84 | name = "crc32fast" 85 | version = "1.3.2" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 88 | dependencies = [ 89 | "cfg-if", 90 | ] 91 | 92 | [[package]] 93 | name = "dasp_frame" 94 | version = "0.11.0" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6" 97 | dependencies = [ 98 | "dasp_sample", 99 | ] 100 | 101 | [[package]] 102 | name = "dasp_sample" 103 | version = "0.11.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" 106 | 107 | [[package]] 108 | name = "deflate" 109 | version = "0.8.6" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" 112 | dependencies = [ 113 | "adler32", 114 | "byteorder", 115 | ] 116 | 117 | [[package]] 118 | name = "fontdue" 119 | version = "0.5.2" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "c75712fff1702bac51b7eaa5a5ca9f9853b8055ef5906088a32f4fe196595a1d" 122 | dependencies = [ 123 | "hashbrown", 124 | "ttf-parser", 125 | ] 126 | 127 | [[package]] 128 | name = "glam" 129 | version = "0.14.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "333928d5eb103c5d4050533cec0384302db6be8ef7d3cebd30ec6a35350353da" 132 | 133 | [[package]] 134 | name = "hashbrown" 135 | version = "0.9.1" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" 138 | dependencies = [ 139 | "ahash", 140 | ] 141 | 142 | [[package]] 143 | name = "hound" 144 | version = "3.4.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549" 147 | 148 | [[package]] 149 | name = "image" 150 | version = "0.23.14" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" 153 | dependencies = [ 154 | "bytemuck", 155 | "byteorder", 156 | "color_quant", 157 | "num-iter", 158 | "num-rational", 159 | "num-traits", 160 | "png", 161 | ] 162 | 163 | [[package]] 164 | name = "lewton" 165 | version = "0.9.4" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "8d542c1a317036c45c2aa1cf10cc9d403ca91eb2d333ef1a4917e5cb10628bd0" 168 | dependencies = [ 169 | "byteorder", 170 | "ogg", 171 | "smallvec", 172 | ] 173 | 174 | [[package]] 175 | name = "libc" 176 | version = "0.2.126" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 179 | 180 | [[package]] 181 | name = "macroquad" 182 | version = "0.3.16" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "6b723ade71357d07177c769af9ae42beff73c854e827ee5a4ec765dd2beb95e1" 185 | dependencies = [ 186 | "bumpalo", 187 | "fontdue", 188 | "glam", 189 | "image", 190 | "macroquad_macro", 191 | "miniquad", 192 | "quad-rand", 193 | "quad-snd", 194 | ] 195 | 196 | [[package]] 197 | name = "macroquad_macro" 198 | version = "0.1.7" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "f5cecfede1e530599c8686f7f2d609489101d3d63741a6dc423afc997ce3fcc8" 201 | 202 | [[package]] 203 | name = "mapgen" 204 | version = "0.1.0" 205 | dependencies = [ 206 | "macroquad", 207 | ] 208 | 209 | [[package]] 210 | name = "maybe-uninit" 211 | version = "2.0.0" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" 214 | 215 | [[package]] 216 | name = "miniquad" 217 | version = "0.3.0-alpha.46" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "c1462459fb1813decc7115ab4aed8f41183612557b08bf604bfb46b05286d703" 220 | dependencies = [ 221 | "sapp-android", 222 | "sapp-darwin", 223 | "sapp-dummy", 224 | "sapp-ios", 225 | "sapp-linux", 226 | "sapp-wasm", 227 | "sapp-windows", 228 | ] 229 | 230 | [[package]] 231 | name = "miniz_oxide" 232 | version = "0.3.7" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" 235 | dependencies = [ 236 | "adler32", 237 | ] 238 | 239 | [[package]] 240 | name = "ndk-sys" 241 | version = "0.2.2" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" 244 | 245 | [[package]] 246 | name = "num-integer" 247 | version = "0.1.45" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 250 | dependencies = [ 251 | "autocfg", 252 | "num-traits", 253 | ] 254 | 255 | [[package]] 256 | name = "num-iter" 257 | version = "0.1.43" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" 260 | dependencies = [ 261 | "autocfg", 262 | "num-integer", 263 | "num-traits", 264 | ] 265 | 266 | [[package]] 267 | name = "num-rational" 268 | version = "0.3.2" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" 271 | dependencies = [ 272 | "autocfg", 273 | "num-integer", 274 | "num-traits", 275 | ] 276 | 277 | [[package]] 278 | name = "num-traits" 279 | version = "0.2.15" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 282 | dependencies = [ 283 | "autocfg", 284 | ] 285 | 286 | [[package]] 287 | name = "ogg" 288 | version = "0.7.1" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "13e571c3517af9e1729d4c63571a27edd660ade0667973bfc74a67c660c2b651" 291 | dependencies = [ 292 | "byteorder", 293 | ] 294 | 295 | [[package]] 296 | name = "png" 297 | version = "0.16.8" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" 300 | dependencies = [ 301 | "bitflags", 302 | "crc32fast", 303 | "deflate", 304 | "miniz_oxide", 305 | ] 306 | 307 | [[package]] 308 | name = "quad-alsa-sys" 309 | version = "0.3.2" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "c66c2f04a6946293477973d85adc251d502da51c57b08cd9c997f0cfd8dcd4b5" 312 | dependencies = [ 313 | "libc", 314 | ] 315 | 316 | [[package]] 317 | name = "quad-rand" 318 | version = "0.2.1" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "658fa1faf7a4cc5f057c9ee5ef560f717ad9d8dc66d975267f709624d6e1ab88" 321 | 322 | [[package]] 323 | name = "quad-snd" 324 | version = "0.2.3" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "86e0b4259cfd6a317a46df7b7cb4c09a08ba150642e6f6fb7df5a6b3450a0a29" 327 | dependencies = [ 328 | "audir-sles", 329 | "audrey", 330 | "libc", 331 | "quad-alsa-sys", 332 | "winapi", 333 | ] 334 | 335 | [[package]] 336 | name = "sapp-android" 337 | version = "0.1.10" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "9c0d0e6f562c01c533f693ac9c045d69cbeab24d2e16caaaa0e67d06ae6e0940" 340 | dependencies = [ 341 | "libc", 342 | "ndk-sys", 343 | ] 344 | 345 | [[package]] 346 | name = "sapp-darwin" 347 | version = "0.1.8" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "f1282369f486bc334efa2d76fa4d54c20b37664d83da14fc60afbe941814e374" 350 | dependencies = [ 351 | "cc", 352 | ] 353 | 354 | [[package]] 355 | name = "sapp-dummy" 356 | version = "0.1.5" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "66f1ad26a5b6c682b9ca27c66db9aa91002b8d98a82ac7101ded57285215a478" 359 | dependencies = [ 360 | "libc", 361 | ] 362 | 363 | [[package]] 364 | name = "sapp-ios" 365 | version = "0.1.2" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "081e6e5261c9ac2e938979b6a854a53b439f065fc3c897205ce7e69d3028b4a9" 368 | dependencies = [ 369 | "cc", 370 | ] 371 | 372 | [[package]] 373 | name = "sapp-linux" 374 | version = "0.1.14" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "dc71510a7781d8f31d58046a9117682c8e8546ad43f52c0e31275db214495c3d" 377 | dependencies = [ 378 | "libc", 379 | ] 380 | 381 | [[package]] 382 | name = "sapp-wasm" 383 | version = "0.1.26" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "00e859e8645a3bcb85aecd40bab883438e4105f21b21bccbeac2348760f508bb" 386 | 387 | [[package]] 388 | name = "sapp-windows" 389 | version = "0.2.20" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "088f921b591fdcabf5f2dc17237be5203b6a1a6988f252d36e153f1442a8138e" 392 | dependencies = [ 393 | "winapi", 394 | ] 395 | 396 | [[package]] 397 | name = "smallvec" 398 | version = "0.6.14" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" 401 | dependencies = [ 402 | "maybe-uninit", 403 | ] 404 | 405 | [[package]] 406 | name = "ttf-parser" 407 | version = "0.12.3" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "7ae2f58a822f08abdaf668897e96a5656fe72f5a9ce66422423e8849384872e6" 410 | 411 | [[package]] 412 | name = "winapi" 413 | version = "0.3.9" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 416 | dependencies = [ 417 | "winapi-i686-pc-windows-gnu", 418 | "winapi-x86_64-pc-windows-gnu", 419 | ] 420 | 421 | [[package]] 422 | name = "winapi-i686-pc-windows-gnu" 423 | version = "0.4.0" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 426 | 427 | [[package]] 428 | name = "winapi-x86_64-pc-windows-gnu" 429 | version = "0.4.0" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 432 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mapgen" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | macroquad = "0.3.16" 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 optimistic-nihilist 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 | # mapgen 2 | A small collection of procedural dungeon generation algorithms. 3 | Built with Rust & [macroquad](https://github.com/not-fl3/macroquad). 4 | WebAssembly build deployed [here](https://optimistic-nihilist.github.io/mapgen). 5 | 6 | Animated version (separate repo so I could deploy it too using github pages): [mapgen-animated](https://github.com/optimistic-nihilist/mapgen-animated) 7 | deployed [here](https://optimistic-nihilist.github.io/mapgen-animated/) 8 | 9 | ### Run 10 | Clone the repository, then `cargo run --release`. 11 | 12 | ### Build WASM 13 | `cargo build --release --target wasm32-unknown-unknown` produces `mapgen.wasm` under `target/wasm32-unknown-unknown/release`. 14 | Read [this](https://github.com/not-fl3/macroquad#wasm) for a detailed example on what to do with it. 15 | 16 | ### Credits 17 | - Heavily inspired (and on occasion, downright stolen from) https://github.com/AtTheMatinee/dungeon-generation 18 | - Tiles are from 0x72's amazing 16x16 dungeon tileset ([v1](https://0x72.itch.io/16x16-dungeon-tileset) & [v2](https://0x72.itch.io/dungeontileset-ii)) 19 | - Algorithms (sometimes loosely, oftentimes lousily) based on: 20 | - Tunneling algorithm: [roguebasin](http://www.roguebasin.com/index.php/Complete_Roguelike_Tutorial,_using_python%2Blibtcod,_part_3) 21 | - BSP tree: [roguebasin](http://www.roguebasin.com/index.php/Basic_BSP_Dungeon_generation) 22 | - Random walk: [roguebasin](http://www.roguebasin.com/index.php/Random_Walk_Cave_Generation) 23 | - Cellular automata: [roguebasin](http://www.roguebasin.com/index.php/Cellular_Automata_Method_for_Generating_Random_Cave-Like_Levels) 24 | - Room placement: [rockpapershotgun](https://www.rockpapershotgun.com/how-do-roguelikes-generate-levels) 25 | - Maze with rooms: [journal.stuffwithstuff.com](http://journal.stuffwithstuff.com/2014/12/21/rooms-and-mazes/) 26 | -------------------------------------------------------------------------------- /assets/Hack-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimistic-nihilist/mapgen/95039478c1a92e6f034b835444a6fbf8eecaa1b4/assets/Hack-Regular.ttf -------------------------------------------------------------------------------- /assets/mapgen.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimistic-nihilist/mapgen/95039478c1a92e6f034b835444a6fbf8eecaa1b4/assets/mapgen.wasm -------------------------------------------------------------------------------- /assets/tilesheet_colored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/optimistic-nihilist/mapgen/95039478c1a92e6f034b835444a6fbf8eecaa1b4/assets/tilesheet_colored.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mapgen 6 | 35 | 36 | 37 | 38 |
39 | mapgen 40 | 41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/fundamentals.rs: -------------------------------------------------------------------------------- 1 | pub const WINW: i32 = 1120; 2 | pub const WINH: i32 = 640; 3 | pub const TILESIZE: i32 = 16; 4 | pub const COLS: i32 = WINW / TILESIZE; 5 | pub const ROWS: i32 = WINH / TILESIZE; 6 | 7 | pub const BSPTREE_LEAF_MIN_SIZE: i32 = 10; 8 | pub const BSPTREE_ROOM_MIN_SIZE: i32 = 6; 9 | pub const BSPTREE_ROOM_MAX_SIZE: i32 = 25; 10 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod fundamentals; 2 | mod maptools; 3 | mod procgen; 4 | mod utils; 5 | use fundamentals::{COLS, ROWS, TILESIZE, WINH, WINW}; 6 | use macroquad::prelude::*; 7 | use maptools::{new_map, randomize_map, Map, TileType}; 8 | use procgen::bsp_tree::BSPTreeGenerator; 9 | use procgen::cellular_automata::CellularAutomataGenerator; 10 | use procgen::maze_with_rooms::MazeGenerator; 11 | use procgen::room_placement::RoomPlacementGenerator; 12 | use procgen::rwalk::RandomWalkGenerator; 13 | use procgen::tunneling::TunnelingGenerator; 14 | use std::collections::HashMap; 15 | 16 | fn window_conf() -> Conf { 17 | Conf { 18 | window_title: "mapgen".to_owned(), 19 | fullscreen: false, 20 | window_resizable: true, 21 | window_width: WINW, 22 | window_height: WINH, 23 | ..Default::default() 24 | } 25 | } 26 | 27 | fn render_map(tiles: &HashMap, texture: Texture2D, m: &Map) { 28 | for row in 0..ROWS { 29 | for col in 0..COLS { 30 | let curr_tile = m[row as usize][col as usize]; 31 | let curr_type = match curr_tile { 32 | 0 => TileType::Wall, 33 | 1 => TileType::Floor, 34 | _ => TileType::Hero, 35 | }; 36 | draw_texture_ex( 37 | texture, 38 | col as f32 * TILESIZE as f32, 39 | row as f32 * TILESIZE as f32, 40 | WHITE, 41 | tiles.get(&curr_type).unwrap().clone(), 42 | ); 43 | } 44 | } 45 | } 46 | 47 | fn render_help_full(params: TextParams) { 48 | const HELP_TEXT: [&str; 4] = [ 49 | "[r] - randomize map [1] - tunneling [5] - room placement", 50 | "[c] - clear map [2] - BSP [6] - maze with rooms", 51 | "[f] - toggle FPS [3] - random walk [LShift+num] - frenzy", 52 | "[h] - toggle help [4] - cell. automata [ESC] - quit", 53 | ]; 54 | for (idx, row) in HELP_TEXT.iter().enumerate() { 55 | draw_text_ex( 56 | row, 57 | 10.0, 58 | WINH as f32 - (HELP_TEXT.len() as f32 - 1.0 - idx as f32) * 19.0 - 16.0, 59 | params, 60 | ); 61 | } 62 | } 63 | 64 | #[macroquad::main(window_conf)] 65 | async fn main() { 66 | // seed PRNG 67 | rand::srand(macroquad::miniquad::date::now() as _); 68 | 69 | // load texture tile sheet 70 | let texture: Texture2D = load_texture("assets/tilesheet_colored.png").await.unwrap(); 71 | texture.set_filter(FilterMode::Nearest); 72 | 73 | // setup DrawTextureParams for individual tile types 74 | let tile_wall: DrawTextureParams = DrawTextureParams { 75 | source: Some(Rect::new( 76 | 0.0 * TILESIZE as f32, 77 | 0.0, 78 | TILESIZE as f32, 79 | TILESIZE as f32, 80 | )), 81 | ..Default::default() 82 | }; 83 | let tile_floor: DrawTextureParams = DrawTextureParams { 84 | source: Some(Rect::new( 85 | 1.0 * TILESIZE as f32, 86 | 0.0, 87 | TILESIZE as f32, 88 | TILESIZE as f32, 89 | )), 90 | ..Default::default() 91 | }; 92 | let tile_hero: DrawTextureParams = DrawTextureParams { 93 | source: Some(Rect::new( 94 | 2.0 * TILESIZE as f32, 95 | 0.0, 96 | TILESIZE as f32, 97 | TILESIZE as f32, 98 | )), 99 | ..Default::default() 100 | }; 101 | 102 | // store each tiles' DrawTextureParams with their respective TileType as key 103 | let tiles: HashMap = HashMap::from([ 104 | (TileType::Wall, tile_wall), 105 | (TileType::Floor, tile_floor), 106 | (TileType::Hero, tile_hero), 107 | ]); 108 | 109 | // load font 110 | let font: Font = load_ttf_font("assets/Hack-Regular.ttf").await.unwrap(); 111 | let font_params: TextParams = TextParams { 112 | font, 113 | font_size: 16, 114 | ..Default::default() 115 | }; 116 | 117 | // create initial empty map 118 | let mut map = new_map(TileType::Floor); 119 | 120 | let mut measurement_start_time = get_time(); 121 | let mut current_fps = get_fps(); 122 | 123 | let mut show_help = true; 124 | let mut show_fps = true; 125 | 126 | // main loop 127 | loop { 128 | let bg = Color::from_rgba(40, 40, 40, 255); 129 | clear_background(bg); 130 | 131 | render_map(&tiles, texture, &map); 132 | 133 | if show_fps { 134 | draw_text_ex(&format!("FPS: {}", current_fps), 10.0, 26.0, font_params); 135 | } 136 | 137 | if show_help { 138 | render_help_full(font_params); 139 | } else { 140 | let mut gray_text = font_params.clone(); 141 | gray_text.color = Color::from_rgba(160, 160, 160, 255); 142 | draw_text_ex("[h]", 10.0, WINH as f32 - 16.0, gray_text); 143 | } 144 | 145 | if is_key_pressed(KeyCode::H) { 146 | show_help = !show_help; 147 | } 148 | if is_key_pressed(KeyCode::F) { 149 | show_fps = !show_fps; 150 | } 151 | if is_key_pressed(KeyCode::C) { 152 | map = new_map(TileType::Floor); 153 | } 154 | if is_key_pressed(KeyCode::R) { 155 | map = randomize_map(); 156 | } 157 | if is_key_pressed(KeyCode::Key1) { 158 | map = TunnelingGenerator::generate_map(6, 16, 30); 159 | } 160 | if is_key_pressed(KeyCode::Key2) { 161 | map = BSPTreeGenerator::generate_map(); 162 | } 163 | if is_key_pressed(KeyCode::Key3) { 164 | map = RandomWalkGenerator::generate_map(); 165 | } 166 | if is_key_pressed(KeyCode::Key4) { 167 | map = CellularAutomataGenerator::generate_map(); 168 | } 169 | if is_key_pressed(KeyCode::Key5) { 170 | map = RoomPlacementGenerator::generate_map(); 171 | } 172 | if is_key_pressed(KeyCode::Key6) { 173 | map = MazeGenerator::generate_map(); 174 | } 175 | if is_key_pressed(KeyCode::Escape) { 176 | break; 177 | } 178 | 179 | if is_key_down(KeyCode::LeftShift) { 180 | if is_key_down(KeyCode::R) { 181 | map = randomize_map(); 182 | } 183 | if is_key_down(KeyCode::Key1) { 184 | map = TunnelingGenerator::generate_map(6, 16, 30); 185 | } 186 | if is_key_down(KeyCode::Key2) { 187 | map = BSPTreeGenerator::generate_map(); 188 | } 189 | if is_key_down(KeyCode::Key3) { 190 | map = RandomWalkGenerator::generate_map(); 191 | } 192 | if is_key_down(KeyCode::Key4) { 193 | map = CellularAutomataGenerator::generate_map(); 194 | } 195 | if is_key_down(KeyCode::Key5) { 196 | map = RoomPlacementGenerator::generate_map(); 197 | } 198 | if is_key_down(KeyCode::Key6) { 199 | map = MazeGenerator::generate_map(); 200 | } 201 | } 202 | 203 | // update FPS counter roughly every 0.5 second 204 | if get_time() - measurement_start_time > 0.5 { 205 | measurement_start_time = get_time(); 206 | current_fps = get_fps(); 207 | } 208 | next_frame().await 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/maptools.rs: -------------------------------------------------------------------------------- 1 | use crate::fundamentals::*; 2 | use crate::utils::*; 3 | 4 | #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] 5 | pub enum TileType { 6 | Wall, 7 | Floor, 8 | Hero, 9 | } 10 | 11 | pub type Map = [[i32; COLS as usize]; ROWS as usize]; 12 | 13 | pub fn new_map(fill_with: TileType) -> Map { 14 | [[fill_with as i32; COLS as usize]; ROWS as usize] 15 | } 16 | 17 | pub fn randomize_map() -> Map { 18 | let mut map = [[0; COLS as usize]; ROWS as usize]; 19 | for row in 0..ROWS { 20 | for col in 0..COLS { 21 | map[row as usize][col as usize] = randr(0..2); 22 | } 23 | } 24 | map 25 | } 26 | 27 | #[derive(Debug)] 28 | pub struct Rect { 29 | pub x: i32, 30 | pub y: i32, 31 | pub w: i32, 32 | pub h: i32, 33 | } 34 | 35 | #[derive(Copy, Clone, Debug)] 36 | pub struct Room { 37 | pub x1: i32, 38 | pub x2: i32, 39 | pub y1: i32, 40 | pub y2: i32, 41 | } 42 | 43 | impl Room { 44 | pub fn new(x: i32, y: i32, w: i32, h: i32) -> Self { 45 | Self { 46 | x1: x, 47 | x2: x + w, 48 | y1: y, 49 | y2: y + h, 50 | } 51 | } 52 | 53 | pub fn center(&self) -> (i32, i32) { 54 | let cx = (self.x1 + self.x2) / 2; 55 | let cy = (self.y1 + self.y2) / 2; 56 | (cx, cy) 57 | } 58 | 59 | pub fn overlaps(&self, other: Room) -> bool { 60 | self.x1 <= other.x2 && self.x2 >= other.x1 && self.y1 <= other.y2 && self.y2 >= other.y1 61 | } 62 | 63 | pub fn carve(&self, m: &mut Map) { 64 | for x in self.x1..self.x2 { 65 | for y in self.y1..self.y2 { 66 | m[y as usize][x as usize] = TileType::Floor as i32; 67 | } 68 | } 69 | } 70 | } 71 | 72 | pub fn carve_horz_tunnel(m: &mut Map, x1: i32, x2: i32, y: i32) { 73 | let min_x = std::cmp::min(x1, x2); 74 | let max_x = std::cmp::max(x1, x2); 75 | for x in min_x..max_x + 1 { 76 | m[y as usize][x as usize] = TileType::Floor as i32; 77 | } 78 | } 79 | 80 | pub fn carve_vert_tunnel(m: &mut Map, y1: i32, y2: i32, x: i32) { 81 | let min_y = std::cmp::min(y1, y2); 82 | let max_y = std::cmp::max(y1, y2); 83 | for y in min_y..max_y + 1 { 84 | m[y as usize][x as usize] = TileType::Floor as i32; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/procgen/bsp_tree.rs: -------------------------------------------------------------------------------- 1 | use crate::{fundamentals::*, maptools::*, utils::*}; 2 | 3 | #[derive(Clone, Debug)] 4 | struct BSPNode { 5 | x: i32, 6 | y: i32, 7 | w: i32, 8 | h: i32, 9 | room: Option, 10 | left_child: Option>, 11 | right_child: Option>, 12 | } 13 | 14 | impl BSPNode { 15 | fn new( 16 | x: i32, 17 | y: i32, 18 | w: i32, 19 | h: i32, 20 | room: Option, 21 | left: Option>, 22 | right: Option>, 23 | ) -> Self { 24 | Self { 25 | x, 26 | y, 27 | w, 28 | h, 29 | room, 30 | left_child: left, 31 | right_child: right, 32 | } 33 | } 34 | 35 | fn split(&mut self) -> bool { 36 | if self.left_child.is_some() && self.right_child.is_some() { 37 | // node is already split 38 | return false; 39 | } 40 | 41 | let mut hsplit = true; 42 | let ratio = self.w as f32 / self.h as f32; 43 | if ratio > 1.25 { 44 | hsplit = false; 45 | } 46 | 47 | let max = match hsplit { 48 | true => self.h - BSPTREE_LEAF_MIN_SIZE, 49 | false => self.w - BSPTREE_LEAF_MIN_SIZE, 50 | }; 51 | 52 | if max <= BSPTREE_LEAF_MIN_SIZE { 53 | // node too small to split further 54 | return false; 55 | } 56 | 57 | let split = randr(BSPTREE_LEAF_MIN_SIZE..max); 58 | 59 | if hsplit { 60 | self.left_child = Some(Box::new(BSPNode::new( 61 | self.x, self.y, self.w, split, None, None, None, 62 | ))); 63 | self.right_child = Some(Box::new(BSPNode::new( 64 | self.x, 65 | self.y + split, 66 | self.w, 67 | self.h - split, 68 | None, 69 | None, 70 | None, 71 | ))); 72 | } else { 73 | self.left_child = Some(Box::new(BSPNode::new( 74 | self.x, self.y, split, self.h, None, None, None, 75 | ))); 76 | self.right_child = Some(Box::new(BSPNode::new( 77 | self.x + split, 78 | self.y, 79 | self.w - split, 80 | self.h, 81 | None, 82 | None, 83 | None, 84 | ))); 85 | } 86 | 87 | true 88 | } 89 | } 90 | 91 | fn carve_leafs(curr: &mut BSPNode, map: &mut Map) { 92 | if curr.left_child.is_some() || curr.right_child.is_some() { 93 | if curr.left_child.is_some() { 94 | carve_leafs(curr.left_child.as_mut().unwrap(), map); 95 | } 96 | if curr.right_child.is_some() { 97 | carve_leafs(curr.right_child.as_mut().unwrap(), map); 98 | } 99 | if let (Some(l), Some(r)) = (curr.left_child.as_mut(), curr.right_child.as_mut()) { 100 | let lroom = get_room(l).unwrap(); 101 | let rroom = get_room(r).unwrap(); 102 | 103 | let (lx, ly) = lroom.center(); 104 | let (rx, ry) = rroom.center(); 105 | 106 | if randr(0..1) == 1 { 107 | carve_horz_tunnel(map, lx, rx, ly); 108 | carve_vert_tunnel(map, ly, ry, rx); 109 | } else { 110 | carve_vert_tunnel(map, ly, ry, lx); 111 | carve_horz_tunnel(map, lx, rx, ry); 112 | } 113 | } 114 | } else { 115 | if curr.room.is_none() { 116 | let w = randr(BSPTREE_ROOM_MIN_SIZE..std::cmp::min(BSPTREE_ROOM_MAX_SIZE, curr.w - 1)); 117 | let h = randr(BSPTREE_ROOM_MIN_SIZE..std::cmp::min(BSPTREE_ROOM_MAX_SIZE, curr.h - 1)); 118 | let x = randr(curr.x..curr.x + (curr.w - 1) - w); 119 | let y = randr(curr.y..curr.y + (curr.h - 1) - h); 120 | 121 | curr.room = Some(Room::new(x, y, w, h)); 122 | curr.room.unwrap().carve(map); 123 | } 124 | } 125 | } 126 | 127 | fn get_room(curr: &mut BSPNode) -> Option { 128 | if let Some(r) = curr.room { 129 | return Some(r); 130 | } 131 | if curr.left_child.is_some() { 132 | return get_room(curr.left_child.as_mut().unwrap()); 133 | } 134 | if curr.right_child.is_some() { 135 | return get_room(curr.right_child.as_mut().unwrap()); 136 | } 137 | None 138 | } 139 | 140 | fn split_until_fail(curr: &mut BSPNode) { 141 | if !curr.split() { 142 | return; 143 | } 144 | split_until_fail(curr.left_child.as_mut().unwrap()); 145 | split_until_fail(curr.right_child.as_mut().unwrap()); 146 | } 147 | 148 | pub struct BSPTreeGenerator {} 149 | 150 | impl BSPTreeGenerator { 151 | pub fn generate_map() -> Map { 152 | let mut map = new_map(TileType::Wall); 153 | let mut root = BSPNode::new(1, 1, COLS, ROWS, None, None, None); 154 | split_until_fail(&mut root); 155 | carve_leafs(&mut root, &mut map); 156 | map 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/procgen/cellular_automata.rs: -------------------------------------------------------------------------------- 1 | use crate::{fundamentals::*, maptools::*, utils::*}; 2 | 3 | const DEATH_LIMIT: i32 = 3; 4 | const BIRTH_LIMIT: i32 = 4; 5 | 6 | fn randomize_map_seal_edges(m: &mut Map) { 7 | for y in 0..ROWS { 8 | for x in 0..COLS { 9 | if (x == 0) | (x == COLS - 1) | (y == 0) | (y == ROWS - 1) { 10 | m[y as usize][x as usize] = TileType::Wall as i32; 11 | } else { 12 | m[y as usize][x as usize] = match randr(0..100) { 13 | 0..=32 => TileType::Wall as i32, 14 | _ => TileType::Floor as i32, 15 | }; 16 | } 17 | } 18 | } 19 | } 20 | 21 | pub fn evolve_map(m: &mut Map) { 22 | for y in 0..ROWS { 23 | for x in 0..COLS { 24 | let neighbor_count = count_alive_neighbors(m, x, y); 25 | if m[y as usize][x as usize] == TileType::Wall as i32 { 26 | if neighbor_count < DEATH_LIMIT { 27 | m[y as usize][x as usize] = TileType::Floor as i32 28 | } 29 | } else { 30 | if BIRTH_LIMIT < neighbor_count { 31 | m[y as usize][x as usize] = TileType::Wall as i32 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | fn count_alive_neighbors(m: &Map, x: i32, y: i32) -> i32 { 39 | let mut count = 0; 40 | for i in -1..2 { 41 | for j in -1..2 { 42 | let nx = x + i; 43 | let ny = y + j; 44 | if (nx < 0) | (nx >= COLS) | (ny < 0) | (ny >= ROWS) { 45 | count += 1; 46 | } else if m[ny as usize][nx as usize] == TileType::Wall as i32 { 47 | count += 1; 48 | } 49 | } 50 | } 51 | count 52 | } 53 | fn get_random_cave_size(m: &mut Map) -> i32 { 54 | // get random floor starting position for DFS 55 | let mut rx = randr(0..COLS); 56 | let mut ry = randr(0..ROWS); 57 | while m[ry as usize][rx as usize] != TileType::Floor as i32 { 58 | rx = randr(0..COLS); 59 | ry = randr(0..ROWS); 60 | } 61 | let mut visited = new_map(TileType::Wall); 62 | dfs(rx, ry, &mut visited, m); 63 | 64 | for y in 0..ROWS { 65 | for x in 0..COLS { 66 | if visited[y as usize][x as usize] == 1 { 67 | m[y as usize][x as usize] = TileType::Floor as i32; 68 | } else { 69 | m[y as usize][x as usize] = TileType::Wall as i32; 70 | } 71 | } 72 | } 73 | 74 | let num_floor: i32 = visited 75 | .iter() 76 | .flat_map(|x: &[i32; COLS as usize]| x.iter()) 77 | .sum(); 78 | 79 | num_floor 80 | } 81 | 82 | fn dfs(x: i32, y: i32, v: &mut Map, m: &Map) { 83 | v[y as usize][x as usize] = TileType::Floor as i32; 84 | if is_valid(x - 1, y, v, m) { 85 | dfs(x - 1, y, v, m); 86 | } 87 | if is_valid(x + 1, y, v, m) { 88 | dfs(x + 1, y, v, m); 89 | } 90 | if is_valid(x, y - 1, v, m) { 91 | dfs(x, y - 1, v, m); 92 | } 93 | if is_valid(x, y + 1, v, m) { 94 | dfs(x, y + 1, v, m); 95 | } 96 | } 97 | 98 | fn is_valid(x: i32, y: i32, v: &Map, m: &Map) -> bool { 99 | if (x < 0) | (x >= COLS) | (y < 0) | (y >= ROWS) { 100 | return false; 101 | } 102 | if v[y as usize][x as usize] == TileType::Floor as i32 { 103 | return false; 104 | } 105 | if m[y as usize][x as usize] == TileType::Wall as i32 { 106 | return false; 107 | } 108 | true 109 | } 110 | 111 | fn generate_caves(m: &mut Map) { 112 | randomize_map_seal_edges(m); 113 | for _ in 0..15 { 114 | evolve_map(m); 115 | } 116 | } 117 | 118 | pub struct CellularAutomataGenerator {} 119 | impl CellularAutomataGenerator { 120 | pub fn generate_map() -> Map { 121 | let mut map = new_map(TileType::Wall); 122 | generate_caves(&mut map); 123 | while get_random_cave_size(&mut map) < 1000 { 124 | generate_caves(&mut map); 125 | } 126 | map 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/procgen/maze_with_rooms.rs: -------------------------------------------------------------------------------- 1 | use crate::{fundamentals::*, maptools::*, utils::*}; 2 | 3 | #[derive(Copy, Clone, Debug)] 4 | struct Cell { 5 | x: i32, 6 | y: i32, 7 | } 8 | 9 | impl Cell { 10 | fn new(x: i32, y: i32, visited: &mut Map, visited_positions: &mut Vec) -> Self { 11 | visited[y as usize][x as usize] = TileType::Floor as i32; 12 | visited_positions.push(Pos { x, y }); 13 | Self { x, y } 14 | } 15 | 16 | fn step(&mut self, m: &mut Map, v: &mut Map, visited_positions: &mut Vec) { 17 | let neighbors = get_neighbors(self.x, self.y, m); 18 | let mut valid_neighbors: Vec = Vec::new(); 19 | if let Some(n) = neighbors.north { 20 | valid_neighbors.push(n); 21 | } 22 | if let Some(e) = neighbors.east { 23 | valid_neighbors.push(e); 24 | } 25 | if let Some(s) = neighbors.south { 26 | valid_neighbors.push(s); 27 | } 28 | if let Some(w) = neighbors.west { 29 | valid_neighbors.push(w); 30 | } 31 | // backtrack if no valid neighbors 32 | if valid_neighbors.is_empty() { 33 | if visited_positions.is_empty() { 34 | return; 35 | } 36 | let prev_loc = visited_positions.pop().unwrap(); 37 | (self.x, self.y) = (prev_loc.x, prev_loc.y); 38 | return; 39 | } 40 | 41 | let next = valid_neighbors[randr(0..valid_neighbors.len() as i32) as usize]; 42 | (self.x, self.y) = (next.x, next.y); 43 | v[self.y as usize][self.x as usize] = TileType::Floor as i32; 44 | visited_positions.push(Pos { 45 | x: self.x, 46 | y: self.y, 47 | }); 48 | m[self.y as usize][self.x as usize] = TileType::Floor as i32; 49 | } 50 | } 51 | 52 | struct Neighbors { 53 | north: Option, 54 | east: Option, 55 | south: Option, 56 | west: Option, 57 | } 58 | 59 | fn can_grow_tunnel(x: i32, y: i32, v: &Map) -> bool { 60 | if in_bounds(x, y) && v[y as usize][x as usize] == TileType::Wall as i32 { 61 | return true; 62 | } 63 | false 64 | } 65 | 66 | fn get_neighbors(x: i32, y: i32, m: &Map) -> Neighbors { 67 | let mut n: Option = None; 68 | let mut e: Option = None; 69 | let mut s: Option = None; 70 | let mut w: Option = None; 71 | 72 | if can_grow_tunnel(x, y - 1, m) 73 | && can_grow_tunnel(x, y - 2, m) 74 | && can_grow_tunnel(x - 1, y - 1, m) 75 | && can_grow_tunnel(x - 1, y - 2, m) 76 | && can_grow_tunnel(x + 1, y - 1, m) 77 | && can_grow_tunnel(x + 1, y - 2, m) 78 | { 79 | n = Some(Cell { x, y: y - 1 }); 80 | } 81 | if can_grow_tunnel(x + 1, y, m) 82 | && can_grow_tunnel(x + 2, y, m) 83 | && can_grow_tunnel(x + 1, y - 1, m) 84 | && can_grow_tunnel(x + 2, y - 1, m) 85 | && can_grow_tunnel(x + 1, y + 1, m) 86 | && can_grow_tunnel(x + 2, y + 1, m) 87 | { 88 | e = Some(Cell { x: x + 1, y }); 89 | } 90 | if can_grow_tunnel(x, y + 1, m) 91 | && can_grow_tunnel(x, y + 2, m) 92 | && can_grow_tunnel(x - 1, y + 1, m) 93 | && can_grow_tunnel(x - 1, y + 2, m) 94 | && can_grow_tunnel(x + 1, y + 1, m) 95 | && can_grow_tunnel(x + 1, y + 2, m) 96 | { 97 | s = Some(Cell { x, y: y + 1 }); 98 | } 99 | if can_grow_tunnel(x - 1, y, m) 100 | && can_grow_tunnel(x - 2, y, m) 101 | && can_grow_tunnel(x - 2, y - 1, m) 102 | && can_grow_tunnel(x - 1, y - 1, m) 103 | && can_grow_tunnel(x - 2, y + 1, m) 104 | && can_grow_tunnel(x - 1, y + 1, m) 105 | { 106 | w = Some(Cell { x: x - 1, y }); 107 | } 108 | 109 | Neighbors { 110 | north: n, 111 | east: e, 112 | south: s, 113 | west: w, 114 | } 115 | } 116 | 117 | fn count_neighbors(p: &Pos, v: &Map) -> i32 { 118 | let mut count = 0; 119 | if in_bounds(p.x - 1, p.y) && v[p.y as usize][p.x as usize - 1] == TileType::Floor as i32 { 120 | count = count + 1; 121 | } 122 | if in_bounds(p.x + 1, p.y) && v[p.y as usize][p.x as usize + 1] == TileType::Floor as i32 { 123 | count = count + 1; 124 | } 125 | if in_bounds(p.x, p.y - 1) && v[p.y as usize - 1][p.x as usize] == TileType::Floor as i32 { 126 | count = count + 1; 127 | } 128 | if in_bounds(p.x, p.y + 1) && v[p.y as usize + 1][p.x as usize] == TileType::Floor as i32 { 129 | count = count + 1; 130 | } 131 | count 132 | } 133 | 134 | fn trim_dead_ends(m: &mut Map) { 135 | // find dead ends - cells with exactly one neighbor 136 | let dead_ends: Vec = m 137 | .iter() 138 | .flat_map(|x: &[i32; COLS as usize]| x.iter()) 139 | .enumerate() 140 | .filter(|(_, &d)| d == 1) 141 | .map(|(idx, _)| { 142 | let x = idx as i32 % COLS; 143 | let y = idx as i32 / COLS; 144 | Pos { x, y } 145 | }) 146 | .filter(|p| count_neighbors(p, m) == 1) 147 | .collect(); 148 | for d in dead_ends { 149 | m[d.y as usize][d.x as usize] = TileType::Wall as i32; 150 | } 151 | } 152 | 153 | fn place_rooms(m: &mut Map) -> Vec { 154 | const ROOM_SIZE_MIN: i32 = 6; 155 | const ROOMS_SIZE_MAX: i32 = 16; 156 | let mut rooms: Vec = vec![]; 157 | 158 | for _ in 0..20 { 159 | let w: i32 = randr(ROOM_SIZE_MIN..ROOMS_SIZE_MAX); 160 | let h: i32 = randr(ROOM_SIZE_MIN..ROOMS_SIZE_MAX); 161 | let x: i32 = randr(1..COLS - w); 162 | let y: i32 = randr(1..ROWS - h); 163 | 164 | let mut curr_room = Room::new(x, y, w, h); 165 | 166 | let mut overlaps = false; 167 | 168 | for room in &rooms { 169 | if curr_room.overlaps(*room) { 170 | overlaps = true; 171 | break; 172 | } 173 | } 174 | 175 | if !overlaps { 176 | // increase room rect size to allow for sparser room placement 177 | curr_room.carve(m); 178 | curr_room.x1 = curr_room.x1 - 3; 179 | curr_room.y1 = curr_room.y1 - 3; 180 | curr_room.x2 = curr_room.x2 + 3; 181 | curr_room.y2 = curr_room.y2 + 3; 182 | rooms.push(curr_room); 183 | } 184 | } 185 | 186 | rooms 187 | } 188 | 189 | fn connect_rooms(rooms: &mut Vec, m: &mut Map) { 190 | for room in rooms { 191 | // find possible connection points 192 | // = walls of the room which have a tunnel next to them 193 | 194 | // first shrink the room rect after placement 195 | room.x1 = room.x1 + 3; 196 | room.x2 = room.x2 - 3; 197 | room.y1 = room.y1 + 3; 198 | room.y2 = room.y2 - 3; 199 | 200 | // find the room walls 201 | let mut perimeter: Vec = Vec::new(); 202 | for x in room.x1..room.x2 { 203 | for y in room.y1..room.y2 { 204 | if x == room.x1 || x == room.x2 - 1 || y == room.y1 || y == room.y2 - 1 { 205 | perimeter.push(Pos { x, y }); 206 | } 207 | } 208 | } 209 | 210 | let mut possible_connection_points: Vec = Vec::new(); 211 | for p in perimeter { 212 | if in_bounds(p.x - 1, p.y) 213 | && in_bounds(p.x - 2, p.y) 214 | && m[p.y as usize][p.x as usize - 1] == TileType::Wall as i32 215 | && m[p.y as usize][p.x as usize - 2] == TileType::Floor as i32 216 | { 217 | possible_connection_points.push(p); 218 | } 219 | if in_bounds(p.x + 1, p.y) 220 | && in_bounds(p.x + 2, p.y) 221 | && m[p.y as usize][p.x as usize + 1] == TileType::Wall as i32 222 | && m[p.y as usize][p.x as usize + 2] == TileType::Floor as i32 223 | { 224 | possible_connection_points.push(p); 225 | } 226 | if in_bounds(p.x, p.y - 1) 227 | && in_bounds(p.x, p.y - 2) 228 | && m[p.y as usize - 1][p.x as usize] == TileType::Wall as i32 229 | && m[p.y as usize - 2][p.x as usize] == TileType::Floor as i32 230 | { 231 | possible_connection_points.push(p); 232 | } 233 | if in_bounds(p.x, p.y + 1) 234 | && in_bounds(p.x, p.y + 2) 235 | && m[p.y as usize + 1][p.x as usize] == TileType::Wall as i32 236 | && m[p.y as usize + 2][p.x as usize] == TileType::Floor as i32 237 | { 238 | possible_connection_points.push(p); 239 | } 240 | } 241 | 242 | if possible_connection_points.is_empty() { 243 | return; 244 | } 245 | 246 | let mut connection_points: Vec = Vec::new(); 247 | for _ in 0..2 { 248 | let connection_point = possible_connection_points 249 | [randr(0..possible_connection_points.len() as i32) as usize]; 250 | connection_points.push(connection_point); 251 | } 252 | 253 | for p in connection_points { 254 | if in_bounds(p.x - 1, p.y) 255 | && in_bounds(p.x - 2, p.y) 256 | && m[p.y as usize][p.x as usize - 1] == TileType::Wall as i32 257 | && m[p.y as usize][p.x as usize - 2] == TileType::Floor as i32 258 | { 259 | m[p.y as usize][p.x as usize - 1] = TileType::Floor as i32; 260 | continue; 261 | } 262 | if in_bounds(p.x + 1, p.y) 263 | && in_bounds(p.x + 2, p.y) 264 | && m[p.y as usize][p.x as usize + 1] == TileType::Wall as i32 265 | && m[p.y as usize][p.x as usize + 2] == TileType::Floor as i32 266 | { 267 | m[p.y as usize][p.x as usize + 1] = TileType::Floor as i32; 268 | continue; 269 | } 270 | if in_bounds(p.x, p.y - 1) 271 | && in_bounds(p.x, p.y - 2) 272 | && m[p.y as usize - 1][p.x as usize] == TileType::Wall as i32 273 | && m[p.y as usize - 2][p.x as usize] == TileType::Floor as i32 274 | { 275 | m[p.y as usize - 1][p.x as usize] = TileType::Floor as i32; 276 | continue; 277 | } 278 | if in_bounds(p.x, p.y + 1) 279 | && in_bounds(p.x, p.y + 2) 280 | && m[p.y as usize + 1][p.x as usize] == TileType::Wall as i32 281 | && m[p.y as usize + 2][p.x as usize] == TileType::Floor as i32 282 | { 283 | m[p.y as usize + 1][p.x as usize] = TileType::Floor as i32; 284 | continue; 285 | } 286 | } 287 | } 288 | } 289 | 290 | #[derive(Copy, Clone, Debug)] 291 | struct Pos { 292 | x: i32, 293 | y: i32, 294 | } 295 | 296 | pub struct MazeGenerator {} 297 | impl MazeGenerator { 298 | pub fn generate_map() -> Map { 299 | let mut map = new_map(TileType::Wall); 300 | let mut visited = new_map(TileType::Wall); 301 | let mut visited_positions: Vec = Vec::new(); 302 | 303 | let mut rooms = place_rooms(&mut map); 304 | 305 | // pick a random wall location 306 | let mut startx = COLS / 2; 307 | let mut starty = ROWS / 2; 308 | while map[starty as usize][startx as usize] != TileType::Wall as i32 { 309 | startx = randr(0..COLS); 310 | starty = randr(0..ROWS); 311 | } 312 | 313 | let mut c = Cell::new(startx, starty, &mut visited, &mut visited_positions); 314 | 315 | while !visited_positions.is_empty() { 316 | c.step(&mut map, &mut visited, &mut visited_positions); 317 | } 318 | 319 | for y in 0..ROWS { 320 | for x in 0..COLS { 321 | if visited[y as usize][x as usize] == TileType::Floor as i32 { 322 | map[y as usize][x as usize] = TileType::Floor as i32; 323 | } 324 | } 325 | } 326 | 327 | // make maze passages sparser by trimming some dead ends 328 | for _ in 0..5 { 329 | trim_dead_ends(&mut map); 330 | } 331 | 332 | // connect rooms with passages 333 | connect_rooms(&mut rooms, &mut map); 334 | 335 | map 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/procgen/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bsp_tree; 2 | pub mod cellular_automata; 3 | pub mod maze_with_rooms; 4 | pub mod room_placement; 5 | pub mod rwalk; 6 | pub mod tunneling; 7 | -------------------------------------------------------------------------------- /src/procgen/room_placement.rs: -------------------------------------------------------------------------------- 1 | use crate::{fundamentals::*, maptools::*, utils::*}; 2 | 3 | const SQUARE_ROOM_MIN_SIZE: i32 = 4; 4 | const SQUARE_ROOM_MAX_SIZE: i32 = 8; 5 | const CROSS_ROOM_MIN_SIZE: i32 = 6; 6 | const CROSS_ROOM_MAX_SIZE: i32 = 16; 7 | const CAVE_ROOM_MIN_SIZE: i32 = 8; 8 | const CAVE_ROOM_MAX_SIZE: i32 = 14; 9 | 10 | #[derive(Debug)] 11 | struct Room { 12 | rect: Rect, 13 | tiles: Vec, 14 | } 15 | 16 | impl Room { 17 | fn center(&self) -> (i32, i32) { 18 | (self.rect.w / 2, self.rect.h / 2) 19 | } 20 | } 21 | 22 | fn room_get_xy(x: i32, y: i32, cols: i32) -> i32 { 23 | y * cols + x 24 | } 25 | 26 | fn room_get_coord_from_idx(idx: i32, cols: i32) -> (i32, i32) { 27 | let x = idx % cols; 28 | let y = idx / cols; 29 | (x, y) 30 | } 31 | 32 | #[derive(Copy, Clone, Debug, PartialEq)] 33 | struct TileWithXYIdx { 34 | tile: TileType, 35 | x: i32, 36 | y: i32, 37 | idx: i32, 38 | } 39 | 40 | #[derive(Copy, Clone, Debug, PartialEq)] 41 | struct ConnectionPoint { 42 | tile: TileWithXYIdx, 43 | loc: ConnectionPointLocation, 44 | } 45 | 46 | #[derive(Copy, Clone, Debug, PartialEq)] 47 | enum ConnectionPointLocation { 48 | Top, 49 | Right, 50 | Bottom, 51 | Left, 52 | } 53 | 54 | fn find_connection_points(r: &Room) -> Vec { 55 | // get all (x,y) coords for all tiles 56 | let tiles_with_idx: Vec = r 57 | .tiles 58 | .iter() 59 | .enumerate() 60 | .map(|(i, t)| { 61 | let (x, y) = room_get_coord_from_idx(i as i32, r.rect.w); 62 | return TileWithXYIdx { 63 | tile: *t, 64 | idx: i as i32, 65 | x, 66 | y, 67 | }; 68 | }) 69 | .collect(); 70 | 71 | // find all floors, work only with those further 72 | let floor_tiles: Vec = tiles_with_idx 73 | .into_iter() 74 | .filter(|&t| t.tile as i32 == TileType::Floor as i32) 75 | .collect(); 76 | 77 | // find boundaries 78 | let min_x = floor_tiles.iter().min_by_key(|&t| t.x).unwrap().x; 79 | let max_x = floor_tiles.iter().max_by_key(|&t| t.x).unwrap().x; 80 | let min_y = floor_tiles.iter().min_by_key(|&t| t.y).unwrap().y; 81 | let max_y = floor_tiles.iter().max_by_key(|&t| t.y).unwrap().y; 82 | 83 | let mut boundaries: Vec> = Vec::new(); 84 | boundaries.push(floor_tiles.iter().filter(|&t| t.x == min_x).collect()); 85 | boundaries.push(floor_tiles.iter().filter(|&t| t.x == max_x).collect()); 86 | boundaries.push(floor_tiles.iter().filter(|&t| t.y == min_y).collect()); 87 | boundaries.push(floor_tiles.iter().filter(|&t| t.y == max_y).collect()); 88 | 89 | // select random point from boundary points 90 | let mut connection_points: Vec = Vec::new(); 91 | for (i, b) in boundaries.iter().enumerate() { 92 | let connection_idx = if b.len() <= 2 { 93 | randr(0..b.len() as i32) 94 | } else { 95 | randr(1..b.len() as i32 - 1) 96 | }; 97 | 98 | let connection_point = b[connection_idx as usize]; 99 | connection_points.push(ConnectionPoint { 100 | tile: *connection_point, 101 | loc: match i { 102 | 0 => ConnectionPointLocation::Left, 103 | 1 => ConnectionPointLocation::Right, 104 | 2 => ConnectionPointLocation::Top, 105 | _ => ConnectionPointLocation::Bottom, 106 | }, 107 | }); 108 | } 109 | 110 | for c in &mut connection_points { 111 | c.tile.x = c.tile.x + r.rect.x; 112 | c.tile.y = c.tile.y + r.rect.y; 113 | c.tile.idx = get_xy_idx(c.tile.x, c.tile.y); 114 | } 115 | 116 | connection_points 117 | } 118 | 119 | fn get_xy_idx(x: i32, y: i32) -> i32 { 120 | y * COLS + x 121 | } 122 | 123 | fn generate_square_room(size_min: i32, size_max: i32) -> Room { 124 | let room_size = randr(size_min..size_max + 1); 125 | let mut tiles: Vec = vec![TileType::Wall; (room_size * room_size) as usize]; 126 | let rect = Rect { 127 | x: 0, 128 | y: 0, 129 | w: room_size, 130 | h: room_size, 131 | }; 132 | for x in 0..room_size { 133 | for y in 0..room_size { 134 | tiles[room_get_xy(x, y, room_size) as usize] = TileType::Floor; 135 | } 136 | } 137 | 138 | Room { rect, tiles } 139 | } 140 | 141 | fn generate_rectangular_room(size_min: i32, size_max: i32) -> Room { 142 | let room_width = randr(size_min..size_max + 1); 143 | let room_height = randr(size_min..size_max + 1); 144 | let mut tiles: Vec = vec![TileType::Wall; (room_width * room_height) as usize]; 145 | let rect = Rect { 146 | x: 0, 147 | y: 0, 148 | w: room_width, 149 | h: room_height, 150 | }; 151 | for x in 0..room_width { 152 | for y in 0..room_height { 153 | tiles[room_get_xy(x, y, room_width) as usize] = TileType::Floor; 154 | } 155 | } 156 | Room { rect, tiles } 157 | } 158 | 159 | fn generate_cross_room(size_min: i32, size_max: i32) -> Room { 160 | let mut r = generate_rectangular_room(size_min, size_max); 161 | let w_third = r.rect.w / 3; 162 | let h_third = r.rect.h / 3; 163 | for x in 0..w_third { 164 | for y in 0..h_third { 165 | r.tiles[room_get_xy(x, y, r.rect.w) as usize] = TileType::Wall; 166 | } 167 | for y in r.rect.h - h_third..r.rect.h { 168 | r.tiles[room_get_xy(x, y, r.rect.w) as usize] = TileType::Wall; 169 | } 170 | } 171 | for x in r.rect.w - w_third..r.rect.w { 172 | for y in 0..h_third { 173 | r.tiles[room_get_xy(x, y, r.rect.w) as usize] = TileType::Wall; 174 | } 175 | for y in r.rect.h - h_third..r.rect.h { 176 | r.tiles[room_get_xy(x, y, r.rect.w) as usize] = TileType::Wall; 177 | } 178 | } 179 | r 180 | } 181 | 182 | fn generate_circular_room(size_min: i32, size_max: i32) -> Room { 183 | let mut r = generate_square_room(size_min, size_max); 184 | let radius = r.rect.w / 2; 185 | let (cx, cy) = r.center(); 186 | for x in 0..r.rect.w { 187 | for y in 0..r.rect.h { 188 | let dist = (((x - cx).pow(2) + (y - cy).pow(2)) as f64).sqrt(); 189 | if dist.round() >= radius as f64 { 190 | r.tiles[room_get_xy(x, y, r.rect.w) as usize] = TileType::Wall; 191 | } 192 | } 193 | } 194 | r 195 | } 196 | 197 | fn generate_cave_room(size_min: i32, size_max: i32) -> Room { 198 | let mut r = generate_square_room(size_min, size_max); 199 | 200 | generate_cave(&mut r); 201 | while get_random_cave_size(&mut r) < 80 { 202 | r = generate_square_room(size_min, size_max); 203 | generate_cave(&mut r); 204 | } 205 | r 206 | } 207 | 208 | fn generate_cave(r: &mut Room) { 209 | for x in 0..r.rect.w { 210 | for y in 0..r.rect.h { 211 | if x == 0 || x == r.rect.w || y == 0 || y == r.rect.h { 212 | r.tiles[room_get_xy(x, y, r.rect.w) as usize] = TileType::Wall; 213 | } else { 214 | if randr(0..100) > 70 { 215 | r.tiles[room_get_xy(x, y, r.rect.w) as usize] = TileType::Wall; 216 | } 217 | } 218 | } 219 | } 220 | for _ in 0..5 { 221 | evolve_map(r); 222 | } 223 | } 224 | 225 | fn evolve_map(r: &mut Room) { 226 | let death_limit = 3; 227 | let birth_limit = 4; 228 | 229 | for y in 0..r.rect.w { 230 | for x in 0..r.rect.h { 231 | let neighbor_count = count_alive_neighbors(r, x, y); 232 | let tile = &mut r.tiles[room_get_xy(x, y, r.rect.w) as usize]; 233 | if *tile as i32 == TileType::Wall as i32 { 234 | if neighbor_count < death_limit { 235 | *tile = TileType::Floor; 236 | } 237 | } else { 238 | if birth_limit < neighbor_count { 239 | *tile = TileType::Wall; 240 | } 241 | } 242 | } 243 | } 244 | } 245 | 246 | fn count_alive_neighbors(r: &Room, x: i32, y: i32) -> i32 { 247 | let mut count = 0; 248 | for i in -1..2 { 249 | for j in -1..2 { 250 | let nx = x + i; 251 | let ny = y + j; 252 | if (nx < 0) | (nx >= r.rect.w) | (ny < 0) | (ny >= r.rect.h) { 253 | count += 1; 254 | } else if r.tiles[room_get_xy(nx, ny, r.rect.w) as usize] as i32 255 | == TileType::Wall as i32 256 | { 257 | count += 1; 258 | } 259 | } 260 | } 261 | count 262 | } 263 | 264 | fn get_random_cave_size(r: &mut Room) -> i32 { 265 | // count as 0 if less than 10 floors 266 | let mut num_floor: i32 = r.tiles.iter().map(|x| (*x) as i32).sum(); 267 | if num_floor < 10 { 268 | return 0; 269 | } 270 | 271 | // get random floor starting position for DFS 272 | let mut rx = randr(0..r.rect.w); 273 | let mut ry = randr(0..r.rect.h); 274 | while r.tiles[room_get_xy(rx, ry, r.rect.w) as usize] as i32 != TileType::Floor as i32 { 275 | rx = randr(0..r.rect.w); 276 | ry = randr(0..r.rect.h); 277 | } 278 | let mut visited: Vec = Vec::with_capacity((r.rect.w * r.rect.h) as usize); 279 | for _ in r.tiles.iter() { 280 | visited.push(0); 281 | } 282 | dfs(rx, ry, &mut visited, r); 283 | 284 | for (pos, tile) in r.tiles.iter_mut().enumerate() { 285 | if visited[pos] == 1 { 286 | *tile = TileType::Floor; 287 | } else { 288 | *tile = TileType::Wall; 289 | } 290 | } 291 | 292 | num_floor = visited.iter().sum(); 293 | num_floor 294 | } 295 | 296 | fn dfs(x: i32, y: i32, v: &mut [i32], r: &Room) { 297 | v[room_get_xy(x, y, r.rect.w) as usize] = 1; 298 | if is_valid(x - 1, y, v, r) { 299 | dfs(x - 1, y, v, r); 300 | } 301 | if is_valid(x + 1, y, v, r) { 302 | dfs(x + 1, y, v, r); 303 | } 304 | if is_valid(x, y - 1, v, r) { 305 | dfs(x, y - 1, v, r); 306 | } 307 | if is_valid(x, y + 1, v, r) { 308 | dfs(x, y + 1, v, r); 309 | } 310 | } 311 | 312 | fn is_valid(x: i32, y: i32, v: &[i32], r: &Room) -> bool { 313 | if (x < 0) | (x >= r.rect.w) | (y < 0) | (y >= r.rect.h) { 314 | return false; 315 | } 316 | if v[room_get_xy(x, y, r.rect.w) as usize] == 1 { 317 | return false; 318 | } 319 | if r.tiles[room_get_xy(x, y, r.rect.w) as usize] as i32 == TileType::Wall as i32 { 320 | return false; 321 | } 322 | 323 | true 324 | } 325 | 326 | /// place_room transposes the Room rect to map coordinates, and carves the Room tiles 327 | fn place_room(r: &mut Room, m: &mut Map, xoff: i32, yoff: i32) -> bool { 328 | r.rect.x = xoff; 329 | r.rect.y = yoff; 330 | // first pass, check if new room is in bounds & no overlap 331 | for x in xoff - 1..xoff + r.rect.w + 1 { 332 | for y in yoff - 1..yoff + r.rect.h + 1 { 333 | if !in_bounds(x, y) { 334 | return false; 335 | } 336 | if m[y as usize][x as usize] == TileType::Floor as i32 { 337 | return false; 338 | } 339 | } 340 | } 341 | // second pass, place room if first pass was OK 342 | for x in xoff..xoff + r.rect.w { 343 | for y in yoff..yoff + r.rect.h { 344 | m[y as usize][x as usize] = 345 | r.tiles[room_get_xy(x - xoff, y - yoff, r.rect.w) as usize] as i32; 346 | } 347 | } 348 | true 349 | } 350 | 351 | const N_ROOM_TYPE: i32 = 5; 352 | enum RoomType { 353 | Square, 354 | Rectangle, 355 | Cross, 356 | Circle, 357 | Cave, 358 | } 359 | 360 | fn generate_random_room() -> Room { 361 | let room_type: RoomType = match randr(0..N_ROOM_TYPE) { 362 | 0 => RoomType::Square, 363 | 1 => RoomType::Rectangle, 364 | 2 => RoomType::Cross, 365 | 3 => RoomType::Circle, 366 | _ => RoomType::Cave, 367 | }; 368 | let r = match room_type { 369 | RoomType::Square => generate_square_room(SQUARE_ROOM_MIN_SIZE, SQUARE_ROOM_MAX_SIZE), 370 | RoomType::Rectangle => { 371 | generate_rectangular_room(SQUARE_ROOM_MIN_SIZE, SQUARE_ROOM_MAX_SIZE) 372 | } 373 | RoomType::Cross => generate_cross_room(CROSS_ROOM_MIN_SIZE, CROSS_ROOM_MAX_SIZE), 374 | RoomType::Circle => generate_circular_room(SQUARE_ROOM_MIN_SIZE, SQUARE_ROOM_MAX_SIZE), 375 | RoomType::Cave => generate_cave_room(CAVE_ROOM_MIN_SIZE, CAVE_ROOM_MAX_SIZE), 376 | }; 377 | r 378 | } 379 | 380 | fn try_place_room(free_connection_points: &mut Vec, map: &mut Map) -> bool { 381 | // generate a random type room 382 | let mut r = generate_random_room(); 383 | 384 | // select random connection point on starting room 385 | let cp = free_connection_points[randr(0..free_connection_points.len() as i32) as usize]; 386 | 387 | // try to move room next to starting room connection point, depending on cp location 388 | let placement_offset = 4; 389 | let (tx, ty) = match cp.loc { 390 | ConnectionPointLocation::Top => ( 391 | cp.tile.x - r.rect.w / 2, 392 | cp.tile.y - r.rect.h - placement_offset, 393 | ), 394 | ConnectionPointLocation::Right => (cp.tile.x + placement_offset, cp.tile.y - r.rect.h / 2), 395 | ConnectionPointLocation::Bottom => (cp.tile.x - r.rect.w / 2, cp.tile.y + placement_offset), 396 | ConnectionPointLocation::Left => ( 397 | cp.tile.x - r.rect.w - placement_offset, 398 | cp.tile.y - r.rect.h / 2, 399 | ), 400 | }; 401 | 402 | if !place_room(&mut r, map, tx, ty) { 403 | return false; 404 | } 405 | 406 | let mut new_room_connection_points = find_connection_points(&r); 407 | 408 | match cp.loc { 409 | ConnectionPointLocation::Top => { 410 | new_room_connection_points.retain(|&p| p.loc != ConnectionPointLocation::Bottom) 411 | } 412 | ConnectionPointLocation::Right => { 413 | new_room_connection_points.retain(|&p| p.loc != ConnectionPointLocation::Left) 414 | } 415 | ConnectionPointLocation::Bottom => { 416 | new_room_connection_points.retain(|&p| p.loc != ConnectionPointLocation::Top) 417 | } 418 | ConnectionPointLocation::Left => { 419 | new_room_connection_points.retain(|&p| p.loc != ConnectionPointLocation::Right) 420 | } 421 | } 422 | 423 | // connect rooms 424 | match cp.loc { 425 | ConnectionPointLocation::Top => { 426 | let tx = cp.tile.x; 427 | let mut ty = cp.tile.y; 428 | while map[ty as usize - 1][tx as usize] != TileType::Floor as i32 { 429 | map[ty as usize - 1][tx as usize] = TileType::Floor as i32; 430 | ty = ty - 1; 431 | } 432 | } 433 | ConnectionPointLocation::Bottom => { 434 | let tx = cp.tile.x; 435 | let mut ty = cp.tile.y; 436 | while map[ty as usize + 1][tx as usize] != TileType::Floor as i32 { 437 | map[ty as usize + 1][tx as usize] = TileType::Floor as i32; 438 | ty = ty + 1; 439 | } 440 | } 441 | ConnectionPointLocation::Left => { 442 | let mut tx = cp.tile.x; 443 | let ty = cp.tile.y; 444 | while map[ty as usize][tx as usize - 1] != TileType::Floor as i32 { 445 | map[ty as usize][tx as usize - 1] = TileType::Floor as i32; 446 | tx = tx - 1; 447 | } 448 | } 449 | ConnectionPointLocation::Right => { 450 | let mut tx = cp.tile.x; 451 | let ty = cp.tile.y; 452 | while map[ty as usize][tx as usize + 1] != TileType::Floor as i32 { 453 | map[ty as usize][tx as usize + 1] = TileType::Floor as i32; 454 | tx = tx + 1; 455 | } 456 | } 457 | } 458 | 459 | // remove used up connection point on previous room 460 | free_connection_points.retain(|&p| (p != cp)); 461 | // add new room's free connection points 462 | free_connection_points.extend(new_room_connection_points); 463 | 464 | true 465 | } 466 | 467 | pub struct RoomPlacementGenerator {} 468 | 469 | impl RoomPlacementGenerator { 470 | pub fn generate_map() -> Map { 471 | let mut map = new_map(TileType::Wall); 472 | 473 | // generate & place starting room in center 474 | let mut r1 = generate_random_room(); 475 | let starting_room_x = COLS / 2 - r1.rect.w / 2; 476 | let starting_room_y = ROWS / 2 - r1.rect.h / 2; 477 | place_room(&mut r1, &mut map, starting_room_x, starting_room_y); 478 | 479 | // add starting room's connection points to vec containg all free connection points 480 | let mut free_connection_points = find_connection_points(&r1); 481 | 482 | let max_attempts = 100; 483 | let mut rooms_placed = 1; 484 | for _ in 0..max_attempts { 485 | if try_place_room(&mut free_connection_points, &mut map) { 486 | rooms_placed = rooms_placed + 1; 487 | } 488 | } 489 | 490 | // println!( 491 | // "Placed {} rooms out of {} attempts.", 492 | // rooms_placed, max_attempts 493 | // ); 494 | 495 | // for c in free_connection_points { 496 | // map[get_xy_idx(c.tile.x, c.tile.y) as usize] = TileType::Hero as i32; 497 | // } 498 | 499 | map 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /src/procgen/rwalk.rs: -------------------------------------------------------------------------------- 1 | use crate::{fundamentals::*, maptools::*, utils::*}; 2 | 3 | const MAX_WALKERS: i32 = 10; 4 | const MAX_STEPS: i32 = 200; 5 | 6 | #[derive(Copy, Clone, Debug)] 7 | struct Walker { 8 | x: i32, 9 | y: i32, 10 | pub steps: i32, 11 | } 12 | 13 | impl Walker { 14 | fn new(x: i32, y: i32) -> Self { 15 | Self { 16 | x, 17 | y, 18 | steps: MAX_STEPS, 19 | } 20 | } 21 | 22 | fn step(&mut self, map: &mut Map) { 23 | if self.steps <= 0 { 24 | return; 25 | } 26 | 27 | let direction = randr(0..4); 28 | let (mut dx, mut dy) = (0, 0); 29 | match direction { 30 | 0 => dx = 1, 31 | 1 => dx = -1, 32 | 2 => dy = 1, 33 | _ => dy = -1, 34 | } 35 | 36 | let tx = self.x + dx; 37 | let ty = self.y + dy; 38 | 39 | if tx < 1 || tx >= COLS - 1 || ty < 1 || ty >= ROWS - 1 { 40 | return; 41 | } 42 | 43 | self.x = tx; 44 | self.y = ty; 45 | self.steps -= 1; 46 | map[self.y as usize][self.x as usize] = TileType::Floor as i32; 47 | } 48 | } 49 | 50 | fn spawn_walker<'a>(x: i32, y: i32, vec: &'a mut Vec, map: &mut Map) { 51 | vec.push(Walker::new(x, y)); 52 | map[y as usize][x as usize] = TileType::Floor as i32; 53 | } 54 | 55 | pub struct RandomWalkGenerator {} 56 | impl RandomWalkGenerator { 57 | pub fn generate_map() -> Map { 58 | let mut map = new_map(TileType::Wall); 59 | let mut walkers: Vec = Vec::new(); 60 | let mut num_walkers = 0; 61 | 62 | spawn_walker(COLS / 2, ROWS / 2, &mut walkers, &mut map); 63 | num_walkers += 1; 64 | 65 | // until we have active walkers 66 | while walkers.len() > 0 { 67 | // create vector for possible newly spawned walkers in this iteration 68 | let mut walkers_spawned: Vec = Vec::new(); 69 | 70 | // each walker takes step 71 | for w in &mut walkers { 72 | w.step(&mut map); 73 | // after each step, chance to spawn new walker at walker's current location (if we can) 74 | if (num_walkers < MAX_WALKERS) & (randr(0..100) > 80) { 75 | spawn_walker(w.x, w.y, &mut walkers_spawned, &mut map); 76 | num_walkers += 1; 77 | } 78 | } 79 | 80 | // if we did spawn new walker in this iteration, append it to the main walkers vector 81 | if walkers_spawned.len() > 0 { 82 | walkers.append(&mut walkers_spawned); 83 | } 84 | 85 | // keep only walkers that still have steps left in main walkers vector 86 | walkers.retain(|x| x.steps > 0); 87 | } 88 | 89 | map 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/procgen/tunneling.rs: -------------------------------------------------------------------------------- 1 | use crate::fundamentals::*; 2 | use crate::maptools::*; 3 | use crate::utils::*; 4 | 5 | pub struct TunnelingGenerator {} 6 | impl TunnelingGenerator { 7 | pub fn generate_map(room_size_min: i32, room_size_max: i32, max_rooms: i32) -> Map { 8 | let mut map = new_map(TileType::Wall); 9 | let mut rooms: Vec = vec![]; 10 | let mut num_rooms = 0; 11 | 12 | for _ in 0..max_rooms { 13 | let w = randr(room_size_min..room_size_max); 14 | let h = randr(room_size_min..room_size_max); 15 | let x = randr(1..COLS - w); 16 | let y = randr(1..ROWS - h); 17 | 18 | let curr_room = Room::new(x, y, w, h); 19 | let mut overlaps = false; 20 | 21 | for room in &rooms { 22 | if curr_room.overlaps(*room) { 23 | overlaps = true; 24 | break; 25 | } 26 | } 27 | 28 | if !overlaps { 29 | curr_room.carve(&mut map); 30 | 31 | let (curr_x, curr_y) = curr_room.center(); 32 | 33 | if num_rooms != 0 { 34 | let prev_room = rooms[num_rooms - 1]; 35 | let (prev_x, prev_y) = prev_room.center(); 36 | 37 | if randr(0..1) == 1 { 38 | carve_horz_tunnel(&mut map, curr_x, prev_x, curr_y); 39 | carve_vert_tunnel(&mut map, curr_y, prev_y, prev_x); 40 | } else { 41 | carve_vert_tunnel(&mut map, curr_y, prev_y, curr_x); 42 | carve_horz_tunnel(&mut map, curr_x, prev_x, prev_y); 43 | } 44 | } 45 | 46 | num_rooms += 1; 47 | rooms.push(curr_room); 48 | } 49 | } 50 | map 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::fundamentals::{COLS, ROWS}; 2 | use macroquad::rand; 3 | use std::ops::Range; 4 | 5 | pub fn randr(r: Range) -> i32 { 6 | rand::gen_range::(r.start, r.end) 7 | } 8 | 9 | pub fn in_bounds(x: i32, y: i32) -> bool { 10 | 0 <= x && x < COLS && 0 <= y && y < ROWS 11 | } 12 | --------------------------------------------------------------------------------