Compare commits

49 Commits

Author SHA1 Message Date
dev 0d83cb89ed Update readme 2026-08-10 11:34:32 +05:30
cool-mist 82d13cfd66 move to retained mode complete 2026-03-15 17:24:41 +05:30
cool-mist 194136dfc7 --> retained mode ui 2026-03-15 12:05:02 +05:30
cool-mist 3cb08bf08e Major refactor 2026-03-14 00:41:33 +05:30
cool-mist 810a4cb33c Some updates to sizing button text 2026-02-15 00:13:54 +05:30
cool-mist 280150b0b0 add age 2026-01-26 20:00:50 +05:30
cool-mist dc61d6d956 separate logic state and draw state 2026-01-25 01:50:52 +05:30
cool-mist 52fb443d5b fix re-exports 2026-01-24 16:52:05 +05:30
cool-mist 04b39e53a4 Add widgets, proper resources.rs 2026-01-24 01:05:33 +05:30
cool-mist 96a4f007cc Adjust everything relative to the board 2026-01-22 19:10:21 +05:30
cool-mist 0bd216893e update readme 2026-01-22 18:27:32 +05:30
cool-mist 0c88bfa24b Change font 2026-01-22 16:29:29 +05:30
cool-mist 86d160cab0 move to workspace mode 2026-01-22 15:52:52 +05:30
cool-mist 8cd1f86ba6 remove recursion in solver 2026-01-21 20:14:57 +05:30
cool-mist 27d57299ba allow lowercase for specifying the board 2026-01-21 18:31:47 +05:30
cool-mist 5dc87df1fc Use Rc instead of Arc for fonts 2026-01-21 00:07:48 +05:30
cool-mist d7ec3d7096 config-ify 2026-01-21 00:01:44 +05:30
cool-mist 2d33b20ded update font 2026-01-18 23:59:09 +05:30
cool-mist b15fb61c9e base64 encode id 2026-01-17 00:31:49 +05:30
cool-mist bce815d477 handle rules menu close properly 2026-01-16 18:34:09 +05:30
cool-mist 01b2832402 reduce volume, move buttons to the right 2026-01-15 22:29:38 +05:30
cool-mist 3a3c681899 bundle mq.js along with the code 2025-08-22 20:18:43 +05:30
cool-mist ab0c333935 Add sounds 2025-06-04 16:05:11 +05:30
cool-mist c551715a52 Finish, mudinji pochi 2025-05-25 23:21:47 +05:30
cool-mist d758d03f2d format difficulty buttons 2025-05-18 00:22:21 +05:30
cool-mist d90e270cd5 Add difficulty levels 2025-05-18 00:09:11 +05:30
cool-mist 373d3da91b center everything correctly 2025-05-17 23:11:02 +05:30
cool-mist 1698f3ce87 Refactor tools 2025-05-17 20:30:50 +05:30
cool-mist ace89713d7 Fix scripts 2025-05-17 14:39:59 +05:30
cool-mist 77a640e1b7 Add clean script 2025-05-17 10:37:45 +05:30
cool-mist 32ff856cfa update readme 2025-05-11 23:47:49 +05:30
cool-mist ff13e49720 decorations 2025-05-11 23:47:03 +05:30
cool-mist 654469bb4a enable tests 2025-05-11 23:10:02 +05:30
cool-mist c1b0ea24a6 Better on browsers 2025-05-11 19:25:22 +05:30
cool-mist 977c24968b Grey out inactive buttons 2025-05-11 19:19:52 +05:30
cool-mist 1dfe56ac50 Animation on button click 2025-05-11 19:00:52 +05:30
cool-mist 8b71bce89c Add shadows to buttons 2025-05-11 17:22:07 +05:30
cool-mist dc4ae27074 Correctly center button text 2025-05-11 16:48:42 +05:30
cool-mist c8f1c043ee Automatically move dev instance to different workspace 2025-05-11 16:12:05 +05:30
Surya Prakash 9221f3f38a Update README.md 2025-03-24 00:24:09 +05:30
cool-mist 6b321e034f Add buttons, refactor, make it more mouse friendly 2025-03-24 00:19:34 +05:30
cool-mist 5560013e33 Seed random number generation 2025-02-23 17:16:15 +05:30
cool-mist 89e43686ca Do the actual deployment using sudo 2025-02-23 15:31:59 +05:30
cool-mist 111a01faac Fix scripts 2025-02-23 15:26:42 +05:30
cool-mist 74a91f5a1f Remove unnecessary, package as tar 2025-02-23 15:15:26 +05:30
cool-mist 6a8d9a47af Working wasm 2025-02-22 19:29:20 +05:30
cool-mist 2ada9183e7 Package texture inside the binary 2025-02-22 13:43:18 +05:30
cool-mist e957903a39 Auto resize based on screen size 2025-02-20 22:59:00 +05:30
cool-mist f4b41bc6de add game 2025-02-02 00:38:15 +05:30
57 changed files with 3089 additions and 1248 deletions
+4
View File
@@ -19,3 +19,7 @@ Cargo.lock
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/ .idea/
dist/
sol_chess.tar.gz
local-deploy/
Generated
+199 -286
View File
@@ -4,21 +4,15 @@ version = 4
[[package]] [[package]]
name = "adler2" name = "adler2"
version = "2.0.0" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "ahash" name = "allocator-api2"
version = "0.8.11" version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]] [[package]]
name = "argh" name = "argh"
@@ -53,10 +47,28 @@ dependencies = [
] ]
[[package]] [[package]]
name = "autocfg" name = "audir-sles"
version = "1.4.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" checksum = "ea47348666a8edb7ad80cbee3940eb2bccf70df0e6ce09009abe1a836cb779f5"
[[package]]
name = "audrey"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58b92a84e89497e3cd25d3672cd5d1c288abaac02c18ff21283f17d118b889b8"
dependencies = [
"dasp_frame",
"dasp_sample",
"hound",
"lewton",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
@@ -64,17 +76,11 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.21.0" version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
@@ -84,9 +90,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]] [[package]]
name = "color_quant" name = "color_quant"
@@ -94,33 +100,35 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "console"
version = "0.15.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width",
"windows-sys",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.4.2" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]] [[package]]
name = "encode_unicode" name = "dasp_frame"
version = "1.0.0" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6"
dependencies = [
"dasp_sample",
]
[[package]]
name = "dasp_sample"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]] [[package]]
name = "fdeflate" name = "fdeflate"
@@ -133,19 +141,25 @@ dependencies = [
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.35" version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide", "miniz_oxide",
] ]
[[package]] [[package]]
name = "fontdue" name = "foldhash"
version = "0.7.3" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0793f5137567643cf65ea42043a538804ff0fbf288649e2141442b602d81f9bc" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "fontdue"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b"
dependencies = [ dependencies = [
"hashbrown", "hashbrown",
"ttf-parser", "ttf-parser",
@@ -153,13 +167,14 @@ dependencies = [
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.15" version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"wasi", "r-efi",
"wasip2",
] ]
[[package]] [[package]]
@@ -170,13 +185,21 @@ checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.13.2" version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [ dependencies = [
"ahash", "allocator-api2",
"equivalent",
"foldhash",
] ]
[[package]]
name = "hound"
version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
[[package]] [[package]]
name = "image" name = "image"
version = "0.24.9" version = "0.24.9"
@@ -191,45 +214,27 @@ dependencies = [
] ]
[[package]] [[package]]
name = "indicatif" name = "lewton"
version = "0.17.9" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" checksum = "8d542c1a317036c45c2aa1cf10cc9d403ca91eb2d333ef1a4917e5cb10628bd0"
dependencies = [ dependencies = [
"console", "byteorder",
"number_prefix", "ogg",
"portable-atomic", "smallvec",
"unicode-width",
"web-time",
]
[[package]]
name = "js-sys"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
dependencies = [
"once_cell",
"wasm-bindgen",
] ]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.169" version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "log"
version = "0.4.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6ea2a48c204030ee31a7d7fc72c93294c92fe87ecb1789881c9543516e1a0d"
[[package]] [[package]]
name = "macroquad" name = "macroquad"
version = "0.4.13" version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fef16b2d4de22ac372b5d7d76273c0ead0a31f9de9bc649af8f816816db8a8" checksum = "d2befbae373456143ef55aa93a73594d080adfb111dc32ec96a1123a3e4ff4ae"
dependencies = [ dependencies = [
"fontdue", "fontdue",
"glam", "glam",
@@ -237,7 +242,7 @@ dependencies = [
"macroquad_macro", "macroquad_macro",
"miniquad", "miniquad",
"quad-rand", "quad-rand",
"slotmap", "quad-snd",
] ]
[[package]] [[package]]
@@ -256,22 +261,28 @@ dependencies = [
] ]
[[package]] [[package]]
name = "miniquad" name = "maybe-uninit"
version = "0.4.6" version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2124e5e6bab86d96f263d78dc763c29e0da4c4f7ff0e1bb33f1d3905d1121262" checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
[[package]]
name = "miniquad"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fb3e758e46dbc45716a8a49ca9edc54b15bcca826277e80b1f690708f67f9e3"
dependencies = [ dependencies = [
"libc", "libc",
"ndk-sys", "ndk-sys",
"objc", "objc-rs",
"winapi", "winapi",
] ]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.3" version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [ dependencies = [
"adler2", "adler2",
"simd-adler32", "simd-adler32",
@@ -293,25 +304,22 @@ dependencies = [
] ]
[[package]] [[package]]
name = "number_prefix" name = "objc-rs"
version = "0.4.0" version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" checksum = "64a1e7069a2525126bf12a9f1f7916835fafade384fb27cabf698e745e2a1eb8"
[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [ dependencies = [
"malloc_buf", "malloc_buf",
] ]
[[package]] [[package]]
name = "once_cell" name = "ogg"
version = "1.20.2" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" checksum = "13e571c3517af9e1729d4c63571a27edd660ade0667973bfc74a67c660c2b651"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "png" name = "png"
@@ -326,17 +334,11 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "portable-atomic"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.20" version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [ dependencies = [
"zerocopy", "zerocopy",
] ]
@@ -350,6 +352,15 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "quad-alsa-sys"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66c2f04a6946293477973d85adc251d502da51c57b08cd9c997f0cfd8dcd4b5"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "quad-rand" name = "quad-rand"
version = "0.2.3" version = "0.2.3"
@@ -357,30 +368,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40" checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40"
[[package]] [[package]]
name = "quote" name = "quad-snd"
version = "1.0.38" version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" checksum = "cba0c4943fc67147fbe9d1eb731fb9e678bfc9d926507eebbbfe0103e154e5b0"
dependencies = [
"audir-sles",
"audrey",
"libc",
"quad-alsa-sys",
"winapi",
]
[[package]]
name = "quote"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]] [[package]]
name = "rand" name = "r-efi"
version = "0.8.5" version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [ dependencies = [
"libc",
"rand_chacha", "rand_chacha",
"rand_core", "rand_core",
] ]
[[package]] [[package]]
name = "rand_chacha" name = "rand_chacha"
version = "0.3.1" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core", "rand_core",
@@ -388,9 +417,9 @@ dependencies = [
[[package]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.6.4" version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [ dependencies = [
"getrandom", "getrandom",
] ]
@@ -403,18 +432,28 @@ checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.217" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.217" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -423,26 +462,41 @@ dependencies = [
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.7" version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]] [[package]]
name = "slotmap" name = "smallvec"
version = "1.0.7" version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0"
dependencies = [ dependencies = [
"version_check", "maybe-uninit",
] ]
[[package]] [[package]]
name = "sol_chess" name = "sol_chess"
version = "0.1.1" version = "0.0.1"
dependencies = [
"macroquad",
"quad-snd",
"sol_lib",
]
[[package]]
name = "sol_cli"
version = "0.0.1"
dependencies = [ dependencies = [
"argh", "argh",
"indicatif", "rand",
"macroquad", "sol_lib",
]
[[package]]
name = "sol_lib"
version = "0.0.1"
dependencies = [
"rand", "rand",
] ]
@@ -459,9 +513,9 @@ dependencies = [
[[package]] [[package]]
name = "ttf-parser" name = "ttf-parser"
version = "0.15.2" version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd" checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
@@ -470,85 +524,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]] [[package]]
name = "unicode-width" name = "wasip2"
version = "0.2.0" version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
dependencies = [ dependencies = [
"cfg-if", "wit-bindgen",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
[[package]]
name = "web-time"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
dependencies = [
"js-sys",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -574,93 +555,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "windows-sys" name = "wit-bindgen"
version = "0.59.0" version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.35" version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
dependencies = [ dependencies = [
"byteorder",
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.7.35" version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
+11 -16
View File
@@ -1,18 +1,13 @@
[package] [workspace]
name = "sol_chess" members = [
version = "0.1.1" "lib",
edition = "2021" "cli",
default-run = "sol_chess" "gui",
]
resolver = "3"
[dependencies] [workspace.dependencies]
argh = "0.1.13" argh = "0.1.13"
indicatif = "0.17.9" macroquad = { version = "0.4.13", features = ["audio"] }
macroquad = "0.4.13" quad-snd = "0.2.8"
rand = "0.8.5" rand = "0.9.1"
[profile.release]
opt-level = 's'
lto = true
codegen-units = 1
panic = 'abort'
strip = true
+31 -36
View File
@@ -2,71 +2,66 @@
Goal: Generate 'hard' puzzles. Goal: Generate 'hard' puzzles.
### Play a demo of the game [here](https://games.neophyte.me/sol_chess/)
## Install ## Install
- Install Rust from [here](https://www.rust-lang.org/tools/install). - `cargo install --git https://github.com/cool-mist/sol_chess sol_chess`
- Run `cargo install --git https://github.com/cool-mist/sol_chess` to install the tool. - Run `sol_chess` to start a windowed GUI game.
- This installs 2 binaries: `sol_chess` and `sol_cli`.
## Usage ## Update
- [WIP] Run `sol_chess` to start a windowed GUI game. - `cargo install --git https://github.com/cool-mist/sol_chess sol_chess --force`
- Run `sol_cli` to start the CLI tool.
## CLI Usage ## CLI Usage
- `cargo install --git https://github.com/cool-mist/sol_chess sol_cli`
- Run `sol_chess` to start the game.
- Run `sol_cli` to start the CLI tool.
- Generate a puzzle - Generate a puzzle
```bash ```bash
$ sol_cli -g -n 6 $ sol_cli -g -n 6
Generating a puzzle with 6 pieces with a maximum of 5 solutions Generating a puzzle with 6 pieces with a maximum of 5 solutions
Total attempts: 7 Total attempts: 16
Total pieces placed: 71 Total pieces placed: 131
Success pieces placed: 42 Success pieces placed: 96
Total time (ms): 69
. . . ♙ ♗ . .
. . . . . .
♔ . ♘ ♙ ♗ ♖ . .
. . . . . .
id: 202859896274992 id: wmgYAAAG
``` ```
- Solve a puzzle by ID, or by board string - Solve a puzzle by ID, or by board string
```bash ```bash
$ sol_cli --solve 202859896274992 $ sol_cli --solve wmgYAAAG
$ sol_cli --solve-board N...P.R.K.NP.... $ sol_cli --solve-board pb......br..p..p
. . . ♙ ♗ . .
. . . . . .
♔ . ♘ ♙ ♗ ♖ . .
. . . . . .
id: 202859896274992 id: wmgYAAAG
Found 3 solutions 1. RxBb4
1. RxNc2 2. RxPa4
2. RxPd2 3. RxBa2
3. RxKa2 4. RxPa1
4. RxPa3 5. RxPd1
5. RxNa4 There are atleast 1 solutions to this puzzle
``` ```
## Heuristics of current algorithm
1. About 6-7 pieces on the board.
2. Select pieces to place based on its weight.
3. Eg: Queen is too powerful, so it has lower weightage.
4. Eg: Knights are confusing. More knights.
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "sol_cli"
version = "0.0.1"
edition = "2024"
[dependencies]
sol_lib = { path = "../lib" }
argh = { workspace = true }
rand = { workspace = true }
[profile.release]
opt-level = 's'
lto = true
codegen-units = 1
panic = 'abort'
strip = true
+46 -31
View File
@@ -1,56 +1,63 @@
use argh::FromArgs; use argh::FromArgs;
use sol_chess::board::Board; use rand::Rng;
use sol_chess::generator; use sol_lib::board::Board;
use sol_chess::solver::Solver; use sol_lib::generator::{self, Puzzle, RandomRange};
fn main() { fn main() {
let args: Args = argh::from_env(); let args: Args = argh::from_env();
if args.generate { if args.generate {
let puzzle = generate_puzzle(args.num_pieces, args.solutions); let puzzle = generate_puzzle(args.num_pieces, args.solutions);
let Some(board) = puzzle else { let Some(puzzle) = puzzle else {
println!("Failed to generate a puzzle, try adjusting the generation parameters");
return; return;
}; };
board.pretty_print(); puzzle.board.pretty_print();
if args.print { if args.print {
solve_puzzle(board); print_solutions(&puzzle);
} }
} else {
let board = if let Some(board_string) = args.solve_board { return;
Board::from_string(board_string)
} else if let Some(board_id) = args.solve {
Board::from_id(board_id)
} else {
println!("Use --help to see available options");
return;
};
let Ok(board) = board else {
println!("Invalid board string/id");
return;
};
board.pretty_print();
solve_puzzle(board);
} }
let board = if let Some(board_string) = args.solve_board {
Board::from_string(board_string)
} else if let Some(board_id) = args.solve {
Board::from_id(&board_id)
} else {
println!("Use --help to see available options");
return;
};
let Ok(board) = board else {
println!("Invalid board string/id");
return;
};
let puzzle = board.solve();
puzzle.board.pretty_print();
print_solutions(&puzzle);
} }
fn solve_puzzle(board: Board) { fn print_solutions(puzzle: &Puzzle) {
let solutions = Solver::new(board).solve(); let solutions = &puzzle.solutions;
if solutions.len() == 0 { if solutions.len() == 0 {
println!("No solutions found"); println!("No solutions found");
return; return;
} }
println!("Found {} solutions", solutions.len());
let solution = solutions.first().unwrap(); let solution = solutions.first().unwrap();
let mut idx = 0; let mut idx = 0;
solution.iter().for_each(|m| { solution.iter().for_each(|m| {
idx += 1; idx += 1;
println!("{}. {}", idx, m.notation()); println!("{}. {}", idx, m.notation());
}); });
println!(
"There are atleast {} solutions to this puzzle",
solutions.len()
);
} }
fn generate_puzzle(num_pieces: Option<u32>, num_solutions: Option<u32>) -> Option<Board> { fn generate_puzzle(num_pieces: Option<u32>, num_solutions: Option<u32>) -> Option<Puzzle> {
let mut num_pieces = num_pieces.unwrap_or(5); let mut num_pieces = num_pieces.unwrap_or(5);
if num_pieces < 2 { if num_pieces < 2 {
num_pieces = 2; num_pieces = 2;
@@ -65,15 +72,16 @@ fn generate_puzzle(num_pieces: Option<u32>, num_solutions: Option<u32>) -> Optio
"Generating a puzzle with {} pieces with a maximum of {} solutions", "Generating a puzzle with {} pieces with a maximum of {} solutions",
num_pieces, num_solutions num_pieces, num_solutions
); );
let gen = generator::generate(num_pieces, num_solutions); let gen_result =
gen.print_stats(); generator::generate_weighted_random(num_pieces, num_solutions, num_pieces, &RandRngImpl);
gen_result.print_stats();
let Some(board) = gen.board() else { let Some(puzzle) = gen_result.puzzle() else {
println!("Failed to generate a puzzle, try again"); println!("Failed to generate a puzzle, try again");
return None; return None;
}; };
Some(board) Some(puzzle)
} }
/// Solitaire Chess puzzle generator and solver /// Solitaire Chess puzzle generator and solver
@@ -98,9 +106,16 @@ struct Args {
#[argh(option, short = 's')] #[argh(option, short = 's')]
/// the id of the board to solve /// the id of the board to solve
solve: Option<u128>, solve: Option<String>,
#[argh(option)] #[argh(option)]
/// the board to solve in board representation /// the board to solve in board representation
solve_board: Option<String>, solve_board: Option<String>,
} }
struct RandRngImpl;
impl RandomRange for RandRngImpl {
fn gen_range(&self, min: usize, max: usize) -> usize {
rand::rng().random_range(min..max)
}
}
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "sol_chess"
version = "0.0.1"
edition = "2024"
[dependencies]
sol_lib = { path = "../lib" }
macroquad = { workspace = true }
quad-snd = { workspace = true }
[profile.release]
opt-level = 's'
lto = true
codegen-units = 1
panic = 'abort'
strip = true
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.
+77
View File
@@ -0,0 +1,77 @@
pub mod constants;
mod draw;
mod logic;
use std::collections::HashMap;
use crate::{resources::Resources, widgets::*};
use macroquad::prelude::*;
use sol_lib::{board::BoardState, generator::Puzzle};
#[derive(Default)]
pub struct Game {
resources: Resources,
settings: Settings,
puzzle: Puzzle,
show_rules: bool,
game_mode: GameMode,
game_mode_btns: HashMap<GameMode, WidgetId>,
window_height: f32,
window_width: f32,
widgets: HashMap<WidgetId, Widget>,
board: WidgetId,
counter_pieces: WidgetId,
counter_moves: WidgetId,
label_pieces: WidgetId,
label_max_age: WidgetId,
label_heading: WidgetId,
btn_rules: WidgetId,
btn_reset: WidgetId,
btn_next: WidgetId,
btn_generate: WidgetId,
}
#[derive(Default)]
pub struct Settings {
pub volume: f32,
pub debug: bool,
pub max_pieces: u32,
pub max_age: u32,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum GameMode {
Easy,
Medium,
Hard,
Custom,
}
impl Default for GameMode {
fn default() -> Self {
GameMode::Medium
}
}
#[derive(Copy, Clone)]
pub enum Command {
NoInteraction,
ResetBoard,
NextPuzzle,
ToggleRules,
UpdateBoard(BoardState),
SwitchGameMode(GameMode),
UpdateMaxPieceCount(u32),
UpdateMaxAge(u32),
NewCustomPuzzle,
ToggleDebug,
HideRules,
Quit,
}
+44
View File
@@ -0,0 +1,44 @@
pub const WINDOW_TITLE: &str = "Solitaire Chess";
pub const VOLUME: f32 = 0.1; // Between 0 and 1
pub const SCREEN_HEIGHT_MIN: f32 = 200.0;
pub const SCREEN_HEIGHT_MAX: f32 = 10000.0;
pub const SCREEN_WIDTH_MIN: f32 = 200.0;
pub const SCREEN_WIDTH_MAX: f32 = 10000.0;
// How big the square should be relative to the screen size, between 0 and 1
pub const BOARD_SQUARE_WIDTH_MULTIPLIER: f32 = 0.11;
pub const BOARD_SHADOW_MULTIPLIER: f32 = 0.1;
pub const BOARD_PIECE_WIDTH_MULTIPLIER: f32 = 0.8;
// How big the heading text font size should be relative to the screen size, between 0 and 1
pub const HEADING_FONT_SIZE_MULTIPLIER: f32 = 0.07;
// Height of the button relative to the screen size.
pub const BUTTON_HEIGHT_MULTIPLIER: f32 = 0.08;
// Width of the button relative to the 'board size'
pub const BUTTON_WIDTH_MULTIPLIER: f32 = 0.20;
// Gap between the bottom of the board and the first row of buttons
pub const BOTTOM_ROW_OFFSET_MULTIPLIER: f32 = 0.3;
// Gap between the right of the board and the first row of buttons
pub const RIGHT_COLUMN_OFFSET_MULTIPLIER: f32 = 0.3;
// Gap between the top of the board and the heading
pub const TOP_HEADING_OFFSET_MULTIPLIER: f32 = 0.5;
pub const HEADING_TEXT: &str = "Solitaire Chess";
pub const RESET_BUTTON_TEXT: &str = "RESET";
pub const NEXT_BUTTON_TEXT: &str = "NEXT";
pub const RULES_BUTTON_TEXT: &str = "RULES";
pub const RULES_BUTTON_ALT_TEXT: &str = "CLOSE";
pub const EASY_BUTTON_TEXT: &str = "EASY";
pub const MEDIUM_BUTTON_TEXT: &str = "MEDIUM";
pub const HARD_BUTTON_TEXT: &str = "HARD";
pub const CUSTOM_BUTTON_TEXT: &str = "CUSTOM";
pub const GENERATE_BUTTON_TEXT: &str = "GENERATE";
pub const PIECES_LABEL_TEXT: &str = "Pieces";
pub const AGE_LABEL_TEXT: &str = " Age";
+223
View File
@@ -0,0 +1,223 @@
use super::{Game, constants};
use crate::{
game::{self, GameMode},
widgets::*,
};
use macroquad::{math, prelude::*};
impl Game {
fn initialize_scene(&mut self, new_width: f32, new_height: f32) {
self.window_height = new_height;
self.window_width = new_width;
let min_dimension = f32::min(self.window_height, self.window_width);
let square_width = constants::BOARD_SQUARE_WIDTH_MULTIPLIER * min_dimension;
let num_squares = w_board!(self, board).num_squares;
let board_width = square_width * num_squares as f32;
let board_x = (self.window_width * 0.6 - board_width) / 2.0;
let board_y = (self.window_height - board_width) / 2.0;
let heading_font_size = constants::HEADING_FONT_SIZE_MULTIPLIER * min_dimension;
let f = heading_font_size.floor() as u16;
let heading_text_dims = measure_text(
w_label!(self, label_heading).text.as_str(),
Some(&self.resources.font()),
f,
1.0,
);
let heading_rect = Rect::new(
(self.window_width - heading_text_dims.width) / 2.0,
board_y
- heading_text_dims.height
- constants::TOP_HEADING_OFFSET_MULTIPLIER * square_width,
heading_text_dims.width,
heading_text_dims.height,
);
w_label!(self, label_heading).place(heading_rect, heading_font_size, &self.resources);
let board_rect = Rect::new(board_x, board_y, board_width, board_width);
w_board!(self, board).place(square_width, board_rect, heading_font_size * 0.4);
// Buttons
let btn_h = constants::BUTTON_HEIGHT_MULTIPLIER * min_dimension;
let btn_w = constants::BUTTON_WIDTH_MULTIPLIER * board_width;
let btn_font_size = heading_font_size * 0.3;
// Bottom row
let bottom_row_y =
board_width + board_y + constants::BOTTOM_ROW_OFFSET_MULTIPLIER * square_width;
w_button!(self, btn_rules).place(
Rect::new(
board_x + (square_width - btn_w) / 2.,
bottom_row_y,
btn_w,
btn_h,
),
FontSize::Fixed(btn_font_size),
);
w_button!(self, btn_reset).place(
Rect::new(
board_x + square_width + (square_width - btn_w) / 2.,
bottom_row_y,
btn_w,
btn_h,
),
FontSize::Fixed(btn_font_size),
);
let btn_next_x_offset = (board_width - square_width) + (square_width - btn_w) / 2.;
w_button!(self, btn_next).place(
Rect::new(board_x + btn_next_x_offset, bottom_row_y, btn_w, btn_h),
FontSize::Fixed(btn_font_size),
);
// Right column
let right_column_x =
board_x + board_width + constants::RIGHT_COLUMN_OFFSET_MULTIPLIER * square_width;
self.get_game_mode_button(&GameMode::Easy).place(
Rect::new(
right_column_x,
board_y + (square_width - btn_h) / 2.,
btn_w,
btn_h,
),
FontSize::Fixed(btn_font_size),
);
self.get_game_mode_button(&GameMode::Medium).place(
Rect::new(
right_column_x,
board_y + square_width + (square_width - btn_h) / 2.,
btn_w,
btn_h,
),
FontSize::Fixed(btn_font_size),
);
self.get_game_mode_button(&GameMode::Hard).place(
Rect::new(
right_column_x,
board_y + 2. * square_width + (square_width - btn_h) / 2.,
btn_w,
btn_h,
),
FontSize::Fixed(btn_font_size),
);
self.get_game_mode_button(&GameMode::Custom).place(
Rect::new(
right_column_x,
board_y + 3. * square_width + (square_width - btn_h) / 2.,
btn_w,
btn_h,
),
FontSize::Fixed(btn_font_size),
);
// Right column 2
let right_column_2_x =
right_column_x + btn_w + constants::RIGHT_COLUMN_OFFSET_MULTIPLIER * square_width;
w_button!(self, btn_generate).place(
Rect::new(
right_column_2_x,
board_y + board_width - square_width + (square_width - btn_h) / 2.,
2. * btn_w,
btn_h,
),
FontSize::Fixed(btn_font_size),
);
w_label!(self, label_pieces).place(
Rect::new(
right_column_2_x,
board_y + (square_width + btn_h) / 2.,
btn_w,
btn_h,
),
0.30 * btn_h,
&self.resources,
);
w_counter!(self, counter_pieces).place(
Rect::new(
right_column_2_x,
board_y + square_width + (2. * square_width - 2. * btn_h) / 2.,
btn_w,
2. * btn_h,
),
0.50 * btn_h,
&self.resources,
);
// Right column 3
let right_column_3_x = right_column_2_x + btn_w;
w_label!(self, label_max_age).place(
Rect::new(
right_column_2_x + btn_w,
board_y + (square_width + btn_h) / 2.,
btn_w,
btn_h,
),
0.30 * btn_h,
&self.resources,
);
w_counter!(self, counter_moves).place(
Rect::new(
right_column_3_x,
board_y + square_width + (2. * square_width - 2. * btn_h) / 2.,
btn_w,
2. * btn_h,
),
0.50 * btn_h,
&self.resources,
);
}
pub fn draw(&mut self) {
self.update_window_size();
for (_, widget) in &mut self.widgets {
widget.draw(&self.resources, &self.settings);
}
self.draw_fps();
}
fn get_game_mode_button<'a>(&'a mut self, mode: &GameMode) -> &'a mut ButtonWidget {
let id = self.game_mode_btns.get(mode).unwrap();
game::button(&mut self.widgets, *id)
}
fn draw_fps(&self) {
if self.settings.debug {
let fps = get_fps();
draw_text(
&format!("FPS: {}", fps),
10.0,
screen_height() - 20.0,
20.0,
BLACK,
);
}
}
fn update_window_size(&mut self) {
let new_height = math::clamp(
screen_height(),
constants::SCREEN_HEIGHT_MIN,
constants::SCREEN_HEIGHT_MAX,
);
let new_width = math::clamp(
screen_width(),
constants::SCREEN_WIDTH_MIN,
constants::SCREEN_WIDTH_MAX,
);
if new_height == self.window_height && new_width == self.window_width {
return;
}
self.initialize_scene(new_width, new_height);
}
}
+320
View File
@@ -0,0 +1,320 @@
use std::collections::HashMap;
use super::{Game, GameMode, Settings, constants};
use crate::{
game::{self, Command},
resources::{self, *},
widgets::*,
};
use macroquad::prelude::*;
use sol_lib::{
board::BoardState,
generator::{self, Puzzle, RandomRange},
};
impl Game {
pub async fn new() -> Self {
let resources = resources::init().await;
let game_mode = GameMode::Medium;
let settings = Settings {
volume: constants::VOLUME,
max_age: 3,
max_pieces: 2,
..Default::default()
};
let puzzle = generate_new_puzzle(game_mode, &settings);
let game_mode = GameMode::Medium;
let show_rules = false;
let mut widgets = HashMap::new();
let w_label_heading = LabelWidget::new(constants::HEADING_TEXT);
let label_heading = add_widget(&mut widgets, Widget::Label(w_label_heading));
let w_label_pieces = LabelWidget::new(constants::PIECES_LABEL_TEXT);
let label_pieces = add_widget(&mut widgets, Widget::Label(w_label_pieces));
let w_label_max_age = LabelWidget::new(constants::AGE_LABEL_TEXT);
let label_max_age = add_widget(&mut widgets, Widget::Label(w_label_max_age));
let mut w_counter_pieces =
CounterWidget::new(2, 7, settings.max_pieces, false, UiColor::Yellow);
w_counter_pieces.on_increment = |c| Command::UpdateMaxPieceCount(c.current);
w_counter_pieces.on_decrement = |c| Command::UpdateMaxPieceCount(c.current);
let counter_pieces = add_widget(&mut widgets, Widget::Counter(w_counter_pieces));
let mut w_counter_moves =
CounterWidget::new(1, 7, settings.max_age, false, UiColor::Yellow);
w_counter_moves.on_increment = |c| Command::UpdateMaxAge(c.current);
w_counter_moves.on_decrement = |c| Command::UpdateMaxAge(c.current);
let counter_moves = add_widget(&mut widgets, Widget::Counter(w_counter_moves));
let mut w_btn_reset = ButtonWidget::new(
true,
UiColor::Yellow,
constants::RESET_BUTTON_TEXT,
SoundKind::Button,
);
w_btn_reset.on_click = |_| Command::ResetBoard;
let btn_reset = add_widget(&mut widgets, Widget::Button(w_btn_reset));
let mut w_btn_next = ButtonWidget::new(
false,
UiColor::Green,
constants::NEXT_BUTTON_TEXT,
SoundKind::Button,
);
w_btn_next.on_click = |_| Command::NextPuzzle;
let btn_next = add_widget(&mut widgets, Widget::Button(w_btn_next));
let mut w_btn_rules = ButtonWidget::new(
true,
UiColor::Brown,
constants::RULES_BUTTON_TEXT,
SoundKind::Button,
);
w_btn_rules.on_click = |_| Command::ToggleRules;
let btn_rules = add_widget(&mut widgets, Widget::Button(w_btn_rules));
let mut w_btn_generate = ButtonWidget::new(
false,
UiColor::Pink,
constants::GENERATE_BUTTON_TEXT,
SoundKind::Button,
);
w_btn_generate.on_click = |_| Command::NewCustomPuzzle;
let btn_generate = add_widget(&mut widgets, Widget::Button(w_btn_generate));
let mut game_mode_btns = HashMap::new();
let mut w_game_mode_easy_btn = ButtonWidget::new(
true,
UiColor::Yellow,
constants::EASY_BUTTON_TEXT,
SoundKind::Mode,
);
w_game_mode_easy_btn.on_click = |_| Command::SwitchGameMode(GameMode::Easy);
game_mode_btns.insert(
GameMode::Easy,
add_widget(&mut widgets, Widget::Button(w_game_mode_easy_btn)),
);
let mut w_game_mode_medium_btn = ButtonWidget::new(
true,
UiColor::Yellow,
constants::MEDIUM_BUTTON_TEXT,
SoundKind::Mode,
);
w_game_mode_medium_btn.on_click = |_| Command::SwitchGameMode(GameMode::Medium);
game_mode_btns.insert(
GameMode::Medium,
add_widget(&mut widgets, Widget::Button(w_game_mode_medium_btn)),
);
let mut w_game_mode_hard_btn = ButtonWidget::new(
true,
UiColor::Yellow,
constants::HARD_BUTTON_TEXT,
SoundKind::Mode,
);
w_game_mode_hard_btn.on_click = |_| Command::SwitchGameMode(GameMode::Hard);
game_mode_btns.insert(
GameMode::Hard,
add_widget(&mut widgets, Widget::Button(w_game_mode_hard_btn)),
);
let mut w_game_mode_custom_btn = ButtonWidget::new(
true,
UiColor::Blue,
constants::CUSTOM_BUTTON_TEXT,
SoundKind::Mode,
);
w_game_mode_custom_btn.on_click = |_| Command::SwitchGameMode(GameMode::Custom);
game_mode_btns.insert(
GameMode::Custom,
add_widget(&mut widgets, Widget::Button(w_game_mode_custom_btn)),
);
let active_game_mode_btn = game_mode_btns.get(&game_mode).unwrap();
game::button(&mut widgets, *active_game_mode_btn).is_active = false;
let mut w_board = BoardWidget::new(puzzle.board.size, puzzle.board.clone());
w_board.on_board_state_changed = |s| Command::UpdateBoard(s.new_board_state);
let board = add_widget(&mut widgets, Widget::Board(w_board));
let mut w_controls = ControlsWidget::new();
w_controls.add(KeyCode::Q, Command::Quit);
w_controls.add(KeyCode::Escape, Command::HideRules);
w_controls.add(KeyCode::D, Command::ToggleDebug);
add_widget(&mut widgets, Widget::Controls(w_controls));
Game {
puzzle,
board,
resources,
settings,
game_mode,
game_mode_btns,
btn_reset,
btn_next,
btn_rules,
show_rules,
widgets,
counter_pieces,
counter_moves,
btn_generate,
label_pieces,
label_max_age,
label_heading,
..Default::default()
}
}
pub fn update(&mut self, interaction: Interaction) {
let command = self.map_to_command(interaction);
self.update_state(command);
}
fn map_to_command(&mut self, interaction: Interaction) -> Command {
for (_, widget) in &mut self.widgets {
if let Some(command) = widget.update(&interaction, &self.resources, &self.settings) {
return command;
}
}
return Command::NoInteraction;
}
fn update_state(&mut self, command: Command) {
match command {
Command::NoInteraction => (),
Command::UpdateBoard(new_board_state) => self.update_board(new_board_state),
Command::SwitchGameMode(mode) => self.switch_game_mode(mode),
Command::ToggleDebug => self.settings.debug = !self.settings.debug,
Command::HideRules => self.hide_rules(),
Command::ResetBoard => self.reset(),
Command::NextPuzzle => self.next_puzzle(),
Command::ToggleRules => self.toggle_rules(),
Command::UpdateMaxPieceCount(new_value) => self.settings.max_pieces = new_value,
Command::UpdateMaxAge(new_value) => self.settings.max_age = new_value,
Command::NewCustomPuzzle => self.new_custom_puzzle(),
Command::Quit => std::process::exit(0),
}
}
fn new_custom_puzzle(&mut self) {
if self.game_mode == GameMode::Custom {
self.next_puzzle();
}
}
fn toggle_rules(&mut self) {
self.show_rules = !self.show_rules;
w_board!(self, board).show_rules = self.show_rules;
w_button!(self, btn_rules).text = match self.show_rules {
true => constants::RULES_BUTTON_ALT_TEXT.to_string(),
false => constants::RULES_BUTTON_TEXT.to_string(),
};
}
fn hide_rules(&mut self) {
if self.show_rules {
play_sound_once(
&self.resources.sound(&SoundKind::Button),
self.settings.volume,
);
self.show_rules = false;
w_board!(self, board).show_rules = false;
w_button!(self, btn_rules).text = constants::RULES_BUTTON_TEXT.to_string();
}
}
fn switch_game_mode(&mut self, mode: GameMode) {
self.game_mode = mode;
for (mode, btn_id) in &mut self.game_mode_btns {
// The active mode button is 'disabled' to indicate it is active
let is_active = !(*mode == self.game_mode);
game::button(&mut self.widgets, *btn_id).is_active = is_active;
}
let is_custom_game_mode = self.game_mode == GameMode::Custom;
w_button!(self, btn_generate).is_active = is_custom_game_mode;
w_counter!(self, counter_pieces).set_active(is_custom_game_mode);
w_counter!(self, counter_moves).set_active(is_custom_game_mode);
self.next_puzzle();
}
fn update_board(&mut self, new_board_state: BoardState) {
match new_board_state {
BoardState::Won => {
w_button!(self, btn_next).is_active = true;
}
_ => {}
}
}
fn reset(&mut self) {
self.reset_game(Default::default());
}
fn reset_game(&mut self, options: ResetOptions) {
if options.create_new_puzzle {
self.puzzle = generate_new_puzzle(self.game_mode, &self.settings);
}
w_button!(self, btn_next).is_active = false;
w_board!(self, board).reset(&self.puzzle);
}
fn next_puzzle(&mut self) {
self.reset_game(ResetOptions {
create_new_puzzle: true,
..Default::default()
});
}
}
fn add_widget(widgets: &mut HashMap<WidgetId, Widget>, w: Widget) -> WidgetId {
let id = rand::rand() as u64;
let widget_id = WidgetId(id);
widgets.insert(widget_id, w);
widget_id
}
fn generate_new_puzzle(mode: GameMode, settings: &Settings) -> Puzzle {
let piece_count = match mode {
GameMode::Easy => 3,
GameMode::Medium => 5,
GameMode::Hard => 7,
GameMode::Custom => settings.max_pieces,
};
let max_moves_per_piece = match mode {
GameMode::Easy => 2,
GameMode::Medium => 2,
GameMode::Hard => 2,
GameMode::Custom => settings.max_age,
};
let generated = generator::generate_weighted_random(
piece_count,
100,
max_moves_per_piece,
&MacroquadRandAdapter,
);
let puzzle = generated.puzzle();
puzzle.expect("No puzzle was generated")
}
#[derive(Default)]
struct ResetOptions {
create_new_puzzle: bool,
}
pub struct MacroquadRandAdapter;
impl RandomRange for MacroquadRandAdapter {
fn gen_range(&self, min: usize, max: usize) -> usize {
rand::gen_range(min, max)
}
}
+66
View File
@@ -0,0 +1,66 @@
mod game;
mod resources;
mod widgets;
use std::collections::HashSet;
use macroquad::prelude::*;
use miniquad::date;
use game::constants;
use crate::{game::Game, widgets::Interaction};
fn window_conf() -> Conf {
let window_title = {
if cfg!(debug_assertions) {
"Debug: Puzzle Game"
} else {
constants::WINDOW_TITLE
}
};
Conf {
window_title: String::from(window_title),
fullscreen: false,
..Default::default()
}
}
#[macroquad::main(window_conf)]
async fn main() {
rand::srand(date::now() as u64);
let background_color = Color::from_rgba(196, 195, 208, 255);
let mut game = Game::new().await;
loop {
clear_background(background_color);
game.update(get_interaction());
game.draw();
next_frame().await;
}
}
fn get_interaction() -> Interaction {
let mut mouse_pressed = HashSet::new();
let mut mouse_released = HashSet::new();
let buttons = vec![MouseButton::Left, MouseButton::Right];
for btn in buttons {
if is_mouse_button_pressed(btn) {
mouse_pressed.insert(btn);
}
if is_mouse_button_released(btn) {
mouse_released.insert(btn);
}
}
let (mx, my) = mouse_position();
Interaction {
mouse_pos: Circle::new(mx, my, 0.),
keys_released: get_keys_released(),
mouse_pressed,
mouse_released,
}
}
+137
View File
@@ -0,0 +1,137 @@
use macroquad::{
audio::{self, Sound},
prelude::*,
};
use sol_lib::board::piece::{Piece, PieceKind};
use std::collections::HashMap;
#[derive(Default)]
pub struct Resources {
texture_pieces: Option<Texture2D>,
texture_rects_pieces: HashMap<PieceKind, Rect>,
texture_rects_inactive_pieces: HashMap<PieceKind, Rect>,
sounds: HashMap<SoundKind, Sound>,
font: Option<Font>,
}
pub struct Texture<'a> {
pub texture: &'a Texture2D,
pub texture_rect: Rect,
}
macro_rules! load_sound {
($file_name:expr) => {
audio::load_sound_from_bytes(include_bytes!($file_name))
.await
.unwrap()
};
}
pub async fn init() -> Resources {
// SOUNDS
let click = load_sound!("../assets/click.wav");
let win = load_sound!("../assets/win.wav");
let loss = load_sound!("../assets/loss.wav");
let button = load_sound!("../assets/button.wav");
let mode = load_sound!("../assets/mode.wav");
let mut sounds = HashMap::new();
sounds.insert(SoundKind::Click, click);
sounds.insert(SoundKind::Win, win);
sounds.insert(SoundKind::Loss, loss);
sounds.insert(SoundKind::Button, button);
sounds.insert(SoundKind::Mode, mode);
// FONT
let font_ttf = include_bytes!("../assets/Junction-regular.otf");
let font = load_ttf_font_from_bytes(font_ttf).expect("Failed to load font");
// TEXTURES
let texture_pieces_bytes = include_bytes!("../assets/pieces.png");
let texture_pieces = Texture2D::from_file_with_format(&texture_pieces_bytes[..], None);
texture_pieces.set_filter(FilterMode::Nearest);
build_textures_atlas();
let texture_rects_pieces = generate_texture_rects_pieces(true);
let texture_rects_inactive_pieces = generate_texture_rects_pieces(false);
Resources {
texture_pieces: Some(texture_pieces),
texture_rects_pieces,
texture_rects_inactive_pieces,
sounds,
font: Some(font),
}
}
fn generate_texture_rects_pieces(active: bool) -> HashMap<PieceKind, Rect> {
let mut texture_rects_pieces = HashMap::new();
texture_rects_pieces.insert(PieceKind::Pawn, piece_texture_rect(PieceKind::Pawn, active));
texture_rects_pieces.insert(
PieceKind::Knight,
piece_texture_rect(PieceKind::Knight, active),
);
texture_rects_pieces.insert(
PieceKind::Bishop,
piece_texture_rect(PieceKind::Bishop, active),
);
texture_rects_pieces.insert(PieceKind::Rook, piece_texture_rect(PieceKind::Rook, active));
texture_rects_pieces.insert(
PieceKind::Queen,
piece_texture_rect(PieceKind::Queen, active),
);
texture_rects_pieces.insert(PieceKind::King, piece_texture_rect(PieceKind::King, active));
texture_rects_pieces
}
fn piece_texture_rect(piece: PieceKind, active: bool) -> Rect {
let index = match piece {
PieceKind::Pawn => 0,
PieceKind::Knight => 1,
PieceKind::Bishop => 2,
PieceKind::Rook => 3,
PieceKind::Queen => 4,
PieceKind::King => 5,
};
let color = match active {
true => 1,
false => 0,
};
Rect::new(index as f32 * 128.0, color as f32 * 128.0, 128.0, 128.0)
}
#[derive(Eq, Copy, Clone, PartialEq, Hash)]
pub enum SoundKind {
Click,
Win,
Loss,
Button,
Mode,
}
impl Resources {
pub fn get_piece_texture<'a>(&'a self, piece: &Piece) -> Texture<'a> {
let texture = self.texture_pieces.as_ref().unwrap();
let texture_rects_lookup = if piece.active {
&self.texture_rects_pieces
} else {
&self.texture_rects_inactive_pieces
};
let texture_rect = texture_rects_lookup.get(&piece.kind).unwrap();
Texture {
texture: texture,
texture_rect: *texture_rect,
}
}
pub fn font(&self) -> &Font {
self.font.as_ref().unwrap()
}
pub fn sound(&self, kind: &SoundKind) -> &Sound {
self.sounds.get(kind).unwrap()
}
}
+226
View File
@@ -0,0 +1,226 @@
use std::collections::{HashMap, HashSet};
mod board;
mod button;
mod controls;
mod counter;
mod label;
pub use board::*;
pub use button::*;
pub use controls::*;
pub use counter::*;
pub use label::*;
use macroquad::{
audio::{self, Sound},
prelude::*,
};
use quad_snd::PlaySoundParams;
use crate::{
game::{Command, Settings},
resources::Resources,
};
pub struct Interaction {
pub mouse_pos: Circle,
pub keys_released: HashSet<KeyCode>,
pub mouse_pressed: HashSet<MouseButton>,
pub mouse_released: HashSet<MouseButton>,
}
#[derive(Eq, PartialEq, Copy, Clone, Hash, Default)]
pub struct WidgetId(pub u64);
pub enum Widget {
Label(LabelWidget),
Counter(CounterWidget),
Button(ButtonWidget),
Board(BoardWidget),
Controls(ControlsWidget),
}
impl Widget {
pub fn draw(&mut self, resources: &Resources, settings: &Settings) {
match self {
Widget::Label(label) => label.draw(&resources),
Widget::Counter(counter) => counter.draw(&resources),
Widget::Button(btn) => btn.draw(&resources),
Widget::Board(board) => board.draw(&resources, &settings),
Widget::Controls(_) => (),
}
}
pub fn update(
&mut self,
interaction: &Interaction,
resources: &Resources,
settings: &Settings,
) -> Option<Command> {
match self {
Widget::Label(_) => None,
Widget::Counter(c) => handle_counter_input(interaction, c, resources, settings),
Widget::Button(b) => handle_button_input(interaction, b, resources, settings),
Widget::Board(b) => handle_board_input(interaction, b, resources, settings),
Widget::Controls(c) => c.handle_input(interaction),
}
}
}
fn handle_board_input(
interaction: &Interaction,
b: &mut BoardWidget,
resources: &Resources,
settings: &Settings,
) -> Option<Command> {
let board_interaction = b.handle_input(interaction, resources, settings);
let Some(board_interaction) = board_interaction else {
return None;
};
Some((b.on_board_state_changed)(board_interaction))
}
fn handle_counter_input(
interaction: &Interaction,
counter: &mut CounterWidget,
resources: &Resources,
settings: &Settings,
) -> Option<Command> {
let counter_interaction = counter.handle_input(interaction, resources, settings);
let Some(interaction_type) = counter_interaction.interaction_type else {
return None;
};
let command = match interaction_type {
CounterInteractionType::Increment => (counter.on_increment)(counter_interaction),
CounterInteractionType::Decrement => (counter.on_decrement)(counter_interaction),
};
Some(command)
}
fn handle_button_input(
interaction: &Interaction,
button: &mut ButtonWidget,
resources: &Resources,
settings: &Settings,
) -> Option<Command> {
let Some(ButtonInteraction { clicked: true }) =
button.handle_input(interaction, resources, settings)
else {
return None;
};
return Some((button.on_click)(button));
}
macro_rules! get_widget_fn {
($name:ident, $ret:ty, $variant:path) => {
pub fn $name(widgets: &mut HashMap<WidgetId, Widget>, id: WidgetId) -> &mut $ret {
match widgets.get_mut(&id) {
Some(w) => match w {
$variant(v) => return v,
_ => panic!("Widget with id {} not found", id.0),
},
None => panic!("Widget with id {} not found", id.0),
}
}
};
}
get_widget_fn!(label, LabelWidget, Widget::Label);
get_widget_fn!(button, ButtonWidget, Widget::Button);
get_widget_fn!(counter, CounterWidget, Widget::Counter);
get_widget_fn!(board, BoardWidget, Widget::Board);
macro_rules! widget_accessor {
($name:ident, $fn:ident) => {
macro_rules! $name {
($this:expr, $widget_id:ident) => {
game::$fn(&mut $this.widgets, $this.$widget_id)
};
}
pub(crate) use $name;
};
}
widget_accessor!(w_button, button);
widget_accessor!(w_label, label);
widget_accessor!(w_counter, counter);
widget_accessor!(w_board, board);
#[allow(unused)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UiColor {
Grey,
Green,
Pink,
Brown,
Yellow,
Blue,
}
impl UiColor {
pub fn to_bg_color(&self) -> Color {
match self {
UiColor::Grey => Color::from_rgba(140, 140, 140, 200),
UiColor::Green => Color::from_rgba(16, 60, 50, 200),
UiColor::Pink => Color::from_rgba(255, 60, 60, 200),
UiColor::Brown => Color::from_rgba(123, 61, 35, 200),
UiColor::Yellow => Color::from_rgba(242, 230, 190, 200),
UiColor::Blue => Color::from_rgba(47, 85, 172, 200),
}
}
pub fn to_fg_color(&self) -> Color {
match self {
UiColor::Grey => Color::from_rgba(255, 255, 255, 200),
UiColor::Green => Color::from_rgba(255, 255, 255, 200),
UiColor::Pink => Color::from_rgba(255, 255, 255, 200),
UiColor::Brown => Color::from_rgba(255, 255, 255, 200),
UiColor::Yellow => Color::from_rgba(0, 0, 0, 200),
UiColor::Blue => Color::from_rgba(255, 255, 255, 200),
}
}
pub fn to_shadow_color(&self) -> Color {
let bg_color = self.to_bg_color();
Color::from_rgba(
(bg_color.r * 255.) as u8,
(bg_color.g * 255.) as u8,
(bg_color.b * 255.) as u8,
100,
)
}
}
pub fn draw_shadow(rect: &Rect, shadow_width: f32) {
let shadow_color = Color::new(0., 0., 0., 0.8);
draw_rectangle(
rect.x + rect.w,
rect.y + shadow_width,
shadow_width,
rect.h,
shadow_color,
);
draw_rectangle(
rect.x + shadow_width,
rect.y + rect.h,
rect.w - shadow_width,
shadow_width,
shadow_color,
);
}
pub fn play_sound_once(sound: &Sound, volume: f32) {
audio::play_sound(
sound,
PlaySoundParams {
looped: false,
volume,
},
);
}
+447
View File
@@ -0,0 +1,447 @@
use core::fmt;
use std::fmt::{Display, Formatter};
use crate::{
game::{Command, Settings, constants},
resources::{Resources, SoundKind},
widgets::*,
};
use sol_lib::{
board::{Board, BoardState, piece::Piece},
generator::Puzzle,
};
pub struct BoardWidget {
pub num_squares: usize, // this is a constant = 4 for now.
pub show_rules: bool,
pub on_board_state_changed: fn(BoardInteraction) -> Command,
current_board: Board,
square_width: f32,
rules_font_size: f32,
board_rect: Rect,
squares: Vec<GameSquare>,
state: GameState,
}
pub struct BoardInteraction {
pub new_board_state: BoardState,
}
impl BoardWidget {
pub fn new(num_squares: usize, current_board: Board) -> BoardWidget {
let mut board = BoardWidget::default();
board.num_squares = num_squares;
board.current_board = current_board;
board
}
pub fn place(&mut self, square_width: f32, board_rect: Rect, rules_font_size: f32) {
self.square_width = square_width;
self.board_rect = board_rect;
self.rules_font_size = rules_font_size;
let dark = UiColor::Brown.to_bg_color();
let light = UiColor::Yellow.to_bg_color();
let mut rects = Vec::new();
for i in 0..self.num_squares {
for j in 0..self.num_squares {
let x_eff = self.board_rect.x + (i as f32 * self.square_width);
let y_eff = self.board_rect.y + (j as f32 * self.square_width);
let rect = Rect::new(x_eff, y_eff, self.square_width, self.square_width);
let color = match (i + j) % 2 {
1 => dark,
_ => light,
};
rects.push(GameSquare {
rect,
color,
i,
j,
is_source: false,
is_target: false,
is_previous_target: false,
});
}
}
self.squares = rects;
}
pub fn draw(&self, resources: &Resources, settings: &Settings) {
let board_shadow_width = constants::BOARD_SHADOW_MULTIPLIER * self.square_width;
draw_shadow(&self.board_rect, board_shadow_width);
if self.show_rules {
draw_rectangle(
self.board_rect.x,
self.board_rect.y,
self.board_rect.w,
self.board_rect.h,
UiColor::Yellow.to_bg_color(),
);
let rules = "\
Every move should be a \n\
capture. Win when only \n\
one piece is left.\n\
Age: Each piece can only \n\
move 'age' times";
let measurement = measure_text(
rules,
Some(resources.font()),
self.rules_font_size as u16,
1.0,
);
let draw_text_params = TextParams {
font_size: self.rules_font_size as u16,
font: Some(resources.font()),
color: UiColor::Brown.to_bg_color(),
..Default::default()
};
draw_multiline_text_ex(
rules,
self.board_rect.x + 0.5 * self.square_width,
self.board_rect.y + 0.4 * (self.board_rect.h - measurement.height)
- 2. * measurement.offset_y,
Some(2.),
draw_text_params,
);
return;
}
let sprite_size = constants::BOARD_PIECE_WIDTH_MULTIPLIER * self.square_width;
let mut selected_square = None;
self.squares.iter().for_each(|square| {
let color = match square.is_source {
true => square.color,
false => match square.is_target {
true => UiColor::Pink.to_shadow_color(),
false => square.color,
},
};
draw_rectangle(
square.rect.x,
square.rect.y,
square.rect.w,
square.rect.h,
color,
);
if let Some(p) = &self.current_board.cells[square.i][square.j] {
let offset = (square.rect.w - sprite_size) / 2.0;
let texture_params = piece_draw_texture_params(&p, sprite_size, resources);
if !square.is_source {
draw_texture_ex(
texture_params.texture,
square.rect.x + offset,
square.rect.y + offset,
WHITE,
texture_params.draw_text_params,
);
} else {
selected_square = Some(square);
}
}
});
if let Some(selected_square) = selected_square {
if let Some(p) = self.current_board.cells[selected_square.i][selected_square.j] {
let texture_params = piece_draw_texture_params(&p, sprite_size, resources);
draw_texture_ex(
texture_params.texture,
mouse_position().0 - sprite_size / 2.0,
mouse_position().1 - sprite_size / 2.0,
WHITE,
texture_params.draw_text_params,
);
}
}
if settings.debug {
self.draw_debug();
}
}
pub fn handle_input(
&mut self,
interaction: &Interaction,
resources: &Resources,
settings: &Settings,
) -> Option<BoardInteraction> {
let mouse_pos = interaction.mouse_pos;
if mouse_pos.overlaps_rect(&self.board_rect) == false {
return None;
}
let old_board_state = self.current_board.state;
if is_mouse_button_released(MouseButton::Left) {
let current_state = self.state.clone();
let new_state = match current_state {
GameState::SelectSource(previous_target) => {
self.handle_select_source(mouse_pos, previous_target)
}
GameState::SelectTarget(source) => {
let next = self.handle_select_target(mouse_pos, source, resources, settings);
if let GameState::SelectTarget(_) = next {
self.reset_squares();
GameState::SelectSource(None)
} else {
next
}
}
GameState::GameOver(previous_target) => GameState::GameOver(previous_target),
};
self.state = new_state;
}
if is_mouse_button_pressed(MouseButton::Left) {
let current_state = self.state.clone();
let new_state = match current_state {
GameState::SelectSource(previous_target) => {
self.handle_select_source(mouse_pos, previous_target)
}
GameState::SelectTarget(source) => GameState::SelectTarget(source),
GameState::GameOver(previous_target) => GameState::GameOver(previous_target),
};
self.state = new_state;
}
if old_board_state == self.current_board.state {
return None;
}
let board_state = self.current_board.state;
return Some(BoardInteraction {
new_board_state: board_state,
});
}
fn get(&mut self, i: usize, j: usize) -> &mut GameSquare {
&mut self.squares[i * self.num_squares + j]
}
fn draw_debug(&self) {
let mut debug_lines = vec![];
let (mx, my) = mouse_position();
let hover_square = self.squares.iter().find(|s| {
let c = Circle::new(mx, my, 0.0);
if c.overlaps_rect(&s.rect) {
return true;
}
return false;
});
debug_lines.push(format!("Game State: {}", self.state));
debug_lines.push(format!("Board State: {}", self.current_board.state));
if let Some(hover_square) = hover_square {
debug_lines.push(format!("Hover: [ {}, {} ]", hover_square.i, hover_square.j));
}
self.add_debug_info(debug_lines);
}
fn add_debug_info(&self, lines: Vec<String>) {
let mut y = 20.0;
for line in lines {
draw_text(&line, 10.0, y, 20.0, BLACK);
y += 25.0;
}
}
pub fn reset(&mut self, puzzle: &Puzzle) {
self.current_board = puzzle.board.clone();
self.state = GameState::SelectSource(None);
self.reset_squares();
}
fn handle_select_source(
&mut self,
mouse_pos: Circle,
previous_target: Option<(usize, usize)>,
) -> GameState {
self.reset_squares();
let mut selected = None;
for square in &mut self.squares {
if mouse_pos.overlaps_rect(&square.rect) {
if let Some(_) = self.current_board.cells[square.i][square.j] {
selected = Some((square.i, square.j));
}
}
}
if let Some((i, j)) = selected {
self.get(i, j).is_source = true;
let mut target_squares = vec![];
for m in self.current_board.legal_moves.iter() {
if m.from.file == i && m.from.rank == j {
target_squares.push((m.to.file, m.to.rank));
}
}
for (i, j) in target_squares {
self.get(i, j).is_target = true;
}
return GameState::SelectTarget(selected.unwrap());
}
if let Some((i, j)) = previous_target {
self.get(i, j).is_previous_target = true;
}
return GameState::SelectSource(None);
}
fn handle_select_target(
&mut self,
mouse_pos: Circle,
source: (usize, usize),
resources: &Resources,
settings: &Settings,
) -> GameState {
let mut selected = None;
for square in &mut self.squares {
if mouse_pos.overlaps_rect(&square.rect) {
if let Some(_) = self.current_board.cells[square.i][square.j] {
selected = Some((square.i, square.j));
}
}
}
let (s_x, s_y) = source;
let Some((x, y)) = selected else {
self.get(s_x, s_y).is_source = true;
return GameState::SelectTarget(source);
};
if x == s_x && y == s_y {
self.get(s_x, s_y).is_source = true;
return GameState::SelectTarget(source);
}
let mut is_legal = false;
if self.get(x, y).is_target {
is_legal = true;
}
if is_legal {
let m = self.current_board.legal_moves.iter().find(|m| {
m.from.file == s_x && m.from.rank == s_y && m.to.file == x && m.to.rank == y
});
let m = m.expect("legal move should be found");
self.current_board.make_move(m.clone());
if self.current_board.state == BoardState::Won
|| self.current_board.state == BoardState::Lost
{
self.reset_squares();
if self.current_board.state == BoardState::Won {
play_sound_once(resources.sound(&SoundKind::Win), settings.volume);
} else {
play_sound_once(resources.sound(&SoundKind::Loss), settings.volume);
}
return GameState::GameOver((x, y));
}
self.reset_squares();
self.get(x, y).is_target = true;
play_sound_once(resources.sound(&SoundKind::Click), settings.volume);
return GameState::SelectSource(Some((x, y)));
}
self.reset_squares();
return GameState::SelectSource(None);
}
fn reset_squares(&mut self) {
for i in 0..self.num_squares {
for j in 0..self.num_squares {
self.get(i, j).is_source = false;
self.get(i, j).is_target = false;
}
}
}
}
struct GameSquare {
rect: Rect,
color: Color,
is_source: bool,
is_target: bool,
is_previous_target: bool,
i: usize,
j: usize,
}
#[derive(Copy, Clone)]
pub enum GameState {
SelectSource(Option<(usize, usize)>),
SelectTarget((usize, usize)),
GameOver((usize, usize)),
}
impl Default for GameState {
fn default() -> Self {
GameState::SelectSource(None)
}
}
impl Display for GameState {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
GameState::SelectSource(Some(x)) => write!(f, "Select Source [ {}, {} ]", x.0, x.1),
GameState::SelectSource(None) => write!(f, "Select Source [ ]"),
GameState::SelectTarget(x) => write!(f, "Select Target [ {}, {} ]", x.0, x.1),
GameState::GameOver(x) => write!(f, "Game Over [ {}, {} ]", x.0, x.1),
}
}
}
fn piece_draw_texture_params<'a>(
piece: &Piece,
sprite_size: f32,
resources: &'a Resources,
) -> PieceDrawTextureParams<'a> {
let texture = resources.get_piece_texture(&piece);
let dtp = DrawTextureParams {
source: Some(Rect::new(
texture.texture_rect.x,
texture.texture_rect.y,
texture.texture_rect.w,
texture.texture_rect.h,
)),
dest_size: Some(Vec2::new(sprite_size, sprite_size)),
..DrawTextureParams::default()
};
PieceDrawTextureParams {
texture: texture.texture,
draw_text_params: dtp,
}
}
struct PieceDrawTextureParams<'a> {
texture: &'a Texture2D,
draw_text_params: DrawTextureParams,
}
impl Default for BoardWidget {
fn default() -> Self {
Self {
on_board_state_changed: |_| Command::NoInteraction,
num_squares: Default::default(),
show_rules: Default::default(),
current_board: Default::default(),
square_width: Default::default(),
rules_font_size: Default::default(),
board_rect: Default::default(),
squares: Default::default(),
state: Default::default(),
}
}
}
+188
View File
@@ -0,0 +1,188 @@
use crate::{
game::{Command, Settings},
resources::{Resources, SoundKind},
widgets::*,
};
pub struct ButtonWidget {
pub is_active: bool,
pub text: String,
pub color: UiColor,
is_down: bool,
rect: Rect,
shadow_width: f32,
sound: SoundKind,
font_size: FontSize,
pub on_click: fn(&mut ButtonWidget) -> Command,
}
pub struct ButtonInteraction {
pub clicked: bool,
}
impl ButtonInteraction {
fn clicked() -> Self {
Self { clicked: true }
}
}
#[derive(Copy, Clone)]
pub enum FontSize {
Fixed(f32),
Fill,
}
impl ButtonWidget {
pub fn new(is_active: bool, color: UiColor, text: impl Into<String>, sound: SoundKind) -> Self {
Self {
is_active,
text: text.into(),
color,
shadow_width: 5.0,
sound,
..Default::default()
}
}
pub fn place(&mut self, rect: Rect, font_size: FontSize) {
self.rect = rect;
self.font_size = font_size;
}
pub fn draw(&self, resources: &Resources) {
self.draw_button(&self.color);
self.draw_label(&self.text, resources, &self.color);
}
fn draw_button(&self, color: &UiColor) {
let bg_color = match self.is_active {
true => color.to_bg_color(),
false => color.to_shadow_color(),
};
let button_draw_offset = self.get_button_draw_offset();
draw_rectangle(
self.rect.x + button_draw_offset,
self.rect.y + button_draw_offset,
self.rect.w,
self.rect.h,
bg_color,
);
self.draw_shadow();
}
fn draw_shadow(&self) {
if !self.is_active {
return;
}
if self.is_down {
return;
}
draw_shadow(&self.rect, self.shadow_width);
}
fn draw_label(&self, text: &str, resources: &Resources, color: &UiColor) {
let font_color = match self.is_active {
true => color.to_fg_color(),
false => Color::from_rgba(100, 100, 100, 255),
};
let font = resources.font();
let font_size = match self.font_size {
FontSize::Fixed(s) => s as u16,
FontSize::Fill => {
let target_w = self.rect.w * 0.9;
let target_h = self.rect.h;
let mut font_size = 22;
loop {
font_size -= 2;
let dims = measure_text(&text, Some(&font), font_size as u16, 1.0);
if dims.width <= target_w && dims.height <= target_h {
break;
}
}
font_size
}
};
let dims = measure_text(&text, Some(&font), font_size, 1.0);
let button_draw_offset = self.get_button_draw_offset();
let text_params = TextParams {
font_size,
color: font_color,
font: Some(&font),
..Default::default()
};
draw_text_ex(
&text,
self.rect.x + (self.rect.w - dims.width) * 0.5 + button_draw_offset,
self.rect.y + (self.rect.h - dims.height) * 0.5 + dims.offset_y + button_draw_offset,
text_params,
);
}
fn get_button_draw_offset(&self) -> f32 {
let button_pressed_correction = match self.is_down {
true => self.shadow_width,
false => match self.is_active {
true => 0.0,
false => self.shadow_width,
},
};
button_pressed_correction
}
pub fn handle_input(
&mut self,
interaction: &Interaction,
resources: &Resources,
settings: &Settings,
) -> Option<ButtonInteraction> {
if !self.is_active {
self.is_down = false;
return None;
}
let mouse = interaction.mouse_pos;
if interaction.mouse_pressed.contains(&MouseButton::Left) {
if mouse.overlaps_rect(&self.rect) {
self.is_down = true;
return None;
}
}
if interaction.mouse_released.contains(&MouseButton::Left) {
if mouse.overlaps_rect(&self.rect) {
let click_sound = resources.sound(&self.sound);
let volume = settings.volume;
play_sound_once(&click_sound, volume);
self.is_down = false;
return Some(ButtonInteraction::clicked());
}
self.is_down = false;
}
None
}
}
impl Default for ButtonWidget {
fn default() -> Self {
Self {
is_active: true,
is_down: Default::default(),
rect: Default::default(),
shadow_width: Default::default(),
color: UiColor::Grey,
text: String::new(),
sound: SoundKind::Click,
font_size: FontSize::Fill,
on_click: |_| Command::NoInteraction,
}
}
}
+31
View File
@@ -0,0 +1,31 @@
use std::collections::HashMap;
use macroquad::input::KeyCode;
use crate::{game::Command, widgets::Interaction};
pub struct ControlsWidget {
key_release_bindings: HashMap<KeyCode, Command>,
}
impl ControlsWidget {
pub fn new() -> Self {
Self {
key_release_bindings: HashMap::new(),
}
}
pub fn add(&mut self, key: KeyCode, command: Command) {
self.key_release_bindings.insert(key, command);
}
pub fn handle_input(&self, interaction: &Interaction) -> Option<Command> {
for (key, command) in &self.key_release_bindings {
if interaction.keys_released.contains(&key) {
return Some(*command);
}
}
None
}
}
+174
View File
@@ -0,0 +1,174 @@
use crate::{
game::{Command, Settings},
resources::{Resources, SoundKind},
widgets::*,
};
use macroquad::prelude::*;
pub struct CounterWidget {
visual_rect: Rect,
increment: ButtonWidget,
decrement: ButtonWidget,
pub on_increment: fn(CounterWidgetInteraction) -> Command,
pub on_decrement: fn(CounterWidgetInteraction) -> Command,
min: u32,
max: u32,
current: u32,
font_size: f32,
visual_dims: TextDimensions,
increment_dims: TextDimensions,
decrement_dims: TextDimensions,
is_active: bool,
}
pub struct CounterWidgetInteraction {
pub current: u32,
pub interaction_type: Option<CounterInteractionType>,
}
#[derive(Copy, Clone)]
pub enum CounterInteractionType {
Increment,
Decrement,
}
impl CounterWidgetInteraction {
fn decrement(current: u32) -> Self {
Self {
current,
interaction_type: Some(CounterInteractionType::Decrement),
}
}
fn increment(current: u32) -> Self {
Self {
current,
interaction_type: Some(CounterInteractionType::Increment),
}
}
fn not_interacted(current: u32) -> Self {
Self {
current,
interaction_type: None,
}
}
}
impl CounterWidget {
pub fn new(min: u32, max: u32, current: u32, is_active: bool, color: UiColor) -> Self {
let increment = ButtonWidget::new(is_active, color, "+", SoundKind::Click);
let decrement = ButtonWidget::new(is_active, color, "-", SoundKind::Click);
Self {
min,
max,
current,
is_active,
increment,
decrement,
..Default::default()
}
}
pub fn set_active(&mut self, is_active: bool) {
self.is_active = is_active;
self.increment.is_active = is_active;
self.decrement.is_active = is_active;
}
pub fn place(&mut self, total_area: Rect, font_size: f32, resources: &Resources) {
let margin_y = 0.05 * total_area.h;
let btn_h = total_area.h * 0.3;
let btn_w = total_area.w * 0.9;
let btn_x = total_area.x + (total_area.w - btn_w) / 2.;
let increment_y = total_area.y + margin_y;
let visual_y = increment_y + btn_h + margin_y;
let decrement_y = visual_y + btn_h + margin_y;
self.visual_rect = Rect::new(btn_x, visual_y, btn_w, btn_h);
let increment_rect = Rect::new(btn_x, increment_y, btn_w, btn_h);
self.increment.place(increment_rect, FontSize::Fill);
let decrement_rect = Rect::new(btn_x, decrement_y, btn_w, btn_h);
self.decrement.place(decrement_rect, FontSize::Fill);
self.increment_dims = measure_text("+", Some(resources.font()), font_size as u16, 1.0);
self.decrement_dims = measure_text("-", Some(resources.font()), font_size as u16, 1.0);
self.visual_dims = measure_text("0", Some(resources.font()), font_size as u16, 1.0);
self.font_size = font_size;
}
pub fn handle_input(
&mut self,
interaction: &Interaction,
resources: &Resources,
settings: &Settings,
) -> CounterWidgetInteraction {
if let Some(interaction) = self
.increment
.handle_input(&interaction, &resources, &settings)
{
if interaction.clicked {
if self.current < self.max {
self.current += 1;
return CounterWidgetInteraction::increment(self.current);
}
}
}
if let Some(interaction) = self
.decrement
.handle_input(&interaction, &resources, &settings)
{
if interaction.clicked {
if self.current > self.min {
self.current -= 1;
return CounterWidgetInteraction::decrement(self.current);
}
}
}
return CounterWidgetInteraction::not_interacted(self.current);
}
pub fn draw(&mut self, resources: &Resources) {
self.increment.draw(resources);
self.decrement.draw(resources);
let font_size = self.font_size as u16;
draw_text_ex(
&self.current.to_string(),
self.visual_rect.x + (self.visual_rect.w - self.visual_dims.width) / 2.,
self.visual_rect.y + (self.visual_rect.h + self.font_size) / 2.,
TextParams {
font: Some(resources.font()),
font_size,
color: UiColor::Yellow.to_fg_color(),
..Default::default()
},
);
}
}
impl Default for CounterWidget {
fn default() -> Self {
Self {
on_increment: |_| Command::NoInteraction,
on_decrement: |_| Command::NoInteraction,
visual_rect: Default::default(),
increment: Default::default(),
decrement: Default::default(),
min: Default::default(),
max: Default::default(),
current: Default::default(),
font_size: Default::default(),
visual_dims: Default::default(),
increment_dims: Default::default(),
decrement_dims: Default::default(),
is_active: Default::default(),
}
}
}
+43
View File
@@ -0,0 +1,43 @@
use macroquad::prelude::*;
use crate::resources::Resources;
#[derive(Default)]
pub struct LabelWidget {
rect: Rect,
font_size: f32,
text_dimensions: TextDimensions,
pub text: String,
}
impl LabelWidget {
pub fn new(text: &str) -> Self {
Self {
text: text.to_string(),
..Default::default()
}
}
pub fn place(&mut self, rect: Rect, font_size: f32, resources: &Resources) {
self.rect = rect;
self.font_size = font_size;
self.text_dimensions =
measure_text(&self.text, Some(resources.font()), font_size as u16, 1.0);
}
pub fn draw(&self, resources: &Resources) {
let draw_text_params = TextParams {
font: Some(&resources.font()),
font_size: self.font_size as u16,
color: BLACK,
..Default::default()
};
draw_text_ex(
&self.text,
self.rect.x,
self.rect.y + self.text_dimensions.offset_y,
draw_text_params,
);
}
}
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "sol_lib"
version = "0.0.1"
edition = "2024"
[dev-dependencies]
rand = { workspace = true }
[profile.release]
opt-level = 's'
lto = true
codegen-units = 1
panic = 'abort'
strip = true
+298 -71
View File
@@ -4,50 +4,91 @@ pub mod errors;
pub mod piece; pub mod piece;
pub mod square; pub mod square;
use std::{collections::HashSet, mem}; use core::fmt;
use std::{
collections::HashSet,
fmt::{Display, Formatter},
mem,
};
use cmove::CMove; use cmove::CMove;
use constants::BOARD_SIZE; use constants::BOARD_SIZE;
use errors::SError; use errors::SError;
use piece::Piece; use piece::PieceKind;
use square::{Square, SquarePair}; use square::{Square, SquarePair};
#[derive(Clone)] use crate::{board::piece::Piece, generator::Puzzle};
#[derive(Clone, Default)]
pub struct Board { pub struct Board {
pub cells: [[Option<Piece>; BOARD_SIZE]; BOARD_SIZE], pub cells: [[Option<Piece>; BOARD_SIZE]; BOARD_SIZE],
pub legal_moves: HashSet<CMove>, pub legal_moves: HashSet<CMove>,
pub game_state: GameState, pub state: BoardState,
pub id: String,
pub size: usize,
pieces_remaining: u8, pieces_remaining: u8,
max_moves_per_piece: u32,
} }
#[derive(PartialEq, Eq, Debug, Clone)] #[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub enum GameState { pub enum BoardState {
NotStarted, NotStarted,
InProgress, InProgress,
Lost, Lost,
Won, Won,
} }
impl Default for BoardState {
fn default() -> Self {
BoardState::NotStarted
}
}
pub struct BoardOptions {
pub max_moves_per_piece: u32,
}
impl Board { impl Board {
pub fn new() -> Self { pub fn new() -> Self {
let options = BoardOptions {
max_moves_per_piece: 2,
};
Board::create(options)
}
pub fn create(options: BoardOptions) -> Self {
let cells = [[None; BOARD_SIZE]; BOARD_SIZE];
let id = Board::encode(cells);
Board { Board {
cells: [[None; BOARD_SIZE]; BOARD_SIZE], cells,
legal_moves: HashSet::new(), legal_moves: HashSet::new(),
id,
pieces_remaining: 0, pieces_remaining: 0,
game_state: GameState::NotStarted, state: BoardState::NotStarted,
size: BOARD_SIZE,
max_moves_per_piece: options.max_moves_per_piece,
} }
} }
pub fn from_id(board_id: u128) -> Result<Self, SError> { pub fn from_id(board_id: &str) -> Result<Self, SError> {
// TODO: validate board_id before decode
let mut board_id_bytes = [0; 8];
board_id_bytes.copy_from_slice(board_id.as_bytes());
let mut working_bytes_slice = [0; 6];
b64_decode_exact_48(&board_id_bytes, &mut working_bytes_slice);
let mut working_bytes = [0; 8];
working_bytes[2..].copy_from_slice(&working_bytes_slice);
let mut working = u64::from_be_bytes(working_bytes);
let mut board = Board::new(); let mut board = Board::new();
let mut working = board_id; let mask = 0b111;
for i in (0..BOARD_SIZE).rev() { for i in (0..BOARD_SIZE).rev() {
for j in (0..BOARD_SIZE).rev() { for j in (0..BOARD_SIZE).rev() {
let mask = 0b111; let piece_kind = Board::get_piece_from_encoding((working & mask) as u8);
let piece = Board::get_piece_from_encoding((working & mask) as u8);
working = working >> 3; working = working >> 3;
let piece = piece?; let piece_kind = piece_kind?;
board.set(Square::new(i, j, piece)); board.set(Square::new(i, j, Piece::from_kind(piece_kind)));
} }
} }
Ok(board) Ok(board)
@@ -63,18 +104,18 @@ impl Board {
for r in 0..BOARD_SIZE { for r in 0..BOARD_SIZE {
for f in 0..BOARD_SIZE { for f in 0..BOARD_SIZE {
let c = chars.next().unwrap(); let c = chars.next().unwrap();
let piece = match c { let piece_kind = match c {
'K' => Piece::King, 'K' | 'k' => PieceKind::King,
'Q' => Piece::Queen, 'Q' | 'q' => PieceKind::Queen,
'B' => Piece::Bishop, 'B' | 'b' => PieceKind::Bishop,
'N' => Piece::Knight, 'N' | 'n' => PieceKind::Knight,
'R' => Piece::Rook, 'R' | 'r' => PieceKind::Rook,
'P' => Piece::Pawn, 'P' | 'p' => PieceKind::Pawn,
'.' => continue, '.' => continue,
_ => return Err(SError::InvalidBoard), _ => return Err(SError::InvalidBoard),
}; };
let square = Square::new(f, r, Some(piece)); let square = Square::new(f, r, Some(Piece::new(piece_kind)));
board.set(square); board.set(square);
} }
} }
@@ -109,7 +150,14 @@ impl Board {
return None; return None;
} }
let from_piece = mem::replace(&mut self.cells[mv.from.file][mv.from.rank], None); let mut from_piece = mem::replace(&mut self.cells[mv.from.file][mv.from.rank], None);
if let Some(p) = &mut from_piece {
p.moves_made += 1;
if p.moves_made >= self.max_moves_per_piece {
p.active = false;
}
}
self.cells[mv.to.file][mv.to.rank] = from_piece; self.cells[mv.to.file][mv.to.rank] = from_piece;
self.pieces_remaining -= 1; self.pieces_remaining -= 1;
@@ -131,21 +179,86 @@ impl Board {
pub fn pretty_print(&self) { pub fn pretty_print(&self) {
println!("{}", self.print(true)); println!("{}", self.print(true));
println!("{:^40}\n", format!("id: {}", self.id())); // println!("{:^40}\n", format!("id: {:#018x}", self.id()));
println!("{:^40}\n", format!("id: {}", self.id));
} }
pub fn id(&self) -> u128 { pub fn solve(&self) -> Puzzle {
let mut res: u128 = 0; struct StackItem {
board: Board,
moves_so_far: Vec<CMove>,
next_move: CMove,
}
if let BoardState::Won = self.state {
return Puzzle {
board: self.clone(),
solutions: vec![vec![]],
solved: true,
};
}
let mut stack = Vec::new();
for mv in &self.legal_moves {
let item = StackItem {
board: self.clone(),
moves_so_far: vec![],
next_move: mv.clone(),
};
stack.push(item);
}
let mut solutions = Vec::new();
loop {
let top = stack.pop();
let Some(top) = top else {
let solved = solutions.len() > 0;
return Puzzle {
board: self.clone(),
solutions,
solved,
};
};
let (mut board, mut moves_so_far, next) = (top.board, top.moves_so_far, top.next_move);
board.make_move(next.clone());
match board.state {
BoardState::Won => {
moves_so_far.push(next);
solutions.push(moves_so_far);
}
BoardState::InProgress => {
moves_so_far.push(next);
for mv in &board.legal_moves {
let item = StackItem {
board: board.clone(),
moves_so_far: moves_so_far.clone(),
next_move: mv.clone(),
};
stack.push(item);
}
}
_ => {}
}
}
}
fn encode(cells: [[Option<Piece>; BOARD_SIZE]; BOARD_SIZE]) -> String {
let mut res: u64 = 0;
for i in 0..BOARD_SIZE { for i in 0..BOARD_SIZE {
for j in 0..BOARD_SIZE { for j in 0..BOARD_SIZE {
res = res << 3; res = res << 3;
let byte = Board::get_piece_encoding(self.cells[i][j]); let byte = Board::get_piece_encoding(cells[i][j]);
res = res | byte as u128 res = res | byte as u64
} }
} }
res let mut id_bytes = [0; 6];
id_bytes.copy_from_slice(&res.to_be_bytes()[2..]);
b64_encode_exact_48(&id_bytes)
} }
fn print(&self, pretty: bool) -> String { fn print(&self, pretty: bool) -> String {
@@ -185,13 +298,17 @@ impl Board {
return None; return None;
}; };
let legal = match piece { if piece.moves_made >= self.max_moves_per_piece {
Piece::King => self.is_king_legal(&pair), return None;
Piece::Queen => self.is_queen_legal(&pair), }
Piece::Bishop => self.is_bishop_legal(&pair),
Piece::Knight => self.is_knight_legal(&pair), let legal = match piece.kind {
Piece::Rook => self.is_rook_legal(&pair), PieceKind::King => self.is_king_legal(&pair),
Piece::Pawn => self.is_pawn_legal(&pair), PieceKind::Queen => self.is_queen_legal(&pair),
PieceKind::Bishop => self.is_bishop_legal(&pair),
PieceKind::Knight => self.is_knight_legal(&pair),
PieceKind::Rook => self.is_rook_legal(&pair),
PieceKind::Pawn => self.is_pawn_legal(&pair),
}; };
if legal { if legal {
@@ -257,14 +374,14 @@ impl Board {
} }
fn calc_game_state(&mut self) { fn calc_game_state(&mut self) {
self.game_state = if self.pieces_remaining == 0 { self.state = if self.pieces_remaining == 0 {
GameState::NotStarted BoardState::NotStarted
} else if self.pieces_remaining == 1 { } else if self.pieces_remaining == 1 {
GameState::Won BoardState::Won
} else if self.legal_moves.is_empty() { } else if self.legal_moves.is_empty() {
GameState::Lost BoardState::Lost
} else { } else {
GameState::InProgress BoardState::InProgress
} }
} }
@@ -302,42 +419,47 @@ impl Board {
fn board_state_changed(&mut self) { fn board_state_changed(&mut self) {
self.calc_legal_moves(); self.calc_legal_moves();
self.calc_game_state(); self.calc_game_state();
self.calc_id();
} }
fn get_piece_encoding(piece: Option<Piece>) -> u8 { fn get_piece_encoding(piece: Option<Piece>) -> u8 {
match piece { match piece {
Some(p) => match p { Some(p) => match p.kind {
Piece::King => 0b001, PieceKind::King => 0b001,
Piece::Queen => 0b010, PieceKind::Queen => 0b010,
Piece::Rook => 0b011, PieceKind::Rook => 0b011,
Piece::Bishop => 0b100, PieceKind::Bishop => 0b100,
Piece::Knight => 0b101, PieceKind::Knight => 0b101,
Piece::Pawn => 0b110, PieceKind::Pawn => 0b110,
}, },
None => 0b000, None => 0b000,
} }
} }
fn get_piece_from_encoding(encoding: u8) -> Result<Option<Piece>, SError> { fn get_piece_from_encoding(encoding: u8) -> Result<Option<PieceKind>, SError> {
match encoding { match encoding {
0b001 => Ok(Some(Piece::King)), 0b001 => Ok(Some(PieceKind::King)),
0b010 => Ok(Some(Piece::Queen)), 0b010 => Ok(Some(PieceKind::Queen)),
0b011 => Ok(Some(Piece::Rook)), 0b011 => Ok(Some(PieceKind::Rook)),
0b100 => Ok(Some(Piece::Bishop)), 0b100 => Ok(Some(PieceKind::Bishop)),
0b101 => Ok(Some(Piece::Knight)), 0b101 => Ok(Some(PieceKind::Knight)),
0b110 => Ok(Some(Piece::Pawn)), 0b110 => Ok(Some(PieceKind::Pawn)),
0b000 => Ok(None), 0b000 => Ok(None),
_ => Err(SError::InvalidBoard), _ => Err(SError::InvalidBoard),
} }
} }
fn calc_id(&mut self) {
self.id = Board::encode(self.cells);
}
} }
fn get_square_for_display(piece: &Option<Piece>, pretty: bool) -> String { fn get_square_for_display(piece: &Option<Piece>, pretty: bool) -> String {
let contents = if let Some(piece) = piece { let contents = if let Some(piece) = piece {
if pretty { if pretty {
piece.pretty() piece.kind.pretty()
} else { } else {
piece.notation() piece.kind.notation()
} }
} else { } else {
".".to_string() ".".to_string()
@@ -350,6 +472,58 @@ fn get_square_for_display(piece: &Option<Piece>, pretty: bool) -> String {
} }
} }
impl Display for BoardState {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let display = match self {
BoardState::NotStarted => "Not Started",
BoardState::InProgress => "In Progress",
BoardState::Lost => "Lost",
BoardState::Won => "Won",
};
write!(f, "{}", display)
}
}
fn b64_encode_exact_48(input: &[u8; 6]) -> String {
let mut output = [0 as char; 8];
for (byte_chunk, output_slice) in input.chunks_exact(3).zip(output.chunks_exact_mut(4)) {
let byte1 = byte_chunk[0];
let byte2 = byte_chunk[1];
let byte3 = byte_chunk[2];
output_slice[0] = lookup((byte1 & 0b1111_1100) >> 2);
output_slice[1] = lookup((byte1 & 0b0000_0011) << 4 | (byte2 & 0b1111_0000) >> 4);
output_slice[2] = lookup((byte2 & 0b0000_1111) << 2 | (byte3 & 0b1100_0000) >> 6);
output_slice[3] = lookup(byte3 & 0b0011_1111);
}
output.iter().collect()
}
fn b64_decode_exact_48(input: &[u8; 8], output: &mut [u8; 6]) {
for (char_chunk, output_slice) in input.chunks_exact(4).zip(output.chunks_exact_mut(3)) {
let char_1 = reverse_lookup(char_chunk[0] as char);
let char_2 = reverse_lookup(char_chunk[1] as char);
let char_3 = reverse_lookup(char_chunk[2] as char);
let char_4 = reverse_lookup(char_chunk[3] as char);
output_slice[0] = (char_1 << 2) | (char_2 >> 4);
output_slice[1] = (char_2 << 4) | (char_3 >> 2);
output_slice[2] = (char_3 << 6) | char_4;
}
}
const ALPHABET: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
fn lookup(idx: u8) -> char {
ALPHABET.chars().nth(idx as usize).unwrap()
}
fn reverse_lookup(c: char) -> u8 {
ALPHABET.chars().position(|x| x == c).unwrap() as u8
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -361,9 +535,7 @@ mod tests {
} }
macro_rules! mv { macro_rules! mv {
($from:literal, $to:literal) => {{ ($from:literal, $to:literal) => {{ CMove::new(sq!($from), sq!($to)) }};
CMove::new(sq!($from), sq!($to))
}};
} }
macro_rules! validate_board { macro_rules! validate_board {
@@ -409,7 +581,7 @@ mod tests {
assert!(board.set(sq!("Nb2")).is_none()); assert!(board.set(sq!("Nb2")).is_none());
let existing = board.set(sq!("Pc4")); let existing = board.set(sq!("Pc4"));
assert!(existing.is_some()); assert!(existing.is_some());
assert_eq!(Piece::Knight, existing.unwrap()); assert_eq!(PieceKind::Knight, existing.unwrap().kind);
validate_board!(board, "..PP", "..B.", "QN..", "K..R"); validate_board!(board, "..PP", "..B.", "QN..", "K..R");
} }
@@ -502,7 +674,7 @@ mod tests {
#[test] #[test]
fn test_smoke_puzzle() { fn test_smoke_puzzle() {
let mut board = Board::new(); let mut board = Board::new();
assert_eq!(GameState::NotStarted, board.game_state); assert_eq!(BoardState::NotStarted, board.state);
assert_eq!(0, board.pieces_remaining); assert_eq!(0, board.pieces_remaining);
// K . . . // K . . .
@@ -510,22 +682,22 @@ mod tests {
// . . R . // . . R .
// N . . . // N . . .
board.set(sq!("Ka4")); board.set(sq!("Ka4"));
assert_eq!(GameState::Won, board.game_state); assert_eq!(BoardState::Won, board.state);
board.set(sq!("Pb3")); board.set(sq!("Pb3"));
board.set(sq!("Rc2")); board.set(sq!("Rc2"));
board.set(sq!("Na1")); board.set(sq!("Na1"));
assert_eq!(GameState::InProgress, board.game_state); assert_eq!(BoardState::InProgress, board.state);
assert_eq!(4, board.pieces_remaining); assert_eq!(4, board.pieces_remaining);
assert!(board.make_move(mv!("Na1", "Rc2")).is_some()); assert!(board.make_move(mv!("Na1", "Rc2")).is_some());
assert_eq!(3, board.pieces_remaining); assert_eq!(3, board.pieces_remaining);
assert_eq!(GameState::InProgress, board.game_state); assert_eq!(BoardState::InProgress, board.state);
assert!(board.make_move(mv!("Pb3", "Ka4")).is_some()); assert!(board.make_move(mv!("Pb3", "Ka4")).is_some());
assert_eq!(2, board.pieces_remaining); assert_eq!(2, board.pieces_remaining);
assert_eq!(GameState::Lost, board.game_state); assert_eq!(BoardState::Lost, board.state);
// P . . . // P . . .
// . . . . // . . . .
@@ -540,12 +712,12 @@ mod tests {
// . . N . // . . N .
// P . . . // P . . .
assert_eq!(4, board.pieces_remaining); assert_eq!(4, board.pieces_remaining);
assert_eq!(GameState::InProgress, board.game_state); assert_eq!(BoardState::InProgress, board.state);
board.make_move(mv!("Qa3", "Pa4")); board.make_move(mv!("Qa3", "Pa4"));
board.make_move(mv!("Nc2", "Pa1")); board.make_move(mv!("Nc2", "Pa1"));
assert_eq!(2, board.pieces_remaining); assert_eq!(2, board.pieces_remaining);
assert_eq!(GameState::InProgress, board.game_state); assert_eq!(BoardState::InProgress, board.state);
// Q . . . // Q . . .
// . . . . // . . . .
@@ -553,7 +725,7 @@ mod tests {
// N . . . // N . . .
board.make_move(mv!("Qa4", "Na1")); board.make_move(mv!("Qa4", "Na1"));
assert_eq!(1, board.pieces_remaining); assert_eq!(1, board.pieces_remaining);
assert_eq!(GameState::Won, board.game_state); assert_eq!(BoardState::Won, board.state);
} }
#[test] #[test]
@@ -566,10 +738,65 @@ mod tests {
board.set(sq!("Bd4")); board.set(sq!("Bd4"));
board.set(sq!("Nc4")); board.set(sq!("Nc4"));
let id = board.id(); let id = board.id;
let board2 = Board::from_id(id); let board2 = Board::from_id(&id);
let board2 = board2.unwrap(); let board2 = board2.unwrap();
validate_board!(board2, "..NB", "....", "RQ.K", "P..."); validate_board!(board2, "..NB", "....", "RQ.K", "P...");
} }
macro_rules! sq {
($sq:literal) => {
Square::parse($sq)
};
}
#[test]
fn solver_smoke() {
let mut board = Board::new();
// . R . .
// R . . P
// B . B N
// P . N .
board.set(sq!("Pa1"));
board.set(sq!("Ba2"));
board.set(sq!("Ra3"));
board.set(sq!("Rb4"));
board.set(sq!("Nc1"));
board.set(sq!("Bc2"));
board.set(sq!("Nd2"));
board.set(sq!("Pd3"));
let solutions = board.solve().solutions;
for solution in solutions {
let mut board = board.clone();
solution
.into_iter()
.for_each(|m| assert!(board.make_move(m).is_some()));
assert_eq!(BoardState::Won, board.state);
}
}
#[test]
fn solver_smoke_no_solution() {
// . R . .
// R . . .
// B . B N
// P . N .
let mut board = Board::new();
board.set(sq!("Pa1"));
board.set(sq!("Ba2"));
board.set(sq!("Ra3"));
board.set(sq!("Rb4"));
board.set(sq!("Nc1"));
board.set(sq!("Bc2"));
board.set(sq!("Nd2"));
let solutions = board.solve().solutions;
assert_eq!(0, solutions.len());
}
} }
@@ -1,4 +1,6 @@
use super::{piece::Piece, square::Square}; use crate::board::piece::Piece;
use super::{piece::PieceKind, square::Square};
#[derive(PartialEq, Hash, Eq, Clone)] #[derive(PartialEq, Hash, Eq, Clone)]
pub struct CMove { pub struct CMove {
@@ -21,13 +23,13 @@ impl CMove {
from, from,
to_piece, to_piece,
to, to,
disambig disambig,
} }
} }
pub fn notation(&self) -> String { pub fn notation(&self) -> String {
let piece_qualifier = match &self.from_piece { let piece_qualifier = match &self.from_piece.kind {
Piece::Pawn => self.from.file_notation(), PieceKind::Pawn => self.from.file_notation(),
p => p.notation(), p => p.notation(),
}; };
format!( format!(
+1
View File
@@ -0,0 +1 @@
pub const BOARD_SIZE: usize = 4;
+91
View File
@@ -0,0 +1,91 @@
#[derive(Clone, Eq, Hash, Copy, Debug, PartialEq)]
pub struct Piece {
pub kind: PieceKind,
pub moves_made: u32,
pub active: bool,
}
impl Piece {
pub fn new(kind: PieceKind) -> Self {
Self {
kind,
moves_made: 0,
active: true,
}
}
pub fn from_kind(kind: Option<PieceKind>) -> Option<Self> {
kind.map(|k| Piece::new(k))
}
}
#[derive(Clone, Eq, Hash, Copy, Debug, PartialEq)]
pub enum PieceKind {
King,
Queen,
Bishop,
Knight,
Rook,
Pawn,
}
impl PieceKind {
pub fn parse(piece: &str) -> Option<Self> {
match piece {
"K" => Some(PieceKind::King),
"Q" => Some(PieceKind::Queen),
"B" => Some(PieceKind::Bishop),
"N" => Some(PieceKind::Knight),
"R" => Some(PieceKind::Rook),
"P" => Some(PieceKind::Pawn),
"." => None,
p => panic!("Invalid piece {}", p),
}
}
pub fn notation(&self) -> String {
let n = match self {
PieceKind::King => "K",
PieceKind::Queen => "Q",
PieceKind::Bishop => "B",
PieceKind::Knight => "N",
PieceKind::Rook => "R",
PieceKind::Pawn => "P",
};
n.to_string()
}
pub fn pretty(&self) -> String {
let n = match self {
PieceKind::King => "",
PieceKind::Queen => "",
PieceKind::Bishop => "",
PieceKind::Knight => "",
PieceKind::Rook => "",
PieceKind::Pawn => "",
};
n.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! p {
($piece:literal) => {
PieceKind::parse($piece)
};
}
#[test]
fn test_piece_parse() {
assert_eq!(p!("K"), Some(PieceKind::King));
assert_eq!(p!("Q"), Some(PieceKind::Queen));
assert_eq!(p!("B"), Some(PieceKind::Bishop));
assert_eq!(p!("N"), Some(PieceKind::Knight));
assert_eq!(p!("R"), Some(PieceKind::Rook));
assert_eq!(p!("P"), Some(PieceKind::Pawn));
}
}
@@ -1,5 +1,7 @@
use crate::board::piece::Piece;
use super::constants::BOARD_SIZE; use super::constants::BOARD_SIZE;
use super::piece::Piece; use super::piece::PieceKind;
use core::fmt; use core::fmt;
#[derive(Clone, Eq, Hash, PartialEq)] #[derive(Clone, Eq, Hash, PartialEq)]
@@ -29,8 +31,8 @@ impl Square {
pub fn parse(notation: &str) -> Self { pub fn parse(notation: &str) -> Self {
let mut chars = notation.chars(); let mut chars = notation.chars();
let piece = chars.next().expect("Piece missing"); let piece_kind = chars.next().expect("Piece missing");
let piece = Piece::parse(&piece.to_string()); let piece_kind = PieceKind::parse(&piece_kind.to_string());
let file = chars.next().expect("File missing"); let file = chars.next().expect("File missing");
let file = match file { let file = match file {
'a' => 0, 'a' => 0,
@@ -45,6 +47,7 @@ impl Square {
panic!("rank should be between 1-{}", BOARD_SIZE); panic!("rank should be between 1-{}", BOARD_SIZE);
} }
let rank = BOARD_SIZE - rank; let rank = BOARD_SIZE - rank;
let piece = piece_kind.map(|kind| Piece::new(kind));
Square::new(file, rank, piece) Square::new(file, rank, piece)
} }
@@ -73,7 +76,7 @@ impl Square {
if self.piece.is_none() { if self.piece.is_none() {
"".to_string() "".to_string()
} else { } else {
self.piece.unwrap().notation() self.piece.unwrap().kind.notation()
} }
} }
} }
@@ -131,7 +134,7 @@ mod tests {
let square = Square::parse(&notation); let square = Square::parse(&notation);
assert_eq!(square.file, $file); assert_eq!(square.file, $file);
assert_eq!(square.rank, $rank); assert_eq!(square.rank, $rank);
assert_eq!(square.piece, Some(Piece::King)); assert_eq!(square.piece.unwrap().kind, PieceKind::King);
assert_eq!(square.notation(), notation); assert_eq!(square.notation(), notation);
}; };
} }
+213
View File
@@ -0,0 +1,213 @@
use std::fmt::Display;
use crate::board::{
Board, BoardOptions,
cmove::CMove,
piece::{Piece, PieceKind},
};
pub trait RandomRange {
fn gen_range(&self, min: usize, max: usize) -> usize;
}
#[derive(Default)]
pub struct Puzzle {
pub board: Board,
pub solutions: Vec<Vec<CMove>>,
pub solved: bool,
}
pub struct GenerateStats {
piece_total: u32,
piece_success: u32,
total: u32,
board: Option<Board>,
solutions: Vec<Vec<CMove>>,
}
pub fn generate_weighted_random(
num_pieces: u32,
num_solutions: u32,
max_moves_per_piece: u32,
rand: &impl RandomRange,
) -> GenerateStats {
let candidate_pieces = vec![
PieceKind::Pawn,
PieceKind::Pawn,
PieceKind::Pawn,
PieceKind::Pawn,
PieceKind::Bishop,
PieceKind::Bishop,
PieceKind::Bishop,
PieceKind::Bishop,
PieceKind::Knight,
PieceKind::Knight,
PieceKind::Knight,
PieceKind::Queen,
PieceKind::Rook,
PieceKind::Rook,
];
if num_pieces > candidate_pieces.len().try_into().unwrap() {
panic!(
"Number of pieces to place on the board should be <= {}",
candidate_pieces.len()
);
}
let attempts: u32 = 1000;
let mut overall_stats = GenerateStats::new(0, 0, 0, None, vec![]);
for _ in 0..attempts {
let stats = try_generate(
num_pieces,
num_solutions,
max_moves_per_piece,
rand,
candidate_pieces.clone(),
);
overall_stats.piece_total += stats.piece_total;
overall_stats.piece_success += stats.piece_success;
overall_stats.total += stats.total;
overall_stats.board = stats.board;
if overall_stats.board.is_some() {
return overall_stats;
}
}
overall_stats
}
impl GenerateStats {
fn new(
piece_total: u32,
piece_success: u32,
total: u32,
board: Option<Board>,
solutions: Vec<Vec<CMove>>,
) -> Self {
Self {
piece_total,
piece_success,
total,
board,
solutions,
}
}
pub fn print_stats(&self) {
let mut stats = String::new();
add_stat(&mut stats, "Total attempts", self.total);
add_stat(&mut stats, "Total pieces placed", self.piece_total);
add_stat(&mut stats, "Success pieces placed", self.piece_success);
println!("{}", stats);
}
pub fn puzzle(self) -> Option<Puzzle> {
let Some(board) = self.board else {
return None;
};
let solved = self.solutions.len() > 0;
Some(Puzzle {
board,
solutions: self.solutions,
solved,
})
}
}
fn add_stat<T>(stats: &mut String, name: &str, val: T)
where
T: Display,
{
stats.push_str(&format!("{:>30}:{:>6}\n", name, val));
}
fn try_generate(
num_pieces: u32,
num_solutions: u32,
max_moves_per_piece: u32,
rand: &impl RandomRange,
mut candidate_pieces: Vec<PieceKind>,
) -> GenerateStats {
let mut board = Board::create(BoardOptions {
max_moves_per_piece,
});
let mut piece_total = 0;
let mut piece_success = 0;
for _ in 0..num_pieces {
let mut placed = false;
let empty_squares = board.empty_squares();
let mut attempts = 15;
while !placed {
if attempts == 0 {
return GenerateStats::new(piece_total, piece_success, 1, None, vec![]);
}
attempts -= 1;
piece_total += 1;
let index = rand.gen_range(0, candidate_pieces.len());
let piece_kind = candidate_pieces[index];
let square_index = rand.gen_range(0, empty_squares.len());
let mut random_square = empty_squares[square_index].clone();
random_square.piece = Some(Piece::new(piece_kind));
board.set(random_square.clone());
let puzzle = board.solve();
if puzzle.solutions.len() > 0 {
placed = true;
piece_success += 1;
candidate_pieces.remove(index);
continue;
}
random_square.piece = None;
board.set(random_square);
}
}
let puzzle = board.solve();
if puzzle.solutions.len() > num_solutions as usize {
GenerateStats::new(piece_total, piece_success, 1, None, vec![])
} else {
GenerateStats::new(
piece_total,
piece_success,
1,
Some(puzzle.board),
puzzle.solutions,
)
}
}
#[cfg(test)]
mod tests {
use crate::board::BoardState;
use super::*;
use rand::Rng;
struct TestRandom;
impl RandomRange for TestRandom {
fn gen_range(&self, min: usize, max: usize) -> usize {
rand::rng().random_range(min..max)
}
}
#[test]
fn generator_smoke() {
for _ in 0..10 {
let gen_stats = generate_weighted_random(5, 5, 10, &TestRandom);
let board = gen_stats.board.expect("No puzzle was generated");
assert_eq!(board.game_state, BoardState::InProgress);
let puzzle = board.solve();
assert!(puzzle.solutions.len() <= 5);
assert!(puzzle.solutions.len() >= 1);
}
}
}
-1
View File
@@ -1,3 +1,2 @@
pub mod board; pub mod board;
pub mod generator; pub mod generator;
pub mod solver;
-1
View File
@@ -1 +0,0 @@
pub(crate) const BOARD_SIZE: usize = 4;
-71
View File
@@ -1,71 +0,0 @@
#[derive(Clone, Eq, Hash, Copy, Debug, PartialEq)]
pub enum Piece {
King,
Queen,
Bishop,
Knight,
Rook,
Pawn,
}
impl Piece {
pub fn parse(piece: &str) -> Option<Self> {
match piece {
"K" => Some(Piece::King),
"Q" => Some(Piece::Queen),
"B" => Some(Piece::Bishop),
"N" => Some(Piece::Knight),
"R" => Some(Piece::Rook),
"P" => Some(Piece::Pawn),
"." => None,
p => panic!("Invalid piece {}", p),
}
}
pub fn notation(&self) -> String {
let n = match self {
Piece::King => "K",
Piece::Queen => "Q",
Piece::Bishop => "B",
Piece::Knight => "N",
Piece::Rook => "R",
Piece::Pawn => "P",
};
n.to_string()
}
pub fn pretty(&self) -> String {
let n = match self {
Piece::King => "",
Piece::Queen => "",
Piece::Bishop => "",
Piece::Knight => "",
Piece::Rook => "",
Piece::Pawn => "",
};
n.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! p {
($piece:literal) => {
Piece::parse($piece)
};
}
#[test]
fn test_piece_parse() {
assert_eq!(p!("K"), Some(Piece::King));
assert_eq!(p!("Q"), Some(Piece::Queen));
assert_eq!(p!("B"), Some(Piece::Bishop));
assert_eq!(p!("N"), Some(Piece::Knight));
assert_eq!(p!("R"), Some(Piece::Rook));
assert_eq!(p!("P"), Some(Piece::Pawn));
}
}
-1
View File
@@ -1 +0,0 @@
pub mod texture;
-45
View File
@@ -1,45 +0,0 @@
use macroquad::prelude::*;
use sol_chess::board::piece::Piece;
pub struct PieceTexture {
x: f32,
y: f32,
w: f32,
h: f32,
}
impl PieceTexture {
fn new(x: u32, y: u32) -> Self {
Self {
x: x as f32 * 128.0,
y: y as f32 * 128.0,
w: 128.0,
h: 128.0,
}
}
pub fn for_piece(piece: Piece, sprite_size: f32) -> DrawTextureParams {
let index = match piece {
Piece::Pawn => 0,
Piece::Knight => 1,
Piece::Bishop => 2,
Piece::Rook => 3,
Piece::Queen => 4,
Piece::King => 5,
};
let color = 0;
let texture_rect = PieceTexture::new(index, color);
DrawTextureParams {
source: Some(Rect::new(
texture_rect.x,
texture_rect.y,
texture_rect.w,
texture_rect.h,
)),
dest_size: Some(Vec2::new(sprite_size, sprite_size)),
..DrawTextureParams::default()
}
}
}
-180
View File
@@ -1,180 +0,0 @@
use std::{fmt::Display, time::Duration};
use crate::{
board::{piece::Piece, Board},
solver::Solver,
};
use indicatif::ProgressBar;
use rand::{seq::*, Rng};
pub fn generate(num_pieces: u32, num_solutions: u32) -> GenerateStats {
let rand = rand::thread_rng();
let candidate_pieces = vec![
Piece::Pawn,
Piece::Pawn,
Piece::Pawn,
Piece::Pawn,
Piece::Bishop,
Piece::Bishop,
Piece::Bishop,
Piece::Bishop,
Piece::Knight,
Piece::Knight,
Piece::Knight,
Piece::Queen,
Piece::Rook,
Piece::Rook,
];
if num_pieces > candidate_pieces.len().try_into().unwrap() {
panic!(
"Number of pieces to place on the board should be <= {}",
candidate_pieces.len()
);
}
let attempts: u32 = 1000;
let bar = ProgressBar::new_spinner();
bar.enable_steady_tick(Duration::from_millis(100));
let mut overall_stats = GenerateStats::new(0, 0, 0, 0, None);
for _ in 0..attempts {
let stats = try_generate(
num_pieces,
num_solutions,
candidate_pieces.clone(),
rand.clone(),
);
overall_stats.piece_total += stats.piece_total;
overall_stats.piece_success += stats.piece_success;
overall_stats.total += stats.total;
overall_stats.total_millis += stats.total_millis;
overall_stats.board = stats.board;
bar.set_message(format!(
"Generating puzzle.. Elapsed: {}s",
overall_stats.total_millis / 1000
));
if overall_stats.board.is_some() {
return overall_stats;
}
}
bar.finish_and_clear();
overall_stats
}
pub struct GenerateStats {
piece_total: u32,
piece_success: u32,
total: u32,
total_millis: u128,
board: Option<Board>,
}
impl GenerateStats {
fn new(
piece_total: u32,
piece_success: u32,
total: u32,
total_millis: u128,
board: Option<Board>,
) -> Self {
Self {
piece_total,
piece_success,
total,
total_millis,
board,
}
}
pub fn print_stats(&self) {
let mut stats = String::new();
add_stat(&mut stats, "Total attempts", self.total);
add_stat(&mut stats, "Total pieces placed", self.piece_total);
add_stat(&mut stats, "Success pieces placed", self.piece_success);
add_stat(&mut stats, "Total time (ms)", self.total_millis);
println!("{}", stats);
}
pub fn board(self) -> Option<Board> {
self.board
}
}
fn add_stat<T>(stats: &mut String, name: &str, val: T)
where
T: Display,
{
stats.push_str(&format!("{:>30}:{:>6}\n", name, val));
}
fn try_generate(
num_pieces: u32,
num_solutions: u32,
mut candidate_pieces: Vec<Piece>,
mut rand: rand::prelude::ThreadRng,
) -> GenerateStats {
let mut board = Board::new();
let mut piece_total = 0;
let mut piece_success = 0;
let now = std::time::Instant::now();
for _ in 0..num_pieces {
let mut placed = false;
let empty_squares = board.empty_squares();
let mut attempts = 15;
while !placed {
if attempts == 0 {
let elapsed = now.elapsed().as_millis();
return GenerateStats::new(piece_total, piece_success, 1, elapsed, None);
}
attempts -= 1;
piece_total += 1;
let index = rand.gen_range(0..candidate_pieces.len());
let piece = candidate_pieces[index];
let mut random_square = empty_squares.choose(&mut rand).unwrap().clone();
random_square.piece = Some(piece);
board.set(random_square.clone());
let solutions = Solver::new(board.clone()).solve();
if solutions.len() > 0 {
placed = true;
piece_success += 1;
candidate_pieces.remove(index);
continue;
}
random_square.piece = None;
board.set(random_square);
}
}
let solutions = Solver::new(board.clone()).solve();
let elapsed = now.elapsed().as_millis();
if solutions.len() > num_solutions as usize {
GenerateStats::new(piece_total, piece_success, 1, elapsed, None)
} else {
GenerateStats::new(piece_total, piece_success, 1, elapsed, Some(board))
}
}
#[cfg(test)]
mod tests {
use crate::{board::GameState, solver::Solver};
use super::*;
#[test]
fn generator_smoke() {
for _ in 0..10 {
let gen_stats = generate(5, 5);
let board = gen_stats.board.expect("No puzzle was generated");
assert_eq!(board.game_state, GameState::InProgress);
let solutions = Solver::new(board).solve();
assert!(solutions.len() <= 5);
assert!(solutions.len() >= 1);
}
}
}
-122
View File
@@ -1,122 +0,0 @@
use game::texture::PieceTexture;
use macroquad::prelude::*;
use sol_chess::board::{square::Square, Board};
mod game;
#[macroquad::main("Solitaire Chess")]
async fn main() {
let background_color = Color::from_rgba(196, 195, 208, 255);
let game = init().await;
loop {
clear_background(background_color);
game.draw();
next_frame().await
}
}
async fn init() -> Game {
set_pc_assets_folder("./assets");
let texture_res = load_texture("pieces.png").await.unwrap();
texture_res.set_filter(FilterMode::Nearest);
build_textures_atlas();
let mut board = Board::new();
board.set(Square::parse("Pa4"));
board.set(Square::parse("Pa3"));
board.set(Square::parse("Na2"));
board.set(Square::parse("Na1"));
board.set(Square::parse("Bb4"));
board.set(Square::parse("Bb3"));
board.set(Square::parse("Rb2"));
board.set(Square::parse("Rb1"));
board.set(Square::parse("Kc4"));
board.set(Square::parse("Kc3"));
board.set(Square::parse("Qc2"));
board.set(Square::parse("Qc1"));
let square_width = 128.0;
let num_squares = 4;
let x = (screen_width() - (square_width * num_squares as f32)) / 2.0;
let y = (screen_height() - (square_width * num_squares as f32)) / 2.0;
let game = Game::new(board, x, y, square_width, num_squares, texture_res);
game
}
struct Game {
board: Board,
squares: Vec<GameSquare>,
texture_res: Texture2D,
num_squares: usize,
}
impl Game {
fn new(
board: Board,
x: f32,
y: f32,
square_width: f32,
num_squares: usize,
texture_res: Texture2D,
) -> Self {
let dark = Color::from_rgba(83, 104, 120, 255);
let light = Color::from_rgba(190, 190, 190, 255);
let mut rects = Vec::new();
for i in 0..num_squares {
for j in 0..num_squares {
let x_eff = x + (i as f32 * square_width);
let y_eff = y + (j as f32 * square_width);
let rect = Rect::new(x_eff, y_eff, square_width, square_width);
let color = match (i + j) % 2 {
1 => dark,
_ => light,
};
rects.push(GameSquare { rect, color, i, j });
}
}
Self {
board,
squares: rects,
num_squares,
texture_res,
}
}
fn get(&mut self, i: usize, j: usize) -> &mut GameSquare {
&mut self.squares[i * self.num_squares + j]
}
fn draw(&self) {
let sprite_size = 100.0;
self.squares.iter().for_each(|square| {
draw_rectangle(
square.rect.x,
square.rect.y,
square.rect.w,
square.rect.h,
square.color,
);
if let Some(p) = &self.board.cells[square.i][square.j] {
let offset = (square.rect.w - sprite_size) / 2.0;
let dtp = PieceTexture::for_piece(*p, sprite_size);
draw_texture_ex(
&self.texture_res,
square.rect.x + offset,
square.rect.y + offset,
WHITE,
dtp,
);
}
});
}
}
struct GameSquare {
rect: Rect,
color: Color,
i: usize,
j: usize,
}
-109
View File
@@ -1,109 +0,0 @@
use crate::board::{
cmove::CMove,
{Board, GameState},
};
pub struct Solver {
pub board: Board,
moves: Vec<CMove>,
}
impl Solver {
pub fn new(board: Board) -> Solver {
Solver {
board,
moves: vec![],
}
}
fn clone(&self, m: CMove) -> Self {
let mut moves = self.moves.clone();
let mut board = self.board.clone();
moves.push(m.clone());
board.make_move(m);
Solver { board, moves }
}
pub fn solve(&self) -> Vec<Vec<CMove>> {
let mut solutions = Vec::new();
if let GameState::Won = self.board.game_state {
solutions.push(self.moves.clone());
return solutions;
}
let GameState::InProgress = self.board.game_state else {
return solutions;
};
self.board.legal_moves.iter().for_each(|m| {
let solver = self.clone(m.clone());
let more_solutions = solver.solve();
solutions.extend(more_solutions);
});
solutions
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::board::{square::Square, Board};
macro_rules! sq {
($sq:literal) => {
Square::parse($sq)
};
}
#[test]
fn solver_smoke() {
let mut board = Board::new();
// . R . .
// R . . P
// B . B N
// P . N .
board.set(sq!("Pa1"));
board.set(sq!("Ba2"));
board.set(sq!("Ra3"));
board.set(sq!("Rb4"));
board.set(sq!("Nc1"));
board.set(sq!("Bc2"));
board.set(sq!("Nd2"));
board.set(sq!("Pd3"));
let solver = Solver::new(board.clone());
let solutions = solver.solve();
for solution in solutions {
let mut board = board.clone();
solution
.into_iter()
.for_each(|m| assert!(board.make_move(m).is_some()));
assert_eq!(GameState::Won, board.game_state);
}
}
#[test]
fn solver_smoke_no_solution() {
// . R . .
// R . . .
// B . B N
// P . N .
let mut board = Board::new();
board.set(sq!("Pa1"));
board.set(sq!("Ba2"));
board.set(sq!("Ra3"));
board.set(sq!("Rb4"));
board.set(sq!("Nc1"));
board.set(sq!("Bc2"));
board.set(sq!("Nd2"));
let solver = Solver::new(board.clone());
let solutions = solver.solve();
assert_eq!(0, solutions.len());
}
}
+1
View File
@@ -0,0 +1 @@
cargo build -p sol_chess --target wasm32-unknown-unknown --release && scp target/wasm32-unknown-unknown/release/sol_chess.wasm potato@neophyte.me:~/site/games/sol_chess/.
-26
View File
@@ -1,26 +0,0 @@
*********** Game 1 ************
Generating a puzzle with 7 pieces with a maximum of 5 solutions
Total attempts: 328
Total pieces placed: 3363
Success pieces placed: 2296
Total time (ms): 1840
♗ ♗ ♙ ♘
. . . ♖
. . ♙ .
. . . ♙
id: 140771860875974
Found 1 solutions
1. BxPc2
2. BxPd1
3. RxBd1
4. RxNd4
5. RxPc4
6. RxBb4
-26
View File
@@ -1,26 +0,0 @@
*********** Game 10 ************
Generating a puzzle with 7 pieces with a maximum of 5 solutions
Total attempts: 43
Total pieces placed: 407
Success pieces placed: 301
Total time (ms): 238
♙ . ♖ .
. . ♘ ♙
. ♙ ♘ .
♗ . . .
id: 211381923512704
Found 4 solutions
1. BxPb2
2. NxPa4
3. RxNc2
4. RxBb2
5. NxRb2
6. NxPd3
-26
View File
@@ -1,26 +0,0 @@
*********** Game 2 ************
Generating a puzzle with 7 pieces with a maximum of 5 solutions
Total attempts: 47
Total pieces placed: 473
Success pieces placed: 329
Total time (ms): 279
. . . ♗
♘ . ♗ .
♙ . . .
. ♘ ♖ ♗
id: 25288852387844
Found 4 solutions
1. RxBd1
2. BxBc3
3. RxNb1
4. NxRb1
5. NxBc3
6. NxPa2
-26
View File
@@ -1,26 +0,0 @@
*********** Game 3 ************
Generating a puzzle with 7 pieces with a maximum of 5 solutions
Total attempts: 22
Total pieces placed: 239
Success pieces placed: 154
Total time (ms): 160
♗ . ♖ ♙
. . . ♙
. . . ♙
. ♙ . ♗
id: 140737595313588
Found 5 solutions
1. RxBa4
2. RxPd4
3. RxPd3
4. RxPd2
5. RxBd1
6. RxPb1
-26
View File
@@ -1,26 +0,0 @@
*********** Game 4 ************
Generating a puzzle with 7 pieces with a maximum of 5 solutions
Total attempts: 200
Total pieces placed: 2059
Success pieces placed: 1388
Total time (ms): 929
♙ ♘ . ♗
. ♖ ♙ ♘
. . . .
. . ♙ .
id: 211152405031232
Found 1 solutions
1. RxNb4
2. RxPa4
3. RxBd4
4. RxNd3
5. RxPc3
6. RxPc1
-26
View File
@@ -1,26 +0,0 @@
*********** Game 5 ************
Generating a puzzle with 7 pieces with a maximum of 5 solutions
Total attempts: 74
Total pieces placed: 771
Success pieces placed: 512
Total time (ms): 437
♗ ♙ . ♘
. ♖ . ♘
. . . .
. ♗ ♗ .
id: 140792316316480
Found 4 solutions
1. BxRb3
2. BxNd3
3. NxBb3
4. NxBc1
5. NxBd3
6. NxPb4
-26
View File
@@ -1,26 +0,0 @@
*********** Game 6 ************
Generating a puzzle with 7 pieces with a maximum of 5 solutions
Total attempts: 1
Total pieces placed: 9
Success pieces placed: 7
Total time (ms): 0
. ♙ . .
. . . .
♗ . ♗ ♗
♖ ♙ . ♘
id: 2456822087717
Found 5 solutions
1. RxBa2
2. RxBc2
3. RxBd2
4. RxNd1
5. RxPb1
6. RxPb4
-26
View File
@@ -1,26 +0,0 @@
*********** Game 7 ************
Generating a puzzle with 7 pieces with a maximum of 5 solutions
Total attempts: 22
Total pieces placed: 230
Success pieces placed: 154
Total time (ms): 109
♖ . . ♙
. . . ♗
♗ ♙ ♘ .
. . ♖ .
id: 107752945007872
Found 2 solutions
1. RxNc2
2. RxPb2
3. RxBa2
4. RxRa4
5. RxPd4
6. RxBd3
-26
View File
@@ -1,26 +0,0 @@
*********** Game 8 ************
Generating a puzzle with 7 pieces with a maximum of 5 solutions
Total attempts: 63
Total pieces placed: 639
Success pieces placed: 441
Total time (ms): 345
. . ♖ .
. ♙ . .
♙ . ♗ .
♗ ♙ . ♗
id: 3579962327044
Found 5 solutions
1. BxBc2
2. RxBc2
3. RxPa2
4. RxBa1
5. RxPb1
6. RxPb3
-26
View File
@@ -1,26 +0,0 @@
*********** Game 9 ************
Generating a puzzle with 7 pieces with a maximum of 5 solutions
Total attempts: 250
Total pieces placed: 2557
Success pieces placed: 1750
Total time (ms): 1298
. . . .
♘ ♘ . ♙
. ♙ . ♗
♙ . ♘ .
id: 22408723452320
Found 1 solutions
1. BxNc1
2. NxBc1
3. NxPd3
4. NxPb2
5. axNb2
6. bxNa3
-9
View File
@@ -1,9 +0,0 @@
if [ ! -d games ]; then
mkdir games
fi
for i in {1..10}; do
echo "*********** Game $i ************" >> games/$i.txt
echo "" >> games/$i.txt
sol_cli -g -n 7 --print >> games/$i.txt
done
+128
View File
@@ -0,0 +1,128 @@
#! /usr/bin/bash
rootd() {
pushd $(git rev-parse --show-toplevel) 2>&1 > /dev/null
}
restored() {
popd 2>&1 > /dev/null
}
get_parameter() {
DEFAULT="${1}"
if [ -z ${2} ]; then
echo "${DEFAULT}"
elif [ ! -d "${2}" ]; then
echo "${DEFAULT}"
else
echo "${2}"
fi
}
# Usage: build_web debug|release target_dir [archive_dir]
# Build profile location to place files location to place compressed archive
build_web() {
rootd
local BINARY_NAME="sol_chess"
local BUILD_PROFILE="debug"
local BUILD_PROFILE_SWITCH=""
if [ -n "${1}" ]; then
if [ "${1}" = "release" ]; then
local BUILD_PROFILE="release"
local BUILD_PROFILE_SWITCH="--release"
fi
fi
local TARGET_DIR=$(get_parameter "./target/dist" ${2})
local ARCHIVE_DIR=$(get_parameter "" ${3})
echo "Build profile: ${BUILD_PROFILE}"
echo "Build profile switch: ${BUILD_PROFILE_SWITCH}"
echo "Target directory: ${TARGET_DIR}"
echo "Archive directory: ${ARCHIVE_DIR}"
set -x
cargo build -p ${BINARY_NAME} --target wasm32-unknown-unknown ${BUILD_PROFILE_SWITCH}
set +x
if [ $? -ne 0 ]; then
echo "Wasm build failed"
return 1
fi
rm -rf ${TARGET_DIR} && mkdir -p ${TARGET_DIR} && mv ./target/wasm32-unknown-unknown/${BUILD_PROFILE}/${BINARY_NAME}.wasm ${TARGET_DIR}/${BINARY_NAME}.wasm && cp ./tools/web/index.html ${TARGET_DIR}/index.html && cp ./tools/web/mq.js ${TARGET_DIR}/mq.js
if [ $? -ne 0 ]; then
echo "Failed to assemble the build in ${TARGET_DIR}"
return 1
fi
if [ -n "${ARCHIVE_DIR}" ]; then
local TAR_NAME="${ARCHIVE_DIR}/${BINARY_NAME}.tar.gz"
set -x
tar -czvf ${TAR_NAME} -C ${TARGET_DIR} . && echo "Created ${TAR_NAME}"
set +x
fi
restored
}
run_web() {
rootd
local TARGET_DIR=$(get_parameter "./target/dist" ${1})
echo "Building web app in ${TARGET_DIR}"
build_web "debug" $TARGET_DIR
if [ $? -ne 0 ]; then
echo "Failed to build the web app"
return 1
fi
basic-http-server $TARGET_DIR
restored
}
run_dev() {
rootd
TESTING=1 cargo run
restored
}
deploy() {
rootd
local BINARY_NAME="sol_chess"
if [ $# -ne 1 ]; then
echo "Usage: deploy <serve_root>"
return 1
fi
if [ ! -d $1 ]; then
echo "Directory $1 does not exist"
return 1
fi
local serve_root=$1
build_web "release" "./target/dist" "./target"
if [ $? -ne 0 ]; then
echo "Failed to build the web app"
return 1
fi
sudo mv ./target/${BINARY_NAME}.tar.gz $serve_root/${BINARY_NAME}.tar.gz && \
sudo tar -xzvf $serve_root/${BINARY_NAME}.tar.gz -C $serve_root && \
sudo rm $serve_root/${BINARY_NAME}.tar.gz
echo "Deployment complete"
restored
}
clean() {
rootd
rm -rf ./target
restored
}
+27
View File
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Solitaire Chess</title>
<style>
html,
body,
canvas {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
position: absolute;
background: black;
z-index: 0;
}
</style>
</head>
<body>
<canvas id="glcanvas" tabindex='1'></canvas>
<script src="./mq.js"></script>
<script>load("sol_chess.wasm");</script> <!-- Your compiled WASM binary -->
</body>
</html>
+3
View File
File diff suppressed because one or more lines are too long