Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d83cb89ed | |||
| 82d13cfd66 | |||
| 194136dfc7 | |||
| 3cb08bf08e | |||
| 810a4cb33c | |||
| 280150b0b0 | |||
| dc61d6d956 | |||
| 52fb443d5b | |||
| 04b39e53a4 | |||
| 96a4f007cc | |||
| 0bd216893e | |||
| 0c88bfa24b | |||
| 86d160cab0 | |||
| 8cd1f86ba6 | |||
| 27d57299ba | |||
| 5dc87df1fc | |||
| d7ec3d7096 | |||
| 2d33b20ded | |||
| b15fb61c9e | |||
| bce815d477 | |||
| 01b2832402 | |||
| 3a3c681899 | |||
| ab0c333935 | |||
| c551715a52 | |||
| d758d03f2d | |||
| d90e270cd5 | |||
| 373d3da91b | |||
| 1698f3ce87 | |||
| ace89713d7 | |||
| 77a640e1b7 | |||
| 32ff856cfa | |||
| ff13e49720 | |||
| 654469bb4a | |||
| c1b0ea24a6 | |||
| 977c24968b | |||
| 1dfe56ac50 | |||
| 8b71bce89c | |||
| dc4ae27074 | |||
| c8f1c043ee | |||
| 9221f3f38a | |||
| 6b321e034f | |||
| 5560013e33 | |||
| 89e43686ca | |||
| 111a01faac | |||
| 74a91f5a1f |
@@ -21,3 +21,5 @@ Cargo.lock
|
|||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
|
sol_chess.tar.gz
|
||||||
|
local-deploy/
|
||||||
|
|||||||
Generated
+252
-71
@@ -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"
|
||||||
@@ -66,9 +78,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
|||||||
|
|
||||||
[[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"
|
||||||
@@ -78,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"
|
||||||
@@ -90,13 +102,34 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
|||||||
|
|
||||||
[[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]]
|
||||||
|
name = "dasp_frame"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
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"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
@@ -108,24 +141,42 @@ 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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasip2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glam"
|
name = "glam"
|
||||||
version = "0.27.0"
|
version = "0.27.0"
|
||||||
@@ -134,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"
|
||||||
@@ -155,16 +214,27 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "lewton"
|
||||||
version = "0.2.169"
|
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 = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
checksum = "8d542c1a317036c45c2aa1cf10cc9d403ca91eb2d333ef1a4917e5cb10628bd0"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"ogg",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.180"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||||
|
|
||||||
[[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",
|
||||||
@@ -172,7 +242,7 @@ dependencies = [
|
|||||||
"macroquad_macro",
|
"macroquad_macro",
|
||||||
"miniquad",
|
"miniquad",
|
||||||
"quad-rand",
|
"quad-rand",
|
||||||
"slotmap",
|
"quad-snd",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -191,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",
|
||||||
@@ -228,19 +304,22 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc"
|
name = "objc-rs"
|
||||||
version = "0.2.7"
|
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 = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
|
checksum = "64a1e7069a2525126bf12a9f1f7916835fafade384fb27cabf698e745e2a1eb8"
|
||||||
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"
|
||||||
@@ -255,6 +334,15 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.92"
|
version = "1.0.92"
|
||||||
@@ -264,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"
|
||||||
@@ -271,14 +368,62 @@ 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]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-fuzzy-search"
|
name = "rust-fuzzy-search"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -287,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",
|
||||||
@@ -307,25 +462,42 @@ 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",
|
||||||
"macroquad",
|
"rand",
|
||||||
|
"sol_lib",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sol_lib"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"rand",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -341,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"
|
||||||
@@ -352,10 +524,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "wasip2"
|
||||||
version = "0.9.5"
|
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 = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
@@ -380,19 +555,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "wit-bindgen"
|
||||||
version = "0.7.35"
|
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 = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"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
-14
@@ -1,16 +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"
|
||||||
macroquad = "0.4.13"
|
macroquad = { version = "0.4.13", features = ["audio"] }
|
||||||
|
quad-snd = "0.2.8"
|
||||||
[profile.release]
|
rand = "0.9.1"
|
||||||
opt-level = 's'
|
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
|
||||||
panic = 'abort'
|
|
||||||
strip = true
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
- This installs 2 binaries: `sol_chess` and `sol_cli`.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
- Run `sol_chess` to start a windowed GUI game.
|
- Run `sol_chess` to start a windowed GUI game.
|
||||||
- Run `sol_cli` to start the CLI tool.
|
|
||||||
|
## Update
|
||||||
|
|
||||||
|
- `cargo install --git https://github.com/cool-mist/sol_chess sol_chess --force`
|
||||||
|
|
||||||
## 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
|
♙ . . ♙
|
||||||
|
|
||||||
Found 3 solutions
|
|
||||||
1. RxNc2
|
id: wmgYAAAG
|
||||||
2. RxPd2
|
|
||||||
3. RxKa2
|
1. RxBb4
|
||||||
4. RxPa3
|
2. RxPa4
|
||||||
5. RxNa4
|
3. RxBa2
|
||||||
|
4. RxPa1
|
||||||
|
5. RxPd1
|
||||||
|
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.
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
cargo build --target wasm32-unknown-unknown --release
|
|
||||||
mkdir -p ./dist
|
|
||||||
rm ./web/sol_chess.wasm
|
|
||||||
mv ./target/wasm32-unknown-unknown/release/sol_chess.wasm ./dist/sol_chess.wasm
|
|
||||||
|
|
||||||
basic-http-server ./dist
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
cargo build --target wasm32-unknown-unknown --release
|
|
||||||
mkdir -p ./dist
|
|
||||||
rm ./web/sol_chess.wasm
|
|
||||||
mv ./target/wasm32-unknown-unknown/release/sol_chess.wasm ./dist/sol_chess.wasm
|
|
||||||
|
|
||||||
basic-http-server ./dist
|
|
||||||
@@ -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
|
||||||
@@ -1,27 +1,30 @@
|
|||||||
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 {
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let board = if let Some(board_string) = args.solve_board {
|
let board = if let Some(board_string) = args.solve_board {
|
||||||
Board::from_string(board_string)
|
Board::from_string(board_string)
|
||||||
} else if let Some(board_id) = args.solve {
|
} else if let Some(board_id) = args.solve {
|
||||||
Board::from_id(board_id)
|
Board::from_id(&board_id)
|
||||||
} else {
|
} else {
|
||||||
println!("Use --help to see available options");
|
println!("Use --help to see available options");
|
||||||
return;
|
return;
|
||||||
@@ -30,27 +33,31 @@ fn main() {
|
|||||||
println!("Invalid board string/id");
|
println!("Invalid board string/id");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
board.pretty_print();
|
|
||||||
solve_puzzle(board);
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
@@ -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,
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
+274
-65
@@ -14,18 +14,23 @@ use std::{
|
|||||||
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: BoardState,
|
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 BoardState {
|
pub enum BoardState {
|
||||||
NotStarted,
|
NotStarted,
|
||||||
InProgress,
|
InProgress,
|
||||||
@@ -33,26 +38,57 @@ pub enum BoardState {
|
|||||||
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: BoardState::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)
|
||||||
@@ -68,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,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;
|
||||||
@@ -136,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 {
|
||||||
@@ -190,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 {
|
||||||
@@ -262,7 +374,7 @@ 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 {
|
||||||
BoardState::NotStarted
|
BoardState::NotStarted
|
||||||
} else if self.pieces_remaining == 1 {
|
} else if self.pieces_remaining == 1 {
|
||||||
BoardState::Won
|
BoardState::Won
|
||||||
@@ -307,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()
|
||||||
@@ -368,6 +485,45 @@ impl Display for BoardState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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::*;
|
||||||
@@ -379,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 {
|
||||||
@@ -427,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");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,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!(BoardState::NotStarted, board.game_state);
|
assert_eq!(BoardState::NotStarted, board.state);
|
||||||
assert_eq!(0, board.pieces_remaining);
|
assert_eq!(0, board.pieces_remaining);
|
||||||
|
|
||||||
// K . . .
|
// K . . .
|
||||||
@@ -528,22 +682,22 @@ mod tests {
|
|||||||
// . . R .
|
// . . R .
|
||||||
// N . . .
|
// N . . .
|
||||||
board.set(sq!("Ka4"));
|
board.set(sq!("Ka4"));
|
||||||
assert_eq!(BoardState::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!(BoardState::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!(BoardState::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!(BoardState::Lost, board.game_state);
|
assert_eq!(BoardState::Lost, board.state);
|
||||||
|
|
||||||
// P . . .
|
// P . . .
|
||||||
// . . . .
|
// . . . .
|
||||||
@@ -558,12 +712,12 @@ mod tests {
|
|||||||
// . . N .
|
// . . N .
|
||||||
// P . . .
|
// P . . .
|
||||||
assert_eq!(4, board.pieces_remaining);
|
assert_eq!(4, board.pieces_remaining);
|
||||||
assert_eq!(BoardState::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!(BoardState::InProgress, board.game_state);
|
assert_eq!(BoardState::InProgress, board.state);
|
||||||
|
|
||||||
// Q . . .
|
// Q . . .
|
||||||
// . . . .
|
// . . . .
|
||||||
@@ -571,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!(BoardState::Won, board.game_state);
|
assert_eq!(BoardState::Won, board.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -584,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!(
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub const BOARD_SIZE: usize = 4;
|
||||||
@@ -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(¬ation);
|
let square = Square::parse(¬ation);
|
||||||
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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -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,3 +1,2 @@
|
|||||||
pub mod board;
|
pub mod board;
|
||||||
pub mod generator;
|
pub mod generator;
|
||||||
pub mod solver;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub(crate) const BOARD_SIZE: usize = 4;
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
pub mod texture;
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
board::{piece::Piece, Board},
|
|
||||||
solver::Solver,
|
|
||||||
};
|
|
||||||
|
|
||||||
use macroquad::{prelude::rand, time};
|
|
||||||
|
|
||||||
pub fn generate(num_pieces: u32, num_solutions: u32) -> GenerateStats {
|
|
||||||
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 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());
|
|
||||||
overall_stats.piece_total += stats.piece_total;
|
|
||||||
overall_stats.piece_success += stats.piece_success;
|
|
||||||
overall_stats.total += stats.total;
|
|
||||||
overall_stats.total_seconds += stats.total_seconds;
|
|
||||||
overall_stats.board = stats.board;
|
|
||||||
println!(
|
|
||||||
"Generating puzzle.. Elapsed: {}s",
|
|
||||||
overall_stats.total_seconds,
|
|
||||||
);
|
|
||||||
if overall_stats.board.is_some() {
|
|
||||||
return overall_stats;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
overall_stats
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct GenerateStats {
|
|
||||||
piece_total: u32,
|
|
||||||
piece_success: u32,
|
|
||||||
total: u32,
|
|
||||||
total_seconds: f64,
|
|
||||||
board: Option<Board>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GenerateStats {
|
|
||||||
fn new(
|
|
||||||
piece_total: u32,
|
|
||||||
piece_success: u32,
|
|
||||||
total: u32,
|
|
||||||
total_millis: f64,
|
|
||||||
board: Option<Board>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
piece_total,
|
|
||||||
piece_success,
|
|
||||||
total,
|
|
||||||
total_seconds: 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_seconds);
|
|
||||||
|
|
||||||
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>,
|
|
||||||
) -> GenerateStats {
|
|
||||||
let mut board = Board::new();
|
|
||||||
let mut piece_total = 0;
|
|
||||||
let mut piece_success = 0;
|
|
||||||
let now = time::get_time();
|
|
||||||
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 = time::get_time() - now;
|
|
||||||
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 square_index = rand::gen_range(0, empty_squares.len());
|
|
||||||
let mut random_square = empty_squares[square_index].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 = time::get_time() - now;
|
|
||||||
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::BoardState, solver::Solver};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
// Figure out a way to remove the macroquad dependencies from this package
|
|
||||||
// #[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, BoardState::InProgress);
|
|
||||||
|
|
||||||
let solutions = Solver::new(board).solve();
|
|
||||||
assert!(solutions.len() <= 5);
|
|
||||||
assert!(solutions.len() >= 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-494
@@ -1,494 +0,0 @@
|
|||||||
use core::fmt;
|
|
||||||
use std::fmt::{Display, Formatter};
|
|
||||||
|
|
||||||
use game::texture::PieceTexture;
|
|
||||||
use macroquad::prelude::*;
|
|
||||||
use sol_chess::{
|
|
||||||
board::{Board, BoardState},
|
|
||||||
generator,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod game;
|
|
||||||
|
|
||||||
#[macroquad::main("Solitaire Chess")]
|
|
||||||
async fn main() {
|
|
||||||
let background_color = Color::from_rgba(196, 195, 208, 255);
|
|
||||||
let mut game = init().await;
|
|
||||||
loop {
|
|
||||||
clear_background(background_color);
|
|
||||||
draw_heading("Solitaire Chess");
|
|
||||||
game.update_window_size();
|
|
||||||
game.draw();
|
|
||||||
game.handle_input();
|
|
||||||
next_frame().await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_heading(title: &str) {
|
|
||||||
let dims = measure_text(title, None, 60, 1.0);
|
|
||||||
let x = screen_width() / 2.0 - dims.width / 2.0;
|
|
||||||
let y = 2.0 * dims.height;
|
|
||||||
draw_text(title, x, y, 60.0, BLACK);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn init() -> Game {
|
|
||||||
let texture_bytes = include_bytes!("../assets/pieces.png");
|
|
||||||
let texture_res = Texture2D::from_file_with_format(&texture_bytes[..], None);
|
|
||||||
texture_res.set_filter(FilterMode::Nearest);
|
|
||||||
build_textures_atlas();
|
|
||||||
let generate = generator::generate(6, 100);
|
|
||||||
let board = generate.board().expect("No puzzle was generated");
|
|
||||||
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 {
|
|
||||||
original_board: Board,
|
|
||||||
board: Board,
|
|
||||||
squares: Vec<GameSquare>,
|
|
||||||
texture_res: Texture2D,
|
|
||||||
num_squares: usize,
|
|
||||||
state: GameState,
|
|
||||||
debug: bool,
|
|
||||||
info_square: Rect,
|
|
||||||
window_height: f32,
|
|
||||||
window_width: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GameSquare {
|
|
||||||
rect: Rect,
|
|
||||||
color: Color,
|
|
||||||
is_source: bool,
|
|
||||||
is_target: bool,
|
|
||||||
is_previous_target: bool,
|
|
||||||
i: usize,
|
|
||||||
j: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
enum GameState {
|
|
||||||
SelectSource(Option<(usize, usize)>),
|
|
||||||
SelectTarget((usize, usize)),
|
|
||||||
GameOver((usize, 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,
|
|
||||||
is_source: false,
|
|
||||||
is_target: false,
|
|
||||||
is_previous_target: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let info_x = x;
|
|
||||||
let info_y = y + (num_squares as f32 * square_width) + square_width / 2.0;
|
|
||||||
let info_w = square_width * num_squares as f32;
|
|
||||||
|
|
||||||
Self {
|
|
||||||
original_board: board.clone(),
|
|
||||||
board,
|
|
||||||
squares: rects,
|
|
||||||
num_squares,
|
|
||||||
texture_res,
|
|
||||||
state: GameState::SelectSource(None),
|
|
||||||
debug: false,
|
|
||||||
info_square: Rect::new(info_x, info_y, info_w, square_width),
|
|
||||||
window_height: screen_height(),
|
|
||||||
window_width: screen_width(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_window_size(&mut self) {
|
|
||||||
let new_height = screen_height();
|
|
||||||
let new_width = screen_width();
|
|
||||||
|
|
||||||
if new_height == self.window_height && new_width == self.window_width {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.window_height = screen_height();
|
|
||||||
self.window_width = screen_width();
|
|
||||||
|
|
||||||
let square_width = 128.0;
|
|
||||||
let num_squares = 4;
|
|
||||||
let x = (self.window_width - (square_width * num_squares as f32)) / 2.0;
|
|
||||||
let y = (self.window_height - (square_width * num_squares as f32)) / 2.0;
|
|
||||||
|
|
||||||
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,
|
|
||||||
is_source: false,
|
|
||||||
is_target: false,
|
|
||||||
is_previous_target: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let info_x = x;
|
|
||||||
let info_y = y + (num_squares as f32 * square_width) + square_width / 2.0;
|
|
||||||
let info_w = square_width * num_squares as f32;
|
|
||||||
|
|
||||||
self.squares = rects;
|
|
||||||
self.info_square = Rect::new(info_x, info_y, info_w, square_width);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
let mut selected_square = None;
|
|
||||||
self.squares.iter().for_each(|square| {
|
|
||||||
let color = if square.is_source {
|
|
||||||
Color::from_rgba(152, 152, 152, 255)
|
|
||||||
} else if square.is_target {
|
|
||||||
Color::from_rgba(152, 129, 123, 255)
|
|
||||||
} else {
|
|
||||||
square.color
|
|
||||||
};
|
|
||||||
|
|
||||||
draw_rectangle(
|
|
||||||
square.rect.x,
|
|
||||||
square.rect.y,
|
|
||||||
square.rect.w,
|
|
||||||
square.rect.h,
|
|
||||||
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);
|
|
||||||
if !square.is_source {
|
|
||||||
draw_texture_ex(
|
|
||||||
&self.texture_res,
|
|
||||||
square.rect.x + offset,
|
|
||||||
square.rect.y + offset,
|
|
||||||
WHITE,
|
|
||||||
dtp,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
selected_square = Some(square);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(selected_square) = selected_square {
|
|
||||||
if let Some(p) = self.board.cells[selected_square.i][selected_square.j] {
|
|
||||||
let dtp = PieceTexture::for_piece(p, sprite_size);
|
|
||||||
draw_texture_ex(
|
|
||||||
&self.texture_res,
|
|
||||||
mouse_position().0 - sprite_size / 2.0,
|
|
||||||
mouse_position().1 - sprite_size / 2.0,
|
|
||||||
WHITE,
|
|
||||||
dtp,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
draw_text(
|
|
||||||
&format!("Press 'R' to reset"),
|
|
||||||
self.info_square.x + 20.0,
|
|
||||||
self.info_square.y + 20.0,
|
|
||||||
20.0,
|
|
||||||
BLACK,
|
|
||||||
);
|
|
||||||
|
|
||||||
draw_text(
|
|
||||||
&format!("Press 'N' for new game (when the current game is won)"),
|
|
||||||
self.info_square.x + 20.0,
|
|
||||||
self.info_square.y + 40.0,
|
|
||||||
20.0,
|
|
||||||
BLACK,
|
|
||||||
);
|
|
||||||
|
|
||||||
draw_text(
|
|
||||||
&format!("Press 'D' to toggle debug mode"),
|
|
||||||
self.info_square.x + 20.0,
|
|
||||||
self.info_square.y + 60.0,
|
|
||||||
20.0,
|
|
||||||
GRAY,
|
|
||||||
);
|
|
||||||
|
|
||||||
if self.debug {
|
|
||||||
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.board.game_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);
|
|
||||||
|
|
||||||
self.show_fps();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_input(&mut self) {
|
|
||||||
if is_key_released(KeyCode::R) {
|
|
||||||
self.reset();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_key_released(KeyCode::N) {
|
|
||||||
if let GameState::GameOver(_) = self.state {
|
|
||||||
self.next_puzzle();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_key_released(KeyCode::D) {
|
|
||||||
self.debug = !self.debug;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_key_released(KeyCode::Q) {
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_mouse_button_pressed(MouseButton::Right) {
|
|
||||||
let current_state = self.state.clone();
|
|
||||||
let new_state = match current_state {
|
|
||||||
GameState::SelectSource(_) => GameState::SelectSource(None),
|
|
||||||
GameState::SelectTarget((_, _)) => {
|
|
||||||
self.reset_squares();
|
|
||||||
GameState::SelectSource(None)
|
|
||||||
}
|
|
||||||
GameState::GameOver((i, j)) => GameState::SelectSource(Some((i, j))),
|
|
||||||
};
|
|
||||||
self.state = new_state;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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_position(), previous_target)
|
|
||||||
}
|
|
||||||
GameState::SelectTarget(source) => GameState::SelectTarget(source),
|
|
||||||
GameState::GameOver(previous_target) => GameState::GameOver(previous_target),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.state = new_state;
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_mouse_button_released(MouseButton::Left) {
|
|
||||||
let current_state = self.state.clone();
|
|
||||||
let new_state = match current_state {
|
|
||||||
GameState::SelectSource(previous_target) => {
|
|
||||||
GameState::SelectSource(previous_target)
|
|
||||||
}
|
|
||||||
GameState::SelectTarget(source) => {
|
|
||||||
self.handle_select_target(mouse_position(), source)
|
|
||||||
}
|
|
||||||
GameState::GameOver(previous_target) => GameState::GameOver(previous_target),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.state = new_state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_select_source(
|
|
||||||
&mut self,
|
|
||||||
mouse_position: (f32, f32),
|
|
||||||
previous_target: Option<(usize, usize)>,
|
|
||||||
) -> GameState {
|
|
||||||
self.reset_squares();
|
|
||||||
let (x, y) = mouse_position;
|
|
||||||
let mouse = Circle::new(x, y, 0.0);
|
|
||||||
let mut selected = None;
|
|
||||||
for square in &mut self.squares {
|
|
||||||
if mouse.overlaps_rect(&square.rect) {
|
|
||||||
if let Some(_) = self.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.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_position: (f32, f32),
|
|
||||||
source: (usize, usize),
|
|
||||||
) -> GameState {
|
|
||||||
let (x, y) = mouse_position;
|
|
||||||
let mouse = Circle::new(x, y, 0.0);
|
|
||||||
|
|
||||||
let mut selected = None;
|
|
||||||
for square in &mut self.squares {
|
|
||||||
if mouse.overlaps_rect(&square.rect) {
|
|
||||||
if let Some(_) = self.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.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.board.make_move(m.clone());
|
|
||||||
|
|
||||||
if self.board.game_state == BoardState::Won || self.board.game_state == BoardState::Lost
|
|
||||||
{
|
|
||||||
self.reset_squares();
|
|
||||||
return GameState::GameOver((x, y));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.reset_squares();
|
|
||||||
self.get(x, y).is_target = true;
|
|
||||||
return GameState::SelectSource(Some((x, y)));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.reset_squares();
|
|
||||||
return GameState::SelectSource(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(&mut self) {
|
|
||||||
self.board = self.original_board.clone();
|
|
||||||
self.reset_squares();
|
|
||||||
self.state = GameState::SelectSource(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_puzzle(&mut self) {
|
|
||||||
self.reset();
|
|
||||||
let generate = generator::generate(6, 100);
|
|
||||||
let board = generate.board().expect("No puzzle was generated");
|
|
||||||
self.original_board = board.clone();
|
|
||||||
self.board = board;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_fps(&self) {
|
|
||||||
let fps = get_fps();
|
|
||||||
draw_text(
|
|
||||||
&format!("FPS: {}", fps),
|
|
||||||
10.0,
|
|
||||||
screen_height() - 20.0,
|
|
||||||
20.0,
|
|
||||||
BLACK,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-109
@@ -1,109 +0,0 @@
|
|||||||
use crate::board::{
|
|
||||||
cmove::CMove,
|
|
||||||
{Board, BoardState},
|
|
||||||
};
|
|
||||||
|
|
||||||
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 BoardState::Won = self.board.game_state {
|
|
||||||
solutions.push(self.moves.clone());
|
|
||||||
return solutions;
|
|
||||||
}
|
|
||||||
|
|
||||||
let BoardState::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!(BoardState::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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Executable
+1
@@ -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/.
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user