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
This commit is contained in:
2026-10-19 20:38:32 +00:00
parent dd3b918561
commit f17d328ed5
20 changed files with 997 additions and 1290 deletions
Generated
+200 -296
View File
@@ -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"
+7 -3
View File
@@ -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"
+28 -7
View File
@@ -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
+1 -3
View File
@@ -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 {
+1 -1
View File
@@ -1,7 +1,7 @@
use gtk4::{AlertDialog, Window, prelude::IsA};
use rdraught::Player;
pub(crate) async fn create_dialog<W: IsA<Window>>(window: W, winner: Player) {
pub(crate) fn create_dialog<W: IsA<Window>>(window: W, winner: Player) {
let msg = match winner {
Player::Red => String::from("Red player wins"),
Player::White => String::from("White player wins"),
+3 -11
View File
@@ -4,7 +4,7 @@ use rdraught::Player;
use rdraught_ui_common::SharedMutable;
pub(crate) fn create(application: &Application, current_player: SharedMutable<Player>) -> 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<Pl
layout.set_margin_bottom(10);
layout.set_margin_start(10);
layout.set_margin_end(10);
let player_select_dialog = Window::builder()
Window::builder()
.application(application)
.title("Rdraught")
.modal(true)
.child(&layout)
.visible(true)
.build();
player_select_dialog.connect_close_request(move |window| {
if let Some(application) = window.application() {
application.remove_window(window);
}
glib::Propagation::Proceed
});
player_select_dialog
.build()
}
+158 -113
View File
@@ -1,9 +1,9 @@
use core::f64::consts::PI;
use gdk4::cairo::{Context as CairoContext, Matrix, Rectangle};
use glib::ExitCode;
use gtk4::glib::{MainContext, Propagation};
use gtk4::glib::Propagation;
use gtk4::{self as gtk, gdk::ffi::GDK_BUTTON_PRIMARY};
use gtk4::{Application, Button, DrawingArea, HeaderBar, prelude::*};
use gtk4::{Application, Button, DrawingArea, HeaderBar, Label, prelude::*};
use rdraught::{
DraughtsBoard, DraughtsGame, Move, Piece, Player, Position, RDraughtApplication,
RectangularBoard,
@@ -14,7 +14,7 @@ const SQUARE_SIZE: f64 = 1.0;
use super::final_dialog;
use super::greeting_dialog;
use rdraught_ui_common::{
Point, SharedMutable, SharedMutableRef, new_shared_mut, new_shared_mut_ref,
Point, SharedMutable, SharedMutableRef, board_position, new_shared_mut, new_shared_mut_ref,
};
const CROWN_RED: &[u8] = include_bytes!("crown_red.svg");
@@ -25,6 +25,47 @@ fn transform_point(p: &Point, m: &Matrix) -> 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<RDraughtApplication>,
status: &Label,
undo_button: &Button,
redo_button: &Button,
) -> Option<Player> {
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<Player>,
finished: &SharedMutable<bool>,
drawing_area: &DrawingArea,
window: &gtk::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<bool> = new_shared_mut(false);
let status = Label::new(None);
let new_game_button = Button::with_label("New Game");
let selected_piece: SharedMutable<Option<Position>> = new_shared_mut(None);
let available_moves = new_shared_mut_ref(Vec::<Move>::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();
+70
View File
@@ -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<Position, Error> {
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());
}
}
+39 -1
View File
@@ -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());
}
}
+2
View File
@@ -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};
-6
View File
@@ -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
+193 -186
View File
@@ -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<Position>,
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()
}
-29
View File
@@ -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<T: Into<usize>>(idx: T) -> Palette {
PALETTES[idx.into() % PALETTES.len()]
}
pub fn change_palette<T: Into<usize>>(idx: T) {
unsafe {
*PALETTE = palette_by_idx(idx);
}
}
pub fn set_draw_color<T: Into<u16>>(idx: T) {
unsafe { *DRAW_COLORS = idx.into() }
}
-196
View File
@@ -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);
}
+1 -9
View File
@@ -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',
]
-27
View File
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
<g style="fill:none; fill-opacity:0; fill-rule:evenodd; stroke:#ffc837; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0)">
<path
d="M 22.5,11.63 L 22.5,6"
style="fill:none; stroke:#ffc837; stroke-linejoin:miter;" />
<path
d="M 20,8 L 25,8"
style="fill:none; stroke:#ffc837; stroke-linejoin:miter;" />
<path
d="M 22.5,25 C 22.5,25 27,17.5 25.5,14.5 C 25.5,14.5 24.5,12 22.5,12 C 20.5,12 19.5,14.5 19.5,14.5 C 18,17.5 22.5,25 22.5,25"
style="fill:#ffffff; stroke:#ffc837; stroke-linecap:butt; stroke-linejoin:miter;" />
<path
d="M 11.5,37 C 17,40.5 27,40.5 32.5,37 L 32.5,30 C 32.5,30 41.5,25.5 38.5,19.5 C 34.5,13 25,16 22.5,23.5 L 22.5,27 L 22.5,23.5 C 19,16 9.5,13 6.5,19.5 C 3.5,25.5 11.5,29.5 11.5,29.5 L 11.5,37 z "
style="fill:#ffffff; stroke:#ffc837;" />
<path
d="M 11.5,30 C 17,27 27,27 32.5,30"
style="fill:none; stroke:#ffc837;" />
<path
d="M 11.5,33.5 C 17,30.5 27,30.5 32.5,33.5"
style="fill:none; stroke:#ffc837;" />
<path
d="M 11.5,37 C 17,34 27,34 32.5,37"
style="fill:none; stroke:#ffc837;" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

-27
View File
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
<g style="fill:none; fill-opacity:0; fill-rule:evenodd; stroke:#776b00; stroke-width:1.5; stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4; stroke-dasharray:none; stroke-opacity:1;" transform="translate(0,0)">
<path
d="M 22.5,11.63 L 22.5,6"
style="fill:none; stroke:#776b00; stroke-linejoin:miter;" />
<path
d="M 20,8 L 25,8"
style="fill:none; stroke:#776b00; stroke-linejoin:miter;" />
<path
d="M 22.5,25 C 22.5,25 27,17.5 25.5,14.5 C 25.5,14.5 24.5,12 22.5,12 C 20.5,12 19.5,14.5 19.5,14.5 C 18,17.5 22.5,25 22.5,25"
style="fill:#ffffff; stroke:#776b00; stroke-linecap:butt; stroke-linejoin:miter;" />
<path
d="M 11.5,37 C 17,40.5 27,40.5 32.5,37 L 32.5,30 C 32.5,30 41.5,25.5 38.5,19.5 C 34.5,13 25,16 22.5,23.5 L 22.5,27 L 22.5,23.5 C 19,16 9.5,13 6.5,19.5 C 3.5,25.5 11.5,29.5 11.5,29.5 L 11.5,37 z "
style="fill:#ffffff; stroke:#776b00;" />
<path
d="M 11.5,30 C 17,27 27,27 32.5,30"
style="fill:none; stroke:#776b00;" />
<path
d="M 11.5,33.5 C 17,30.5 27,30.5 32.5,33.5"
style="fill:none; stroke:#776b00;" />
<path
d="M 11.5,37 C 17,34 27,34 32.5,37"
style="fill:none; stroke:#776b00;" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

-61
View File
@@ -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::<web_sys::HtmlCanvasElement>()?;
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::<web_sys::CanvasRenderingContext2d>()?;
let context = Rc::new(context);
let pressed = Rc::new(Cell::new(false));
{
let context = context.clone();
let pressed = pressed.clone();
let closure = Closure::<dyn FnMut(_)>::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::<dyn FnMut(_)>::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::<dyn FnMut(_)>::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(())
}
+294 -302
View File
@@ -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<PieceDrawer, JsValue> {
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::<HtmlImageElement>()?;
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::<HtmlImageElement>()?;
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<Xform, JsValue> {
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<Position>,
available_moves: Vec<Move>,
main_player: Player,
red_agent: Box<dyn Agent>,
white_agent: Box<dyn Agent>,
}
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<Position> = self
let available_positions: Vec<Position> = 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::<web_sys::HtmlCanvasElement>()?;
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::<web_sys::CanvasRenderingContext2d>()?;
let piece_drawer = new_shared_mut_ref(PieceDrawer::new(&document)?);
.dyn_into::<CanvasRenderingContext2d>()?;
{
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::<dyn FnMut()>::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::<Move>::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<dyn FnMut(MouseEvent)>);
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<dyn FnMut(KeyboardEvent)>);
window.set_onkeydown(Some(key_handler.as_ref().unchecked_ref()));
key_handler.forget();
}
Ok(())
}
-12
View File
@@ -1,12 +0,0 @@
use std::cell::{Cell, RefCell};
use std::rc::Rc;
pub(crate) type SharedMutable<T> = Rc<Cell<T>>;
pub(crate) type SharedMutableRef<T> = Rc<RefCell<T>>;
pub(crate) fn new_shared_mut_ref<T>(obj: T) -> SharedMutableRef<T> {
Rc::new(RefCell::new(obj))
}
pub(crate) fn new_shared_mut<T>(obj: T) -> SharedMutable<T> {
Rc::new(Cell::new(obj))
}