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:
woggioni committed 2026-10-19 20:38:32 +00:00
1 parent dd3b918561
commit f17d328ed5
20 files changed
+997 -1290

No files matched your search

Generated
+200 -296
View File
@@ -32,12 +32,6 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@@ -87,20 +81,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e3bd0f4e25afa9cabc157908d14eeef9067d6448c49414d17b3fb55f0eadd0" checksum = "91e3bd0f4e25afa9cabc157908d14eeef9067d6448c49414d17b3fb55f0eadd0"
dependencies = [ dependencies = [
"bitflags 2.13.2", "bitflags 2.13.2",
"cairo-sys-rs 0.20.10", "cairo-sys-rs",
"glib 0.20.12", "glib",
"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",
"libc", "libc",
] ]
@@ -110,18 +92,7 @@ version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "059cc746549898cbfd9a47754288e5a958756650ef4652bbb6c5f71a6bda4f8b" checksum = "059cc746549898cbfd9a47754288e5a958756650ef4652bbb6c5f71a6bda4f8b"
dependencies = [ dependencies = [
"glib-sys 0.20.10", "glib-sys",
"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",
"libc", "libc",
"system-deps", "system-deps",
] ]
@@ -234,26 +205,17 @@ checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
[[package]] [[package]]
name = "cssparser" name = "cssparser"
version = "0.35.0" version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be"
dependencies = [ dependencies = [
"cssparser-macros", "cssparser-macros",
"dtoa-short", "dtoa-short",
"itoa", "itoa",
"phf", "phf 0.11.3",
"smallvec", "smallvec",
] ]
[[package]]
name = "cssparser-color"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eeef9ae8c0e112edd89eb6406b3156ffa99c7e037b3baef1dbdf4158d35c324"
dependencies = [
"cssparser",
]
[[package]] [[package]]
name = "cssparser-macros" name = "cssparser-macros"
version = "0.6.1" version = "0.6.1"
@@ -272,22 +234,12 @@ checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "2.1.1" version = "0.99.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
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"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustc_version",
"syn 2.0.119", "syn 2.0.119",
] ]
@@ -382,9 +334,9 @@ dependencies = [
[[package]] [[package]]
name = "float-cmp" name = "float-cmp"
version = "0.10.0" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
dependencies = [ dependencies = [
"num-traits", "num-traits",
] ]
@@ -486,8 +438,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd242894c084f4beed508a56952750bce3e96e85eb68fdc153637daa163e10c" checksum = "2fd242894c084f4beed508a56952750bce3e96e85eb68fdc153637daa163e10c"
dependencies = [ dependencies = [
"gdk-pixbuf-sys", "gdk-pixbuf-sys",
"gio 0.20.12", "gio",
"glib 0.20.12", "glib",
"libc", "libc",
] ]
@@ -497,9 +449,9 @@ version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b34f3b580c988bd217e9543a2de59823fafae369d1a055555e5f95a8b130b96" checksum = "5b34f3b580c988bd217e9543a2de59823fafae369d1a055555e5f95a8b130b96"
dependencies = [ dependencies = [
"gio-sys 0.20.10", "gio-sys",
"glib-sys 0.20.10", "glib-sys",
"gobject-sys 0.20.10", "gobject-sys",
"libc", "libc",
"system-deps", "system-deps",
] ]
@@ -510,13 +462,13 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4850c9d9c1aecd1a3eb14fadc1cdb0ac0a2298037e116264c7473e1740a32d60" checksum = "4850c9d9c1aecd1a3eb14fadc1cdb0ac0a2298037e116264c7473e1740a32d60"
dependencies = [ dependencies = [
"cairo-rs 0.20.12", "cairo-rs",
"gdk-pixbuf", "gdk-pixbuf",
"gdk4-sys", "gdk4-sys",
"gio 0.20.12", "gio",
"glib 0.20.12", "glib",
"libc", "libc",
"pango 0.20.12", "pango",
] ]
[[package]] [[package]]
@@ -525,17 +477,28 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f6eb95798e2b46f279cf59005daf297d5b69555428f185650d71974a910473a" checksum = "6f6eb95798e2b46f279cf59005daf297d5b69555428f185650d71974a910473a"
dependencies = [ dependencies = [
"cairo-sys-rs 0.20.10", "cairo-sys-rs",
"gdk-pixbuf-sys", "gdk-pixbuf-sys",
"gio-sys 0.20.10", "gio-sys",
"glib-sys 0.20.10", "glib-sys",
"gobject-sys 0.20.10", "gobject-sys",
"libc", "libc",
"pango-sys 0.20.10", "pango-sys",
"pkg-config", "pkg-config",
"system-deps", "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]] [[package]]
name = "gif" name = "gif"
version = "0.14.2" version = "0.14.2"
@@ -556,25 +519,8 @@ dependencies = [
"futures-core", "futures-core",
"futures-io", "futures-io",
"futures-util", "futures-util",
"gio-sys 0.20.10", "gio-sys",
"glib 0.20.12", "glib",
"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",
"libc", "libc",
"pin-project-lite", "pin-project-lite",
"smallvec", "smallvec",
@@ -586,24 +532,11 @@ version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83"
dependencies = [ dependencies = [
"glib-sys 0.20.10", "glib-sys",
"gobject-sys 0.20.10", "gobject-sys",
"libc", "libc",
"system-deps", "system-deps",
"windows-sys 0.59.0", "windows-sys",
]
[[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",
] ]
[[package]] [[package]]
@@ -618,31 +551,10 @@ dependencies = [
"futures-executor", "futures-executor",
"futures-task", "futures-task",
"futures-util", "futures-util",
"gio-sys 0.20.10", "gio-sys",
"glib-macros 0.20.12", "glib-macros",
"glib-sys 0.20.10", "glib-sys",
"gobject-sys 0.20.10", "gobject-sys",
"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",
"libc", "libc",
"memchr", "memchr",
"smallvec", "smallvec",
@@ -661,19 +573,6 @@ dependencies = [
"syn 2.0.119", "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]] [[package]]
name = "glib-sys" name = "glib-sys"
version = "0.20.10" version = "0.20.10"
@@ -684,34 +583,13 @@ dependencies = [
"system-deps", "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]] [[package]]
name = "gobject-sys" name = "gobject-sys"
version = "0.20.10" version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda"
dependencies = [ dependencies = [
"glib-sys 0.20.10", "glib-sys",
"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",
"libc", "libc",
"system-deps", "system-deps",
] ]
@@ -722,7 +600,7 @@ version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b86dfad7d14251c9acaf1de63bc8754b7e3b4e5b16777b6f5a748208fe9519b" checksum = "6b86dfad7d14251c9acaf1de63bc8754b7e3b4e5b16777b6f5a748208fe9519b"
dependencies = [ dependencies = [
"glib 0.20.12", "glib",
"graphene-sys", "graphene-sys",
"libc", "libc",
] ]
@@ -733,7 +611,7 @@ version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df583a85ba2d5e15e1797e40d666057b28bc2f60a67c9c24145e6db2cc3861ea" checksum = "df583a85ba2d5e15e1797e40d666057b28bc2f60a67c9c24145e6db2cc3861ea"
dependencies = [ dependencies = [
"glib-sys 0.20.10", "glib-sys",
"libc", "libc",
"pkg-config", "pkg-config",
"system-deps", "system-deps",
@@ -745,13 +623,13 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61f5e72f931c8c9f65fbfc89fe0ddc7746f147f822f127a53a9854666ac1f855" checksum = "61f5e72f931c8c9f65fbfc89fe0ddc7746f147f822f127a53a9854666ac1f855"
dependencies = [ dependencies = [
"cairo-rs 0.20.12", "cairo-rs",
"gdk4", "gdk4",
"glib 0.20.12", "glib",
"graphene-rs", "graphene-rs",
"gsk4-sys", "gsk4-sys",
"libc", "libc",
"pango 0.20.12", "pango",
] ]
[[package]] [[package]]
@@ -760,13 +638,13 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "755059de55fa6f85a46bde8caf03e2184c96bfda1f6206163c72fb0ea12436dc" checksum = "755059de55fa6f85a46bde8caf03e2184c96bfda1f6206163c72fb0ea12436dc"
dependencies = [ dependencies = [
"cairo-sys-rs 0.20.10", "cairo-sys-rs",
"gdk4-sys", "gdk4-sys",
"glib-sys 0.20.10", "glib-sys",
"gobject-sys 0.20.10", "gobject-sys",
"graphene-sys", "graphene-sys",
"libc", "libc",
"pango-sys 0.20.10", "pango-sys",
"system-deps", "system-deps",
] ]
@@ -776,19 +654,19 @@ version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f274dd0102c21c47bbfa8ebcb92d0464fab794a22fad6c3f3d5f165139a326d6" checksum = "f274dd0102c21c47bbfa8ebcb92d0464fab794a22fad6c3f3d5f165139a326d6"
dependencies = [ dependencies = [
"cairo-rs 0.20.12", "cairo-rs",
"field-offset", "field-offset",
"futures-channel", "futures-channel",
"gdk-pixbuf", "gdk-pixbuf",
"gdk4", "gdk4",
"gio 0.20.12", "gio",
"glib 0.20.12", "glib",
"graphene-rs", "graphene-rs",
"gsk4", "gsk4",
"gtk4-macros", "gtk4-macros",
"gtk4-sys", "gtk4-sys",
"libc", "libc",
"pango 0.20.12", "pango",
] ]
[[package]] [[package]]
@@ -809,16 +687,16 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6" checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6"
dependencies = [ dependencies = [
"cairo-sys-rs 0.20.10", "cairo-sys-rs",
"gdk-pixbuf-sys", "gdk-pixbuf-sys",
"gdk4-sys", "gdk4-sys",
"gio-sys 0.20.10", "gio-sys",
"glib-sys 0.20.10", "glib-sys",
"gobject-sys 0.20.10", "gobject-sys",
"graphene-sys", "graphene-sys",
"gsk4-sys", "gsk4-sys",
"libc", "libc",
"pango-sys 0.20.10", "pango-sys",
"system-deps", "system-deps",
] ]
@@ -1002,9 +880,9 @@ dependencies = [
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.14.0" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [ dependencies = [
"either", "either",
] ]
@@ -1046,19 +924,18 @@ checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]] [[package]]
name = "librsvg" name = "librsvg"
version = "2.61.3" version = "2.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44b612e3a9ec82c8ae9f2ebc9b2e8ff50aa831742686c0071998f76896cf214f" checksum = "a3a70cc57e9d915b91b56ae5dde1c52aaf5aec17579ab2158451d1d7a40b0c1b"
dependencies = [ dependencies = [
"cairo-rs 0.21.5", "cairo-rs",
"cast", "cast",
"cssparser", "cssparser",
"cssparser-color",
"data-url", "data-url",
"encoding_rs", "encoding_rs",
"float-cmp", "float-cmp",
"gio 0.21.5", "gio",
"glib 0.21.5", "glib",
"image", "image",
"itertools", "itertools",
"language-tags", "language-tags",
@@ -1067,15 +944,14 @@ dependencies = [
"markup5ever", "markup5ever",
"nalgebra", "nalgebra",
"num-traits", "num-traits",
"pango 0.21.5", "pango",
"pangocairo", "pangocairo",
"precomputed-hash",
"rayon", "rayon",
"rctree", "rctree",
"regex", "regex",
"rgb", "rgb",
"selectors", "selectors",
"string_cache 0.9.0", "string_cache",
"system-deps", "system-deps",
"tinyvec", "tinyvec",
"url", "url",
@@ -1133,13 +1009,16 @@ dependencies = [
[[package]] [[package]]
name = "markup5ever" name = "markup5ever"
version = "0.35.0" version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45"
dependencies = [ dependencies = [
"log", "log",
"phf 0.11.3",
"phf_codegen 0.11.3",
"string_cache",
"string_cache_codegen",
"tendril", "tendril",
"web_atoms",
] ]
[[package]] [[package]]
@@ -1342,22 +1221,10 @@ version = "0.20.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6576b311f6df659397043a5fa8a021da8f72e34af180b44f7d57348de691ab5c" checksum = "6576b311f6df659397043a5fa8a021da8f72e34af180b44f7d57348de691ab5c"
dependencies = [ dependencies = [
"gio 0.20.12", "gio",
"glib 0.20.12", "glib",
"libc", "libc",
"pango-sys 0.20.10", "pango-sys",
]
[[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",
] ]
[[package]] [[package]]
@@ -1366,47 +1233,35 @@ version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186909673fc09be354555c302c0b3dcf753cd9fa08dcb8077fa663c80fb243fa" checksum = "186909673fc09be354555c302c0b3dcf753cd9fa08dcb8077fa663c80fb243fa"
dependencies = [ dependencies = [
"glib-sys 0.20.10", "glib-sys",
"gobject-sys 0.20.10", "gobject-sys",
"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",
"libc", "libc",
"system-deps", "system-deps",
] ]
[[package]] [[package]]
name = "pangocairo" name = "pangocairo"
version = "0.21.5" version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b36c5c84304072939d860595d9bda2a797d3bd6f7215e20b8ccd0e72d84da8c8" checksum = "58890dc451db9964ac2d8874f903a4370a4b3932aa5281ff0c8d9810937ad84f"
dependencies = [ dependencies = [
"cairo-rs 0.21.5", "cairo-rs",
"glib 0.21.5", "glib",
"libc", "libc",
"pango 0.21.5", "pango",
"pangocairo-sys", "pangocairo-sys",
] ]
[[package]] [[package]]
name = "pangocairo-sys" name = "pangocairo-sys"
version = "0.21.5" version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eadbb01ad38be76e0d37e329d40ba0f3f9ef261d7b84b05201d7a0f14f819406" checksum = "b9952903f88aa93e2927e7bca2d1ebae64fc26545a9280b4ce6bddeda26b5c42"
dependencies = [ dependencies = [
"cairo-sys-rs 0.21.5", "cairo-sys-rs",
"glib-sys 0.21.5", "glib-sys",
"libc", "libc",
"pango-sys 0.21.5", "pango-sys",
"system-deps", "system-deps",
] ]
@@ -1445,6 +1300,15 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 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]] [[package]]
name = "phf" name = "phf"
version = "0.11.3" version = "0.11.3"
@@ -1455,16 +1319,36 @@ dependencies = [
"phf_shared 0.11.3", "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]] [[package]]
name = "phf_codegen" name = "phf_codegen"
version = "0.11.3" version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [ dependencies = [
"phf_generator", "phf_generator 0.11.3",
"phf_shared 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]] [[package]]
name = "phf_generator" name = "phf_generator"
version = "0.11.3" version = "0.11.3"
@@ -1481,7 +1365,7 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [ dependencies = [
"phf_generator", "phf_generator 0.11.3",
"phf_shared 0.11.3", "phf_shared 0.11.3",
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1490,20 +1374,20 @@ dependencies = [
[[package]] [[package]]
name = "phf_shared" name = "phf_shared"
version = "0.11.3" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
dependencies = [ dependencies = [
"siphasher", "siphasher 0.3.11",
] ]
[[package]] [[package]]
name = "phf_shared" name = "phf_shared"
version = "0.13.1" version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [ dependencies = [
"siphasher", "siphasher 1.0.4",
] ]
[[package]] [[package]]
@@ -1540,6 +1424,15 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]] [[package]]
name = "precomputed-hash" name = "precomputed-hash"
version = "0.1.1" version = "0.1.1"
@@ -1591,6 +1484,18 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
dependencies = [ 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", "rand_core",
] ]
@@ -1599,6 +1504,9 @@ name = "rand_core"
version = "0.6.4" version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]] [[package]]
name = "rawpointer" name = "rawpointer"
@@ -1652,10 +1560,10 @@ dependencies = [
name = "rdraught-gtk" name = "rdraught-gtk"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"cairo-rs 0.20.12", "cairo-rs",
"gdk4", "gdk4",
"gio 0.20.12", "gio",
"glib 0.20.12", "glib",
"gtk4", "gtk4",
"librsvg", "librsvg",
"rdraught", "rdraught",
@@ -1683,18 +1591,15 @@ version = "0.1.0"
dependencies = [ dependencies = [
"rdraught", "rdraught",
"wasm4", "wasm4",
"wasm4-sys",
] ]
[[package]] [[package]]
name = "rdraught-wasm" name = "rdraught-wasm"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64",
"console_error_panic_hook", "console_error_panic_hook",
"rdraught", "rdraught",
"rdraught-ui-common", "rdraught-ui-common",
"rmath",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
] ]
@@ -1795,7 +1700,7 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
"unicode-width", "unicode-width",
"utf8parse", "utf8parse",
"windows-sys 0.59.0", "windows-sys",
] ]
[[package]] [[package]]
@@ -1895,9 +1800,9 @@ dependencies = [
[[package]] [[package]]
name = "selectors" name = "selectors"
version = "0.31.0" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5685b6ae43bfcf7d2e7dfcfb5d8e8f61b46442c902531e41a32a9a8bf0ee0fb6" checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06"
dependencies = [ dependencies = [
"bitflags 2.13.2", "bitflags 2.13.2",
"cssparser", "cssparser",
@@ -1905,8 +1810,8 @@ dependencies = [
"fxhash", "fxhash",
"log", "log",
"new_debug_unreachable", "new_debug_unreachable",
"phf", "phf 0.10.1",
"phf_codegen", "phf_codegen 0.10.0",
"precomputed-hash", "precomputed-hash",
"servo_arc", "servo_arc",
"smallvec", "smallvec",
@@ -1958,9 +1863,9 @@ dependencies = [
[[package]] [[package]]
name = "servo_arc" name = "servo_arc"
version = "0.4.3" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44"
dependencies = [ dependencies = [
"stable_deref_trait", "stable_deref_trait",
] ]
@@ -1990,6 +1895,12 @@ version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
[[package]]
name = "siphasher"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "1.0.4" version = "1.0.4"
@@ -2027,26 +1938,13 @@ dependencies = [
"serde", "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]] [[package]]
name = "string_cache_codegen" name = "string_cache_codegen"
version = "0.5.4" version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
dependencies = [ dependencies = [
"phf_generator", "phf_generator 0.11.3",
"phf_shared 0.11.3", "phf_shared 0.11.3",
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2266,6 +2164,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" 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]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.129" version = "0.2.129"
@@ -2344,18 +2248,6 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "weezl" name = "weezl"
version = "0.1.12" version = "0.1.12"
@@ -2409,15 +2301,6 @@ dependencies = [
"windows-targets", "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]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"
@@ -2499,11 +2382,12 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]] [[package]]
name = "xml5ever" name = "xml5ever"
version = "0.35.0" version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee3f1e41afb31a75aef076563b0ad3ecc24f5bd9d12a72b132222664eb76b494" checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69"
dependencies = [ dependencies = [
"log", "log",
"mac",
"markup5ever", "markup5ever",
] ]
@@ -2530,6 +2414,26 @@ dependencies = [
"synstructure", "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]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.8" version = "0.1.8"
+7 -3
View File
@@ -25,16 +25,14 @@ rust-version = "1.87"
[workspace.dependencies] [workspace.dependencies]
rdraught = { path = "rdraught", version = "0.1" } rdraught = { path = "rdraught", version = "0.1" }
rdraught-ui-common = { path = "rdraught-ui-common", version = "0.1" } rdraught-ui-common = { path = "rdraught-ui-common", version = "0.1" }
base64 = "0.22"
cairo-rs = "0.20" cairo-rs = "0.20"
cairo-sys-rs = "0.20" cairo-sys-rs = "0.20"
librsvg = "2.60" librsvg = "~2.60"
gtk4 = {version = "0.9", features = ["v4_10"] } gtk4 = {version = "0.9", features = ["v4_10"] }
gdk4 = "0.9" gdk4 = "0.9"
gio = "0.20" gio = "0.20"
glib = "0.20" glib = "0.20"
wasm4 = "0.2" wasm4 = "0.2"
wasm4-sys = "0.1"
strum = "0.27" strum = "0.27"
strum_macros = "0.27" strum_macros = "0.27"
heapless = "0.8" heapless = "0.8"
@@ -44,3 +42,9 @@ console_error_panic_hook = "0.1"
sdl3 = { version = "0.14.35" } sdl3 = { version = "0.14.35" }
sdl3-sys = { version = "0.5" } sdl3-sys = { version = "0.5" }
rmath = {version = "0.1.0", registry = "gitea"} 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` | `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-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-gtk` | Playable GTK4 desktop front end. |
| `rdraught-wasm` | Browser front end using the Canvas 2D API and `wasm-bindgen`. | | `rdraught-wasm` | Playable browser front end using the Canvas 2D API and `wasm-bindgen`. |
| `rdraught-w4` | WASM-4 front end. Work in progress. | | `rdraught-w4` | Playable WASM-4 front end. |
| `rdraught-sdl` | SDL3 front end. Work in progress. | | `rdraught-sdl` | SDL3 prototype; not wired to the game yet. |
| `rdraught-cli` | Scratch `rustyline` program; not a playable CLI 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 ## Rules
The rules implemented by the engine are an 8x8 draughts variant: 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. `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 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 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 ## Requirements
@@ -79,6 +84,11 @@ below instead.
cargo run -p rdraught-gtk 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 ### Browser WASM
```sh ```sh
@@ -87,6 +97,13 @@ cd rdraught-wasm
trunk serve --open 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 The `.cargo/config.toml` inside `rdraught-wasm` is only read when Cargo runs
from that directory. from that directory.
@@ -97,8 +114,12 @@ cd rdraught-w4
cargo build --release --target wasm32-unknown-unknown cargo build --release --target wasm32-unknown-unknown
``` ```
Run Cargo from `rdraught-w4` so its linker configuration is applied. The Run Cargo from `rdraught-w4` so its linker configuration is applied. The module
resulting module is in `target/wasm32-unknown-unknown/release/`. 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 ### SDL3
+1 -3
View File
@@ -1,7 +1,5 @@
use glib::ExitCode; use glib::ExitCode;
use rdraught::{DraughtsBoard, DraughtsGame, Move, Piece, Player, Position}; use rdraught::{DraughtsGame, Move};
use rdraught_gtk::run;
use std::collections::HashMap;
use std::mem::size_of; use std::mem::size_of;
fn main() -> ExitCode { fn main() -> ExitCode {
+1 -1
View File
@@ -1,7 +1,7 @@
use gtk4::{AlertDialog, Window, prelude::IsA}; use gtk4::{AlertDialog, Window, prelude::IsA};
use rdraught::Player; 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 { let msg = match winner {
Player::Red => String::from("Red player wins"), Player::Red => String::from("Red player wins"),
Player::White => String::from("White 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; use rdraught_ui_common::SharedMutable;
pub(crate) fn create(application: &Application, current_player: SharedMutable<Player>) -> Window { 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 red_player_button = CheckButton::builder().label("Red").active(true).build();
let white_player_button = CheckButton::builder() let white_player_button = CheckButton::builder()
.label("White") .label("White")
@@ -44,19 +44,11 @@ pub(crate) fn create(application: &Application, current_player: SharedMutable<Pl
layout.set_margin_bottom(10); layout.set_margin_bottom(10);
layout.set_margin_start(10); layout.set_margin_start(10);
layout.set_margin_end(10); layout.set_margin_end(10);
let player_select_dialog = Window::builder() Window::builder()
.application(application) .application(application)
.title("Rdraught") .title("Rdraught")
.modal(true) .modal(true)
.child(&layout) .child(&layout)
.visible(true) .visible(true)
.build(); .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
} }
+158 -113
View File
@@ -1,9 +1,9 @@
use core::f64::consts::PI; use core::f64::consts::PI;
use gdk4::cairo::{Context as CairoContext, Matrix, Rectangle}; use gdk4::cairo::{Context as CairoContext, Matrix, Rectangle};
use glib::ExitCode; use glib::ExitCode;
use gtk4::glib::{MainContext, Propagation}; use gtk4::glib::Propagation;
use gtk4::{self as gtk, gdk::ffi::GDK_BUTTON_PRIMARY}; 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::{ use rdraught::{
DraughtsBoard, DraughtsGame, Move, Piece, Player, Position, RDraughtApplication, DraughtsBoard, DraughtsGame, Move, Piece, Player, Position, RDraughtApplication,
RectangularBoard, RectangularBoard,
@@ -14,7 +14,7 @@ const SQUARE_SIZE: f64 = 1.0;
use super::final_dialog; use super::final_dialog;
use super::greeting_dialog; use super::greeting_dialog;
use rdraught_ui_common::{ 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"); 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) 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 { trait AugmentedRect {
fn from_points(tl: Point, br: Point) -> Rectangle; fn from_points(tl: Point, br: Point) -> Rectangle;
fn center(&self) -> Point; fn center(&self) -> Point;
@@ -253,6 +294,10 @@ fn create_game_window(
redo_button.set_icon_name("edit-redo-symbolic"); redo_button.set_icon_name("edit-redo-symbolic");
redo_button.set_sensitive(false); 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 selected_piece: SharedMutable<Option<Position>> = new_shared_mut(None);
let available_moves = new_shared_mut_ref(Vec::<Move>::new()); let available_moves = new_shared_mut_ref(Vec::<Move>::new());
// Create a DrawingArea widget where we will draw the chessboard. // Create a DrawingArea widget where we will draw the chessboard.
@@ -261,6 +306,11 @@ fn create_game_window(
window.set_child(Some(&drawing_area)); window.set_child(Some(&drawing_area));
header_bar.pack_start(&undo_button); 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 da = drawing_area.clone();
let rd = rd.clone(); let rd = rd.clone();
@@ -268,19 +318,23 @@ fn create_game_window(
let redo_btn_clone = redo_button.clone(); let redo_btn_clone = redo_button.clone();
let selected_piece = selected_piece.clone(); let selected_piece = selected_piece.clone();
let available_moves = available_moves.clone(); let available_moves = available_moves.clone();
let status = status.clone();
let finished = finished.clone();
let window = window.clone();
undo_button.connect_clicked(move |_| { undo_button.connect_clicked(move |_| {
let mut rd = rd.borrow_mut(); {
if rd.can_undo() { let mut app = rd.borrow_mut();
rd.undo().ok(); if app.can_undo() {
redo_btn_clone.set_sensitive(rd.can_redo()); app.undo().ok();
undo_btn_clone.set_sensitive(rd.can_undo()); selected_piece.set(None);
selected_piece.set(None); available_moves.borrow_mut().clear();
available_moves.borrow_mut().clear(); }
da.queue_draw();
} }
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 da = drawing_area.clone();
let rd = rd.clone(); let rd = rd.clone();
@@ -288,16 +342,40 @@ fn create_game_window(
let redo_btn_clone = redo_button.clone(); let redo_btn_clone = redo_button.clone();
let selected_piece = selected_piece.clone(); let selected_piece = selected_piece.clone();
let available_moves = available_moves.clone(); let available_moves = available_moves.clone();
let status = status.clone();
let finished = finished.clone();
let window = window.clone();
redo_button.connect_clicked(move |_| { redo_button.connect_clicked(move |_| {
let mut rd = rd.borrow_mut(); {
if rd.can_redo() { let mut app = rd.borrow_mut();
rd.redo().ok(); if app.can_redo() {
redo_btn_clone.set_sensitive(rd.can_redo()); app.redo().ok();
undo_btn_clone.set_sensitive(rd.can_undo()); selected_piece.set(None);
selected_piece.set(None); available_moves.borrow_mut().clear();
available_moves.borrow_mut().clear(); }
da.queue_draw();
} }
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. // 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. // Loop over rows and columns to draw each chessboard cell.
for row in 0..DraughtsBoard::rows() { for row in 0..DraughtsBoard::rows() {
for col in 0..DraughtsBoard::columns() { for col in 0..DraughtsBoard::columns() {
let position = match current_player { let position = board_position(current_player, row as u8, col as u8);
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 square = Rectangle::new( let square = Rectangle::new(
col as f64 * SQUARE_SIZE, col as f64 * SQUARE_SIZE,
row as f64 * SQUARE_SIZE, row as f64 * SQUARE_SIZE,
@@ -481,99 +556,74 @@ fn create_game_window(
let window = window.clone(); let window = window.clone();
let undo_btn_clone = undo_button.clone(); let undo_btn_clone = undo_button.clone();
let redo_btn_clone = redo_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.connect_pressed(move |gesture, _, x, y| {
gesture.set_state(gtk::EventSequenceState::Claimed); gesture.set_state(gtk::EventSequenceState::Claimed);
if let Some(winner) = rd.borrow().game().winner() { if finished.get() {
MainContext::default() return;
.spawn_local(final_dialog::create_dialog(window.clone(), winner)); }
} else {
let xform = xform.borrow(); let xform = xform.borrow();
let inverse = { let inverse = {
let mut m = *xform; let mut m = *xform;
m.invert(); m.invert();
m m
}; };
let p = transform_point(&Point::new(x, y), &inverse); let p = transform_point(&Point::new(x, y), &inverse);
if board_clone.contains(&p) { if !board_clone.contains(&p) {
let p = p - board_clone.tl(); selected_piece.set(None);
// println!("Point: {:?}", p); available_moves.borrow_mut().clear();
let position = match current_player { drawing_area.queue_draw();
Player::White => Position::new( return;
(8.0 - (p.y() / SQUARE_SIZE)) as u8, }
(p.x() / SQUARE_SIZE) as u8,
), let p = p - board_clone.tl();
Player::Red => Position::new( let screen_row = (p.y() / SQUARE_SIZE) as u8;
(p.y() / SQUARE_SIZE) as u8, let screen_col = (p.x() / SQUARE_SIZE) as u8;
(8.0 - 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;
// println!("Selected position: {:?}", position); if let Some(pos) = position {
let piece = { for mv in am {
let rd = rd.borrow(); if mv.get_end_position() == Some(pos) {
let draughts_game = rd.game(); let mut rd_app = rd.borrow_mut();
position if rd_app.apply_move(mv).is_err() {
.clone() continue;
.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());
} }
move_applied = true;
break;
} }
if !move_applied { }
let position = position.ok(); }
match piece.player() {
Some(Player::Red) => selected_piece.set(position), if move_applied {
Some(Player::White) => selected_piece.set(position), if let Some(pending) = rd.borrow().game().pending_capture() {
None => selected_piece.set(None), selected_piece.set(Some(pending));
} let mut moves = available_moves.borrow_mut();
if piece.player().is_none() { moves.clear();
selected_piece.set(None) for mv in rd.borrow().game().moves_for_piece(pending) {
} else { moves.push(mv);
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);
}
}
}
} }
} else { } else {
selected_piece.set(None); 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 // Assign the gesture to the treeview
@@ -582,11 +632,6 @@ fn create_game_window(
} }
fn on_activate(application: &Application, game: DraughtsGame) { 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 current_player = new_shared_mut(Player::Red);
let dialog = greeting_dialog::create(application, current_player.clone()); let dialog = greeting_dialog::create(application, current_player.clone());
let application = application.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 { pub fn yx(&self) -> f64 {
self.0[(1, 0)] self.0[(0, 1)]
} }
pub fn yy(&self) -> f64 { pub fn yy(&self) -> f64 {
@@ -259,3 +259,41 @@ impl Display for Xform {
self.0.fmt(f) 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 geo2d;
mod types; mod types;
pub use board::board_position;
pub use geo2d::{Point, Rect, Xform}; pub use geo2d::{Point, Rect, Xform};
pub use types::{SharedMutable, SharedMutableRef, new_shared_mut, new_shared_mut_ref}; pub use types::{SharedMutable, SharedMutableRef, new_shared_mut, new_shared_mut_ref};
-6
View File
@@ -10,13 +10,7 @@ version.workspace = true
[dependencies] [dependencies]
wasm4.workspace = true wasm4.workspace = true
wasm4-sys.workspace = true
rdraught.workspace = true rdraught.workspace = true
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
[profile.release]
opt-level = "z"
lto = true
+193 -186
View File
@@ -1,26 +1,196 @@
#![no_main] #![no_main]
#![no_std] #![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::draw::{Color, DrawIndex, DrawIndices, Framebuffer};
use wasm4::rt::{Resources, Runtime}; use wasm4::rt::{Resources, Runtime};
// use wasm4 as w4; use wasm4::sys::{DRAW_COLORS, MOUSE_BUTTONS, MOUSE_LEFT, MOUSE_X, MOUSE_Y};
use core::arch::wasm32::unreachable;
use wasm4::sys::DRAW_COLORS;
use wasm4::trace; use wasm4::trace;
// mod wasm4;
// use wasm4::{DRAW_COLORS, oval, rect}; const SQUARE_SIZE: i32 = 20;
const BOARD_SIZE: i32 = 160;
// mod palette; struct RdraughtW4 {
// static mut GAME: DraughtsGame = DraughtsGame::default();
struct Rdraught_W4 {
framebuffer: Framebuffer, framebuffer: Framebuffer,
game: DraughtsGame, 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 { fn start(rs: Resources) -> Self {
rs.framebuffer.replace_palette([ rs.framebuffer.replace_palette([
Color(0x00edeada), Color(0x00edeada),
@@ -28,196 +198,33 @@ impl Runtime for Rdraught_W4 {
Color(0x005d8d60), Color(0x005d8d60),
Color(0x00100f24), Color(0x00100f24),
]); ]);
Rdraught_W4 { RdraughtW4 {
framebuffer: rs.framebuffer, framebuffer: rs.framebuffer,
game: DraughtsGame::default(), game: DraughtsGame::default(),
selected: None,
mouse_down: false,
} }
} }
fn update(&mut self) { fn update(&mut self) {
let indices = DrawIndices::from_array([ let (mouse_x, mouse_y, buttons) = unsafe { (*MOUSE_X, *MOUSE_Y, *MOUSE_BUTTONS) };
DrawIndex::Third, let pressed = buttons & MOUSE_LEFT != 0;
DrawIndex::Fourth, if pressed && !self.mouse_down {
DrawIndex::Fourth, self.handle_click(mouse_x as i32, mouse_y as i32);
DrawIndex::Transparent,
]);
unsafe {
*DRAW_COLORS = indices.into_u16();
} }
self.mouse_down = pressed;
self.framebuffer.rect([10, 10], [50, 50]); self.draw_board();
self.framebuffer.oval([50, 50], [10, 10]); if let Some(winner) = self.game.winner() {
let SQUARE_SIZE = 20u32; self.draw_winner(winner);
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 => {}
}
}
} }
} }
} }
wasm4::main! { Rdraught_W4 } wasm4::main! { RdraughtW4 }
// 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 => {}
// }
// }
// }
// }
// }
#[panic_handler] #[panic_handler]
fn panic_handler(_panic_info: &core::panic::PanicInfo<'_>) -> ! { fn panic_handler(_panic_info: &core::panic::PanicInfo<'_>) -> ! {
trace("panic error"); trace("panic error");
#[cfg(debug_assertions)]
if let Some(cause) = _panic_info.payload().downcast_ref::<&str>() {
trace(cause);
}
unreachable() 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 rust-version.workspace = true
version.workspace = true version.workspace = true
# [lib]
# crate-type = ["cdylib"]
[dependencies] [dependencies]
rdraught.workspace = true rdraught.workspace = true
rdraught-ui-common.workspace = true rdraught-ui-common.workspace = true
wasm-bindgen.workspace = true wasm-bindgen.workspace = true
console_error_panic_hook.workspace = true console_error_panic_hook.workspace = true
rmath.workspace = true
base64.workspace = true
[dependencies.web-sys] [dependencies.web-sys]
workspace = true workspace = true
features = [ features = [
'DomMatrix',
'CanvasRenderingContext2d', 'CanvasRenderingContext2d',
'CssStyleDeclaration', 'CssStyleDeclaration',
'Document', 'Document',
@@ -31,9 +24,8 @@ features = [
'EventTarget', 'EventTarget',
'HtmlCanvasElement', 'HtmlCanvasElement',
'HtmlElement', 'HtmlElement',
'KeyboardEvent',
'MouseEvent', 'MouseEvent',
'Node', 'Node',
'HtmlImageElement',
'SvgImageElement',
'Window', '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 core::f64::consts::PI;
use rdraught::{DraughtsBoard, DraughtsGame, Move, Piece, Player, Position, RectangularBoard}; use rdraught::{
use rdraught_ui_common::{ DraughtsBoard, DraughtsGame, Move, Piece, Player, Position, RDraughtApplication,
Point, Rect, SharedMutable, SharedMutableRef, Xform, new_shared_mut, new_shared_mut_ref, RectangularBoard,
}; };
use rdraught_ui_common::{Point, Rect, Xform, board_position, new_shared_mut_ref};
use wasm_bindgen::prelude::*; 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] fn player_name(player: Player) -> &'static str {
extern "C" { match player {
// Use `js_namespace` here to bind `console.log(..)` instead of just Player::Red => "Red",
// `log(..)` Player::White => "White",
#[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(())
} }
} }
@@ -165,42 +27,129 @@ fn apply_xform(ctx: &CanvasRenderingContext2d, xform: &Xform) -> Result<(), JsVa
) )
} }
fn from_ctx(ctx: &CanvasRenderingContext2d) -> Result<Xform, JsValue> { fn draw_crown(ctx: &CanvasRenderingContext2d, center: Point, size: f64) -> Result<(), JsValue> {
let m = ctx.get_transform()?; let width = size * 0.35;
Ok(Xform::new(m.a(), m.c(), m.b(), m.d(), m.e(), m.f())) 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 { fn draw_piece(
match current_player { ctx: &CanvasRenderingContext2d,
Player::White => DraughtsBoard::rows() - 1 - row, xform: &Xform,
Player::Red => row, 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 { struct App {
game: DraughtsGame, rd: RDraughtApplication,
board: Rect, board: Rect,
screen: Rect, screen: Rect,
xform: Xform, xform: Xform,
selected_piece: Option<Position>, selected_piece: Option<Position>,
available_moves: Vec<Move>, available_moves: Vec<Move>,
main_player: Player, 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 { impl Default for App {
fn default() -> Self { fn default() -> Self {
let board = Rect::from_size( let board = Rect::from_size(
@@ -209,172 +158,174 @@ impl Default for App {
App::SQUARE_SIZE * DraughtsBoard::rows() as f64, App::SQUARE_SIZE * DraughtsBoard::rows() as f64,
); );
App { App {
game: DraughtsGame::default(), rd: RDraughtApplication::new(DraughtsGame::default()),
board, board,
screen: Rect::new(Point::new(0.0, 0.0), Point::new(0.0, 0.0)), screen: Rect::new(Point::new(0.0, 0.0), Point::new(0.0, 0.0)),
xform: Xform::default(), xform: Xform::default(),
selected_piece: None, selected_piece: None,
available_moves: Vec::new(), available_moves: Vec::new(),
main_player: Player::White, main_player: Player::White,
red_agent: Box::new(Human::new()),
white_agent: Box::new(Human::new()),
} }
} }
} }
impl App { impl App {
const SQUARE_SIZE: f64 = 100.0; const SQUARE_SIZE: f64 = 100.0;
fn draw(
&mut self, fn new_game(&mut self) {
ctx: &CanvasRenderingContext2d, self.rd = RDraughtApplication::new(DraughtsGame::default());
piece_drawer: &PieceDrawer, self.selected_piece = None;
) -> Result<(), JsValue> { 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()); ctx.clear_rect(0.0, 0.0, self.screen.width(), self.screen.height());
let board = &self.board; let board = &self.board;
let f = f64::min( let factor = f64::min(
self.screen.width() / board.width(), self.screen.width() / board.width(),
self.screen.height() / board.height(), self.screen.height() / board.height(),
); );
ctx.save(); ctx.save();
self.xform = Xform::default() self.xform = Xform::default()
* Xform::xlate(-board.center().x(), -board.center().y()) * 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()); * Xform::xlate(self.screen.center().x(), self.screen.center().y());
apply_xform(ctx, &self.xform)?; apply_xform(ctx, &self.xform)?;
// let board_center = board.center();
// let screen_center = screen.center(); let available_positions: Vec<Position> = self
// 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
.available_moves .available_moves
.iter() .iter()
.filter_map(Move::get_end_position) .filter_map(Move::get_end_position)
.collect(); .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( let cell = Rect::new(
Point::new( Point::new(
board.tl().x() + i as f64 * App::SQUARE_SIZE, board.tl().x() + screen_col as f64 * App::SQUARE_SIZE,
board.tl().y() + j as f64 * App::SQUARE_SIZE, board.tl().y() + screen_row as f64 * App::SQUARE_SIZE,
), ),
Point::new( Point::new(
board.tl().x() + (i + 1) as f64 * App::SQUARE_SIZE, board.tl().x() + (screen_col + 1) as f64 * App::SQUARE_SIZE,
board.tl().y() + (j + 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"); ctx.set_fill_style_str("#cccc99");
} else { } else {
ctx.set_fill_style_str("#666633"); ctx.set_fill_style_str("#666633");
} }
ctx.fill_rect(cell.tl().x(), cell.tl().y(), cell.width(), cell.height()); ctx.fill_rect(cell.tl().x(), cell.tl().y(), cell.width(), cell.height());
if (i + j) % 2 != 0 {
let current_player = Player::White; let board_position =
let position = Position::new(map_row(current_player, j) as u8, i as u8) board_position(self.main_player, screen_row as u8, screen_col as u8).ok();
.expect("board loop only visits valid squares"); if let Some(position) = board_position {
let piece = self.game.piece_at(position); let piece = self.rd.game().piece_at(position);
if piece.is_present() { 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 { if board_position == self.selected_piece {
// console_log!("Selected: {}", selected_piece); draw_highlight(ctx, &cell, "#ffffff")?;
ctx.save(); }
ctx.set_shadow_blur(300.0); if let Some(position) = board_position {
ctx.set_shadow_color("#ffffff"); if available_positions.contains(&position) {
ctx.begin_path(); draw_highlight(ctx, &cell, "#00ff00")?;
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();
} }
} }
} }
} }
ctx.restore(); 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(()) Ok(())
} }
} }
fn main() -> Result<(), JsValue> { fn main() -> Result<(), JsValue> {
console_error_panic_hook::set_once();
let document = web_sys::window().unwrap().document().unwrap(); let document = web_sys::window().unwrap().document().unwrap();
let canvas = document let canvas = document
.create_element("canvas")? .create_element("canvas")?
.dyn_into::<web_sys::HtmlCanvasElement>()?; .dyn_into::<web_sys::HtmlCanvasElement>()?;
canvas.style().set_property("background-color", "black")?; canvas.style().set_property("background-color", "black")?;
document.body().unwrap().append_child(&canvas)?; 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 window = web_sys::window().expect("should have a window in this context");
let width = window.inner_width().unwrap().as_f64().unwrap(); let width = window.inner_width().unwrap().as_f64().unwrap();
let height = window.inner_height().unwrap().as_f64().unwrap(); let height = window.inner_height().unwrap().as_f64().unwrap();
canvas.set_width(width as u32); canvas.set_width(width as u32);
canvas.set_height(height as u32); canvas.set_height(height as u32);
let app = new_shared_mut_ref(App::default()); let app = new_shared_mut_ref(App::default());
let ctx = canvas let ctx = canvas
.get_context("2d")? .get_context("2d")?
.unwrap() .unwrap()
.dyn_into::<web_sys::CanvasRenderingContext2d>()?; .dyn_into::<CanvasRenderingContext2d>()?;
let piece_drawer = new_shared_mut_ref(PieceDrawer::new(&document)?);
{ {
let piece_drawer = piece_drawer.clone(); let app = app.clone();
let screen = Rect::new(Point::new(0.0, 0.0), Point::new(width, height)); app.borrow_mut().screen = Rect::new(Point::new(0.0, 0.0), Point::new(width, height));
app.borrow_mut().screen = screen; app.borrow_mut().draw(&ctx)?;
app.borrow_mut().draw(&ctx, &piece_drawer.borrow())?;
} }
{ {
let app = app.clone(); let app = app.clone();
let canvas = canvas.clone(); let canvas = canvas.clone();
let ctx = ctx.clone(); let ctx = ctx.clone();
let piece_drawer = piece_drawer.clone();
let closure = Closure::<dyn FnMut()>::new(move || { let closure = Closure::<dyn FnMut()>::new(move || {
let window = web_sys::window().expect("should have a window in this context"); let window = web_sys::window().expect("should have a window in this context");
let width = window.inner_width().unwrap().as_f64().unwrap() as u32; 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(0.0, 0.0),
Point::new(canvas.width() as f64, canvas.height() as f64), 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())); window.set_onresize(Some(closure.as_ref().unchecked_ref()));
closure.forget(); closure.forget();
} }
{ {
let app = app.clone(); 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 mouse_click_handler = Closure::wrap(Box::new(move |event: MouseEvent| {
let mut app = app.borrow_mut(); let mut app = app.borrow_mut();
if app.rd.game().winner().is_some() {
return;
}
let xform = app.xform.invert(); let xform = app.xform.invert();
let board = &app.board; let board = &app.board;
let position = Point::new(event.x() as f64, event.y() as f64) * &xform - board.tl(); let position =
if position.x() > 0.0 Point::new(event.offset_x() as f64, event.offset_y() as f64) * &xform - board.tl();
&& position.x() < board.width() if position.x() <= 0.0
&& position.y() > 0.0 || position.x() >= board.width()
&& position.y() < board.height() || position.y() <= 0.0
|| position.y() >= board.height()
{ {
let col = f64::floor(position.x() / App::SQUARE_SIZE) as usize; app.selected_piece = None;
let row = f64::floor(position.y() / App::SQUARE_SIZE) as usize; app.available_moves.clear();
let row = map_row(app.main_player, row); app.draw(&ctx).unwrap();
if (row + col) % 2 == 0 { return;
let position = Position::new(row as u8, col as u8).unwrap(); }
let selected_move = app
.available_moves let screen_col = (position.x() / App::SQUARE_SIZE).floor() as u8;
.iter() let screen_row = (position.y() / App::SQUARE_SIZE).floor() as u8;
.find(|mv| mv.get_end_position() == Some(position)) let Ok(position) = board_position(app.main_player, screen_row, screen_col) else {
.map(Move::clone); app.selected_piece = None;
if app.selected_piece.is_some() && selected_move.is_some() { app.available_moves.clear();
let mv = selected_move.unwrap(); app.draw(&ctx).unwrap();
if app.game.check_and_apply_move(&mv).is_ok() { return;
app.selected_piece = None; };
app.available_moves.clear();
} let selected_move = app
} else { .available_moves
app.selected_piece = Some(position); .iter()
let mut available_moves = Vec::<Move>::new(); .find(|mv| mv.get_end_position() == Some(position))
for mv in app.game.moves_for_piece(position) { .cloned();
available_moves.push(mv); if app.selected_piece.is_some() && selected_move.is_some() {
} if app.rd.apply_move(selected_move.unwrap()).is_ok() {
app.available_moves = available_moves; app.update_selection_after_move();
// console_log!("Clicked at: ({}, {})", col, row); }
} } 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 { } else {
app.selected_piece = None; app.selected_piece = None;
app.available_moves.clear(); 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)>); }) as Box<dyn FnMut(MouseEvent)>);
canvas.set_onclick(Some(mouse_click_handler.as_ref().unchecked_ref())); canvas.set_onclick(Some(mouse_click_handler.as_ref().unchecked_ref()));
mouse_click_handler.forget(); 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(()) 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))
}