Compare commits

..

22 Commits

Author SHA1 Message Date
dev 0d83cb89ed Update readme 2026-08-10 11:34:32 +05:30
cool-mist 82d13cfd66 move to retained mode complete 2026-03-15 17:24:41 +05:30
cool-mist 194136dfc7 --> retained mode ui 2026-03-15 12:05:02 +05:30
cool-mist 3cb08bf08e Major refactor 2026-03-14 00:41:33 +05:30
cool-mist 810a4cb33c Some updates to sizing button text 2026-02-15 00:13:54 +05:30
cool-mist 280150b0b0 add age 2026-01-26 20:00:50 +05:30
cool-mist dc61d6d956 separate logic state and draw state 2026-01-25 01:50:52 +05:30
cool-mist 52fb443d5b fix re-exports 2026-01-24 16:52:05 +05:30
cool-mist 04b39e53a4 Add widgets, proper resources.rs 2026-01-24 01:05:33 +05:30
cool-mist 96a4f007cc Adjust everything relative to the board 2026-01-22 19:10:21 +05:30
cool-mist 0bd216893e update readme 2026-01-22 18:27:32 +05:30
cool-mist 0c88bfa24b Change font 2026-01-22 16:29:29 +05:30
cool-mist 86d160cab0 move to workspace mode 2026-01-22 15:52:52 +05:30
cool-mist 8cd1f86ba6 remove recursion in solver 2026-01-21 20:14:57 +05:30
cool-mist 27d57299ba allow lowercase for specifying the board 2026-01-21 18:31:47 +05:30
cool-mist 5dc87df1fc Use Rc instead of Arc for fonts 2026-01-21 00:07:48 +05:30
cool-mist d7ec3d7096 config-ify 2026-01-21 00:01:44 +05:30
cool-mist 2d33b20ded update font 2026-01-18 23:59:09 +05:30
cool-mist b15fb61c9e base64 encode id 2026-01-17 00:31:49 +05:30
cool-mist bce815d477 handle rules menu close properly 2026-01-16 18:34:09 +05:30
cool-mist 01b2832402 reduce volume, move buttons to the right 2026-01-15 22:29:38 +05:30
cool-mist 3a3c681899 bundle mq.js along with the code 2025-08-22 20:18:43 +05:30
49 changed files with 2690 additions and 1524 deletions
Generated
+107 -125
View File
@@ -4,21 +4,15 @@ version = 4
[[package]]
name = "adler2"
version = "2.0.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "ahash"
version = "0.8.11"
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy 0.7.35",
]
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "argh"
@@ -72,9 +66,9 @@ dependencies = [
[[package]]
name = "autocfg"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
@@ -82,17 +76,11 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "bytemuck"
version = "1.21.0"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
[[package]]
name = "byteorder"
@@ -102,9 +90,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cfg-if"
version = "1.0.0"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "color_quant"
@@ -114,9 +102,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "crc32fast"
version = "1.4.2"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
@@ -136,6 +124,12 @@ 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]]
name = "fdeflate"
version = "0.3.7"
@@ -147,19 +141,25 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.0.35"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "fontdue"
version = "0.7.3"
name = "foldhash"
version = "0.1.5"
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 = [
"hashbrown",
"ttf-parser",
@@ -167,14 +167,14 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.3.3"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi",
"wasip2",
]
[[package]]
@@ -185,11 +185,13 @@ checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9"
[[package]]
name = "hashbrown"
version = "0.13.2"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"ahash",
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]]
@@ -224,15 +226,15 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.169"
version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "macroquad"
version = "0.4.13"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fef16b2d4de22ac372b5d7d76273c0ead0a31f9de9bc649af8f816816db8a8"
checksum = "d2befbae373456143ef55aa93a73594d080adfb111dc32ec96a1123a3e4ff4ae"
dependencies = [
"fontdue",
"glam",
@@ -241,7 +243,6 @@ dependencies = [
"miniquad",
"quad-rand",
"quad-snd",
"slotmap",
]
[[package]]
@@ -267,21 +268,21 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
[[package]]
name = "miniquad"
version = "0.4.6"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2124e5e6bab86d96f263d78dc763c29e0da4c4f7ff0e1bb33f1d3905d1121262"
checksum = "2fb3e758e46dbc45716a8a49ca9edc54b15bcca826277e80b1f690708f67f9e3"
dependencies = [
"libc",
"ndk-sys",
"objc",
"objc-rs",
"winapi",
]
[[package]]
name = "miniz_oxide"
version = "0.8.3"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
@@ -303,10 +304,10 @@ dependencies = [
]
[[package]]
name = "objc"
version = "0.2.7"
name = "objc-rs"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
checksum = "64a1e7069a2525126bf12a9f1f7916835fafade384fb27cabf698e745e2a1eb8"
dependencies = [
"malloc_buf",
]
@@ -320,19 +321,13 @@ dependencies = [
"byteorder",
]
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "png"
version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
dependencies = [
"bitflags 1.3.2",
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
@@ -345,7 +340,7 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy 0.8.25",
"zerocopy",
]
[[package]]
@@ -387,24 +382,24 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.38"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.2.0"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.9.1"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
@@ -422,9 +417,9 @@ dependencies = [
[[package]]
name = "rand_core"
version = "0.9.3"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom",
]
@@ -437,18 +432,28 @@ checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2"
[[package]]
name = "serde"
version = "1.0.217"
version = "1.0.228"
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 = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.217"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -457,18 +462,9 @@ dependencies = [
[[package]]
name = "simd-adler32"
version = "0.3.7"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "slotmap"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a"
dependencies = [
"version_check",
]
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "smallvec"
@@ -481,11 +477,26 @@ dependencies = [
[[package]]
name = "sol_chess"
version = "0.1.1"
version = "0.0.1"
dependencies = [
"argh",
"macroquad",
"quad-snd",
"sol_lib",
]
[[package]]
name = "sol_cli"
version = "0.0.1"
dependencies = [
"argh",
"rand",
"sol_lib",
]
[[package]]
name = "sol_lib"
version = "0.0.1"
dependencies = [
"rand",
]
@@ -502,9 +513,9 @@ dependencies = [
[[package]]
name = "ttf-parser"
version = "0.15.2"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd"
checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
[[package]]
name = "unicode-ident"
@@ -513,18 +524,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "version_check"
version = "0.9.5"
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen-rt",
"wit-bindgen",
]
[[package]]
@@ -550,48 +555,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.0",
]
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[package]]
name = "zerocopy"
version = "0.7.35"
version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
dependencies = [
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
dependencies = [
"zerocopy-derive 0.8.25",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
dependencies = [
"proc-macro2",
"quote",
+8 -15
View File
@@ -1,20 +1,13 @@
[package]
name = "sol_chess"
version = "0.1.1"
edition = "2021"
default-run = "sol_chess"
[workspace]
members = [
"lib",
"cli",
"gui",
]
resolver = "3"
[dependencies]
[workspace.dependencies]
argh = "0.1.13"
macroquad = { version = "0.4.13", features = ["audio"] }
quad-snd = "0.2.8"
[dev-dependencies]
rand = "0.9.1"
[profile.release]
opt-level = 's'
lto = true
codegen-units = 1
panic = 'abort'
strip = true
+30 -43
View File
@@ -4,77 +4,64 @@ Goal: Generate 'hard' puzzles.
### Play a demo of the game [here](https://games.neophyte.me/sol_chess/)
![img](./img/sol_chess.png)
## Install
- Install Rust from [here](https://www.rust-lang.org/tools/install).
- 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
- `cargo install --git https://github.com/cool-mist/sol_chess sol_chess`
- Run `sol_chess` to start a windowed GUI game.
- Run `sol_cli` to start the CLI tool.
## Demo site
## Update
- Play a demo of the game [here](https://games.neophyte.me/sol_chess/)
- `cargo install --git https://github.com/cool-mist/sol_chess sol_chess --force`
## 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
```bash
$ sol_cli -g -n 6
Generating a puzzle with 6 pieces with a maximum of 5 solutions
Total attempts: 7
Total pieces placed: 71
Success pieces placed: 42
Total time (ms): 69
Total attempts: 16
Total pieces placed: 131
Success pieces placed: 96
. . .
♙ . ♖ .
♔ . ♘ ♙
♙ ♗ . .
. . . .
♗ ♖ . .
id: 202859896274992
♙ . . ♙
id: wmgYAAAG
```
- Solve a puzzle by ID, or by board string
```bash
$ sol_cli --solve 202859896274992
$ sol_cli --solve-board N...P.R.K.NP....
$ sol_cli --solve wmgYAAAG
$ sol_cli --solve-board pb......br..p..p
. . .
♙ . ♖ .
♔ . ♘ ♙
♙ ♗ . .
. . . .
♗ ♖ . .
id: 202859896274992
♙ . . ♙
Found 3 solutions
1. RxNc2
2. RxPd2
3. RxKa2
4. RxPa3
5. RxNa4
id: wmgYAAAG
1. RxBb4
2. RxPa4
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.
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "sol_cli"
version = "0.0.1"
edition = "2024"
[dependencies]
sol_lib = { path = "../lib" }
argh = { workspace = true }
rand = { workspace = true }
[profile.release]
opt-level = 's'
lto = true
codegen-units = 1
panic = 'abort'
strip = true
+36 -29
View File
@@ -1,35 +1,30 @@
use argh::FromArgs;
use sol_chess::board::Board;
use sol_chess::generator::{self, RandomRange};
use sol_chess::solver::Solver;
// Learn how to specify a different dependency for this binary
struct MacroquadRngTodo;
impl RandomRange for MacroquadRngTodo {
fn gen_range(&self, min: usize, max: usize) -> usize {
macroquad::rand::gen_range(min, max)
}
}
use rand::Rng;
use sol_lib::board::Board;
use sol_lib::generator::{self, Puzzle, RandomRange};
fn main() {
let args: Args = argh::from_env();
if args.generate {
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;
};
board.pretty_print();
puzzle.board.pretty_print();
if args.print {
solve_puzzle(board);
print_solutions(&puzzle);
}
} else {
return;
}
let board = if let Some(board_string) = args.solve_board {
Board::from_string(board_string)
} else if let Some(board_id) = args.solve {
Board::from_id(board_id)
Board::from_id(&board_id)
} else {
println!("Use --help to see available options");
return;
@@ -38,27 +33,31 @@ fn main() {
println!("Invalid board string/id");
return;
};
board.pretty_print();
solve_puzzle(board);
}
let puzzle = board.solve();
puzzle.board.pretty_print();
print_solutions(&puzzle);
}
fn solve_puzzle(board: Board) {
let solutions = Solver::new(board).solve();
fn print_solutions(puzzle: &Puzzle) {
let solutions = &puzzle.solutions;
if solutions.len() == 0 {
println!("No solutions found");
return;
}
println!("Found {} solutions", solutions.len());
let solution = solutions.first().unwrap();
let mut idx = 0;
solution.iter().for_each(|m| {
idx += 1;
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);
if num_pieces < 2 {
num_pieces = 2;
@@ -73,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",
num_pieces, num_solutions
);
let gen = generator::generate(num_pieces, num_solutions, &MacroquadRngTodo);
gen.print_stats();
let gen_result =
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");
return None;
};
Some(board)
Some(puzzle)
}
/// Solitaire Chess puzzle generator and solver
@@ -106,9 +106,16 @@ struct Args {
#[argh(option, short = 's')]
/// the id of the board to solve
solve: Option<u128>,
solve: Option<String>,
#[argh(option)]
/// the board to solve in board representation
solve_board: Option<String>,
}
struct RandRngImpl;
impl RandomRange for RandRngImpl {
fn gen_range(&self, min: usize, max: usize) -> usize {
rand::rng().random_range(min..max)
}
}
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "sol_chess"
version = "0.0.1"
edition = "2024"
[dependencies]
sol_lib = { path = "../lib" }
macroquad = { workspace = true }
quad-snd = { workspace = true }
[profile.release]
opt-level = 's'
lto = true
codegen-units = 1
panic = 'abort'
strip = true
Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 251 KiB

+77
View File
@@ -0,0 +1,77 @@
pub mod constants;
mod draw;
mod logic;
use std::collections::HashMap;
use crate::{resources::Resources, widgets::*};
use macroquad::prelude::*;
use sol_lib::{board::BoardState, generator::Puzzle};
#[derive(Default)]
pub struct Game {
resources: Resources,
settings: Settings,
puzzle: Puzzle,
show_rules: bool,
game_mode: GameMode,
game_mode_btns: HashMap<GameMode, WidgetId>,
window_height: f32,
window_width: f32,
widgets: HashMap<WidgetId, Widget>,
board: WidgetId,
counter_pieces: WidgetId,
counter_moves: WidgetId,
label_pieces: WidgetId,
label_max_age: WidgetId,
label_heading: WidgetId,
btn_rules: WidgetId,
btn_reset: WidgetId,
btn_next: WidgetId,
btn_generate: WidgetId,
}
#[derive(Default)]
pub struct Settings {
pub volume: f32,
pub debug: bool,
pub max_pieces: u32,
pub max_age: u32,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum GameMode {
Easy,
Medium,
Hard,
Custom,
}
impl Default for GameMode {
fn default() -> Self {
GameMode::Medium
}
}
#[derive(Copy, Clone)]
pub enum Command {
NoInteraction,
ResetBoard,
NextPuzzle,
ToggleRules,
UpdateBoard(BoardState),
SwitchGameMode(GameMode),
UpdateMaxPieceCount(u32),
UpdateMaxAge(u32),
NewCustomPuzzle,
ToggleDebug,
HideRules,
Quit,
}
+44
View File
@@ -0,0 +1,44 @@
pub const WINDOW_TITLE: &str = "Solitaire Chess";
pub const VOLUME: f32 = 0.1; // Between 0 and 1
pub const SCREEN_HEIGHT_MIN: f32 = 200.0;
pub const SCREEN_HEIGHT_MAX: f32 = 10000.0;
pub const SCREEN_WIDTH_MIN: f32 = 200.0;
pub const SCREEN_WIDTH_MAX: f32 = 10000.0;
// How big the square should be relative to the screen size, between 0 and 1
pub const BOARD_SQUARE_WIDTH_MULTIPLIER: f32 = 0.11;
pub const BOARD_SHADOW_MULTIPLIER: f32 = 0.1;
pub const BOARD_PIECE_WIDTH_MULTIPLIER: f32 = 0.8;
// How big the heading text font size should be relative to the screen size, between 0 and 1
pub const HEADING_FONT_SIZE_MULTIPLIER: f32 = 0.07;
// Height of the button relative to the screen size.
pub const BUTTON_HEIGHT_MULTIPLIER: f32 = 0.08;
// Width of the button relative to the 'board size'
pub const BUTTON_WIDTH_MULTIPLIER: f32 = 0.20;
// Gap between the bottom of the board and the first row of buttons
pub const BOTTOM_ROW_OFFSET_MULTIPLIER: f32 = 0.3;
// Gap between the right of the board and the first row of buttons
pub const RIGHT_COLUMN_OFFSET_MULTIPLIER: f32 = 0.3;
// Gap between the top of the board and the heading
pub const TOP_HEADING_OFFSET_MULTIPLIER: f32 = 0.5;
pub const HEADING_TEXT: &str = "Solitaire Chess";
pub const RESET_BUTTON_TEXT: &str = "RESET";
pub const NEXT_BUTTON_TEXT: &str = "NEXT";
pub const RULES_BUTTON_TEXT: &str = "RULES";
pub const RULES_BUTTON_ALT_TEXT: &str = "CLOSE";
pub const EASY_BUTTON_TEXT: &str = "EASY";
pub const MEDIUM_BUTTON_TEXT: &str = "MEDIUM";
pub const HARD_BUTTON_TEXT: &str = "HARD";
pub const CUSTOM_BUTTON_TEXT: &str = "CUSTOM";
pub const GENERATE_BUTTON_TEXT: &str = "GENERATE";
pub const PIECES_LABEL_TEXT: &str = "Pieces";
pub const AGE_LABEL_TEXT: &str = " Age";
+223
View File
@@ -0,0 +1,223 @@
use super::{Game, constants};
use crate::{
game::{self, GameMode},
widgets::*,
};
use macroquad::{math, prelude::*};
impl Game {
fn initialize_scene(&mut self, new_width: f32, new_height: f32) {
self.window_height = new_height;
self.window_width = new_width;
let min_dimension = f32::min(self.window_height, self.window_width);
let square_width = constants::BOARD_SQUARE_WIDTH_MULTIPLIER * min_dimension;
let num_squares = w_board!(self, board).num_squares;
let board_width = square_width * num_squares as f32;
let board_x = (self.window_width * 0.6 - board_width) / 2.0;
let board_y = (self.window_height - board_width) / 2.0;
let heading_font_size = constants::HEADING_FONT_SIZE_MULTIPLIER * min_dimension;
let f = heading_font_size.floor() as u16;
let heading_text_dims = measure_text(
w_label!(self, label_heading).text.as_str(),
Some(&self.resources.font()),
f,
1.0,
);
let heading_rect = Rect::new(
(self.window_width - heading_text_dims.width) / 2.0,
board_y
- heading_text_dims.height
- constants::TOP_HEADING_OFFSET_MULTIPLIER * square_width,
heading_text_dims.width,
heading_text_dims.height,
);
w_label!(self, label_heading).place(heading_rect, heading_font_size, &self.resources);
let board_rect = Rect::new(board_x, board_y, board_width, board_width);
w_board!(self, board).place(square_width, board_rect, heading_font_size * 0.4);
// Buttons
let btn_h = constants::BUTTON_HEIGHT_MULTIPLIER * min_dimension;
let btn_w = constants::BUTTON_WIDTH_MULTIPLIER * board_width;
let btn_font_size = heading_font_size * 0.3;
// Bottom row
let bottom_row_y =
board_width + board_y + constants::BOTTOM_ROW_OFFSET_MULTIPLIER * square_width;
w_button!(self, btn_rules).place(
Rect::new(
board_x + (square_width - btn_w) / 2.,
bottom_row_y,
btn_w,
btn_h,
),
FontSize::Fixed(btn_font_size),
);
w_button!(self, btn_reset).place(
Rect::new(
board_x + square_width + (square_width - btn_w) / 2.,
bottom_row_y,
btn_w,
btn_h,
),
FontSize::Fixed(btn_font_size),
);
let btn_next_x_offset = (board_width - square_width) + (square_width - btn_w) / 2.;
w_button!(self, btn_next).place(
Rect::new(board_x + btn_next_x_offset, bottom_row_y, btn_w, btn_h),
FontSize::Fixed(btn_font_size),
);
// Right column
let right_column_x =
board_x + board_width + constants::RIGHT_COLUMN_OFFSET_MULTIPLIER * square_width;
self.get_game_mode_button(&GameMode::Easy).place(
Rect::new(
right_column_x,
board_y + (square_width - btn_h) / 2.,
btn_w,
btn_h,
),
FontSize::Fixed(btn_font_size),
);
self.get_game_mode_button(&GameMode::Medium).place(
Rect::new(
right_column_x,
board_y + square_width + (square_width - btn_h) / 2.,
btn_w,
btn_h,
),
FontSize::Fixed(btn_font_size),
);
self.get_game_mode_button(&GameMode::Hard).place(
Rect::new(
right_column_x,
board_y + 2. * square_width + (square_width - btn_h) / 2.,
btn_w,
btn_h,
),
FontSize::Fixed(btn_font_size),
);
self.get_game_mode_button(&GameMode::Custom).place(
Rect::new(
right_column_x,
board_y + 3. * square_width + (square_width - btn_h) / 2.,
btn_w,
btn_h,
),
FontSize::Fixed(btn_font_size),
);
// Right column 2
let right_column_2_x =
right_column_x + btn_w + constants::RIGHT_COLUMN_OFFSET_MULTIPLIER * square_width;
w_button!(self, btn_generate).place(
Rect::new(
right_column_2_x,
board_y + board_width - square_width + (square_width - btn_h) / 2.,
2. * btn_w,
btn_h,
),
FontSize::Fixed(btn_font_size),
);
w_label!(self, label_pieces).place(
Rect::new(
right_column_2_x,
board_y + (square_width + btn_h) / 2.,
btn_w,
btn_h,
),
0.30 * btn_h,
&self.resources,
);
w_counter!(self, counter_pieces).place(
Rect::new(
right_column_2_x,
board_y + square_width + (2. * square_width - 2. * btn_h) / 2.,
btn_w,
2. * btn_h,
),
0.50 * btn_h,
&self.resources,
);
// Right column 3
let right_column_3_x = right_column_2_x + btn_w;
w_label!(self, label_max_age).place(
Rect::new(
right_column_2_x + btn_w,
board_y + (square_width + btn_h) / 2.,
btn_w,
btn_h,
),
0.30 * btn_h,
&self.resources,
);
w_counter!(self, counter_moves).place(
Rect::new(
right_column_3_x,
board_y + square_width + (2. * square_width - 2. * btn_h) / 2.,
btn_w,
2. * btn_h,
),
0.50 * btn_h,
&self.resources,
);
}
pub fn draw(&mut self) {
self.update_window_size();
for (_, widget) in &mut self.widgets {
widget.draw(&self.resources, &self.settings);
}
self.draw_fps();
}
fn get_game_mode_button<'a>(&'a mut self, mode: &GameMode) -> &'a mut ButtonWidget {
let id = self.game_mode_btns.get(mode).unwrap();
game::button(&mut self.widgets, *id)
}
fn draw_fps(&self) {
if self.settings.debug {
let fps = get_fps();
draw_text(
&format!("FPS: {}", fps),
10.0,
screen_height() - 20.0,
20.0,
BLACK,
);
}
}
fn update_window_size(&mut self) {
let new_height = math::clamp(
screen_height(),
constants::SCREEN_HEIGHT_MIN,
constants::SCREEN_HEIGHT_MAX,
);
let new_width = math::clamp(
screen_width(),
constants::SCREEN_WIDTH_MIN,
constants::SCREEN_WIDTH_MAX,
);
if new_height == self.window_height && new_width == self.window_width {
return;
}
self.initialize_scene(new_width, new_height);
}
}
+320
View File
@@ -0,0 +1,320 @@
use std::collections::HashMap;
use super::{Game, GameMode, Settings, constants};
use crate::{
game::{self, Command},
resources::{self, *},
widgets::*,
};
use macroquad::prelude::*;
use sol_lib::{
board::BoardState,
generator::{self, Puzzle, RandomRange},
};
impl Game {
pub async fn new() -> Self {
let resources = resources::init().await;
let game_mode = GameMode::Medium;
let settings = Settings {
volume: constants::VOLUME,
max_age: 3,
max_pieces: 2,
..Default::default()
};
let puzzle = generate_new_puzzle(game_mode, &settings);
let game_mode = GameMode::Medium;
let show_rules = false;
let mut widgets = HashMap::new();
let w_label_heading = LabelWidget::new(constants::HEADING_TEXT);
let label_heading = add_widget(&mut widgets, Widget::Label(w_label_heading));
let w_label_pieces = LabelWidget::new(constants::PIECES_LABEL_TEXT);
let label_pieces = add_widget(&mut widgets, Widget::Label(w_label_pieces));
let w_label_max_age = LabelWidget::new(constants::AGE_LABEL_TEXT);
let label_max_age = add_widget(&mut widgets, Widget::Label(w_label_max_age));
let mut w_counter_pieces =
CounterWidget::new(2, 7, settings.max_pieces, false, UiColor::Yellow);
w_counter_pieces.on_increment = |c| Command::UpdateMaxPieceCount(c.current);
w_counter_pieces.on_decrement = |c| Command::UpdateMaxPieceCount(c.current);
let counter_pieces = add_widget(&mut widgets, Widget::Counter(w_counter_pieces));
let mut w_counter_moves =
CounterWidget::new(1, 7, settings.max_age, false, UiColor::Yellow);
w_counter_moves.on_increment = |c| Command::UpdateMaxAge(c.current);
w_counter_moves.on_decrement = |c| Command::UpdateMaxAge(c.current);
let counter_moves = add_widget(&mut widgets, Widget::Counter(w_counter_moves));
let mut w_btn_reset = ButtonWidget::new(
true,
UiColor::Yellow,
constants::RESET_BUTTON_TEXT,
SoundKind::Button,
);
w_btn_reset.on_click = |_| Command::ResetBoard;
let btn_reset = add_widget(&mut widgets, Widget::Button(w_btn_reset));
let mut w_btn_next = ButtonWidget::new(
false,
UiColor::Green,
constants::NEXT_BUTTON_TEXT,
SoundKind::Button,
);
w_btn_next.on_click = |_| Command::NextPuzzle;
let btn_next = add_widget(&mut widgets, Widget::Button(w_btn_next));
let mut w_btn_rules = ButtonWidget::new(
true,
UiColor::Brown,
constants::RULES_BUTTON_TEXT,
SoundKind::Button,
);
w_btn_rules.on_click = |_| Command::ToggleRules;
let btn_rules = add_widget(&mut widgets, Widget::Button(w_btn_rules));
let mut w_btn_generate = ButtonWidget::new(
false,
UiColor::Pink,
constants::GENERATE_BUTTON_TEXT,
SoundKind::Button,
);
w_btn_generate.on_click = |_| Command::NewCustomPuzzle;
let btn_generate = add_widget(&mut widgets, Widget::Button(w_btn_generate));
let mut game_mode_btns = HashMap::new();
let mut w_game_mode_easy_btn = ButtonWidget::new(
true,
UiColor::Yellow,
constants::EASY_BUTTON_TEXT,
SoundKind::Mode,
);
w_game_mode_easy_btn.on_click = |_| Command::SwitchGameMode(GameMode::Easy);
game_mode_btns.insert(
GameMode::Easy,
add_widget(&mut widgets, Widget::Button(w_game_mode_easy_btn)),
);
let mut w_game_mode_medium_btn = ButtonWidget::new(
true,
UiColor::Yellow,
constants::MEDIUM_BUTTON_TEXT,
SoundKind::Mode,
);
w_game_mode_medium_btn.on_click = |_| Command::SwitchGameMode(GameMode::Medium);
game_mode_btns.insert(
GameMode::Medium,
add_widget(&mut widgets, Widget::Button(w_game_mode_medium_btn)),
);
let mut w_game_mode_hard_btn = ButtonWidget::new(
true,
UiColor::Yellow,
constants::HARD_BUTTON_TEXT,
SoundKind::Mode,
);
w_game_mode_hard_btn.on_click = |_| Command::SwitchGameMode(GameMode::Hard);
game_mode_btns.insert(
GameMode::Hard,
add_widget(&mut widgets, Widget::Button(w_game_mode_hard_btn)),
);
let mut w_game_mode_custom_btn = ButtonWidget::new(
true,
UiColor::Blue,
constants::CUSTOM_BUTTON_TEXT,
SoundKind::Mode,
);
w_game_mode_custom_btn.on_click = |_| Command::SwitchGameMode(GameMode::Custom);
game_mode_btns.insert(
GameMode::Custom,
add_widget(&mut widgets, Widget::Button(w_game_mode_custom_btn)),
);
let active_game_mode_btn = game_mode_btns.get(&game_mode).unwrap();
game::button(&mut widgets, *active_game_mode_btn).is_active = false;
let mut w_board = BoardWidget::new(puzzle.board.size, puzzle.board.clone());
w_board.on_board_state_changed = |s| Command::UpdateBoard(s.new_board_state);
let board = add_widget(&mut widgets, Widget::Board(w_board));
let mut w_controls = ControlsWidget::new();
w_controls.add(KeyCode::Q, Command::Quit);
w_controls.add(KeyCode::Escape, Command::HideRules);
w_controls.add(KeyCode::D, Command::ToggleDebug);
add_widget(&mut widgets, Widget::Controls(w_controls));
Game {
puzzle,
board,
resources,
settings,
game_mode,
game_mode_btns,
btn_reset,
btn_next,
btn_rules,
show_rules,
widgets,
counter_pieces,
counter_moves,
btn_generate,
label_pieces,
label_max_age,
label_heading,
..Default::default()
}
}
pub fn update(&mut self, interaction: Interaction) {
let command = self.map_to_command(interaction);
self.update_state(command);
}
fn map_to_command(&mut self, interaction: Interaction) -> Command {
for (_, widget) in &mut self.widgets {
if let Some(command) = widget.update(&interaction, &self.resources, &self.settings) {
return command;
}
}
return Command::NoInteraction;
}
fn update_state(&mut self, command: Command) {
match command {
Command::NoInteraction => (),
Command::UpdateBoard(new_board_state) => self.update_board(new_board_state),
Command::SwitchGameMode(mode) => self.switch_game_mode(mode),
Command::ToggleDebug => self.settings.debug = !self.settings.debug,
Command::HideRules => self.hide_rules(),
Command::ResetBoard => self.reset(),
Command::NextPuzzle => self.next_puzzle(),
Command::ToggleRules => self.toggle_rules(),
Command::UpdateMaxPieceCount(new_value) => self.settings.max_pieces = new_value,
Command::UpdateMaxAge(new_value) => self.settings.max_age = new_value,
Command::NewCustomPuzzle => self.new_custom_puzzle(),
Command::Quit => std::process::exit(0),
}
}
fn new_custom_puzzle(&mut self) {
if self.game_mode == GameMode::Custom {
self.next_puzzle();
}
}
fn toggle_rules(&mut self) {
self.show_rules = !self.show_rules;
w_board!(self, board).show_rules = self.show_rules;
w_button!(self, btn_rules).text = match self.show_rules {
true => constants::RULES_BUTTON_ALT_TEXT.to_string(),
false => constants::RULES_BUTTON_TEXT.to_string(),
};
}
fn hide_rules(&mut self) {
if self.show_rules {
play_sound_once(
&self.resources.sound(&SoundKind::Button),
self.settings.volume,
);
self.show_rules = false;
w_board!(self, board).show_rules = false;
w_button!(self, btn_rules).text = constants::RULES_BUTTON_TEXT.to_string();
}
}
fn switch_game_mode(&mut self, mode: GameMode) {
self.game_mode = mode;
for (mode, btn_id) in &mut self.game_mode_btns {
// The active mode button is 'disabled' to indicate it is active
let is_active = !(*mode == self.game_mode);
game::button(&mut self.widgets, *btn_id).is_active = is_active;
}
let is_custom_game_mode = self.game_mode == GameMode::Custom;
w_button!(self, btn_generate).is_active = is_custom_game_mode;
w_counter!(self, counter_pieces).set_active(is_custom_game_mode);
w_counter!(self, counter_moves).set_active(is_custom_game_mode);
self.next_puzzle();
}
fn update_board(&mut self, new_board_state: BoardState) {
match new_board_state {
BoardState::Won => {
w_button!(self, btn_next).is_active = true;
}
_ => {}
}
}
fn reset(&mut self) {
self.reset_game(Default::default());
}
fn reset_game(&mut self, options: ResetOptions) {
if options.create_new_puzzle {
self.puzzle = generate_new_puzzle(self.game_mode, &self.settings);
}
w_button!(self, btn_next).is_active = false;
w_board!(self, board).reset(&self.puzzle);
}
fn next_puzzle(&mut self) {
self.reset_game(ResetOptions {
create_new_puzzle: true,
..Default::default()
});
}
}
fn add_widget(widgets: &mut HashMap<WidgetId, Widget>, w: Widget) -> WidgetId {
let id = rand::rand() as u64;
let widget_id = WidgetId(id);
widgets.insert(widget_id, w);
widget_id
}
fn generate_new_puzzle(mode: GameMode, settings: &Settings) -> Puzzle {
let piece_count = match mode {
GameMode::Easy => 3,
GameMode::Medium => 5,
GameMode::Hard => 7,
GameMode::Custom => settings.max_pieces,
};
let max_moves_per_piece = match mode {
GameMode::Easy => 2,
GameMode::Medium => 2,
GameMode::Hard => 2,
GameMode::Custom => settings.max_age,
};
let generated = generator::generate_weighted_random(
piece_count,
100,
max_moves_per_piece,
&MacroquadRandAdapter,
);
let puzzle = generated.puzzle();
puzzle.expect("No puzzle was generated")
}
#[derive(Default)]
struct ResetOptions {
create_new_puzzle: bool,
}
pub struct MacroquadRandAdapter;
impl RandomRange for MacroquadRandAdapter {
fn gen_range(&self, min: usize, max: usize) -> usize {
rand::gen_range(min, max)
}
}
+66
View File
@@ -0,0 +1,66 @@
mod game;
mod resources;
mod widgets;
use std::collections::HashSet;
use macroquad::prelude::*;
use miniquad::date;
use game::constants;
use crate::{game::Game, widgets::Interaction};
fn window_conf() -> Conf {
let window_title = {
if cfg!(debug_assertions) {
"Debug: Puzzle Game"
} else {
constants::WINDOW_TITLE
}
};
Conf {
window_title: String::from(window_title),
fullscreen: false,
..Default::default()
}
}
#[macroquad::main(window_conf)]
async fn main() {
rand::srand(date::now() as u64);
let background_color = Color::from_rgba(196, 195, 208, 255);
let mut game = Game::new().await;
loop {
clear_background(background_color);
game.update(get_interaction());
game.draw();
next_frame().await;
}
}
fn get_interaction() -> Interaction {
let mut mouse_pressed = HashSet::new();
let mut mouse_released = HashSet::new();
let buttons = vec![MouseButton::Left, MouseButton::Right];
for btn in buttons {
if is_mouse_button_pressed(btn) {
mouse_pressed.insert(btn);
}
if is_mouse_button_released(btn) {
mouse_released.insert(btn);
}
}
let (mx, my) = mouse_position();
Interaction {
mouse_pos: Circle::new(mx, my, 0.),
keys_released: get_keys_released(),
mouse_pressed,
mouse_released,
}
}
+137
View File
@@ -0,0 +1,137 @@
use macroquad::{
audio::{self, Sound},
prelude::*,
};
use sol_lib::board::piece::{Piece, PieceKind};
use std::collections::HashMap;
#[derive(Default)]
pub struct Resources {
texture_pieces: Option<Texture2D>,
texture_rects_pieces: HashMap<PieceKind, Rect>,
texture_rects_inactive_pieces: HashMap<PieceKind, Rect>,
sounds: HashMap<SoundKind, Sound>,
font: Option<Font>,
}
pub struct Texture<'a> {
pub texture: &'a Texture2D,
pub texture_rect: Rect,
}
macro_rules! load_sound {
($file_name:expr) => {
audio::load_sound_from_bytes(include_bytes!($file_name))
.await
.unwrap()
};
}
pub async fn init() -> Resources {
// SOUNDS
let click = load_sound!("../assets/click.wav");
let win = load_sound!("../assets/win.wav");
let loss = load_sound!("../assets/loss.wav");
let button = load_sound!("../assets/button.wav");
let mode = load_sound!("../assets/mode.wav");
let mut sounds = HashMap::new();
sounds.insert(SoundKind::Click, click);
sounds.insert(SoundKind::Win, win);
sounds.insert(SoundKind::Loss, loss);
sounds.insert(SoundKind::Button, button);
sounds.insert(SoundKind::Mode, mode);
// FONT
let font_ttf = include_bytes!("../assets/Junction-regular.otf");
let font = load_ttf_font_from_bytes(font_ttf).expect("Failed to load font");
// TEXTURES
let texture_pieces_bytes = include_bytes!("../assets/pieces.png");
let texture_pieces = Texture2D::from_file_with_format(&texture_pieces_bytes[..], None);
texture_pieces.set_filter(FilterMode::Nearest);
build_textures_atlas();
let texture_rects_pieces = generate_texture_rects_pieces(true);
let texture_rects_inactive_pieces = generate_texture_rects_pieces(false);
Resources {
texture_pieces: Some(texture_pieces),
texture_rects_pieces,
texture_rects_inactive_pieces,
sounds,
font: Some(font),
}
}
fn generate_texture_rects_pieces(active: bool) -> HashMap<PieceKind, Rect> {
let mut texture_rects_pieces = HashMap::new();
texture_rects_pieces.insert(PieceKind::Pawn, piece_texture_rect(PieceKind::Pawn, active));
texture_rects_pieces.insert(
PieceKind::Knight,
piece_texture_rect(PieceKind::Knight, active),
);
texture_rects_pieces.insert(
PieceKind::Bishop,
piece_texture_rect(PieceKind::Bishop, active),
);
texture_rects_pieces.insert(PieceKind::Rook, piece_texture_rect(PieceKind::Rook, active));
texture_rects_pieces.insert(
PieceKind::Queen,
piece_texture_rect(PieceKind::Queen, active),
);
texture_rects_pieces.insert(PieceKind::King, piece_texture_rect(PieceKind::King, active));
texture_rects_pieces
}
fn piece_texture_rect(piece: PieceKind, active: bool) -> Rect {
let index = match piece {
PieceKind::Pawn => 0,
PieceKind::Knight => 1,
PieceKind::Bishop => 2,
PieceKind::Rook => 3,
PieceKind::Queen => 4,
PieceKind::King => 5,
};
let color = match active {
true => 1,
false => 0,
};
Rect::new(index as f32 * 128.0, color as f32 * 128.0, 128.0, 128.0)
}
#[derive(Eq, Copy, Clone, PartialEq, Hash)]
pub enum SoundKind {
Click,
Win,
Loss,
Button,
Mode,
}
impl Resources {
pub fn get_piece_texture<'a>(&'a self, piece: &Piece) -> Texture<'a> {
let texture = self.texture_pieces.as_ref().unwrap();
let texture_rects_lookup = if piece.active {
&self.texture_rects_pieces
} else {
&self.texture_rects_inactive_pieces
};
let texture_rect = texture_rects_lookup.get(&piece.kind).unwrap();
Texture {
texture: texture,
texture_rect: *texture_rect,
}
}
pub fn font(&self) -> &Font {
self.font.as_ref().unwrap()
}
pub fn sound(&self, kind: &SoundKind) -> &Sound {
self.sounds.get(kind).unwrap()
}
}
+226
View File
@@ -0,0 +1,226 @@
use std::collections::{HashMap, HashSet};
mod board;
mod button;
mod controls;
mod counter;
mod label;
pub use board::*;
pub use button::*;
pub use controls::*;
pub use counter::*;
pub use label::*;
use macroquad::{
audio::{self, Sound},
prelude::*,
};
use quad_snd::PlaySoundParams;
use crate::{
game::{Command, Settings},
resources::Resources,
};
pub struct Interaction {
pub mouse_pos: Circle,
pub keys_released: HashSet<KeyCode>,
pub mouse_pressed: HashSet<MouseButton>,
pub mouse_released: HashSet<MouseButton>,
}
#[derive(Eq, PartialEq, Copy, Clone, Hash, Default)]
pub struct WidgetId(pub u64);
pub enum Widget {
Label(LabelWidget),
Counter(CounterWidget),
Button(ButtonWidget),
Board(BoardWidget),
Controls(ControlsWidget),
}
impl Widget {
pub fn draw(&mut self, resources: &Resources, settings: &Settings) {
match self {
Widget::Label(label) => label.draw(&resources),
Widget::Counter(counter) => counter.draw(&resources),
Widget::Button(btn) => btn.draw(&resources),
Widget::Board(board) => board.draw(&resources, &settings),
Widget::Controls(_) => (),
}
}
pub fn update(
&mut self,
interaction: &Interaction,
resources: &Resources,
settings: &Settings,
) -> Option<Command> {
match self {
Widget::Label(_) => None,
Widget::Counter(c) => handle_counter_input(interaction, c, resources, settings),
Widget::Button(b) => handle_button_input(interaction, b, resources, settings),
Widget::Board(b) => handle_board_input(interaction, b, resources, settings),
Widget::Controls(c) => c.handle_input(interaction),
}
}
}
fn handle_board_input(
interaction: &Interaction,
b: &mut BoardWidget,
resources: &Resources,
settings: &Settings,
) -> Option<Command> {
let board_interaction = b.handle_input(interaction, resources, settings);
let Some(board_interaction) = board_interaction else {
return None;
};
Some((b.on_board_state_changed)(board_interaction))
}
fn handle_counter_input(
interaction: &Interaction,
counter: &mut CounterWidget,
resources: &Resources,
settings: &Settings,
) -> Option<Command> {
let counter_interaction = counter.handle_input(interaction, resources, settings);
let Some(interaction_type) = counter_interaction.interaction_type else {
return None;
};
let command = match interaction_type {
CounterInteractionType::Increment => (counter.on_increment)(counter_interaction),
CounterInteractionType::Decrement => (counter.on_decrement)(counter_interaction),
};
Some(command)
}
fn handle_button_input(
interaction: &Interaction,
button: &mut ButtonWidget,
resources: &Resources,
settings: &Settings,
) -> Option<Command> {
let Some(ButtonInteraction { clicked: true }) =
button.handle_input(interaction, resources, settings)
else {
return None;
};
return Some((button.on_click)(button));
}
macro_rules! get_widget_fn {
($name:ident, $ret:ty, $variant:path) => {
pub fn $name(widgets: &mut HashMap<WidgetId, Widget>, id: WidgetId) -> &mut $ret {
match widgets.get_mut(&id) {
Some(w) => match w {
$variant(v) => return v,
_ => panic!("Widget with id {} not found", id.0),
},
None => panic!("Widget with id {} not found", id.0),
}
}
};
}
get_widget_fn!(label, LabelWidget, Widget::Label);
get_widget_fn!(button, ButtonWidget, Widget::Button);
get_widget_fn!(counter, CounterWidget, Widget::Counter);
get_widget_fn!(board, BoardWidget, Widget::Board);
macro_rules! widget_accessor {
($name:ident, $fn:ident) => {
macro_rules! $name {
($this:expr, $widget_id:ident) => {
game::$fn(&mut $this.widgets, $this.$widget_id)
};
}
pub(crate) use $name;
};
}
widget_accessor!(w_button, button);
widget_accessor!(w_label, label);
widget_accessor!(w_counter, counter);
widget_accessor!(w_board, board);
#[allow(unused)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UiColor {
Grey,
Green,
Pink,
Brown,
Yellow,
Blue,
}
impl UiColor {
pub fn to_bg_color(&self) -> Color {
match self {
UiColor::Grey => Color::from_rgba(140, 140, 140, 200),
UiColor::Green => Color::from_rgba(16, 60, 50, 200),
UiColor::Pink => Color::from_rgba(255, 60, 60, 200),
UiColor::Brown => Color::from_rgba(123, 61, 35, 200),
UiColor::Yellow => Color::from_rgba(242, 230, 190, 200),
UiColor::Blue => Color::from_rgba(47, 85, 172, 200),
}
}
pub fn to_fg_color(&self) -> Color {
match self {
UiColor::Grey => Color::from_rgba(255, 255, 255, 200),
UiColor::Green => Color::from_rgba(255, 255, 255, 200),
UiColor::Pink => Color::from_rgba(255, 255, 255, 200),
UiColor::Brown => Color::from_rgba(255, 255, 255, 200),
UiColor::Yellow => Color::from_rgba(0, 0, 0, 200),
UiColor::Blue => Color::from_rgba(255, 255, 255, 200),
}
}
pub fn to_shadow_color(&self) -> Color {
let bg_color = self.to_bg_color();
Color::from_rgba(
(bg_color.r * 255.) as u8,
(bg_color.g * 255.) as u8,
(bg_color.b * 255.) as u8,
100,
)
}
}
pub fn draw_shadow(rect: &Rect, shadow_width: f32) {
let shadow_color = Color::new(0., 0., 0., 0.8);
draw_rectangle(
rect.x + rect.w,
rect.y + shadow_width,
shadow_width,
rect.h,
shadow_color,
);
draw_rectangle(
rect.x + shadow_width,
rect.y + rect.h,
rect.w - shadow_width,
shadow_width,
shadow_color,
);
}
pub fn play_sound_once(sound: &Sound, volume: f32) {
audio::play_sound(
sound,
PlaySoundParams {
looped: false,
volume,
},
);
}
+447
View File
@@ -0,0 +1,447 @@
use core::fmt;
use std::fmt::{Display, Formatter};
use crate::{
game::{Command, Settings, constants},
resources::{Resources, SoundKind},
widgets::*,
};
use sol_lib::{
board::{Board, BoardState, piece::Piece},
generator::Puzzle,
};
pub struct BoardWidget {
pub num_squares: usize, // this is a constant = 4 for now.
pub show_rules: bool,
pub on_board_state_changed: fn(BoardInteraction) -> Command,
current_board: Board,
square_width: f32,
rules_font_size: f32,
board_rect: Rect,
squares: Vec<GameSquare>,
state: GameState,
}
pub struct BoardInteraction {
pub new_board_state: BoardState,
}
impl BoardWidget {
pub fn new(num_squares: usize, current_board: Board) -> BoardWidget {
let mut board = BoardWidget::default();
board.num_squares = num_squares;
board.current_board = current_board;
board
}
pub fn place(&mut self, square_width: f32, board_rect: Rect, rules_font_size: f32) {
self.square_width = square_width;
self.board_rect = board_rect;
self.rules_font_size = rules_font_size;
let dark = UiColor::Brown.to_bg_color();
let light = UiColor::Yellow.to_bg_color();
let mut rects = Vec::new();
for i in 0..self.num_squares {
for j in 0..self.num_squares {
let x_eff = self.board_rect.x + (i as f32 * self.square_width);
let y_eff = self.board_rect.y + (j as f32 * self.square_width);
let rect = Rect::new(x_eff, y_eff, self.square_width, self.square_width);
let color = match (i + j) % 2 {
1 => dark,
_ => light,
};
rects.push(GameSquare {
rect,
color,
i,
j,
is_source: false,
is_target: false,
is_previous_target: false,
});
}
}
self.squares = rects;
}
pub fn draw(&self, resources: &Resources, settings: &Settings) {
let board_shadow_width = constants::BOARD_SHADOW_MULTIPLIER * self.square_width;
draw_shadow(&self.board_rect, board_shadow_width);
if self.show_rules {
draw_rectangle(
self.board_rect.x,
self.board_rect.y,
self.board_rect.w,
self.board_rect.h,
UiColor::Yellow.to_bg_color(),
);
let rules = "\
Every move should be a \n\
capture. Win when only \n\
one piece is left.\n\
Age: Each piece can only \n\
move 'age' times";
let measurement = measure_text(
rules,
Some(resources.font()),
self.rules_font_size as u16,
1.0,
);
let draw_text_params = TextParams {
font_size: self.rules_font_size as u16,
font: Some(resources.font()),
color: UiColor::Brown.to_bg_color(),
..Default::default()
};
draw_multiline_text_ex(
rules,
self.board_rect.x + 0.5 * self.square_width,
self.board_rect.y + 0.4 * (self.board_rect.h - measurement.height)
- 2. * measurement.offset_y,
Some(2.),
draw_text_params,
);
return;
}
let sprite_size = constants::BOARD_PIECE_WIDTH_MULTIPLIER * self.square_width;
let mut selected_square = None;
self.squares.iter().for_each(|square| {
let color = match square.is_source {
true => square.color,
false => match square.is_target {
true => UiColor::Pink.to_shadow_color(),
false => square.color,
},
};
draw_rectangle(
square.rect.x,
square.rect.y,
square.rect.w,
square.rect.h,
color,
);
if let Some(p) = &self.current_board.cells[square.i][square.j] {
let offset = (square.rect.w - sprite_size) / 2.0;
let texture_params = piece_draw_texture_params(&p, sprite_size, resources);
if !square.is_source {
draw_texture_ex(
texture_params.texture,
square.rect.x + offset,
square.rect.y + offset,
WHITE,
texture_params.draw_text_params,
);
} else {
selected_square = Some(square);
}
}
});
if let Some(selected_square) = selected_square {
if let Some(p) = self.current_board.cells[selected_square.i][selected_square.j] {
let texture_params = piece_draw_texture_params(&p, sprite_size, resources);
draw_texture_ex(
texture_params.texture,
mouse_position().0 - sprite_size / 2.0,
mouse_position().1 - sprite_size / 2.0,
WHITE,
texture_params.draw_text_params,
);
}
}
if settings.debug {
self.draw_debug();
}
}
pub fn handle_input(
&mut self,
interaction: &Interaction,
resources: &Resources,
settings: &Settings,
) -> Option<BoardInteraction> {
let mouse_pos = interaction.mouse_pos;
if mouse_pos.overlaps_rect(&self.board_rect) == false {
return None;
}
let old_board_state = self.current_board.state;
if is_mouse_button_released(MouseButton::Left) {
let current_state = self.state.clone();
let new_state = match current_state {
GameState::SelectSource(previous_target) => {
self.handle_select_source(mouse_pos, previous_target)
}
GameState::SelectTarget(source) => {
let next = self.handle_select_target(mouse_pos, source, resources, settings);
if let GameState::SelectTarget(_) = next {
self.reset_squares();
GameState::SelectSource(None)
} else {
next
}
}
GameState::GameOver(previous_target) => GameState::GameOver(previous_target),
};
self.state = new_state;
}
if is_mouse_button_pressed(MouseButton::Left) {
let current_state = self.state.clone();
let new_state = match current_state {
GameState::SelectSource(previous_target) => {
self.handle_select_source(mouse_pos, previous_target)
}
GameState::SelectTarget(source) => GameState::SelectTarget(source),
GameState::GameOver(previous_target) => GameState::GameOver(previous_target),
};
self.state = new_state;
}
if old_board_state == self.current_board.state {
return None;
}
let board_state = self.current_board.state;
return Some(BoardInteraction {
new_board_state: board_state,
});
}
fn get(&mut self, i: usize, j: usize) -> &mut GameSquare {
&mut self.squares[i * self.num_squares + j]
}
fn draw_debug(&self) {
let mut debug_lines = vec![];
let (mx, my) = mouse_position();
let hover_square = self.squares.iter().find(|s| {
let c = Circle::new(mx, my, 0.0);
if c.overlaps_rect(&s.rect) {
return true;
}
return false;
});
debug_lines.push(format!("Game State: {}", self.state));
debug_lines.push(format!("Board State: {}", self.current_board.state));
if let Some(hover_square) = hover_square {
debug_lines.push(format!("Hover: [ {}, {} ]", hover_square.i, hover_square.j));
}
self.add_debug_info(debug_lines);
}
fn add_debug_info(&self, lines: Vec<String>) {
let mut y = 20.0;
for line in lines {
draw_text(&line, 10.0, y, 20.0, BLACK);
y += 25.0;
}
}
pub fn reset(&mut self, puzzle: &Puzzle) {
self.current_board = puzzle.board.clone();
self.state = GameState::SelectSource(None);
self.reset_squares();
}
fn handle_select_source(
&mut self,
mouse_pos: Circle,
previous_target: Option<(usize, usize)>,
) -> GameState {
self.reset_squares();
let mut selected = None;
for square in &mut self.squares {
if mouse_pos.overlaps_rect(&square.rect) {
if let Some(_) = self.current_board.cells[square.i][square.j] {
selected = Some((square.i, square.j));
}
}
}
if let Some((i, j)) = selected {
self.get(i, j).is_source = true;
let mut target_squares = vec![];
for m in self.current_board.legal_moves.iter() {
if m.from.file == i && m.from.rank == j {
target_squares.push((m.to.file, m.to.rank));
}
}
for (i, j) in target_squares {
self.get(i, j).is_target = true;
}
return GameState::SelectTarget(selected.unwrap());
}
if let Some((i, j)) = previous_target {
self.get(i, j).is_previous_target = true;
}
return GameState::SelectSource(None);
}
fn handle_select_target(
&mut self,
mouse_pos: Circle,
source: (usize, usize),
resources: &Resources,
settings: &Settings,
) -> GameState {
let mut selected = None;
for square in &mut self.squares {
if mouse_pos.overlaps_rect(&square.rect) {
if let Some(_) = self.current_board.cells[square.i][square.j] {
selected = Some((square.i, square.j));
}
}
}
let (s_x, s_y) = source;
let Some((x, y)) = selected else {
self.get(s_x, s_y).is_source = true;
return GameState::SelectTarget(source);
};
if x == s_x && y == s_y {
self.get(s_x, s_y).is_source = true;
return GameState::SelectTarget(source);
}
let mut is_legal = false;
if self.get(x, y).is_target {
is_legal = true;
}
if is_legal {
let m = self.current_board.legal_moves.iter().find(|m| {
m.from.file == s_x && m.from.rank == s_y && m.to.file == x && m.to.rank == y
});
let m = m.expect("legal move should be found");
self.current_board.make_move(m.clone());
if self.current_board.state == BoardState::Won
|| self.current_board.state == BoardState::Lost
{
self.reset_squares();
if self.current_board.state == BoardState::Won {
play_sound_once(resources.sound(&SoundKind::Win), settings.volume);
} else {
play_sound_once(resources.sound(&SoundKind::Loss), settings.volume);
}
return GameState::GameOver((x, y));
}
self.reset_squares();
self.get(x, y).is_target = true;
play_sound_once(resources.sound(&SoundKind::Click), settings.volume);
return GameState::SelectSource(Some((x, y)));
}
self.reset_squares();
return GameState::SelectSource(None);
}
fn reset_squares(&mut self) {
for i in 0..self.num_squares {
for j in 0..self.num_squares {
self.get(i, j).is_source = false;
self.get(i, j).is_target = false;
}
}
}
}
struct GameSquare {
rect: Rect,
color: Color,
is_source: bool,
is_target: bool,
is_previous_target: bool,
i: usize,
j: usize,
}
#[derive(Copy, Clone)]
pub enum GameState {
SelectSource(Option<(usize, usize)>),
SelectTarget((usize, usize)),
GameOver((usize, usize)),
}
impl Default for GameState {
fn default() -> Self {
GameState::SelectSource(None)
}
}
impl Display for GameState {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
GameState::SelectSource(Some(x)) => write!(f, "Select Source [ {}, {} ]", x.0, x.1),
GameState::SelectSource(None) => write!(f, "Select Source [ ]"),
GameState::SelectTarget(x) => write!(f, "Select Target [ {}, {} ]", x.0, x.1),
GameState::GameOver(x) => write!(f, "Game Over [ {}, {} ]", x.0, x.1),
}
}
}
fn piece_draw_texture_params<'a>(
piece: &Piece,
sprite_size: f32,
resources: &'a Resources,
) -> PieceDrawTextureParams<'a> {
let texture = resources.get_piece_texture(&piece);
let dtp = DrawTextureParams {
source: Some(Rect::new(
texture.texture_rect.x,
texture.texture_rect.y,
texture.texture_rect.w,
texture.texture_rect.h,
)),
dest_size: Some(Vec2::new(sprite_size, sprite_size)),
..DrawTextureParams::default()
};
PieceDrawTextureParams {
texture: texture.texture,
draw_text_params: dtp,
}
}
struct PieceDrawTextureParams<'a> {
texture: &'a Texture2D,
draw_text_params: DrawTextureParams,
}
impl Default for BoardWidget {
fn default() -> Self {
Self {
on_board_state_changed: |_| Command::NoInteraction,
num_squares: Default::default(),
show_rules: Default::default(),
current_board: Default::default(),
square_width: Default::default(),
rules_font_size: Default::default(),
board_rect: Default::default(),
squares: Default::default(),
state: Default::default(),
}
}
}
+188
View File
@@ -0,0 +1,188 @@
use crate::{
game::{Command, Settings},
resources::{Resources, SoundKind},
widgets::*,
};
pub struct ButtonWidget {
pub is_active: bool,
pub text: String,
pub color: UiColor,
is_down: bool,
rect: Rect,
shadow_width: f32,
sound: SoundKind,
font_size: FontSize,
pub on_click: fn(&mut ButtonWidget) -> Command,
}
pub struct ButtonInteraction {
pub clicked: bool,
}
impl ButtonInteraction {
fn clicked() -> Self {
Self { clicked: true }
}
}
#[derive(Copy, Clone)]
pub enum FontSize {
Fixed(f32),
Fill,
}
impl ButtonWidget {
pub fn new(is_active: bool, color: UiColor, text: impl Into<String>, sound: SoundKind) -> Self {
Self {
is_active,
text: text.into(),
color,
shadow_width: 5.0,
sound,
..Default::default()
}
}
pub fn place(&mut self, rect: Rect, font_size: FontSize) {
self.rect = rect;
self.font_size = font_size;
}
pub fn draw(&self, resources: &Resources) {
self.draw_button(&self.color);
self.draw_label(&self.text, resources, &self.color);
}
fn draw_button(&self, color: &UiColor) {
let bg_color = match self.is_active {
true => color.to_bg_color(),
false => color.to_shadow_color(),
};
let button_draw_offset = self.get_button_draw_offset();
draw_rectangle(
self.rect.x + button_draw_offset,
self.rect.y + button_draw_offset,
self.rect.w,
self.rect.h,
bg_color,
);
self.draw_shadow();
}
fn draw_shadow(&self) {
if !self.is_active {
return;
}
if self.is_down {
return;
}
draw_shadow(&self.rect, self.shadow_width);
}
fn draw_label(&self, text: &str, resources: &Resources, color: &UiColor) {
let font_color = match self.is_active {
true => color.to_fg_color(),
false => Color::from_rgba(100, 100, 100, 255),
};
let font = resources.font();
let font_size = match self.font_size {
FontSize::Fixed(s) => s as u16,
FontSize::Fill => {
let target_w = self.rect.w * 0.9;
let target_h = self.rect.h;
let mut font_size = 22;
loop {
font_size -= 2;
let dims = measure_text(&text, Some(&font), font_size as u16, 1.0);
if dims.width <= target_w && dims.height <= target_h {
break;
}
}
font_size
}
};
let dims = measure_text(&text, Some(&font), font_size, 1.0);
let button_draw_offset = self.get_button_draw_offset();
let text_params = TextParams {
font_size,
color: font_color,
font: Some(&font),
..Default::default()
};
draw_text_ex(
&text,
self.rect.x + (self.rect.w - dims.width) * 0.5 + button_draw_offset,
self.rect.y + (self.rect.h - dims.height) * 0.5 + dims.offset_y + button_draw_offset,
text_params,
);
}
fn get_button_draw_offset(&self) -> f32 {
let button_pressed_correction = match self.is_down {
true => self.shadow_width,
false => match self.is_active {
true => 0.0,
false => self.shadow_width,
},
};
button_pressed_correction
}
pub fn handle_input(
&mut self,
interaction: &Interaction,
resources: &Resources,
settings: &Settings,
) -> Option<ButtonInteraction> {
if !self.is_active {
self.is_down = false;
return None;
}
let mouse = interaction.mouse_pos;
if interaction.mouse_pressed.contains(&MouseButton::Left) {
if mouse.overlaps_rect(&self.rect) {
self.is_down = true;
return None;
}
}
if interaction.mouse_released.contains(&MouseButton::Left) {
if mouse.overlaps_rect(&self.rect) {
let click_sound = resources.sound(&self.sound);
let volume = settings.volume;
play_sound_once(&click_sound, volume);
self.is_down = false;
return Some(ButtonInteraction::clicked());
}
self.is_down = false;
}
None
}
}
impl Default for ButtonWidget {
fn default() -> Self {
Self {
is_active: true,
is_down: Default::default(),
rect: Default::default(),
shadow_width: Default::default(),
color: UiColor::Grey,
text: String::new(),
sound: SoundKind::Click,
font_size: FontSize::Fill,
on_click: |_| Command::NoInteraction,
}
}
}
+31
View File
@@ -0,0 +1,31 @@
use std::collections::HashMap;
use macroquad::input::KeyCode;
use crate::{game::Command, widgets::Interaction};
pub struct ControlsWidget {
key_release_bindings: HashMap<KeyCode, Command>,
}
impl ControlsWidget {
pub fn new() -> Self {
Self {
key_release_bindings: HashMap::new(),
}
}
pub fn add(&mut self, key: KeyCode, command: Command) {
self.key_release_bindings.insert(key, command);
}
pub fn handle_input(&self, interaction: &Interaction) -> Option<Command> {
for (key, command) in &self.key_release_bindings {
if interaction.keys_released.contains(&key) {
return Some(*command);
}
}
None
}
}
+174
View File
@@ -0,0 +1,174 @@
use crate::{
game::{Command, Settings},
resources::{Resources, SoundKind},
widgets::*,
};
use macroquad::prelude::*;
pub struct CounterWidget {
visual_rect: Rect,
increment: ButtonWidget,
decrement: ButtonWidget,
pub on_increment: fn(CounterWidgetInteraction) -> Command,
pub on_decrement: fn(CounterWidgetInteraction) -> Command,
min: u32,
max: u32,
current: u32,
font_size: f32,
visual_dims: TextDimensions,
increment_dims: TextDimensions,
decrement_dims: TextDimensions,
is_active: bool,
}
pub struct CounterWidgetInteraction {
pub current: u32,
pub interaction_type: Option<CounterInteractionType>,
}
#[derive(Copy, Clone)]
pub enum CounterInteractionType {
Increment,
Decrement,
}
impl CounterWidgetInteraction {
fn decrement(current: u32) -> Self {
Self {
current,
interaction_type: Some(CounterInteractionType::Decrement),
}
}
fn increment(current: u32) -> Self {
Self {
current,
interaction_type: Some(CounterInteractionType::Increment),
}
}
fn not_interacted(current: u32) -> Self {
Self {
current,
interaction_type: None,
}
}
}
impl CounterWidget {
pub fn new(min: u32, max: u32, current: u32, is_active: bool, color: UiColor) -> Self {
let increment = ButtonWidget::new(is_active, color, "+", SoundKind::Click);
let decrement = ButtonWidget::new(is_active, color, "-", SoundKind::Click);
Self {
min,
max,
current,
is_active,
increment,
decrement,
..Default::default()
}
}
pub fn set_active(&mut self, is_active: bool) {
self.is_active = is_active;
self.increment.is_active = is_active;
self.decrement.is_active = is_active;
}
pub fn place(&mut self, total_area: Rect, font_size: f32, resources: &Resources) {
let margin_y = 0.05 * total_area.h;
let btn_h = total_area.h * 0.3;
let btn_w = total_area.w * 0.9;
let btn_x = total_area.x + (total_area.w - btn_w) / 2.;
let increment_y = total_area.y + margin_y;
let visual_y = increment_y + btn_h + margin_y;
let decrement_y = visual_y + btn_h + margin_y;
self.visual_rect = Rect::new(btn_x, visual_y, btn_w, btn_h);
let increment_rect = Rect::new(btn_x, increment_y, btn_w, btn_h);
self.increment.place(increment_rect, FontSize::Fill);
let decrement_rect = Rect::new(btn_x, decrement_y, btn_w, btn_h);
self.decrement.place(decrement_rect, FontSize::Fill);
self.increment_dims = measure_text("+", Some(resources.font()), font_size as u16, 1.0);
self.decrement_dims = measure_text("-", Some(resources.font()), font_size as u16, 1.0);
self.visual_dims = measure_text("0", Some(resources.font()), font_size as u16, 1.0);
self.font_size = font_size;
}
pub fn handle_input(
&mut self,
interaction: &Interaction,
resources: &Resources,
settings: &Settings,
) -> CounterWidgetInteraction {
if let Some(interaction) = self
.increment
.handle_input(&interaction, &resources, &settings)
{
if interaction.clicked {
if self.current < self.max {
self.current += 1;
return CounterWidgetInteraction::increment(self.current);
}
}
}
if let Some(interaction) = self
.decrement
.handle_input(&interaction, &resources, &settings)
{
if interaction.clicked {
if self.current > self.min {
self.current -= 1;
return CounterWidgetInteraction::decrement(self.current);
}
}
}
return CounterWidgetInteraction::not_interacted(self.current);
}
pub fn draw(&mut self, resources: &Resources) {
self.increment.draw(resources);
self.decrement.draw(resources);
let font_size = self.font_size as u16;
draw_text_ex(
&self.current.to_string(),
self.visual_rect.x + (self.visual_rect.w - self.visual_dims.width) / 2.,
self.visual_rect.y + (self.visual_rect.h + self.font_size) / 2.,
TextParams {
font: Some(resources.font()),
font_size,
color: UiColor::Yellow.to_fg_color(),
..Default::default()
},
);
}
}
impl Default for CounterWidget {
fn default() -> Self {
Self {
on_increment: |_| Command::NoInteraction,
on_decrement: |_| Command::NoInteraction,
visual_rect: Default::default(),
increment: Default::default(),
decrement: Default::default(),
min: Default::default(),
max: Default::default(),
current: Default::default(),
font_size: Default::default(),
visual_dims: Default::default(),
increment_dims: Default::default(),
decrement_dims: Default::default(),
is_active: Default::default(),
}
}
}
+43
View File
@@ -0,0 +1,43 @@
use macroquad::prelude::*;
use crate::resources::Resources;
#[derive(Default)]
pub struct LabelWidget {
rect: Rect,
font_size: f32,
text_dimensions: TextDimensions,
pub text: String,
}
impl LabelWidget {
pub fn new(text: &str) -> Self {
Self {
text: text.to_string(),
..Default::default()
}
}
pub fn place(&mut self, rect: Rect, font_size: f32, resources: &Resources) {
self.rect = rect;
self.font_size = font_size;
self.text_dimensions =
measure_text(&self.text, Some(resources.font()), font_size as u16, 1.0);
}
pub fn draw(&self, resources: &Resources) {
let draw_text_params = TextParams {
font: Some(&resources.font()),
font_size: self.font_size as u16,
color: BLACK,
..Default::default()
};
draw_text_ex(
&self.text,
self.rect.x,
self.rect.y + self.text_dimensions.offset_y,
draw_text_params,
);
}
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

+14
View File
@@ -0,0 +1,14 @@
[package]
name = "sol_lib"
version = "0.0.1"
edition = "2024"
[dev-dependencies]
rand = { workspace = true }
[profile.release]
opt-level = 's'
lto = true
codegen-units = 1
panic = 'abort'
strip = true
+274 -65
View File
@@ -14,18 +14,23 @@ use std::{
use cmove::CMove;
use constants::BOARD_SIZE;
use errors::SError;
use piece::Piece;
use piece::PieceKind;
use square::{Square, SquarePair};
#[derive(Clone)]
use crate::{board::piece::Piece, generator::Puzzle};
#[derive(Clone, Default)]
pub struct Board {
pub cells: [[Option<Piece>; BOARD_SIZE]; BOARD_SIZE],
pub legal_moves: HashSet<CMove>,
pub game_state: BoardState,
pub state: BoardState,
pub id: String,
pub size: usize,
pieces_remaining: u8,
max_moves_per_piece: u32,
}
#[derive(PartialEq, Eq, Debug, Clone)]
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub enum BoardState {
NotStarted,
InProgress,
@@ -33,26 +38,57 @@ pub enum BoardState {
Won,
}
impl Default for BoardState {
fn default() -> Self {
BoardState::NotStarted
}
}
pub struct BoardOptions {
pub max_moves_per_piece: u32,
}
impl Board {
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 {
cells: [[None; BOARD_SIZE]; BOARD_SIZE],
cells,
legal_moves: HashSet::new(),
id,
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 working = board_id;
let mask = 0b111;
for i in (0..BOARD_SIZE).rev() {
for j in (0..BOARD_SIZE).rev() {
let mask = 0b111;
let piece = Board::get_piece_from_encoding((working & mask) as u8);
let piece_kind = Board::get_piece_from_encoding((working & mask) as u8);
working = working >> 3;
let piece = piece?;
board.set(Square::new(i, j, piece));
let piece_kind = piece_kind?;
board.set(Square::new(i, j, Piece::from_kind(piece_kind)));
}
}
Ok(board)
@@ -68,18 +104,18 @@ impl Board {
for r in 0..BOARD_SIZE {
for f in 0..BOARD_SIZE {
let c = chars.next().unwrap();
let piece = match c {
'K' => Piece::King,
'Q' => Piece::Queen,
'B' => Piece::Bishop,
'N' => Piece::Knight,
'R' => Piece::Rook,
'P' => Piece::Pawn,
let piece_kind = match c {
'K' | 'k' => PieceKind::King,
'Q' | 'q' => PieceKind::Queen,
'B' | 'b' => PieceKind::Bishop,
'N' | 'n' => PieceKind::Knight,
'R' | 'r' => PieceKind::Rook,
'P' | 'p' => PieceKind::Pawn,
'.' => continue,
_ => 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);
}
}
@@ -114,7 +150,14 @@ impl Board {
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.pieces_remaining -= 1;
@@ -136,21 +179,86 @@ impl Board {
pub fn pretty_print(&self) {
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 {
let mut res: u128 = 0;
pub fn solve(&self) -> Puzzle {
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 j in 0..BOARD_SIZE {
res = res << 3;
let byte = Board::get_piece_encoding(self.cells[i][j]);
res = res | byte as u128
let byte = Board::get_piece_encoding(cells[i][j]);
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 {
@@ -190,13 +298,17 @@ impl Board {
return None;
};
let legal = match piece {
Piece::King => self.is_king_legal(&pair),
Piece::Queen => self.is_queen_legal(&pair),
Piece::Bishop => self.is_bishop_legal(&pair),
Piece::Knight => self.is_knight_legal(&pair),
Piece::Rook => self.is_rook_legal(&pair),
Piece::Pawn => self.is_pawn_legal(&pair),
if piece.moves_made >= self.max_moves_per_piece {
return None;
}
let legal = match piece.kind {
PieceKind::King => self.is_king_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 {
@@ -262,7 +374,7 @@ impl Board {
}
fn calc_game_state(&mut self) {
self.game_state = if self.pieces_remaining == 0 {
self.state = if self.pieces_remaining == 0 {
BoardState::NotStarted
} else if self.pieces_remaining == 1 {
BoardState::Won
@@ -307,42 +419,47 @@ impl Board {
fn board_state_changed(&mut self) {
self.calc_legal_moves();
self.calc_game_state();
self.calc_id();
}
fn get_piece_encoding(piece: Option<Piece>) -> u8 {
match piece {
Some(p) => match p {
Piece::King => 0b001,
Piece::Queen => 0b010,
Piece::Rook => 0b011,
Piece::Bishop => 0b100,
Piece::Knight => 0b101,
Piece::Pawn => 0b110,
Some(p) => match p.kind {
PieceKind::King => 0b001,
PieceKind::Queen => 0b010,
PieceKind::Rook => 0b011,
PieceKind::Bishop => 0b100,
PieceKind::Knight => 0b101,
PieceKind::Pawn => 0b110,
},
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 {
0b001 => Ok(Some(Piece::King)),
0b010 => Ok(Some(Piece::Queen)),
0b011 => Ok(Some(Piece::Rook)),
0b100 => Ok(Some(Piece::Bishop)),
0b101 => Ok(Some(Piece::Knight)),
0b110 => Ok(Some(Piece::Pawn)),
0b001 => Ok(Some(PieceKind::King)),
0b010 => Ok(Some(PieceKind::Queen)),
0b011 => Ok(Some(PieceKind::Rook)),
0b100 => Ok(Some(PieceKind::Bishop)),
0b101 => Ok(Some(PieceKind::Knight)),
0b110 => Ok(Some(PieceKind::Pawn)),
0b000 => Ok(None),
_ => 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 {
let contents = if let Some(piece) = piece {
if pretty {
piece.pretty()
piece.kind.pretty()
} else {
piece.notation()
piece.kind.notation()
}
} else {
".".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)]
mod tests {
use super::*;
@@ -379,9 +535,7 @@ mod tests {
}
macro_rules! mv {
($from:literal, $to:literal) => {{
CMove::new(sq!($from), sq!($to))
}};
($from:literal, $to:literal) => {{ CMove::new(sq!($from), sq!($to)) }};
}
macro_rules! validate_board {
@@ -427,7 +581,7 @@ mod tests {
assert!(board.set(sq!("Nb2")).is_none());
let existing = board.set(sq!("Pc4"));
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");
}
@@ -520,7 +674,7 @@ mod tests {
#[test]
fn test_smoke_puzzle() {
let mut board = Board::new();
assert_eq!(BoardState::NotStarted, board.game_state);
assert_eq!(BoardState::NotStarted, board.state);
assert_eq!(0, board.pieces_remaining);
// K . . .
@@ -528,22 +682,22 @@ mod tests {
// . . R .
// N . . .
board.set(sq!("Ka4"));
assert_eq!(BoardState::Won, board.game_state);
assert_eq!(BoardState::Won, board.state);
board.set(sq!("Pb3"));
board.set(sq!("Rc2"));
board.set(sq!("Na1"));
assert_eq!(BoardState::InProgress, board.game_state);
assert_eq!(BoardState::InProgress, board.state);
assert_eq!(4, board.pieces_remaining);
assert!(board.make_move(mv!("Na1", "Rc2")).is_some());
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_eq!(2, board.pieces_remaining);
assert_eq!(BoardState::Lost, board.game_state);
assert_eq!(BoardState::Lost, board.state);
// P . . .
// . . . .
@@ -558,12 +712,12 @@ mod tests {
// . . N .
// P . . .
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!("Nc2", "Pa1"));
assert_eq!(2, board.pieces_remaining);
assert_eq!(BoardState::InProgress, board.game_state);
assert_eq!(BoardState::InProgress, board.state);
// Q . . .
// . . . .
@@ -571,7 +725,7 @@ mod tests {
// N . . .
board.make_move(mv!("Qa4", "Na1"));
assert_eq!(1, board.pieces_remaining);
assert_eq!(BoardState::Won, board.game_state);
assert_eq!(BoardState::Won, board.state);
}
#[test]
@@ -584,10 +738,65 @@ mod tests {
board.set(sq!("Bd4"));
board.set(sq!("Nc4"));
let id = board.id();
let board2 = Board::from_id(id);
let id = board.id;
let board2 = Board::from_id(&id);
let board2 = board2.unwrap();
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)]
pub struct CMove {
@@ -26,8 +28,8 @@ impl CMove {
}
pub fn notation(&self) -> String {
let piece_qualifier = match &self.from_piece {
Piece::Pawn => self.from.file_notation(),
let piece_qualifier = match &self.from_piece.kind {
PieceKind::Pawn => self.from.file_notation(),
p => p.notation(),
};
format!(
+1
View File
@@ -0,0 +1 @@
pub const BOARD_SIZE: usize = 4;
+91
View File
@@ -0,0 +1,91 @@
#[derive(Clone, Eq, Hash, Copy, Debug, PartialEq)]
pub struct Piece {
pub kind: PieceKind,
pub moves_made: u32,
pub active: bool,
}
impl Piece {
pub fn new(kind: PieceKind) -> Self {
Self {
kind,
moves_made: 0,
active: true,
}
}
pub fn from_kind(kind: Option<PieceKind>) -> Option<Self> {
kind.map(|k| Piece::new(k))
}
}
#[derive(Clone, Eq, Hash, Copy, Debug, PartialEq)]
pub enum PieceKind {
King,
Queen,
Bishop,
Knight,
Rook,
Pawn,
}
impl PieceKind {
pub fn parse(piece: &str) -> Option<Self> {
match piece {
"K" => Some(PieceKind::King),
"Q" => Some(PieceKind::Queen),
"B" => Some(PieceKind::Bishop),
"N" => Some(PieceKind::Knight),
"R" => Some(PieceKind::Rook),
"P" => Some(PieceKind::Pawn),
"." => None,
p => panic!("Invalid piece {}", p),
}
}
pub fn notation(&self) -> String {
let n = match self {
PieceKind::King => "K",
PieceKind::Queen => "Q",
PieceKind::Bishop => "B",
PieceKind::Knight => "N",
PieceKind::Rook => "R",
PieceKind::Pawn => "P",
};
n.to_string()
}
pub fn pretty(&self) -> String {
let n = match self {
PieceKind::King => "",
PieceKind::Queen => "",
PieceKind::Bishop => "",
PieceKind::Knight => "",
PieceKind::Rook => "",
PieceKind::Pawn => "",
};
n.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! p {
($piece:literal) => {
PieceKind::parse($piece)
};
}
#[test]
fn test_piece_parse() {
assert_eq!(p!("K"), Some(PieceKind::King));
assert_eq!(p!("Q"), Some(PieceKind::Queen));
assert_eq!(p!("B"), Some(PieceKind::Bishop));
assert_eq!(p!("N"), Some(PieceKind::Knight));
assert_eq!(p!("R"), Some(PieceKind::Rook));
assert_eq!(p!("P"), Some(PieceKind::Pawn));
}
}
@@ -1,5 +1,7 @@
use crate::board::piece::Piece;
use super::constants::BOARD_SIZE;
use super::piece::Piece;
use super::piece::PieceKind;
use core::fmt;
#[derive(Clone, Eq, Hash, PartialEq)]
@@ -29,8 +31,8 @@ impl Square {
pub fn parse(notation: &str) -> Self {
let mut chars = notation.chars();
let piece = chars.next().expect("Piece missing");
let piece = Piece::parse(&piece.to_string());
let piece_kind = chars.next().expect("Piece missing");
let piece_kind = PieceKind::parse(&piece_kind.to_string());
let file = chars.next().expect("File missing");
let file = match file {
'a' => 0,
@@ -45,6 +47,7 @@ impl Square {
panic!("rank should be between 1-{}", BOARD_SIZE);
}
let rank = BOARD_SIZE - rank;
let piece = piece_kind.map(|kind| Piece::new(kind));
Square::new(file, rank, piece)
}
@@ -73,7 +76,7 @@ impl Square {
if self.piece.is_none() {
"".to_string()
} else {
self.piece.unwrap().notation()
self.piece.unwrap().kind.notation()
}
}
}
@@ -131,7 +134,7 @@ mod tests {
let square = Square::parse(&notation);
assert_eq!(square.file, $file);
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);
};
}
+90 -43
View File
@@ -1,30 +1,51 @@
use std::fmt::Display;
use crate::{
board::{piece::Piece, Board},
solver::Solver,
use crate::board::{
Board, BoardOptions,
cmove::CMove,
piece::{Piece, PieceKind},
};
pub trait RandomRange {
fn gen_range(&self, min: usize, max: usize) -> usize;
}
pub fn generate(num_pieces: u32, num_solutions: u32, rand: &impl RandomRange) -> GenerateStats {
#[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![
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,
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() {
@@ -35,9 +56,15 @@ pub fn generate(num_pieces: u32, num_solutions: u32, rand: &impl RandomRange) ->
}
let attempts: u32 = 1000;
let mut overall_stats = GenerateStats::new(0, 0, 0, None);
let mut overall_stats = GenerateStats::new(0, 0, 0, None, vec![]);
for _ in 0..attempts {
let stats = try_generate(num_pieces, num_solutions, rand, candidate_pieces.clone());
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;
@@ -50,20 +77,20 @@ pub fn generate(num_pieces: u32, num_solutions: u32, rand: &impl RandomRange) ->
overall_stats
}
pub struct GenerateStats {
impl GenerateStats {
fn new(
piece_total: u32,
piece_success: u32,
total: u32,
board: Option<Board>,
}
impl GenerateStats {
fn new(piece_total: u32, piece_success: u32, total: u32, board: Option<Board>) -> Self {
solutions: Vec<Vec<CMove>>,
) -> Self {
Self {
piece_total,
piece_success,
total,
board,
solutions,
}
}
@@ -76,8 +103,18 @@ impl GenerateStats {
println!("{}", stats);
}
pub fn board(self) -> Option<Board> {
self.board
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,
})
}
}
@@ -91,10 +128,14 @@ where
fn try_generate(
num_pieces: u32,
num_solutions: u32,
max_moves_per_piece: u32,
rand: &impl RandomRange,
mut candidate_pieces: Vec<Piece>,
mut candidate_pieces: Vec<PieceKind>,
) -> GenerateStats {
let mut board = Board::new();
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 {
@@ -103,20 +144,20 @@ fn try_generate(
let mut attempts = 15;
while !placed {
if attempts == 0 {
return GenerateStats::new(piece_total, piece_success, 1, None);
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 = candidate_pieces[index];
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);
random_square.piece = Some(Piece::new(piece_kind));
board.set(random_square.clone());
let solutions = Solver::new(board.clone()).solve();
if solutions.len() > 0 {
let puzzle = board.solve();
if puzzle.solutions.len() > 0 {
placed = true;
piece_success += 1;
candidate_pieces.remove(index);
@@ -128,17 +169,23 @@ fn try_generate(
}
}
let solutions = Solver::new(board.clone()).solve();
if solutions.len() > num_solutions as usize {
GenerateStats::new(piece_total, piece_success, 1, None)
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(board))
GenerateStats::new(
piece_total,
piece_success,
1,
Some(puzzle.board),
puzzle.solutions,
)
}
}
#[cfg(test)]
mod tests {
use crate::{board::BoardState, solver::Solver};
use crate::board::BoardState;
use super::*;
@@ -154,13 +201,13 @@ mod tests {
#[test]
fn generator_smoke() {
for _ in 0..10 {
let gen_stats = generate(5, 5, &TestRandom);
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 solutions = Solver::new(board).solve();
assert!(solutions.len() <= 5);
assert!(solutions.len() >= 1);
let puzzle = board.solve();
assert!(puzzle.solutions.len() <= 5);
assert!(puzzle.solutions.len() >= 1);
}
}
}
-1
View File
@@ -1,3 +1,2 @@
pub mod board;
pub mod generator;
pub mod solver;
-1
View File
@@ -1 +0,0 @@
pub(crate) const BOARD_SIZE: usize = 4;
-71
View File
@@ -1,71 +0,0 @@
#[derive(Clone, Eq, Hash, Copy, Debug, PartialEq)]
pub enum Piece {
King,
Queen,
Bishop,
Knight,
Rook,
Pawn,
}
impl Piece {
pub fn parse(piece: &str) -> Option<Self> {
match piece {
"K" => Some(Piece::King),
"Q" => Some(Piece::Queen),
"B" => Some(Piece::Bishop),
"N" => Some(Piece::Knight),
"R" => Some(Piece::Rook),
"P" => Some(Piece::Pawn),
"." => None,
p => panic!("Invalid piece {}", p),
}
}
pub fn notation(&self) -> String {
let n = match self {
Piece::King => "K",
Piece::Queen => "Q",
Piece::Bishop => "B",
Piece::Knight => "N",
Piece::Rook => "R",
Piece::Pawn => "P",
};
n.to_string()
}
pub fn pretty(&self) -> String {
let n = match self {
Piece::King => "",
Piece::Queen => "",
Piece::Bishop => "",
Piece::Knight => "",
Piece::Rook => "",
Piece::Pawn => "",
};
n.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! p {
($piece:literal) => {
Piece::parse($piece)
};
}
#[test]
fn test_piece_parse() {
assert_eq!(p!("K"), Some(Piece::King));
assert_eq!(p!("Q"), Some(Piece::Queen));
assert_eq!(p!("B"), Some(Piece::Bishop));
assert_eq!(p!("N"), Some(Piece::Knight));
assert_eq!(p!("R"), Some(Piece::Rook));
assert_eq!(p!("P"), Some(Piece::Pawn));
}
}
-697
View File
@@ -1,697 +0,0 @@
use std::{
collections::HashMap,
fmt::{self, Display, Formatter},
};
use button::Button;
use color::UiColor;
use macroquad::{audio, math, prelude::*, rand};
use shadow::draw_shadow;
use sol_chess::{
board::{Board, BoardState},
generator::{self, RandomRange},
};
use sound::Sounds;
use texture::PieceTexture;
pub mod button;
pub mod color;
pub mod shadow;
pub mod sound;
pub mod texture;
pub struct MacroquadRandAdapter;
impl RandomRange for MacroquadRandAdapter {
fn gen_range(&self, min: usize, max: usize) -> usize {
rand::gen_range(min, max)
}
}
pub struct Game {
// The generated puzzle. We keep a copy of this to reset the game.
original_board: Board,
// What is shown to the user
board: Board,
// Constants througout the game
texture_res: Texture2D,
sounds: Sounds,
num_squares: usize,
heading_text: String,
// Update below on handle input
state: GameState,
debug: bool,
game_mode: GameMode,
// Update below on window resize
// Used for drawing the state
square_width: f32,
window_height: f32,
window_width: f32,
board_rect: Rect,
squares: Vec<GameSquare>,
heading_rect: Rect,
heading_font_size: f32,
gp_btns: HashMap<ButtonAction, Button>,
mode_btns: HashMap<GameMode, Button>,
rules: bool,
rules_btn: Vec<Button>,
}
struct GameSquare {
rect: Rect,
color: Color,
is_source: bool,
is_target: bool,
is_previous_target: bool,
i: usize,
j: usize,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum ButtonAction {
Reset,
Next,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum GameMode {
Easy,
Medium,
Hard,
}
#[derive(Copy, Clone)]
enum GameState {
SelectSource(Option<(usize, usize)>),
SelectTarget((usize, usize)),
GameOver((usize, usize)),
}
impl Game {
pub fn new(texture_res: Texture2D, sounds: Sounds) -> Self {
let num_squares: usize = 4;
let game_mode = GameMode::Medium;
let board = Game::generate_puzzle(game_mode);
Self {
original_board: board.clone(),
board,
board_rect: Rect::new(0., 0., 0., 0.),
squares: Vec::new(),
heading_rect: Rect::new(0., 0., 0., 0.),
heading_text: "Solitaire Chess".to_string(),
heading_font_size: 0.,
num_squares,
texture_res,
sounds,
state: GameState::SelectSource(None),
game_mode,
debug: false,
gp_btns: HashMap::new(),
mode_btns: HashMap::new(),
rules: false,
rules_btn: Vec::new(),
window_height: 0.,
window_width: 0.,
square_width: 0.,
}
}
pub fn draw(&mut self) {
self.update_window_size();
self.draw_heading();
self.draw_board();
self.draw_buttons();
self.draw_debug();
}
fn update_window_size(&mut self) {
let new_height = math::clamp(screen_height(), 200.0, 10000.0);
let new_width = math::clamp(screen_width(), 200.0, 10000.0);
if new_height == self.window_height && new_width == self.window_width {
return;
}
self.window_height = new_height;
self.window_width = new_width;
self.update_drawables();
}
pub fn handle_input(&mut self) {
let mut gp_btn_clicked = None;
for btn in &mut self.gp_btns {
btn.1.handle_input();
if btn.1.is_clicked() {
gp_btn_clicked = Some(btn.0.clone());
break;
}
}
if let Some(action) = gp_btn_clicked {
match action {
ButtonAction::Reset => self.reset(),
ButtonAction::Next => self.next_puzzle(),
}
} else {
let mut mode_btn_clicked = None;
for btn in &mut self.mode_btns {
btn.1.handle_input();
if btn.1.is_clicked() {
mode_btn_clicked = Some(btn);
break;
}
}
if let Some(btn) = mode_btn_clicked {
self.game_mode = *btn.0;
self.next_puzzle();
} else {
let mut rules_btn_clicked = false;
for btn in &mut self.rules_btn {
btn.handle_input();
if btn.is_clicked() {
rules_btn_clicked = true;
break;
}
}
if rules_btn_clicked {
self.rules = !self.rules;
if self.rules {
self.rules_btn[0].text = "Close".to_string();
self.rules_btn[0].color = UiColor::Green;
} else {
self.rules_btn[0].text = "Rules".to_string();
self.rules_btn[0].color = UiColor::Brown;
}
}
}
}
for btn in &mut self.mode_btns {
if self.game_mode == *btn.0 {
btn.1.is_active = false;
} else {
btn.1.is_active = true;
}
}
if is_key_released(KeyCode::Escape) {
self.rules = false;
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_released(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) => {
let next = self.handle_select_target(mouse_position(), source);
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;
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;
}
}
fn draw_heading(&self) {
let f = self.heading_font_size.floor() as u16;
let dims = measure_text(self.heading_text.as_str(), None, f, 1.0);
draw_text(
self.heading_text.as_str(),
self.heading_rect.x,
self.heading_rect.y + dims.offset_y,
self.heading_font_size,
BLACK,
);
}
fn draw_board(&self) {
let board_shadow_width = 0.1 * self.square_width;
draw_shadow(self.board_rect, board_shadow_width);
if self.rules {
draw_rectangle(
self.board_rect.x,
self.board_rect.y,
self.board_rect.w,
self.board_rect.h,
UiColor::Brown.to_bg_color(),
);
let font_size = self.heading_font_size * 0.8;
let rules = "\
Every move should be a \n\
capture. Win when only \n\
one piece is left.\n";
let measurement = measure_text(rules, None, font_size as u16, 1.0);
draw_multiline_text(
rules,
self.board_rect.x + 0.05 * self.square_width,
self.board_rect.y + 0.5 * (self.board_rect.h - measurement.height)
- 2. * measurement.offset_y,
font_size,
Some(2.),
UiColor::Brown.to_fg_color(),
);
return;
}
let sprite_size = 0.8 * 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.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,
);
}
}
}
fn draw_buttons(&self) {
for btn in &self.gp_btns {
btn.1.draw();
}
for btn in &self.mode_btns {
btn.1.draw();
}
for btn in &self.rules_btn {
btn.draw();
}
}
fn draw_debug(&self) {
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 get(&mut self, i: usize, j: usize) -> &mut GameSquare {
&mut self.squares[i * self.num_squares + j]
}
fn update_drawables(&mut self) {
let min_dimension = f32::min(self.window_height, self.window_width);
self.square_width = 0.15 * min_dimension;
let board_width = self.square_width * self.num_squares as f32;
let board_x = (self.window_width - board_width) / 2.0;
let board_y = (self.window_height - board_width) / 2.0;
self.board_rect = Rect::new(board_x, board_y, board_width, board_width);
self.heading_font_size = 0.07 * min_dimension;
let f = self.heading_font_size.floor() as u16;
let dims = measure_text(self.heading_text.as_str(), None, f, 1.0);
self.heading_rect = Rect::new(
board_x + (board_width - dims.width) / 2.0,
(board_y - dims.height) / 2.0,
dims.width,
dims.height,
);
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 = board_x + (i as f32 * self.square_width);
let y_eff = board_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;
let btn_h = 0.08 * min_dimension;
let btn_w = board_width * 0.2;
let btn_y = board_width + board_y + 0.3 * self.square_width;
let btn_x_offset = 0.5 * (board_width / 2. - btn_w);
let reset_btn = Button::new(
"Reset",
Rect::new(board_x + btn_x_offset, btn_y, btn_w, btn_h),
UiColor::Yellow,
self.sounds.button.clone(),
);
let mut next_btn = Button::new(
"Next",
Rect::new(
board_x + (0.5 * board_width) + btn_x_offset,
btn_y,
btn_w,
btn_h,
),
UiColor::Green,
self.sounds.button.clone(),
);
next_btn.is_active = false;
let rules_button = Button::new(
"Rules",
Rect::new(
(board_x - btn_w) / 2.,
board_y + (self.square_width - btn_h) / 2.,
btn_w,
btn_h,
),
UiColor::Brown,
self.sounds.button.clone(),
);
self.rules_btn = vec![rules_button];
self.gp_btns = HashMap::new();
self.gp_btns.insert(ButtonAction::Next, next_btn);
self.gp_btns.insert(ButtonAction::Reset, reset_btn);
let easy_btn = Button::new(
"Easy",
Rect::new(
(board_x - btn_w) / 2.,
board_y + self.square_width + (self.square_width - btn_h) / 2.,
btn_w,
btn_h,
),
UiColor::Yellow,
self.sounds.mode.clone(),
);
let medium_btn = Button::new(
"Medium",
Rect::new(
(board_x - btn_w) / 2.,
board_y + 2. * self.square_width + (self.square_width - btn_h) / 2.,
btn_w,
btn_h,
),
UiColor::Yellow,
self.sounds.mode.clone(),
);
let hard_button = Button::new(
"Hard",
Rect::new(
(board_x - btn_w) / 2.,
board_y + 3. * self.square_width + (self.square_width - btn_h) / 2.,
btn_w,
btn_h,
),
UiColor::Yellow,
self.sounds.mode.clone(),
);
self.mode_btns = HashMap::new();
self.mode_btns.insert(GameMode::Easy, easy_btn);
self.mode_btns.insert(GameMode::Medium, medium_btn);
self.mode_btns.insert(GameMode::Hard, hard_button);
for btn in &mut self.mode_btns {
btn.1.is_active = true;
if self.game_mode == *btn.0 {
btn.1.is_active = false;
}
}
}
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();
if self.board.game_state == BoardState::Won {
let next_btn = self
.gp_btns
.get_mut(&ButtonAction::Next)
.expect("Cannot find next button");
next_btn.is_active = true;
audio::play_sound_once(&self.sounds.win);
} else {
audio::play_sound_once(&self.sounds.loss);
}
return GameState::GameOver((x, y));
}
self.reset_squares();
self.get(x, y).is_target = true;
audio::play_sound_once(&self.sounds.click);
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();
let next_button = self
.gp_btns
.get_mut(&ButtonAction::Next)
.expect("Cannot find next button");
next_button.is_active = false;
self.state = GameState::SelectSource(None);
}
fn next_puzzle(&mut self) {
self.reset();
let board = Game::generate_puzzle(self.game_mode);
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,
);
}
fn generate_puzzle(mode: GameMode) -> Board {
let piece_count = match mode {
GameMode::Easy => 3,
GameMode::Medium => 5,
GameMode::Hard => 7,
};
let generate = generator::generate(piece_count, 100, &MacroquadRandAdapter);
generate.board().expect("No puzzle was generated")
}
}
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),
}
}
}
-130
View File
@@ -1,130 +0,0 @@
use macroquad::{audio::{self, Sound}, prelude::*};
use super::{color::UiColor, shadow::draw_shadow};
pub struct Button {
pub is_active: bool,
pub text: String,
is_down: bool,
is_clicked: bool,
rect: Rect,
shadow_width: f32,
pub color: UiColor,
sound: Sound,
}
impl Button {
pub fn new(text: &str, rect: Rect, color: UiColor, sound: Sound) -> Self {
Self {
text: text.to_string(),
is_down: false,
is_clicked: false,
is_active: true,
rect,
shadow_width: 5.0,
color,
sound,
}
}
pub fn is_clicked(&mut self) -> bool {
if self.is_clicked {
self.is_clicked = false;
return true;
}
false
}
pub fn draw(&self) {
self.draw_button();
self.draw_label();
}
fn draw_button(&self) {
let bg_color = match self.is_active {
true => self.color.to_bg_color(),
false => self.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) {
let font_color = match self.is_active {
true => self.color.to_fg_color(),
false => Color::from_rgba(100, 100, 100, 255),
};
let font_size = (0.3 * self.rect.w) as u16;
let dims = measure_text(&self.text, None, font_size, 1.0);
let button_draw_offset = self.get_button_draw_offset();
draw_text(
&self.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,
font_size as f32,
font_color,
);
}
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) {
if !self.is_active {
self.is_down = false;
return;
}
let (mx, my) = mouse_position();
let c = Circle::new(mx, my, 0.0);
if is_mouse_button_pressed(MouseButton::Left) {
if c.overlaps_rect(&self.rect) {
self.is_down = true;
return;
}
}
if is_mouse_button_released(MouseButton::Left) {
if c.overlaps_rect(&self.rect) {
self.is_clicked = true;
audio::play_sound_once(&self.sound);
self.is_down = false;
return;
}
self.is_down = false;
}
}
}
-45
View File
@@ -1,45 +0,0 @@
use macroquad::prelude::*;
#[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(234, 128, 71, 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,
)
}
}
-20
View File
@@ -1,20 +0,0 @@
use macroquad::prelude::*;
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,
);
}
-9
View File
@@ -1,9 +0,0 @@
use macroquad::audio::Sound;
pub struct Sounds {
pub click: Sound,
pub win: Sound,
pub loss: Sound,
pub button: Sound,
pub mode: Sound,
}
-45
View File
@@ -1,45 +0,0 @@
use macroquad::prelude::*;
use sol_chess::board::piece::Piece;
pub struct PieceTexture {
x: f32,
y: f32,
w: f32,
h: f32,
}
impl PieceTexture {
fn new(x: u32, y: u32) -> Self {
Self {
x: x as f32 * 128.0,
y: y as f32 * 128.0,
w: 128.0,
h: 128.0,
}
}
pub fn for_piece(piece: Piece, sprite_size: f32) -> DrawTextureParams {
let index = match piece {
Piece::Pawn => 0,
Piece::Knight => 1,
Piece::Bishop => 2,
Piece::Rook => 3,
Piece::Queen => 4,
Piece::King => 5,
};
let color = 0;
let texture_rect = PieceTexture::new(index, color);
DrawTextureParams {
source: Some(Rect::new(
texture_rect.x,
texture_rect.y,
texture_rect.w,
texture_rect.h,
)),
dest_size: Some(Vec2::new(sprite_size, sprite_size)),
..DrawTextureParams::default()
}
}
}
-54
View File
@@ -1,54 +0,0 @@
use game::{sound::Sounds, Game};
use macroquad::{audio, prelude::*};
use miniquad::date;
mod game;
fn window_conf() -> Conf {
let window_name = match std::env::var("TESTING") {
Ok(_) => "DEV TESTING MOVE TO WORKSPACE 10",
Err(_) => "Solitaire Chess",
};
Conf {
window_title: window_name.to_string(),
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 = init().await;
loop {
clear_background(background_color);
game.handle_input();
game.draw();
next_frame().await
}
}
macro_rules! load_sound {
($file_name:expr) => {
audio::load_sound_from_bytes(include_bytes!($file_name))
.await
.unwrap()
};
}
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 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 sounds = Sounds { click, win, loss, button, mode };
let game = Game::new(texture_res, sounds);
game
}
-109
View File
@@ -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());
}
}
+1
View File
@@ -0,0 +1 @@
cargo build -p sol_chess --target wasm32-unknown-unknown --release && scp target/wasm32-unknown-unknown/release/sol_chess.wasm potato@neophyte.me:~/site/games/sol_chess/.
+2 -2
View File
@@ -43,14 +43,14 @@ build_web() {
echo "Archive directory: ${ARCHIVE_DIR}"
set -x
cargo build --target wasm32-unknown-unknown ${BUILD_PROFILE_SWITCH}
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
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
+1 -1
View File
@@ -21,7 +21,7 @@
</head>
<body>
<canvas id="glcanvas" tabindex='1'></canvas>
<script src="https://not-fl3.github.io/miniquad-samples/mq_js_bundle.js"></script>
<script src="./mq.js"></script>
<script>load("sol_chess.wasm");</script> <!-- Your compiled WASM binary -->
</body>
</html>
+3
View File
File diff suppressed because one or more lines are too long