From f17d328ed53698f864e1f58f2da07b599ceb1972 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Mon, 19 Oct 2026 20:38:32 +0000 Subject: [PATCH] fix frontend board mapping, game-over handling, and WASM-4 - fix Xform yx and add a shared view-aware board_position helper - add GTK automatic game-over, restart, status, and multi-capture selection - pin librsvg to the 2.60 series to match GTK4/glib 0.20 - add WASM orientation, undo/redo/new/flip controls, and winner overlay - replace WASM crown images with canvas paths and remove dead files/deps - make WASM-4 playable with mouse input, crowns, winner handling, and correct mapping - move the WASM-4 release profile to the workspace root - clean up frontend dead code and update the README --- Cargo.lock | 496 ++++++++----------- Cargo.toml | 10 +- README.md | 35 +- rdraught-gtk/examples/ai_debugger.rs | 4 +- rdraught-gtk/src/final_dialog.rs | 2 +- rdraught-gtk/src/greeting_dialog.rs | 14 +- rdraught-gtk/src/rdraught_application.rs | 271 ++++++----- rdraught-ui-common/src/board.rs | 70 +++ rdraught-ui-common/src/geo2d.rs | 40 +- rdraught-ui-common/src/lib.rs | 2 + rdraught-w4/Cargo.toml | 6 - rdraught-w4/src/lib.rs | 379 +++++++------- rdraught-w4/src/palette.rs | 29 -- rdraught-w4/src/wasm4.rs | 196 -------- rdraught-wasm/Cargo.toml | 10 +- rdraught-wasm/src/crown_red.svg | 27 - rdraught-wasm/src/crown_white.svg | 27 - rdraught-wasm/src/lib.rs | 61 --- rdraught-wasm/src/main.rs | 596 +++++++++++------------ rdraught-wasm/src/types.rs | 12 - 20 files changed, 997 insertions(+), 1290 deletions(-) create mode 100644 rdraught-ui-common/src/board.rs delete mode 100644 rdraught-w4/src/palette.rs delete mode 100644 rdraught-w4/src/wasm4.rs delete mode 100644 rdraught-wasm/src/crown_red.svg delete mode 100644 rdraught-wasm/src/crown_white.svg delete mode 100644 rdraught-wasm/src/lib.rs delete mode 100644 rdraught-wasm/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index fb6deed..02fc870 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,12 +32,6 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "bitflags" version = "1.3.2" @@ -87,20 +81,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e3bd0f4e25afa9cabc157908d14eeef9067d6448c49414d17b3fb55f0eadd0" dependencies = [ "bitflags 2.13.2", - "cairo-sys-rs 0.20.10", - "glib 0.20.12", - "libc", -] - -[[package]] -name = "cairo-rs" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b01fe135c0bd16afe262b6dea349bd5ea30e6de50708cec639aae7c5c14cc7e4" -dependencies = [ - "bitflags 2.13.2", - "cairo-sys-rs 0.21.5", - "glib 0.21.5", + "cairo-sys-rs", + "glib", "libc", ] @@ -110,18 +92,7 @@ version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "059cc746549898cbfd9a47754288e5a958756650ef4652bbb6c5f71a6bda4f8b" dependencies = [ - "glib-sys 0.20.10", - "libc", - "system-deps", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06c28280c6b12055b5e39e4554271ae4e6630b27c0da9148c4cf6485fc6d245c" -dependencies = [ - "glib-sys 0.21.5", + "glib-sys", "libc", "system-deps", ] @@ -234,26 +205,17 @@ checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "cssparser" -version = "0.35.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" +checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be" dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf", + "phf 0.11.3", "smallvec", ] -[[package]] -name = "cssparser-color" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eeef9ae8c0e112edd89eb6406b3156ffa99c7e037b3baef1dbdf4158d35c324" -dependencies = [ - "cssparser", -] - [[package]] name = "cssparser-macros" version = "0.6.1" @@ -272,22 +234,12 @@ checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" [[package]] name = "derive_more" -version = "2.1.1" +version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ "proc-macro2", "quote", - "rustc_version", "syn 2.0.119", ] @@ -382,9 +334,9 @@ dependencies = [ [[package]] name = "float-cmp" -version = "0.10.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" dependencies = [ "num-traits", ] @@ -486,8 +438,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd242894c084f4beed508a56952750bce3e96e85eb68fdc153637daa163e10c" dependencies = [ "gdk-pixbuf-sys", - "gio 0.20.12", - "glib 0.20.12", + "gio", + "glib", "libc", ] @@ -497,9 +449,9 @@ version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b34f3b580c988bd217e9543a2de59823fafae369d1a055555e5f95a8b130b96" dependencies = [ - "gio-sys 0.20.10", - "glib-sys 0.20.10", - "gobject-sys 0.20.10", + "gio-sys", + "glib-sys", + "gobject-sys", "libc", "system-deps", ] @@ -510,13 +462,13 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4850c9d9c1aecd1a3eb14fadc1cdb0ac0a2298037e116264c7473e1740a32d60" dependencies = [ - "cairo-rs 0.20.12", + "cairo-rs", "gdk-pixbuf", "gdk4-sys", - "gio 0.20.12", - "glib 0.20.12", + "gio", + "glib", "libc", - "pango 0.20.12", + "pango", ] [[package]] @@ -525,17 +477,28 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f6eb95798e2b46f279cf59005daf297d5b69555428f185650d71974a910473a" dependencies = [ - "cairo-sys-rs 0.20.10", + "cairo-sys-rs", "gdk-pixbuf-sys", - "gio-sys 0.20.10", - "glib-sys 0.20.10", - "gobject-sys 0.20.10", + "gio-sys", + "glib-sys", + "gobject-sys", "libc", - "pango-sys 0.20.10", + "pango-sys", "pkg-config", "system-deps", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gif" version = "0.14.2" @@ -556,25 +519,8 @@ dependencies = [ "futures-core", "futures-io", "futures-util", - "gio-sys 0.20.10", - "glib 0.20.12", - "libc", - "pin-project-lite", - "smallvec", -] - -[[package]] -name = "gio" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5ff48bf600c68b476e61dc6b7c762f2f4eb91deef66583ba8bb815c30b5811a" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys 0.21.5", - "glib 0.21.5", + "gio-sys", + "glib", "libc", "pin-project-lite", "smallvec", @@ -586,24 +532,11 @@ version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" dependencies = [ - "glib-sys 0.20.10", - "gobject-sys 0.20.10", + "glib-sys", + "gobject-sys", "libc", "system-deps", - "windows-sys 0.59.0", -] - -[[package]] -name = "gio-sys" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22" -dependencies = [ - "glib-sys 0.21.5", - "gobject-sys 0.21.5", - "libc", - "system-deps", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -618,31 +551,10 @@ dependencies = [ "futures-executor", "futures-task", "futures-util", - "gio-sys 0.20.10", - "glib-macros 0.20.12", - "glib-sys 0.20.10", - "gobject-sys 0.20.10", - "libc", - "memchr", - "smallvec", -] - -[[package]] -name = "glib" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b" -dependencies = [ - "bitflags 2.13.2", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys 0.21.5", - "glib-macros 0.21.5", - "glib-sys 0.21.5", - "gobject-sys 0.21.5", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", "libc", "memchr", "smallvec", @@ -661,19 +573,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "glib-macros" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17" -dependencies = [ - "heck 0.5.0", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "glib-sys" version = "0.20.10" @@ -684,34 +583,13 @@ dependencies = [ "system-deps", ] -[[package]] -name = "glib-sys" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c" -dependencies = [ - "libc", - "system-deps", -] - [[package]] name = "gobject-sys" version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" dependencies = [ - "glib-sys 0.20.10", - "libc", - "system-deps", -] - -[[package]] -name = "gobject-sys" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294" -dependencies = [ - "glib-sys 0.21.5", + "glib-sys", "libc", "system-deps", ] @@ -722,7 +600,7 @@ version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b86dfad7d14251c9acaf1de63bc8754b7e3b4e5b16777b6f5a748208fe9519b" dependencies = [ - "glib 0.20.12", + "glib", "graphene-sys", "libc", ] @@ -733,7 +611,7 @@ version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df583a85ba2d5e15e1797e40d666057b28bc2f60a67c9c24145e6db2cc3861ea" dependencies = [ - "glib-sys 0.20.10", + "glib-sys", "libc", "pkg-config", "system-deps", @@ -745,13 +623,13 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61f5e72f931c8c9f65fbfc89fe0ddc7746f147f822f127a53a9854666ac1f855" dependencies = [ - "cairo-rs 0.20.12", + "cairo-rs", "gdk4", - "glib 0.20.12", + "glib", "graphene-rs", "gsk4-sys", "libc", - "pango 0.20.12", + "pango", ] [[package]] @@ -760,13 +638,13 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "755059de55fa6f85a46bde8caf03e2184c96bfda1f6206163c72fb0ea12436dc" dependencies = [ - "cairo-sys-rs 0.20.10", + "cairo-sys-rs", "gdk4-sys", - "glib-sys 0.20.10", - "gobject-sys 0.20.10", + "glib-sys", + "gobject-sys", "graphene-sys", "libc", - "pango-sys 0.20.10", + "pango-sys", "system-deps", ] @@ -776,19 +654,19 @@ version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f274dd0102c21c47bbfa8ebcb92d0464fab794a22fad6c3f3d5f165139a326d6" dependencies = [ - "cairo-rs 0.20.12", + "cairo-rs", "field-offset", "futures-channel", "gdk-pixbuf", "gdk4", - "gio 0.20.12", - "glib 0.20.12", + "gio", + "glib", "graphene-rs", "gsk4", "gtk4-macros", "gtk4-sys", "libc", - "pango 0.20.12", + "pango", ] [[package]] @@ -809,16 +687,16 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6" dependencies = [ - "cairo-sys-rs 0.20.10", + "cairo-sys-rs", "gdk-pixbuf-sys", "gdk4-sys", - "gio-sys 0.20.10", - "glib-sys 0.20.10", - "gobject-sys 0.20.10", + "gio-sys", + "glib-sys", + "gobject-sys", "graphene-sys", "gsk4-sys", "libc", - "pango-sys 0.20.10", + "pango-sys", "system-deps", ] @@ -1002,9 +880,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.14.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -1046,19 +924,18 @@ checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "librsvg" -version = "2.61.3" +version = "2.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44b612e3a9ec82c8ae9f2ebc9b2e8ff50aa831742686c0071998f76896cf214f" +checksum = "a3a70cc57e9d915b91b56ae5dde1c52aaf5aec17579ab2158451d1d7a40b0c1b" dependencies = [ - "cairo-rs 0.21.5", + "cairo-rs", "cast", "cssparser", - "cssparser-color", "data-url", "encoding_rs", "float-cmp", - "gio 0.21.5", - "glib 0.21.5", + "gio", + "glib", "image", "itertools", "language-tags", @@ -1067,15 +944,14 @@ dependencies = [ "markup5ever", "nalgebra", "num-traits", - "pango 0.21.5", + "pango", "pangocairo", - "precomputed-hash", "rayon", "rctree", "regex", "rgb", "selectors", - "string_cache 0.9.0", + "string_cache", "system-deps", "tinyvec", "url", @@ -1133,13 +1009,16 @@ dependencies = [ [[package]] name = "markup5ever" -version = "0.35.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" dependencies = [ "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", "tendril", - "web_atoms", ] [[package]] @@ -1342,22 +1221,10 @@ version = "0.20.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6576b311f6df659397043a5fa8a021da8f72e34af180b44f7d57348de691ab5c" dependencies = [ - "gio 0.20.12", - "glib 0.20.12", + "gio", + "glib", "libc", - "pango-sys 0.20.10", -] - -[[package]] -name = "pango" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1d85e2078077a065bb7fc072783d5bcd4e51b379f22d67107d0a16937eb69" -dependencies = [ - "gio 0.21.5", - "glib 0.21.5", - "libc", - "pango-sys 0.21.5", + "pango-sys", ] [[package]] @@ -1366,47 +1233,35 @@ version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186909673fc09be354555c302c0b3dcf753cd9fa08dcb8077fa663c80fb243fa" dependencies = [ - "glib-sys 0.20.10", - "gobject-sys 0.20.10", - "libc", - "system-deps", -] - -[[package]] -name = "pango-sys" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f06627d36ed5ff303d2df65211fc2e52ba5b17bf18dd80ff3d9628d6e06cfd" -dependencies = [ - "glib-sys 0.21.5", - "gobject-sys 0.21.5", + "glib-sys", + "gobject-sys", "libc", "system-deps", ] [[package]] name = "pangocairo" -version = "0.21.5" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b36c5c84304072939d860595d9bda2a797d3bd6f7215e20b8ccd0e72d84da8c8" +checksum = "58890dc451db9964ac2d8874f903a4370a4b3932aa5281ff0c8d9810937ad84f" dependencies = [ - "cairo-rs 0.21.5", - "glib 0.21.5", + "cairo-rs", + "glib", "libc", - "pango 0.21.5", + "pango", "pangocairo-sys", ] [[package]] name = "pangocairo-sys" -version = "0.21.5" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadbb01ad38be76e0d37e329d40ba0f3f9ef261d7b84b05201d7a0f14f819406" +checksum = "b9952903f88aa93e2927e7bca2d1ebae64fc26545a9280b4ce6bddeda26b5c42" dependencies = [ - "cairo-sys-rs 0.21.5", - "glib-sys 0.21.5", + "cairo-sys-rs", + "glib-sys", "libc", - "pango-sys 0.21.5", + "pango-sys", "system-deps", ] @@ -1445,6 +1300,15 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + [[package]] name = "phf" version = "0.11.3" @@ -1455,16 +1319,36 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + [[package]] name = "phf_codegen" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "phf_generator", + "phf_generator 0.11.3", "phf_shared 0.11.3", ] +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand", +] + [[package]] name = "phf_generator" version = "0.11.3" @@ -1481,7 +1365,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ - "phf_generator", + "phf_generator 0.11.3", "phf_shared 0.11.3", "proc-macro2", "quote", @@ -1490,20 +1374,20 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" dependencies = [ - "siphasher", + "siphasher 0.3.11", ] [[package]] name = "phf_shared" -version = "0.13.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher", + "siphasher 1.0.4", ] [[package]] @@ -1540,6 +1424,15 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "precomputed-hash" version = "0.1.1" @@ -1591,6 +1484,18 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", "rand_core", ] @@ -1599,6 +1504,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] [[package]] name = "rawpointer" @@ -1652,10 +1560,10 @@ dependencies = [ name = "rdraught-gtk" version = "0.1.0" dependencies = [ - "cairo-rs 0.20.12", + "cairo-rs", "gdk4", - "gio 0.20.12", - "glib 0.20.12", + "gio", + "glib", "gtk4", "librsvg", "rdraught", @@ -1683,18 +1591,15 @@ version = "0.1.0" dependencies = [ "rdraught", "wasm4", - "wasm4-sys", ] [[package]] name = "rdraught-wasm" version = "0.1.0" dependencies = [ - "base64", "console_error_panic_hook", "rdraught", "rdraught-ui-common", - "rmath", "wasm-bindgen", "web-sys", ] @@ -1795,7 +1700,7 @@ dependencies = [ "unicode-segmentation", "unicode-width", "utf8parse", - "windows-sys 0.59.0", + "windows-sys", ] [[package]] @@ -1895,9 +1800,9 @@ dependencies = [ [[package]] name = "selectors" -version = "0.31.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5685b6ae43bfcf7d2e7dfcfb5d8e8f61b46442c902531e41a32a9a8bf0ee0fb6" +checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" dependencies = [ "bitflags 2.13.2", "cssparser", @@ -1905,8 +1810,8 @@ dependencies = [ "fxhash", "log", "new_debug_unreachable", - "phf", - "phf_codegen", + "phf 0.10.1", + "phf_codegen 0.10.0", "precomputed-hash", "servo_arc", "smallvec", @@ -1958,9 +1863,9 @@ dependencies = [ [[package]] name = "servo_arc" -version = "0.4.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44" dependencies = [ "stable_deref_trait", ] @@ -1990,6 +1895,12 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "siphasher" version = "1.0.4" @@ -2027,26 +1938,13 @@ dependencies = [ "serde", ] -[[package]] -name = "string_cache" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared 0.13.1", - "precomputed-hash", - "serde", -] - [[package]] name = "string_cache_codegen" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" dependencies = [ - "phf_generator", + "phf_generator 0.11.3", "phf_shared 0.11.3", "proc-macro2", "quote", @@ -2266,6 +2164,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasm-bindgen" version = "0.2.129" @@ -2344,18 +2248,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web_atoms" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" -dependencies = [ - "phf", - "phf_codegen", - "string_cache 0.8.9", - "string_cache_codegen", -] - [[package]] name = "weezl" version = "0.1.12" @@ -2409,15 +2301,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -2499,11 +2382,12 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "xml5ever" -version = "0.35.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee3f1e41afb31a75aef076563b0ad3ecc24f5bd9d12a72b132222664eb76b494" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" dependencies = [ "log", + "mac", "markup5ever", ] @@ -2530,6 +2414,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zerofrom" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index cc54521..a6e5ba2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,16 +25,14 @@ rust-version = "1.87" [workspace.dependencies] rdraught = { path = "rdraught", version = "0.1" } rdraught-ui-common = { path = "rdraught-ui-common", version = "0.1" } -base64 = "0.22" cairo-rs = "0.20" cairo-sys-rs = "0.20" -librsvg = "2.60" +librsvg = "~2.60" gtk4 = {version = "0.9", features = ["v4_10"] } gdk4 = "0.9" gio = "0.20" glib = "0.20" wasm4 = "0.2" -wasm4-sys = "0.1" strum = "0.27" strum_macros = "0.27" heapless = "0.8" @@ -44,3 +42,9 @@ console_error_panic_hook = "0.1" sdl3 = { version = "0.14.35" } sdl3-sys = { version = "0.5" } rmath = {version = "0.1.0", registry = "gitea"} + +[profile.release] +lto = true + +[profile.release.package.rdraught-w4] +opt-level = "z" diff --git a/README.md b/README.md index 1be186f..b79caa0 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,16 @@ several front ends. | --- | --- | | `rdraught` | `no_std` game engine, move generation, AI, and undo/redo history. | | `rdraught-ui-common` | Small geometry and shared-state helpers used by the graphical front ends. | -| `rdraught-gtk` | GTK4 desktop front end (the most complete one). | -| `rdraught-wasm` | Browser front end using the Canvas 2D API and `wasm-bindgen`. | -| `rdraught-w4` | WASM-4 front end. Work in progress. | -| `rdraught-sdl` | SDL3 front end. Work in progress. | +| `rdraught-gtk` | Playable GTK4 desktop front end. | +| `rdraught-wasm` | Playable browser front end using the Canvas 2D API and `wasm-bindgen`. | +| `rdraught-w4` | Playable WASM-4 front end. | +| `rdraught-sdl` | SDL3 prototype; not wired to the game yet. | | `rdraught-cli` | Scratch `rustyline` program; not a playable CLI yet. | +The GTK, WASM, and WASM-4 front ends are currently hot-seat games: both sides +are played by people. The core AI is available but is not wired into a frontend +yet. + ## Rules The rules implemented by the engine are an 8x8 draughts variant: @@ -35,7 +39,8 @@ The engine exposes a fixed-depth negamax search with alpha-beta pruning through `DraughtsGame::get_best_move(max_depth)`. A depth of zero is treated as one ply. Wins and losses dominate material scoring, and faster wins are preferred. The search has no time or node budget and no draw/repetition detection, so choose a -modest depth for positions with many pieces. +modest depth for positions with many pieces. The current frontends do not use +the AI; they are hot-seat games. ## Requirements @@ -79,6 +84,11 @@ below instead. cargo run -p rdraught-gtk ``` +Use the board orientation selector at startup, click a piece to select it, and +click a highlighted destination to move. The header bar has undo, redo, and +New Game buttons. The game-over dialog appears automatically; use New Game to +start another game. + ### Browser WASM ```sh @@ -87,6 +97,13 @@ cd rdraught-wasm trunk serve --open ``` +Click a piece and then a highlighted destination. Keyboard controls: + +- `Ctrl+Z` — undo +- `Ctrl+Y` or `Ctrl+Shift+Z` — redo +- `N` — new game +- `F` — flip the board orientation + The `.cargo/config.toml` inside `rdraught-wasm` is only read when Cargo runs from that directory. @@ -97,8 +114,12 @@ cd rdraught-w4 cargo build --release --target wasm32-unknown-unknown ``` -Run Cargo from `rdraught-w4` so its linker configuration is applied. The -resulting module is in `target/wasm32-unknown-unknown/release/`. +Run Cargo from `rdraught-w4` so its linker configuration is applied. The module +is written to `../target/wasm32-unknown-unknown/release/rdraught_w4.wasm`. + +Use the mouse to select a piece and click a highlighted destination to move. +After a win, click to start a new game. The board is shown in a fixed +orientation for hot-seat play. ### SDL3 diff --git a/rdraught-gtk/examples/ai_debugger.rs b/rdraught-gtk/examples/ai_debugger.rs index 1ceec91..0cc86c8 100644 --- a/rdraught-gtk/examples/ai_debugger.rs +++ b/rdraught-gtk/examples/ai_debugger.rs @@ -1,7 +1,5 @@ use glib::ExitCode; -use rdraught::{DraughtsBoard, DraughtsGame, Move, Piece, Player, Position}; -use rdraught_gtk::run; -use std::collections::HashMap; +use rdraught::{DraughtsGame, Move}; use std::mem::size_of; fn main() -> ExitCode { diff --git a/rdraught-gtk/src/final_dialog.rs b/rdraught-gtk/src/final_dialog.rs index 128a93a..f236849 100644 --- a/rdraught-gtk/src/final_dialog.rs +++ b/rdraught-gtk/src/final_dialog.rs @@ -1,7 +1,7 @@ use gtk4::{AlertDialog, Window, prelude::IsA}; use rdraught::Player; -pub(crate) async fn create_dialog>(window: W, winner: Player) { +pub(crate) fn create_dialog>(window: W, winner: Player) { let msg = match winner { Player::Red => String::from("Red player wins"), Player::White => String::from("White player wins"), diff --git a/rdraught-gtk/src/greeting_dialog.rs b/rdraught-gtk/src/greeting_dialog.rs index dcd3828..5d7313d 100644 --- a/rdraught-gtk/src/greeting_dialog.rs +++ b/rdraught-gtk/src/greeting_dialog.rs @@ -4,7 +4,7 @@ use rdraught::Player; use rdraught_ui_common::SharedMutable; pub(crate) fn create(application: &Application, current_player: SharedMutable) -> Window { - let label = Label::builder().label("Main player:").build(); + let label = Label::builder().label("Board orientation:").build(); let red_player_button = CheckButton::builder().label("Red").active(true).build(); let white_player_button = CheckButton::builder() .label("White") @@ -44,19 +44,11 @@ pub(crate) fn create(application: &Application, current_player: SharedMutable Point { Point::new(x, y) } +fn player_name(player: Player) -> &'static str { + match player { + Player::Red => "Red", + Player::White => "White", + } +} + +fn refresh_game_state( + rd: &SharedMutableRef, + status: &Label, + undo_button: &Button, + redo_button: &Button, +) -> Option { + let rd = rd.borrow(); + let game = rd.game(); + let winner = game.winner(); + let text = match winner { + Some(winner) => format!("{} wins", player_name(winner)), + None => format!("{} to move", player_name(game.next_move())), + }; + status.set_text(&text); + undo_button.set_sensitive(winner.is_none() && rd.can_undo()); + redo_button.set_sensitive(winner.is_none() && rd.can_redo()); + winner +} + +fn handle_game_over( + winner: Option, + finished: &SharedMutable, + drawing_area: &DrawingArea, + window: >k::ApplicationWindow, +) { + if let Some(winner) = winner { + if !finished.get() { + finished.set(true); + drawing_area.set_sensitive(false); + final_dialog::create_dialog(window.clone(), winner); + } + } +} + trait AugmentedRect { fn from_points(tl: Point, br: Point) -> Rectangle; fn center(&self) -> Point; @@ -253,6 +294,10 @@ fn create_game_window( redo_button.set_icon_name("edit-redo-symbolic"); redo_button.set_sensitive(false); + let finished: SharedMutable = new_shared_mut(false); + let status = Label::new(None); + let new_game_button = Button::with_label("New Game"); + let selected_piece: SharedMutable> = new_shared_mut(None); let available_moves = new_shared_mut_ref(Vec::::new()); // Create a DrawingArea widget where we will draw the chessboard. @@ -261,6 +306,11 @@ fn create_game_window( window.set_child(Some(&drawing_area)); header_bar.pack_start(&undo_button); + header_bar.pack_start(&redo_button); + header_bar.pack_start(&new_game_button); + header_bar.pack_end(&status); + let _ = refresh_game_state(&rd, &status, &undo_button, &redo_button); + { let da = drawing_area.clone(); let rd = rd.clone(); @@ -268,19 +318,23 @@ fn create_game_window( let redo_btn_clone = redo_button.clone(); let selected_piece = selected_piece.clone(); let available_moves = available_moves.clone(); + let status = status.clone(); + let finished = finished.clone(); + let window = window.clone(); undo_button.connect_clicked(move |_| { - let mut rd = rd.borrow_mut(); - if rd.can_undo() { - rd.undo().ok(); - redo_btn_clone.set_sensitive(rd.can_redo()); - undo_btn_clone.set_sensitive(rd.can_undo()); - selected_piece.set(None); - available_moves.borrow_mut().clear(); - da.queue_draw(); + { + let mut app = rd.borrow_mut(); + if app.can_undo() { + app.undo().ok(); + selected_piece.set(None); + available_moves.borrow_mut().clear(); + } } + let winner = refresh_game_state(&rd, &status, &undo_btn_clone, &redo_btn_clone); + handle_game_over(winner, &finished, &da, &window); + da.queue_draw(); }); } - header_bar.pack_start(&redo_button); { let da = drawing_area.clone(); let rd = rd.clone(); @@ -288,16 +342,40 @@ fn create_game_window( let redo_btn_clone = redo_button.clone(); let selected_piece = selected_piece.clone(); let available_moves = available_moves.clone(); + let status = status.clone(); + let finished = finished.clone(); + let window = window.clone(); redo_button.connect_clicked(move |_| { - let mut rd = rd.borrow_mut(); - if rd.can_redo() { - rd.redo().ok(); - redo_btn_clone.set_sensitive(rd.can_redo()); - undo_btn_clone.set_sensitive(rd.can_undo()); - selected_piece.set(None); - available_moves.borrow_mut().clear(); - da.queue_draw(); + { + let mut app = rd.borrow_mut(); + if app.can_redo() { + app.redo().ok(); + selected_piece.set(None); + available_moves.borrow_mut().clear(); + } } + let winner = refresh_game_state(&rd, &status, &undo_btn_clone, &redo_btn_clone); + handle_game_over(winner, &finished, &da, &window); + da.queue_draw(); + }); + } + { + let da = drawing_area.clone(); + let rd = rd.clone(); + let undo_btn_clone = undo_button.clone(); + let redo_btn_clone = redo_button.clone(); + let selected_piece = selected_piece.clone(); + let available_moves = available_moves.clone(); + let status = status.clone(); + let finished = finished.clone(); + new_game_button.connect_clicked(move |_| { + *rd.borrow_mut() = RDraughtApplication::new(DraughtsGame::default()); + finished.set(false); + selected_piece.set(None); + available_moves.borrow_mut().clear(); + da.set_sensitive(true); + let _ = refresh_game_state(&rd, &status, &undo_btn_clone, &redo_btn_clone); + da.queue_draw(); }); } // Get the allocation information for the widget. @@ -362,10 +440,7 @@ fn create_game_window( // Loop over rows and columns to draw each chessboard cell. for row in 0..DraughtsBoard::rows() { for col in 0..DraughtsBoard::columns() { - let position = match current_player { - Player::Red => Position::new(row as u8, (8 - 1 - col) as u8), - Player::White => Position::new((8 - 1 - row) as u8, col as u8), - }; + let position = board_position(current_player, row as u8, col as u8); let square = Rectangle::new( col as f64 * SQUARE_SIZE, row as f64 * SQUARE_SIZE, @@ -481,99 +556,74 @@ fn create_game_window( let window = window.clone(); let undo_btn_clone = undo_button.clone(); let redo_btn_clone = redo_button.clone(); + let selected_piece = selected_piece.clone(); + let finished = finished.clone(); + let status = status.clone(); gesture.connect_pressed(move |gesture, _, x, y| { gesture.set_state(gtk::EventSequenceState::Claimed); - if let Some(winner) = rd.borrow().game().winner() { - MainContext::default() - .spawn_local(final_dialog::create_dialog(window.clone(), winner)); - } else { - let xform = xform.borrow(); - let inverse = { - let mut m = *xform; - m.invert(); - m - }; - let p = transform_point(&Point::new(x, y), &inverse); - if board_clone.contains(&p) { - let p = p - board_clone.tl(); - // println!("Point: {:?}", p); - let position = match current_player { - Player::White => Position::new( - (8.0 - (p.y() / SQUARE_SIZE)) as u8, - (p.x() / SQUARE_SIZE) as u8, - ), - Player::Red => Position::new( - (p.y() / SQUARE_SIZE) as u8, - (8.0 - p.x() / SQUARE_SIZE) as u8, - ), - }; - // println!("Selected position: {:?}", position); - let piece = { - let rd = rd.borrow(); - let draughts_game = rd.game(); - position - .clone() - .map_or(Piece::NoPiece, |it| draughts_game.piece_at(it)) - // println!("Selected piece: {:?}", piece); - }; - let am = available_moves.replace(Vec::new()); - let mut move_applied = false; - if !am.is_empty() { - if let Ok(pos) = position { - for mv in am.into_iter() { - if mv.get_end_position() == Some(pos) { - let mut rd_app = rd.borrow_mut(); - // println!("Applied move: {:?}", mv); - if rd_app.apply_move(mv).is_err() { - continue; - } - // let game_copy = rd_app.game().clone(); - // thread::spawn(move || { - // if let (Some(mv), analyzed_moves) = - // game_copy.get_best_move(10) - // { - // println!( - // "Next best move: {:?}, analyzed moves: {}", - // mv, analyzed_moves - // ); - // } - // }); - move_applied = true; - break; - } - } - } - if move_applied { - selected_piece.set(None); - let rd = rd.borrow(); - undo_btn_clone.set_sensitive(rd.can_undo()); - redo_btn_clone.set_sensitive(rd.can_redo()); + if finished.get() { + return; + } + + let xform = xform.borrow(); + let inverse = { + let mut m = *xform; + m.invert(); + m + }; + let p = transform_point(&Point::new(x, y), &inverse); + if !board_clone.contains(&p) { + selected_piece.set(None); + available_moves.borrow_mut().clear(); + drawing_area.queue_draw(); + return; + } + + let p = p - board_clone.tl(); + let screen_row = (p.y() / SQUARE_SIZE) as u8; + let screen_col = (p.x() / SQUARE_SIZE) as u8; + let position = board_position(current_player, screen_row, screen_col).ok(); + let am = available_moves.replace(Vec::new()); + let mut move_applied = false; + if let Some(pos) = position { + for mv in am { + if mv.get_end_position() == Some(pos) { + let mut rd_app = rd.borrow_mut(); + if rd_app.apply_move(mv).is_err() { + continue; } + move_applied = true; + break; } - if !move_applied { - let position = position.ok(); - match piece.player() { - Some(Player::Red) => selected_piece.set(position), - Some(Player::White) => selected_piece.set(position), - None => selected_piece.set(None), - } - if piece.player().is_none() { - selected_piece.set(None) - } else { - let mut am = available_moves.borrow_mut(); - selected_piece.set(position); - if let Some(position) = position { - for mv in rd.borrow().game().moves_for_piece(position) { - am.push(mv); - } - } - } + } + } + + if move_applied { + if let Some(pending) = rd.borrow().game().pending_capture() { + selected_piece.set(Some(pending)); + let mut moves = available_moves.borrow_mut(); + moves.clear(); + for mv in rd.borrow().game().moves_for_piece(pending) { + moves.push(mv); } } else { selected_piece.set(None); + available_moves.borrow_mut().clear(); + } + let winner = refresh_game_state(&rd, &status, &undo_btn_clone, &redo_btn_clone); + handle_game_over(winner, &finished, &drawing_area, &window); + } else { + let mut moves = available_moves.borrow_mut(); + moves.clear(); + selected_piece.set(None); + if let Some(position) = position { + for mv in rd.borrow().game().moves_for_piece(position) { + selected_piece.set(Some(position)); + moves.push(mv); + } } - drawing_area.queue_draw(); } + drawing_area.queue_draw(); }); } // Assign the gesture to the treeview @@ -582,11 +632,6 @@ fn create_game_window( } fn on_activate(application: &Application, game: DraughtsGame) { - // Initialize GTK before using any GTK functions. - if gtk::init().is_err() { - panic!("Failed to initialize GTK."); - } - let current_player = new_shared_mut(Player::Red); let dialog = greeting_dialog::create(application, current_player.clone()); let application = application.clone(); diff --git a/rdraught-ui-common/src/board.rs b/rdraught-ui-common/src/board.rs new file mode 100644 index 0000000..20a15e5 --- /dev/null +++ b/rdraught-ui-common/src/board.rs @@ -0,0 +1,70 @@ +use rdraught::{DraughtsBoard, Error, Player, Position, RectangularBoard}; + +/// Maps a screen square to a board square for the given point of view. +/// +/// Both views place pieces on screen squares where +/// `(screen_row + screen_col) % 2 == 1`. +pub fn board_position(view: Player, screen_row: u8, screen_col: u8) -> Result { + let rows = DraughtsBoard::rows() as u8; + let columns = DraughtsBoard::columns() as u8; + if screen_row >= rows || screen_col >= columns { + return Err(Error::InvalidPosition); + } + match view { + Player::White => Position::new(rows - 1 - screen_row, screen_col), + Player::Red => Position::new(screen_row, columns - 1 - screen_col), + } +} + +#[cfg(test)] +mod tests { + use super::board_position; + use rdraught::{Player, Position}; + + #[test] + fn white_view_mapping() { + assert_eq!( + Position::new(7, 1).unwrap(), + board_position(Player::White, 0, 1).unwrap() + ); + assert_eq!( + Position::new(0, 0).unwrap(), + board_position(Player::White, 7, 0).unwrap() + ); + assert!(board_position(Player::White, 0, 0).is_err()); + } + + #[test] + fn red_view_mapping() { + assert_eq!( + Position::new(0, 6).unwrap(), + board_position(Player::Red, 0, 1).unwrap() + ); + assert_eq!( + Position::new(7, 7).unwrap(), + board_position(Player::Red, 7, 0).unwrap() + ); + assert!(board_position(Player::Red, 0, 0).is_err()); + } + + #[test] + fn both_views_have_32_squares() { + for view in [Player::White, Player::Red] { + let mut count = 0; + for row in 0..8 { + for col in 0..8 { + if board_position(view, row, col).is_ok() { + count += 1; + } + } + } + assert_eq!(32, count); + } + } + + #[test] + fn out_of_bounds_is_rejected() { + assert!(board_position(Player::White, 8, 0).is_err()); + assert!(board_position(Player::Red, 0, 8).is_err()); + } +} diff --git a/rdraught-ui-common/src/geo2d.rs b/rdraught-ui-common/src/geo2d.rs index b255777..5286b8c 100644 --- a/rdraught-ui-common/src/geo2d.rs +++ b/rdraught-ui-common/src/geo2d.rs @@ -205,7 +205,7 @@ impl Xform { } pub fn yx(&self) -> f64 { - self.0[(1, 0)] + self.0[(0, 1)] } pub fn yy(&self) -> f64 { @@ -259,3 +259,41 @@ impl Display for Xform { self.0.fmt(f) } } + +#[cfg(test)] +mod tests { + use super::{Point, Xform}; + use core::f64::consts::PI; + + fn close(actual: f64, expected: f64) { + assert!((actual - expected).abs() < 1e-9); + } + + #[test] + fn rotation_uses_all_matrix_components() { + let rotation = Xform::rot(PI / 2.0); + close(rotation.yx(), 1.0); + close(rotation.xy(), -1.0); + + let point = Point::new(1.0, 0.0) * &rotation; + close(point.x(), 0.0); + close(point.y(), 1.0); + } + + #[test] + fn translation_and_scaling() { + let transform = Xform::scale(2.0, 3.0) * Xform::xlate(3.0, 4.0); + let point = Point::new(1.0, 2.0) * &transform; + close(point.x(), 5.0); + close(point.y(), 10.0); + } + + #[test] + fn inversion_round_trips() { + let transform = Xform::xlate(3.0, 4.0) * Xform::rot(0.3) * Xform::scale(2.0, 0.5); + let point = Point::new(1.0, 2.0); + let result = (point * &transform) * &transform.invert(); + close(result.x(), point.x()); + close(result.y(), point.y()); + } +} diff --git a/rdraught-ui-common/src/lib.rs b/rdraught-ui-common/src/lib.rs index b23a848..b07ffe5 100644 --- a/rdraught-ui-common/src/lib.rs +++ b/rdraught-ui-common/src/lib.rs @@ -1,5 +1,7 @@ +mod board; mod geo2d; mod types; +pub use board::board_position; pub use geo2d::{Point, Rect, Xform}; pub use types::{SharedMutable, SharedMutableRef, new_shared_mut, new_shared_mut_ref}; diff --git a/rdraught-w4/Cargo.toml b/rdraught-w4/Cargo.toml index 08c4cbc..4f6d39f 100644 --- a/rdraught-w4/Cargo.toml +++ b/rdraught-w4/Cargo.toml @@ -10,13 +10,7 @@ version.workspace = true [dependencies] wasm4.workspace = true -wasm4-sys.workspace = true rdraught.workspace = true [lib] crate-type = ["cdylib"] - -[profile.release] -opt-level = "z" -lto = true - diff --git a/rdraught-w4/src/lib.rs b/rdraught-w4/src/lib.rs index cf97fc5..563fcd7 100644 --- a/rdraught-w4/src/lib.rs +++ b/rdraught-w4/src/lib.rs @@ -1,26 +1,196 @@ #![no_main] #![no_std] -use rdraught::{DraughtsBoard, DraughtsGame, Piece, Position}; +use core::arch::wasm32::unreachable; +use rdraught::{DraughtsGame, Piece, Player, Position}; use wasm4::draw::{Color, DrawIndex, DrawIndices, Framebuffer}; use wasm4::rt::{Resources, Runtime}; -// use wasm4 as w4; -use core::arch::wasm32::unreachable; -use wasm4::sys::DRAW_COLORS; +use wasm4::sys::{DRAW_COLORS, MOUSE_BUTTONS, MOUSE_LEFT, MOUSE_X, MOUSE_Y}; use wasm4::trace; -// mod wasm4; -// use wasm4::{DRAW_COLORS, oval, rect}; +const SQUARE_SIZE: i32 = 20; +const BOARD_SIZE: i32 = 160; -// mod palette; - -// static mut GAME: DraughtsGame = DraughtsGame::default(); -struct Rdraught_W4 { +struct RdraughtW4 { framebuffer: Framebuffer, game: DraughtsGame, + selected: Option, + mouse_down: bool, } -impl Runtime for Rdraught_W4 { +impl RdraughtW4 { + fn set_colors(indices: [DrawIndex; 4]) { + unsafe { *DRAW_COLORS = DrawIndices::from_array(indices).into_u16() } + } + + fn draw_piece(&self, piece: Piece, top_left: [i32; 2]) { + let color = match piece.player() { + Some(Player::White) => DrawIndex::First, + Some(Player::Red) => DrawIndex::Second, + None => return, + }; + Self::set_colors([ + color, + DrawIndex::Transparent, + DrawIndex::Transparent, + DrawIndex::Transparent, + ]); + self.framebuffer.oval( + [top_left[0] + 2, top_left[1] + 2], + [(SQUARE_SIZE - 4) as u32, (SQUARE_SIZE - 4) as u32], + ); + + if piece.is_crowned() { + Self::set_colors([ + DrawIndex::Fourth, + DrawIndex::Transparent, + DrawIndex::Transparent, + DrawIndex::Transparent, + ]); + self.framebuffer + .rect([top_left[0] + 7, top_left[1] + 7], [6, 6]); + } + } + + fn draw_selection(&self) { + let Some(selected) = self.selected else { + return; + }; + let x = selected.col() as i32 * SQUARE_SIZE; + let y = selected.row() as i32 * SQUARE_SIZE; + + Self::set_colors([ + DrawIndex::Fourth, + DrawIndex::Transparent, + DrawIndex::Transparent, + DrawIndex::Transparent, + ]); + self.framebuffer.rect([x, y], [SQUARE_SIZE as u32, 2]); + self.framebuffer + .rect([x, y + SQUARE_SIZE - 2], [SQUARE_SIZE as u32, 2]); + self.framebuffer.rect([x, y], [2, SQUARE_SIZE as u32]); + self.framebuffer + .rect([x + SQUARE_SIZE - 2, y], [2, SQUARE_SIZE as u32]); + + for mv in self.game.moves_for_piece(selected) { + if let Some(end) = mv.get_end_position() { + self.framebuffer.rect( + [ + end.col() as i32 * SQUARE_SIZE + 8, + end.row() as i32 * SQUARE_SIZE + 8, + ], + [4, 4], + ); + } + } + } + + fn draw_board(&self) { + for row in 0..8i32 { + for col in 0..8i32 { + let indices = if (row + col) % 2 == 0 { + [ + DrawIndex::Third, + DrawIndex::Transparent, + DrawIndex::Fourth, + DrawIndex::Transparent, + ] + } else { + [ + DrawIndex::Fourth, + DrawIndex::Transparent, + DrawIndex::Fourth, + DrawIndex::Transparent, + ] + }; + Self::set_colors(indices); + let top_left = [col * SQUARE_SIZE, row * SQUARE_SIZE]; + self.framebuffer + .rect(top_left, [SQUARE_SIZE as u32, SQUARE_SIZE as u32]); + + if let Ok(position) = Position::new(row as u8, col as u8) { + let piece = self.game.piece_at(position); + if piece.is_present() { + self.draw_piece(piece, top_left); + } + } + } + } + self.draw_selection(); + } + + fn draw_winner(&self, winner: Player) { + Self::set_colors([ + DrawIndex::Fourth, + DrawIndex::Transparent, + DrawIndex::Transparent, + DrawIndex::Transparent, + ]); + self.framebuffer.rect([0, 70], [BOARD_SIZE as u32, 28]); + + Self::set_colors([ + DrawIndex::First, + DrawIndex::Transparent, + DrawIndex::Transparent, + DrawIndex::Transparent, + ]); + let (message, x) = match winner { + Player::Red => ("Red wins", 48), + Player::White => ("White wins", 40), + }; + self.framebuffer.text(message, [x, 74]); + self.framebuffer.text("click to restart", [16, 88]); + } + + fn reset(&mut self) { + self.game = DraughtsGame::default(); + self.selected = None; + } + + fn handle_click(&mut self, x: i32, y: i32) { + if self.game.winner().is_some() { + self.reset(); + return; + } + if x < 0 || y < 0 || x >= BOARD_SIZE || y >= BOARD_SIZE { + self.selected = None; + return; + } + + let row = (y / SQUARE_SIZE) as u8; + let col = (x / SQUARE_SIZE) as u8; + let Ok(position) = Position::new(row, col) else { + self.selected = None; + return; + }; + + if let Some(selected) = self.selected { + let found = self + .game + .moves_for_piece(selected) + .find(|mv| mv.get_end_position() == Some(position)); + if let Some(mv) = found { + if self.game.check_and_apply_move(&mv).is_ok() { + self.selected = self.game.pending_capture(); + } else { + self.selected = None; + } + return; + } + if self.game.pending_capture().is_some() { + return; + } + } + + if self.game.moves_for_piece(position).next().is_some() { + self.selected = Some(position); + } else { + self.selected = None; + } + } +} + +impl Runtime for RdraughtW4 { fn start(rs: Resources) -> Self { rs.framebuffer.replace_palette([ Color(0x00edeada), @@ -28,196 +198,33 @@ impl Runtime for Rdraught_W4 { Color(0x005d8d60), Color(0x00100f24), ]); - Rdraught_W4 { + RdraughtW4 { framebuffer: rs.framebuffer, game: DraughtsGame::default(), + selected: None, + mouse_down: false, } } fn update(&mut self) { - let indices = DrawIndices::from_array([ - DrawIndex::Third, - DrawIndex::Fourth, - DrawIndex::Fourth, - DrawIndex::Transparent, - ]); - unsafe { - *DRAW_COLORS = indices.into_u16(); + let (mouse_x, mouse_y, buttons) = unsafe { (*MOUSE_X, *MOUSE_Y, *MOUSE_BUTTONS) }; + let pressed = buttons & MOUSE_LEFT != 0; + if pressed && !self.mouse_down { + self.handle_click(mouse_x as i32, mouse_y as i32); } + self.mouse_down = pressed; - self.framebuffer.rect([10, 10], [50, 50]); - self.framebuffer.oval([50, 50], [10, 10]); - let SQUARE_SIZE = 20u32; - for i in 0..8i32 { - for j in 0..8i32 { - let indices = if (i + j) % 2 == 0 { - DrawIndices::from_array([ - DrawIndex::Third, - DrawIndex::Transparent, - DrawIndex::Fourth, - DrawIndex::Transparent, - ]) - } else { - DrawIndices::from_array([ - DrawIndex::Fourth, - DrawIndex::Transparent, - DrawIndex::Fourth, - DrawIndex::Transparent, - ]) - }; - unsafe { - *DRAW_COLORS = indices.into_u16(); - } - let tl = [i as i32 * SQUARE_SIZE as i32, j as i32 * SQUARE_SIZE as i32]; - self.framebuffer.rect(tl, [SQUARE_SIZE, SQUARE_SIZE]); - - let score = self.game.score_for_player(rdraught::Player::Red); - // let msg = format!("{}", score); - // w4::trace(msg.as_str()); - - let piece = self.game.piece_at(Position::new(i as u8, j as u8).unwrap()); - match piece { - Piece::SimpleWhitePawn => { - let indices = DrawIndices::from_array([ - DrawIndex::First, - DrawIndex::Transparent, - DrawIndex::Transparent, - DrawIndex::Transparent, - ]); - unsafe { - *DRAW_COLORS = indices.into_u16(); - } - self.framebuffer.oval(tl, [SQUARE_SIZE, SQUARE_SIZE]); - } - Piece::SimpleRedPawn => { - let indices = DrawIndices::from_array([ - DrawIndex::Second, - DrawIndex::Transparent, - DrawIndex::Transparent, - DrawIndex::Transparent, - ]); - unsafe { - *DRAW_COLORS = indices.into_u16(); - } - self.framebuffer.oval(tl, [SQUARE_SIZE, SQUARE_SIZE]); - } - Piece::CrownedRedPawn => {} - Piece::CrownedWhitePawn => {} - Piece::NoPiece => {} - } - } + self.draw_board(); + if let Some(winner) = self.game.winner() { + self.draw_winner(winner); } } } -wasm4::main! { Rdraught_W4 } - -// use wasm4::*; -// use wasm4_sys; - -// #[unsafe(no_mangle)] -// fn update() { -// unsafe { -// wasm4_sys::rect(10, 10, 32, 32); -// } -// } -// #[unsafe(no_mangle)] -// fn start() { -// palette::change_palette(0usize); -// palette::set_draw_color(2u16); -// } - -// #[unsafe(no_mangle)] -// fn update() { -// // if self.count % 60 == 0 { -// // w4::trace("tick"); -// // self.count = 0; -// // } -// // self.count += 1; -// // let indices = DrawIndices::from_array([ -// // DrawIndex::Third, -// // DrawIndex::Fourth, -// // DrawIndex::Fourth, -// // DrawIndex::Transparent, -// // ]); -// // unsafe { -// // *DRAW_COLORS = 0x; -// // } -// palette::set_draw_color([3, 4, 4, 0]); - -// rect(10, 10, 50, 50); -// oval(50, 50, 10, 10); -// let SQUARE_SIZE = 20u32; -// for i in 0..8i32 { -// for j in 0..8i32 { -// // let indices = if (i + j) % 2 == 0 { -// // DrawIndices::from_array([ -// // DrawIndex::Third, -// // DrawIndex::Transparent, -// // DrawIndex::Fourth, -// // DrawIndex::Transparent, -// // ]) -// // } else { -// // DrawIndices::from_array([ -// // DrawIndex::Fourth, -// // DrawIndex::Transparent, -// // DrawIndex::Fourth, -// // DrawIndex::Transparent, -// // ]) -// // }; -// // unsafe { -// // *DRAW_COLORS = indices.into_u16(); -// // } -// let tl = [i as i32 * SQUARE_SIZE as i32, j as i32 * SQUARE_SIZE as i32]; -// rect(tl.0, tl.1, SQUARE_SIZE, SQUARE_SIZE); - -// // let score = self.game.score_for_player(rdraught::Player::Red); -// // let msg = format!("{}", score); -// // w4::trace(msg.as_str()); -// unsafe { -// let piece = GAME.piece_at(Position::new(i as u8, j as u8).unwrap()); -// match piece { -// Piece::SimpleWhitePawn => { -// // let indices = DrawIndices::from_array([ -// // DrawIndex::First, -// // DrawIndex::Transparent, -// // DrawIndex::Transparent, -// // DrawIndex::Transparent, -// // ]); -// // unsafe { -// // *DRAW_COLORS = indices.into_u16(); -// // } -// oval(tl, [SQUARE_SIZE, SQUARE_SIZE]); -// } -// Piece::SimpleRedPawn => { -// // let indices = DrawIndices::from_array([ -// // DrawIndex::Second, -// // DrawIndex::Transparent, -// // DrawIndex::Transparent, -// // DrawIndex::Transparent, -// // ]); -// // unsafe { -// // *DRAW_COLORS = indices.into_u16(); -// // } -// oval(tl, [SQUARE_SIZE, SQUARE_SIZE]); -// } -// Piece::CrownedRedPawn => {} -// Piece::CrownedWhitePawn => {} -// Piece::NoPiece => {} -// } -// } -// } -// } -// } +wasm4::main! { RdraughtW4 } #[panic_handler] fn panic_handler(_panic_info: &core::panic::PanicInfo<'_>) -> ! { trace("panic error"); - - #[cfg(debug_assertions)] - if let Some(cause) = _panic_info.payload().downcast_ref::<&str>() { - trace(cause); - } - unreachable() } diff --git a/rdraught-w4/src/palette.rs b/rdraught-w4/src/palette.rs deleted file mode 100644 index d8f4fdd..0000000 --- a/rdraught-w4/src/palette.rs +++ /dev/null @@ -1,29 +0,0 @@ -use super::wasm4::DRAW_COLORS; -use super::wasm4::PALETTE; - -type Palette = [u32; 4]; - -pub const W4_DEFAULT: Palette = [0xe0f8cf, 0x86c06c, 0x306850, 0x071821]; - -pub const PONG_OG: Palette = [0x1b1b1b, 0xdcdcdc, 0x1b1b1b, 0xdcdcdc]; - -pub const ICE_CREAM_GB: Palette = [0xfff6d3, 0xf9a875, 0xeb6b6f, 0x7c3f58]; - -pub const HOLLOW: Palette = [0x0f0f1b, 0x565a75, 0xc5b7be, 0xf9fbf5]; - -const PALETTES: [Palette; 4] = [PONG_OG, ICE_CREAM_GB, HOLLOW, W4_DEFAULT]; - -#[inline] -fn palette_by_idx>(idx: T) -> Palette { - PALETTES[idx.into() % PALETTES.len()] -} - -pub fn change_palette>(idx: T) { - unsafe { - *PALETTE = palette_by_idx(idx); - } -} - -pub fn set_draw_color>(idx: T) { - unsafe { *DRAW_COLORS = idx.into() } -} diff --git a/rdraught-w4/src/wasm4.rs b/rdraught-w4/src/wasm4.rs deleted file mode 100644 index 2c559b6..0000000 --- a/rdraught-w4/src/wasm4.rs +++ /dev/null @@ -1,196 +0,0 @@ -// -// WASM-4: https://wasm4.org/docs - -// ┌───────────────────────────────────────────────────────────────────────────┐ -// │ │ -// │ Platform Constants │ -// │ │ -// └───────────────────────────────────────────────────────────────────────────┘ - -pub const SCREEN_SIZE: u32 = 160; -pub const FONT_SIZE: u32 = 8; - -// ┌───────────────────────────────────────────────────────────────────────────┐ -// │ │ -// │ Memory Addresses │ -// │ │ -// └───────────────────────────────────────────────────────────────────────────┘ - -pub static mut PALETTE: *mut [u32; 4] = 0x04 as *mut [u32; 4]; -pub const DRAW_COLORS: *mut u16 = 0x14 as *mut u16; -pub const GAMEPAD1: *const u8 = 0x16 as *const u8; -pub const GAMEPAD2: *const u8 = 0x17 as *const u8; -pub const GAMEPAD3: *const u8 = 0x18 as *const u8; -pub const GAMEPAD4: *const u8 = 0x19 as *const u8; -pub const MOUSE_X: *const i16 = 0x1a as *const i16; -pub const MOUSE_Y: *const i16 = 0x1c as *const i16; -pub const MOUSE_BUTTONS: *const u8 = 0x1e as *const u8; -pub static mut FRAMEBUFFER: *mut [u8; 6400] = 0xa0 as *mut [u8; 6400]; - -pub const BUTTON_1: u8 = 1; -pub const BUTTON_2: u8 = 2; -pub const BUTTON_LEFT: u8 = 16; -pub const BUTTON_RIGHT: u8 = 32; -pub const BUTTON_UP: u8 = 64; -pub const BUTTON_DOWN: u8 = 128; - -// ┌───────────────────────────────────────────────────────────────────────────┐ -// │ │ -// │ Drawing Functions │ -// │ │ -// └───────────────────────────────────────────────────────────────────────────┘ - -/// Copies pixels to the framebuffer. -pub fn blit(sprite: &[u8], x: i32, y: i32, width: u32, height: u32, flags: u32) { - unsafe { extern_blit(sprite.as_ptr(), x, y, width, height, flags) } -} -extern "C" { - #[link_name = "blit"] - fn extern_blit(sprite: *const u8, x: i32, y: i32, width: u32, height: u32, flags: u32); -} - -/// Copies a subregion within a larger sprite atlas to the framebuffer. -pub fn blit_sub( - sprite: &[u8], - x: i32, - y: i32, - width: u32, - height: u32, - src_x: u32, - src_y: u32, - stride: u32, - flags: u32, -) { - unsafe { - extern_blit_sub( - sprite.as_ptr(), - x, - y, - width, - height, - src_x, - src_y, - stride, - flags, - ) - } -} -extern "C" { - #[link_name = "blitSub"] - fn extern_blit_sub( - sprite: *const u8, - x: i32, - y: i32, - width: u32, - height: u32, - src_x: u32, - src_y: u32, - stride: u32, - flags: u32, - ); -} - -pub const BLIT_2BPP: u32 = 1; -pub const BLIT_1BPP: u32 = 0; -pub const BLIT_FLIP_X: u32 = 2; -pub const BLIT_FLIP_Y: u32 = 4; -pub const BLIT_ROTATE: u32 = 8; - -/// Draws a line between two points. -pub fn line(x1: i32, y1: i32, x2: i32, y2: i32) { - unsafe { extern_line(x1, y1, x2, y2) } -} -extern "C" { - #[link_name = "line"] - fn extern_line(x1: i32, y1: i32, x2: i32, y2: i32); -} - -/// Draws an oval (or circle). -pub fn oval(x: i32, y: i32, width: u32, height: u32) { - unsafe { extern_oval(x, y, width, height) } -} -extern "C" { - #[link_name = "oval"] - fn extern_oval(x: i32, y: i32, width: u32, height: u32); -} - -/// Draws a rectangle. -pub fn rect(x: i32, y: i32, width: u32, height: u32) { - unsafe { extern_rect(x, y, width, height) } -} -extern "C" { - #[link_name = "rect"] - fn extern_rect(x: i32, y: i32, width: u32, height: u32); -} - -/// Draws text using the built-in system font. -pub fn text(text: &str, x: i32, y: i32) { - unsafe { extern_text(text.as_ptr(), text.len(), x, y) } -} -extern "C" { - #[link_name = "textUtf8"] - fn extern_text(text: *const u8, length: usize, x: i32, y: i32); -} - -// ┌───────────────────────────────────────────────────────────────────────────┐ -// │ │ -// │ Sound Functions │ -// │ │ -// └───────────────────────────────────────────────────────────────────────────┘ - -/// Plays a sound tone. -pub fn tone(frequency: u32, duration: u32, volume: u32, flags: u32) { - unsafe { extern_tone(frequency, duration, volume, flags) } -} -extern "C" { - #[link_name = "tone"] - fn extern_tone(frequency: u32, duration: u32, volume: u32, flags: u32); -} - -pub const TONE_PULSE1: u32 = 0; -pub const TONE_PULSE2: u32 = 1; -pub const TONE_TRIANGLE: u32 = 2; -pub const TONE_NOISE: u32 = 3; -pub const TONE_MODE1: u32 = 0; -pub const TONE_MODE2: u32 = 4; -pub const TONE_MODE3: u32 = 8; -pub const TONE_MODE4: u32 = 12; - -// ┌───────────────────────────────────────────────────────────────────────────┐ -// │ │ -// │ Storage Functions │ -// │ │ -// └───────────────────────────────────────────────────────────────────────────┘ - -extern "C" { - /// Reads up to `size` bytes from persistent storage into the pointer `dest`. - pub fn diskr(dest: *mut u8, size: u32) -> u32; - - /// Writes up to `size` bytes from the pointer `src` into persistent storage. - pub fn diskw(src: *const u8, size: u32) -> u32; -} - -// ┌───────────────────────────────────────────────────────────────────────────┐ -// │ │ -// │ Other Functions │ -// │ │ -// └───────────────────────────────────────────────────────────────────────────┘ - -extern "C" { - /// Copies `size` bytes from `srcPtr` into `destPtr`. - #[link_name = "memcpy"] - pub fn memcpy(dest: *mut u8, src: *const u8, size: usize) -> usize; - - /// Fills memory at `destPtr` with `size` bytes of the fixed value `value`. - #[link_name = "memset"] - pub fn memset(dest: *mut u8, byte: u8, size: usize) -> usize; -} - -/// Prints a message to the debug console. -pub fn trace(text: &str) { - unsafe { extern_trace(text.as_ptr(), text.len()) } -} -extern "C" { - #[link_name = "traceUtf8"] - fn extern_trace(trace: *const u8, length: usize); -} diff --git a/rdraught-wasm/Cargo.toml b/rdraught-wasm/Cargo.toml index 08f880a..1439a50 100644 --- a/rdraught-wasm/Cargo.toml +++ b/rdraught-wasm/Cargo.toml @@ -8,22 +8,15 @@ repository.workspace = true rust-version.workspace = true version.workspace = true -# [lib] -# crate-type = ["cdylib"] - [dependencies] rdraught.workspace = true rdraught-ui-common.workspace = true wasm-bindgen.workspace = true console_error_panic_hook.workspace = true -rmath.workspace = true -base64.workspace = true - [dependencies.web-sys] workspace = true features = [ - 'DomMatrix', 'CanvasRenderingContext2d', 'CssStyleDeclaration', 'Document', @@ -31,9 +24,8 @@ features = [ 'EventTarget', 'HtmlCanvasElement', 'HtmlElement', + 'KeyboardEvent', 'MouseEvent', 'Node', - 'HtmlImageElement', - 'SvgImageElement', 'Window', ] diff --git a/rdraught-wasm/src/crown_red.svg b/rdraught-wasm/src/crown_red.svg deleted file mode 100644 index 07cf3f0..0000000 --- a/rdraught-wasm/src/crown_red.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - diff --git a/rdraught-wasm/src/crown_white.svg b/rdraught-wasm/src/crown_white.svg deleted file mode 100644 index 4ac5db8..0000000 --- a/rdraught-wasm/src/crown_white.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - diff --git a/rdraught-wasm/src/lib.rs b/rdraught-wasm/src/lib.rs deleted file mode 100644 index 2c1587a..0000000 --- a/rdraught-wasm/src/lib.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::cell::Cell; -use std::rc::Rc; -use wasm_bindgen::prelude::*; - -use web_sys::CanvasRenderingContext2d; -use web_sys::MouseEvent; -extern crate console_error_panic_hook; - -// #[wasm_bindgen(start)] -fn main() -> Result<(), JsValue> { - let document = web_sys::window().unwrap().document().unwrap(); - let canvas = document - .create_element("canvas")? - .dyn_into::()?; - document.body().unwrap().append_child(&canvas)?; - canvas.set_width(640); - canvas.set_height(480); - canvas.style().set_property("border", "solid")?; - let context = canvas - .get_context("2d")? - .unwrap() - .dyn_into::()?; - let context = Rc::new(context); - let pressed = Rc::new(Cell::new(false)); - { - let context = context.clone(); - let pressed = pressed.clone(); - let closure = Closure::::new(move |event: web_sys::MouseEvent| { - context.begin_path(); - context.move_to(event.offset_x() as f64, event.offset_y() as f64); - pressed.set(true); - }); - canvas.add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?; - closure.forget(); - } - { - let context = context.clone(); - let pressed = pressed.clone(); - let closure = Closure::::new(move |event: web_sys::MouseEvent| { - if pressed.get() { - context.line_to(event.offset_x() as f64, event.offset_y() as f64); - context.stroke(); - context.begin_path(); - context.move_to(event.offset_x() as f64, event.offset_y() as f64); - } - }); - canvas.add_event_listener_with_callback("mousemove", closure.as_ref().unchecked_ref())?; - closure.forget(); - } - { - let closure = Closure::::new(move |event: web_sys::MouseEvent| { - pressed.set(false); - context.line_to(event.offset_x() as f64, event.offset_y() as f64); - context.stroke(); - }); - canvas.add_event_listener_with_callback("mouseup", closure.as_ref().unchecked_ref())?; - closure.forget(); - } - - Ok(()) -} diff --git a/rdraught-wasm/src/main.rs b/rdraught-wasm/src/main.rs index 34e1d88..c0b11ea 100644 --- a/rdraught-wasm/src/main.rs +++ b/rdraught-wasm/src/main.rs @@ -1,156 +1,18 @@ -use base64::Engine; -use base64::engine::general_purpose::STANDARD as b64; use core::f64::consts::PI; -use rdraught::{DraughtsBoard, DraughtsGame, Move, Piece, Player, Position, RectangularBoard}; -use rdraught_ui_common::{ - Point, Rect, SharedMutable, SharedMutableRef, Xform, new_shared_mut, new_shared_mut_ref, +use rdraught::{ + DraughtsBoard, DraughtsGame, Move, Piece, Player, Position, RDraughtApplication, + RectangularBoard, }; +use rdraught_ui_common::{Point, Rect, Xform, board_position, new_shared_mut_ref}; use wasm_bindgen::prelude::*; -use web_sys::{CanvasRenderingContext2d, Document, HtmlImageElement, MouseEvent}; +use web_sys::{CanvasRenderingContext2d, KeyboardEvent, MouseEvent}; -extern crate console_error_panic_hook; +const CROWN_COLOR: &str = "#ffd700"; -#[wasm_bindgen] -extern "C" { - // Use `js_namespace` here to bind `console.log(..)` instead of just - // `log(..)` - #[wasm_bindgen(js_namespace = console)] - fn log(s: &str); - - // The `console.log` is quite polymorphic, so we can bind it with multiple - // signatures. Note that we need to use `js_name` to ensure we always call - // `log` in JS. - #[wasm_bindgen(js_namespace = console, js_name = log)] - fn log_u32(a: u32); - - // Multiple arguments too! - #[wasm_bindgen(js_namespace = console, js_name = log)] - fn log_many(a: &str, b: &str); -} - -macro_rules! console_log { - // Note that this is using the `log` function imported above during - // `bare_bones` - ($($t:tt)*) => (log(&format_args!($($t)*).to_string())) -} - -struct PieceDrawer { - red_crown: HtmlImageElement, - white_crown: HtmlImageElement, -} - -impl PieceDrawer { - fn new(document: &Document) -> Result { - const CROWN_RED: &[u8] = include_bytes!("crown_red.svg"); - const CROWN_WHITE: &[u8] = include_bytes!("crown_white.svg"); - let red_crown = document - .create_element("img")? - .dyn_into::()?; - let src = b64.encode(CROWN_RED); - let src = format!("data:image/svg+xml;base64,{}", src); - red_crown.set_src(&src); - let white_crown = document - .create_element("img")? - .dyn_into::()?; - let src = b64.encode(CROWN_WHITE); - let src = format!("data:image/svg+xml;base64,{}", src); - white_crown.set_src(&src); - Ok(PieceDrawer { - red_crown, - white_crown, - }) - } - - fn draw_piece( - &self, - ctx: &CanvasRenderingContext2d, - xform: &Xform, - cell: Rect, - piece: Piece, - ) -> Result<(), JsValue> { - ctx.save(); - let center = cell.center(); - let outer_radius = cell.width() * 0.3; - let thickness = outer_radius * 0.5; - let vertical_scale_factor = 0.7; - { - let p = Point::new(0.0, cell.center().y()) * xform; - let xform = xform.clone() - * Xform::xlate(-p.x(), -p.y()) - * Xform::scale(1.0, vertical_scale_factor) - * Xform::xlate(p.x(), p.y()); - apply_xform(ctx, &xform)?; - } - - ctx.set_fill_style_str("#000"); - ctx.begin_path(); - - ctx.arc( - center.x(), - center.y() + thickness / 2.0, - outer_radius, - 0.0, - PI, - )?; - ctx.line_to(center.x() - outer_radius, center.y() - thickness / 2.0); - ctx.arc( - center.x(), - center.y() - thickness / 2.0, - outer_radius, - PI, - 2.0 * PI, - )?; - ctx.line_to(center.x() + outer_radius, center.y() + thickness / 2.0); - ctx.fill(); - - match piece.player() { - Some(Player::Red) => { - ctx.set_fill_style_str("#ff0000"); - } - Some(Player::White) => { - ctx.set_fill_style_str("#ffffff"); - } - None => {} - } - let radius = cell.width() * 0.275; - ctx.begin_path(); - ctx.arc( - center.x(), - center.y() - thickness / 2.0, - radius, - 0.0, - 2.0 * PI, - )?; - ctx.fill(); - ctx.restore(); - ctx.save(); - if let Some(player) = piece.player() { - if piece.is_crowned() { - { - let p = Point::new(cell.center().x(), cell.center().y()) * xform; - let f = 0.55; - let xform = xform.clone() - * Xform::xlate(-p.x(), -p.y()) - * Xform::scale(1.0, vertical_scale_factor) - * Xform::scale(f, f) - * Xform::xlate(p.x(), p.y()); - apply_xform(ctx, &xform)?; - } - let image = match player { - Player::White => self.white_crown.clone(), - Player::Red => self.red_crown.clone(), - }; - ctx.draw_image_with_html_image_element_and_dw_and_dh( - &image, - cell.tl().x(), - cell.tl().y() - thickness / 1.0, - cell.width(), - cell.height(), - )?; - } - } - ctx.restore(); - Ok(()) +fn player_name(player: Player) -> &'static str { + match player { + Player::Red => "Red", + Player::White => "White", } } @@ -165,42 +27,129 @@ fn apply_xform(ctx: &CanvasRenderingContext2d, xform: &Xform) -> Result<(), JsVa ) } -fn from_ctx(ctx: &CanvasRenderingContext2d) -> Result { - let m = ctx.get_transform()?; - Ok(Xform::new(m.a(), m.c(), m.b(), m.d(), m.e(), m.f())) +fn draw_crown(ctx: &CanvasRenderingContext2d, center: Point, size: f64) -> Result<(), JsValue> { + let width = size * 0.35; + let height = size * 0.2; + let top = center.y() - height / 2.0; + + ctx.begin_path(); + ctx.move_to(center.x() - width / 2.0, top); + ctx.line_to(center.x() - width / 4.0, top + height); + ctx.line_to(center.x(), top - height * 0.25); + ctx.line_to(center.x() + width / 4.0, top + height); + ctx.line_to(center.x() + width / 2.0, top); + ctx.close_path(); + ctx.set_fill_style_str(CROWN_COLOR); + ctx.fill(); + Ok(()) } -fn map_row(current_player: Player, row: usize) -> usize { - match current_player { - Player::White => DraughtsBoard::rows() - 1 - row, - Player::Red => row, +fn draw_piece( + ctx: &CanvasRenderingContext2d, + xform: &Xform, + cell: Rect, + piece: Piece, +) -> Result<(), JsValue> { + if !piece.is_present() { + return Ok(()); } + + ctx.save(); + let center = cell.center(); + let outer_radius = cell.width() * 0.3; + let thickness = outer_radius * 0.5; + let vertical_scale_factor = 0.7; + { + let p = Point::new(0.0, cell.center().y()) * xform; + let piece_xform = xform.clone() + * Xform::xlate(-p.x(), -p.y()) + * Xform::scale(1.0, vertical_scale_factor) + * Xform::xlate(p.x(), p.y()); + apply_xform(ctx, &piece_xform)?; + } + + ctx.set_fill_style_str("#000"); + ctx.begin_path(); + ctx.arc( + center.x(), + center.y() + thickness / 2.0, + outer_radius, + 0.0, + PI, + )?; + ctx.line_to(center.x() - outer_radius, center.y() - thickness / 2.0); + ctx.arc( + center.x(), + center.y() - thickness / 2.0, + outer_radius, + PI, + 2.0 * PI, + )?; + ctx.line_to(center.x() + outer_radius, center.y() + thickness / 2.0); + ctx.fill(); + + match piece.player() { + Some(Player::Red) => ctx.set_fill_style_str("#ff0000"), + Some(Player::White) => ctx.set_fill_style_str("#ffffff"), + None => {} + } + let radius = cell.width() * 0.275; + ctx.begin_path(); + ctx.arc( + center.x(), + center.y() - thickness / 2.0, + radius, + 0.0, + 2.0 * PI, + )?; + ctx.fill(); + + if piece.is_crowned() { + draw_crown( + ctx, + Point::new(center.x(), center.y() - thickness / 2.0), + cell.width(), + )?; + } + + ctx.restore(); + Ok(()) } -trait Agent {} +fn draw_highlight(ctx: &CanvasRenderingContext2d, cell: &Rect, color: &str) -> Result<(), JsValue> { + ctx.save(); + ctx.set_shadow_blur(300.0); + ctx.set_shadow_color(color); + ctx.begin_path(); + ctx.move_to(cell.tl().x(), cell.tl().y()); + ctx.line_to(cell.tl().x(), cell.br().y()); + ctx.line_to(cell.br().x(), cell.br().y()); + ctx.line_to(cell.br().x(), cell.tl().y()); + ctx.line_to(cell.tl().x(), cell.tl().y()); + ctx.clip(); + ctx.begin_path(); + ctx.set_stroke_style_str(color); + ctx.set_line_width(4.0); + ctx.move_to(cell.tl().x(), cell.tl().y()); + ctx.line_to(cell.tl().x(), cell.br().y()); + ctx.line_to(cell.br().x(), cell.br().y()); + ctx.line_to(cell.br().x(), cell.tl().y()); + ctx.line_to(cell.tl().x(), cell.tl().y()); + ctx.stroke(); + ctx.restore(); + Ok(()) +} struct App { - game: DraughtsGame, + rd: RDraughtApplication, board: Rect, screen: Rect, xform: Xform, selected_piece: Option, available_moves: Vec, main_player: Player, - red_agent: Box, - white_agent: Box, } -struct Human {} - -impl Human { - fn new() -> Human { - Human {} - } -} - -impl Agent for Human {} - impl Default for App { fn default() -> Self { let board = Rect::from_size( @@ -209,172 +158,174 @@ impl Default for App { App::SQUARE_SIZE * DraughtsBoard::rows() as f64, ); App { - game: DraughtsGame::default(), + rd: RDraughtApplication::new(DraughtsGame::default()), board, screen: Rect::new(Point::new(0.0, 0.0), Point::new(0.0, 0.0)), xform: Xform::default(), selected_piece: None, available_moves: Vec::new(), main_player: Player::White, - red_agent: Box::new(Human::new()), - white_agent: Box::new(Human::new()), } } } impl App { const SQUARE_SIZE: f64 = 100.0; - fn draw( - &mut self, - ctx: &CanvasRenderingContext2d, - piece_drawer: &PieceDrawer, - ) -> Result<(), JsValue> { + + fn new_game(&mut self) { + self.rd = RDraughtApplication::new(DraughtsGame::default()); + self.selected_piece = None; + self.available_moves.clear(); + } + + fn flip_view(&mut self) { + self.main_player = match self.main_player { + Player::White => Player::Red, + Player::Red => Player::White, + }; + self.selected_piece = None; + self.available_moves.clear(); + } + + fn update_selection_after_move(&mut self) { + if let Some(pending) = self.rd.game().pending_capture() { + self.selected_piece = Some(pending); + self.available_moves = self.rd.game().moves_for_piece(pending).collect(); + } else { + self.selected_piece = None; + self.available_moves.clear(); + } + } + + fn draw(&mut self, ctx: &CanvasRenderingContext2d) -> Result<(), JsValue> { ctx.clear_rect(0.0, 0.0, self.screen.width(), self.screen.height()); let board = &self.board; - let f = f64::min( + let factor = f64::min( self.screen.width() / board.width(), self.screen.height() / board.height(), ); ctx.save(); self.xform = Xform::default() * Xform::xlate(-board.center().x(), -board.center().y()) - * Xform::scale(f, f) + * Xform::scale(factor, factor) * Xform::xlate(self.screen.center().x(), self.screen.center().y()); apply_xform(ctx, &self.xform)?; - // let board_center = board.center(); - // let screen_center = screen.center(); - // let xlate = -board_center; - // ctx.restore(); - // ctx.translate(-xlate.x(), -xlate.y())?; - // ctx.scale(f, f)?; - // ctx.translate(screen_center.x(), screen_center.y())?; - // console_log!("xform2: {}", from_ctx(ctx)?); - let available_moves: Vec = self + + let available_positions: Vec = self .available_moves .iter() .filter_map(Move::get_end_position) .collect(); - for i in 0..DraughtsBoard::rows() { - for j in 0..DraughtsBoard::columns() { + + for screen_row in 0..DraughtsBoard::rows() { + for screen_col in 0..DraughtsBoard::columns() { let cell = Rect::new( Point::new( - board.tl().x() + i as f64 * App::SQUARE_SIZE, - board.tl().y() + j as f64 * App::SQUARE_SIZE, + board.tl().x() + screen_col as f64 * App::SQUARE_SIZE, + board.tl().y() + screen_row as f64 * App::SQUARE_SIZE, ), Point::new( - board.tl().x() + (i + 1) as f64 * App::SQUARE_SIZE, - board.tl().y() + (j + 1) as f64 * App::SQUARE_SIZE, + board.tl().x() + (screen_col + 1) as f64 * App::SQUARE_SIZE, + board.tl().y() + (screen_row + 1) as f64 * App::SQUARE_SIZE, ), ); - if (i + j) % 2 == 0 { + if (screen_row + screen_col) % 2 == 0 { ctx.set_fill_style_str("#cccc99"); } else { ctx.set_fill_style_str("#666633"); } ctx.fill_rect(cell.tl().x(), cell.tl().y(), cell.width(), cell.height()); - if (i + j) % 2 != 0 { - let current_player = Player::White; - let position = Position::new(map_row(current_player, j) as u8, i as u8) - .expect("board loop only visits valid squares"); - let piece = self.game.piece_at(position); + + let board_position = + board_position(self.main_player, screen_row as u8, screen_col as u8).ok(); + if let Some(position) = board_position { + let piece = self.rd.game().piece_at(position); if piece.is_present() { - piece_drawer.draw_piece(ctx, &self.xform, cell.clone(), piece)?; + draw_piece(ctx, &self.xform, cell.clone(), piece)?; } - if let Some(selected_piece) = self.selected_piece { - if selected_piece == position { - // console_log!("Selected: {}", selected_piece); - ctx.save(); - ctx.set_shadow_blur(300.0); - ctx.set_shadow_color("#ffffff"); - ctx.begin_path(); - ctx.move_to(cell.tl().x(), cell.tl().y()); - ctx.line_to(cell.br().x(), cell.tl().y()); - ctx.line_to(cell.br().x(), cell.br().y()); - ctx.line_to(cell.tl().x(), cell.br().y()); - ctx.line_to(cell.tl().x(), cell.tl().y()); - ctx.clip(); - ctx.set_stroke_style_str("#fff"); - ctx.set_line_width(4.0); - ctx.begin_path(); - ctx.move_to(board.tl().x(), board.tl().y()); - ctx.line_to(board.br().x(), board.tl().y()); - ctx.line_to(board.br().x(), board.br().y()); - ctx.line_to(board.tl().x(), board.br().y()); - ctx.line_to(board.tl().x(), board.tl().y()); - ctx.move_to(cell.tl().x(), cell.tl().y()); - ctx.line_to(cell.br().x(), cell.tl().y()); - ctx.line_to(cell.br().x(), cell.br().y()); - ctx.line_to(cell.tl().x(), cell.br().y()); - ctx.line_to(cell.tl().x(), cell.tl().y()); - ctx.stroke(); - ctx.restore(); - } - } - if available_moves.contains(&position) { - ctx.save(); - ctx.set_shadow_blur(300.0); - ctx.set_shadow_color("#0000ff"); - ctx.begin_path(); - ctx.move_to(cell.tl().x(), cell.tl().y()); - ctx.line_to(cell.br().x(), cell.tl().y()); - ctx.line_to(cell.br().x(), cell.br().y()); - ctx.line_to(cell.tl().x(), cell.br().y()); - ctx.line_to(cell.tl().x(), cell.tl().y()); - ctx.clip(); - ctx.set_stroke_style_str("#0f0"); - ctx.set_line_width(4.0); - ctx.begin_path(); - ctx.move_to(board.tl().x(), board.tl().y()); - ctx.line_to(board.br().x(), board.tl().y()); - ctx.line_to(board.br().x(), board.br().y()); - ctx.line_to(board.tl().x(), board.br().y()); - ctx.line_to(board.tl().x(), board.tl().y()); - ctx.move_to(cell.tl().x(), cell.tl().y()); - ctx.line_to(cell.br().x(), cell.tl().y()); - ctx.line_to(cell.br().x(), cell.br().y()); - ctx.line_to(cell.tl().x(), cell.br().y()); - ctx.line_to(cell.tl().x(), cell.tl().y()); - ctx.stroke(); - ctx.restore(); + } + if board_position == self.selected_piece { + draw_highlight(ctx, &cell, "#ffffff")?; + } + if let Some(position) = board_position { + if available_positions.contains(&position) { + draw_highlight(ctx, &cell, "#00ff00")?; } } } } ctx.restore(); + + if let Some(winner) = self.rd.game().winner() { + ctx.save(); + ctx.set_fill_style_str("rgba(0, 0, 0, 0.65)"); + ctx.fill_rect(0.0, 0.0, self.screen.width(), self.screen.height()); + ctx.set_fill_style_str("#ffffff"); + ctx.set_font("48px sans-serif"); + ctx.set_text_align("center"); + ctx.set_text_baseline("middle"); + ctx.fill_text( + &format!("{} wins", player_name(winner)), + self.screen.center().x(), + self.screen.center().y(), + )?; + ctx.set_font("20px sans-serif"); + ctx.fill_text( + "Press N for a new game", + self.screen.center().x(), + self.screen.center().y() + 40.0, + )?; + ctx.restore(); + } else { + ctx.save(); + ctx.set_fill_style_str("#ffffff"); + ctx.set_font("14px sans-serif"); + ctx.set_text_align("left"); + ctx.set_text_baseline("top"); + ctx.fill_text( + &format!( + "{} to move | Ctrl+Z undo Ctrl+Y redo N new F flip", + player_name(self.rd.game().next_move()) + ), + 10.0, + 10.0, + )?; + ctx.restore(); + } + Ok(()) } } fn main() -> Result<(), JsValue> { + console_error_panic_hook::set_once(); let document = web_sys::window().unwrap().document().unwrap(); let canvas = document .create_element("canvas")? .dyn_into::()?; canvas.style().set_property("background-color", "black")?; document.body().unwrap().append_child(&canvas)?; - // canvas.style().set_property("border", "solid")?; let window = web_sys::window().expect("should have a window in this context"); let width = window.inner_width().unwrap().as_f64().unwrap(); let height = window.inner_height().unwrap().as_f64().unwrap(); canvas.set_width(width as u32); canvas.set_height(height as u32); + let app = new_shared_mut_ref(App::default()); let ctx = canvas .get_context("2d")? .unwrap() - .dyn_into::()?; - let piece_drawer = new_shared_mut_ref(PieceDrawer::new(&document)?); + .dyn_into::()?; { - let piece_drawer = piece_drawer.clone(); - let screen = Rect::new(Point::new(0.0, 0.0), Point::new(width, height)); - app.borrow_mut().screen = screen; - app.borrow_mut().draw(&ctx, &piece_drawer.borrow())?; + let app = app.clone(); + app.borrow_mut().screen = Rect::new(Point::new(0.0, 0.0), Point::new(width, height)); + app.borrow_mut().draw(&ctx)?; } + { let app = app.clone(); let canvas = canvas.clone(); let ctx = ctx.clone(); - let piece_drawer = piece_drawer.clone(); let closure = Closure::::new(move || { let window = web_sys::window().expect("should have a window in this context"); let width = window.inner_width().unwrap().as_f64().unwrap() as u32; @@ -385,64 +336,105 @@ fn main() -> Result<(), JsValue> { Point::new(0.0, 0.0), Point::new(canvas.width() as f64, canvas.height() as f64), ); - app.borrow_mut().draw(&ctx, &piece_drawer.borrow()).unwrap(); + app.borrow_mut().draw(&ctx).unwrap(); }); - let window = web_sys::window().expect("should have a window in this context"); window.set_onresize(Some(closure.as_ref().unchecked_ref())); closure.forget(); } { let app = app.clone(); - let piece_drawer = piece_drawer.clone(); + let ctx = ctx.clone(); let mouse_click_handler = Closure::wrap(Box::new(move |event: MouseEvent| { let mut app = app.borrow_mut(); + if app.rd.game().winner().is_some() { + return; + } + let xform = app.xform.invert(); let board = &app.board; - let position = Point::new(event.x() as f64, event.y() as f64) * &xform - board.tl(); - if position.x() > 0.0 - && position.x() < board.width() - && position.y() > 0.0 - && position.y() < board.height() + let position = + Point::new(event.offset_x() as f64, event.offset_y() as f64) * &xform - board.tl(); + if position.x() <= 0.0 + || position.x() >= board.width() + || position.y() <= 0.0 + || position.y() >= board.height() { - let col = f64::floor(position.x() / App::SQUARE_SIZE) as usize; - let row = f64::floor(position.y() / App::SQUARE_SIZE) as usize; - let row = map_row(app.main_player, row); - if (row + col) % 2 == 0 { - let position = Position::new(row as u8, col as u8).unwrap(); - let selected_move = app - .available_moves - .iter() - .find(|mv| mv.get_end_position() == Some(position)) - .map(Move::clone); - if app.selected_piece.is_some() && selected_move.is_some() { - let mv = selected_move.unwrap(); - if app.game.check_and_apply_move(&mv).is_ok() { - app.selected_piece = None; - app.available_moves.clear(); - } - } else { - app.selected_piece = Some(position); - let mut available_moves = Vec::::new(); - for mv in app.game.moves_for_piece(position) { - available_moves.push(mv); - } - app.available_moves = available_moves; - // console_log!("Clicked at: ({}, {})", col, row); - } + app.selected_piece = None; + app.available_moves.clear(); + app.draw(&ctx).unwrap(); + return; + } + + let screen_col = (position.x() / App::SQUARE_SIZE).floor() as u8; + let screen_row = (position.y() / App::SQUARE_SIZE).floor() as u8; + let Ok(position) = board_position(app.main_player, screen_row, screen_col) else { + app.selected_piece = None; + app.available_moves.clear(); + app.draw(&ctx).unwrap(); + return; + }; + + let selected_move = app + .available_moves + .iter() + .find(|mv| mv.get_end_position() == Some(position)) + .cloned(); + if app.selected_piece.is_some() && selected_move.is_some() { + if app.rd.apply_move(selected_move.unwrap()).is_ok() { + app.update_selection_after_move(); + } + } else { + let has_moves = app.rd.game().moves_for_piece(position).next().is_some(); + if has_moves { + app.selected_piece = Some(position); + app.available_moves = app.rd.game().moves_for_piece(position).collect(); } else { app.selected_piece = None; app.available_moves.clear(); } - } else { - app.selected_piece = None; - app.available_moves.clear(); } - app.draw(&ctx, &piece_drawer.borrow()).unwrap(); + app.draw(&ctx).unwrap(); }) as Box); canvas.set_onclick(Some(mouse_click_handler.as_ref().unchecked_ref())); mouse_click_handler.forget(); } + { + let app = app.clone(); + let ctx = ctx.clone(); + let key_handler = Closure::wrap(Box::new(move |event: KeyboardEvent| { + let mut app = app.borrow_mut(); + let key = event.key().to_ascii_lowercase(); + let ctrl = event.ctrl_key(); + let shift = event.shift_key(); + let handled = if ctrl && (key == "y" || (key == "z" && shift)) { + app.rd.redo().ok(); + app.selected_piece = None; + app.available_moves.clear(); + true + } else if ctrl && key == "z" { + app.rd.undo().ok(); + app.selected_piece = None; + app.available_moves.clear(); + true + } else if key == "n" { + app.new_game(); + true + } else if key == "f" { + app.flip_view(); + true + } else { + false + }; + if handled { + event.prevent_default(); + app.draw(&ctx).unwrap(); + } + }) as Box); + window.set_onkeydown(Some(key_handler.as_ref().unchecked_ref())); + key_handler.forget(); + } + Ok(()) } diff --git a/rdraught-wasm/src/types.rs b/rdraught-wasm/src/types.rs deleted file mode 100644 index a1d5208..0000000 --- a/rdraught-wasm/src/types.rs +++ /dev/null @@ -1,12 +0,0 @@ -use std::cell::{Cell, RefCell}; -use std::rc::Rc; - -pub(crate) type SharedMutable = Rc>; -pub(crate) type SharedMutableRef = Rc>; -pub(crate) fn new_shared_mut_ref(obj: T) -> SharedMutableRef { - Rc::new(RefCell::new(obj)) -} - -pub(crate) fn new_shared_mut(obj: T) -> SharedMutable { - Rc::new(Cell::new(obj)) -}