diff --git a/Cargo.lock b/Cargo.lock index fa260cd..82028d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,6 +148,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "any_spawner" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41058deaa38c9d9dd933d6d238d825227cffa668e2839b52879f6619c63eee3b" +dependencies = [ + "futures", + "thiserror 2.0.18", + "wasm-bindgen-futures", +] + [[package]] name = "anyhow" version = "1.0.101" @@ -234,6 +245,37 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -246,6 +288,36 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" +[[package]] +name = "attribute-derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77" +dependencies = [ + "attribute-derive-macro", + "derive-where", + "manyhow", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn 2.0.114", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -350,6 +422,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "backbone-lib" +version = "0.1.0" +dependencies = [ + "bytes", + "ewebsock", + "futures", + "gloo-timers", + "postcard", + "protocol", + "serde", + "wasm-bindgen-futures", + "web-time", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -566,7 +653,7 @@ dependencies = [ "num-traits", "parking_lot", "portable-atomic", - "spin", + "spin 0.10.0", "tracing", ] @@ -669,7 +756,7 @@ dependencies = [ "rmp-serde", "serde", "serde_json", - "spin", + "spin 0.10.0", "uuid", ] @@ -782,7 +869,7 @@ dependencies = [ "hashbrown 0.16.1", "log", "serde", - "spin", + "spin 0.10.0", "tracing", ] @@ -910,7 +997,7 @@ dependencies = [ "burn-std", "hashbrown 0.16.1", "log", - "spin", + "spin 0.10.0", ] [[package]] @@ -1084,6 +1171,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd7a427adc0135366d99db65b36dae9237130997e560ed61118041fb72be6e8" +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" + [[package]] name = "candle-core" version = "0.9.2" @@ -1287,6 +1380,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "client_web" +version = "0.1.0" +dependencies = [ + "backbone-lib", + "futures", + "gloo-storage", + "leptos", + "serde", + "serde_json", + "trictrac-store", + "wasm-bindgen-futures", +] + [[package]] name = "cmake" version = "0.1.57" @@ -1296,6 +1403,26 @@ dependencies = [ "cc", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "codee" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9dbbdc4b4d349732bc6690de10a9de952bd39ba6a065c586e26600b6b0b91f5" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "codespan-reporting" version = "0.12.0" @@ -1318,6 +1445,12 @@ dependencies = [ "unicode-width 0.2.0", ] +[[package]] +name = "collection_literals" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" + [[package]] name = "color_quant" version = "1.1.0" @@ -1376,6 +1509,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "config" +version = "0.15.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6" +dependencies = [ + "convert_case 0.6.0", + "pathdiff", + "serde_core", + "toml 0.9.11+spec-1.1.0", + "winnow", +] + [[package]] name = "confy" version = "1.0.0" @@ -1408,6 +1554,32 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "const_str_slice_concat" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b" + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -1420,6 +1592,24 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "136d3e02915a2cea4d74caa8681e2d44b1c3254bdbf17d11d41d587ff858832c" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "convert_case" version = "0.8.0" @@ -1679,7 +1869,7 @@ dependencies = [ "serde", "serde_bytes", "serde_json", - "spin", + "spin 0.10.0", "tracing", "wasm-bindgen-futures", "web-time", @@ -1891,7 +2081,7 @@ dependencies = [ "md5", "serde", "serde_json", - "spin", + "spin 0.10.0", "thiserror 2.0.18", "toml 0.9.11+spec-1.1.0", "tracing", @@ -1914,7 +2104,7 @@ dependencies = [ "num-traits", "paste", "serde", - "spin", + "spin 0.10.0", "variadics_please", ] @@ -2281,6 +2471,17 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "derive-where" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -2377,6 +2578,18 @@ dependencies = [ "litrs", ] +[[package]] +name = "drain_filter_polyfill" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "dyn-stack" version = "0.13.2" @@ -2399,6 +2612,16 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "either_of" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f7f86eef3a7e4b9c2107583dbbbe3d9535c4b800796faf1774b82ba22033da" +dependencies = [ + "paste", + "pin-project-lite", +] + [[package]] name = "embassy-futures" version = "0.1.2" @@ -2455,6 +2678,18 @@ dependencies = [ "embedded-hal 1.0.0", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "entities" version = "1.0.1" @@ -2588,6 +2823,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "ewebsock" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "679247b4a005c82218a5f13b713239b0b6d484ec25347a719f5b7066152a748a" +dependencies = [ + "document-features", + "js-sys", + "log", + "tungstenite 0.24.0", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "exr" version = "1.74.0" @@ -2832,6 +3082,7 @@ dependencies = [ "futures-core", "futures-task", "futures-util", + "num_cpus", ] [[package]] @@ -3167,6 +3418,67 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "glow" version = "0.16.0" @@ -3239,6 +3551,12 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "guardian" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f" + [[package]] name = "gym-rs" version = "0.3.1" @@ -3291,6 +3609,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.13.2" @@ -3340,6 +3667,20 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin 0.9.8", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -3367,6 +3708,15 @@ dependencies = [ "digest", ] +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + [[package]] name = "http" version = "1.4.0" @@ -3418,6 +3768,20 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +[[package]] +name = "hydration_context" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35485b3dcbf7e044b8f28c73f04f13e7b509c2466fd10cb2a8a447e38f8a93a" +dependencies = [ + "futures", + "once_cell", + "or_poisoned", + "pin-project-lite", + "serde", + "throw_error", +] + [[package]] name = "hyper" version = "1.8.1" @@ -3687,6 +4051,12 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + [[package]] name = "ipnet" version = "2.11.0" @@ -3832,6 +4202,129 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" +[[package]] +name = "leptos" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b8731cb00f3f0894058155410b95c8955b17273181d2bc72600ab84edd24f1" +dependencies = [ + "any_spawner", + "cfg-if", + "either_of", + "futures", + "hydration_context", + "leptos_config", + "leptos_dom", + "leptos_hot_reload", + "leptos_macro", + "leptos_server", + "oco_ref", + "or_poisoned", + "paste", + "reactive_graph", + "rustc-hash 2.1.1", + "send_wrapper", + "serde", + "serde_qs", + "server_fn", + "slotmap", + "tachys", + "thiserror 2.0.18", + "throw_error", + "typed-builder", + "typed-builder-macro", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_config" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bae3e0ead5a7a814c8340eef7cb8b6cba364125bd8174b15dc9fe1b3cab7e03" +dependencies = [ + "config", + "regex", + "serde", + "thiserror 2.0.18", + "typed-builder", +] + +[[package]] +name = "leptos_dom" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89d4eb263bd5a9e7c49f780f17063f15aca56fd638c90b9dfd5f4739152e87d" +dependencies = [ + "js-sys", + "or_poisoned", + "reactive_graph", + "send_wrapper", + "tachys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_hot_reload" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e80219388501d99b246f43b6e7d08a28f327cdd34ba630a35654d917f3e1788e" +dependencies = [ + "anyhow", + "camino", + "indexmap", + "parking_lot", + "proc-macro2", + "quote", + "rstml", + "serde", + "syn 2.0.114", + "walkdir", +] + +[[package]] +name = "leptos_macro" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e621f8f5342b9bdc93bb263b839cee7405027a74560425a2dabea9de7952b1fd" +dependencies = [ + "attribute-derive", + "cfg-if", + "convert_case 0.7.1", + "html-escape", + "itertools 0.14.0", + "leptos_hot_reload", + "prettyplease", + "proc-macro-error2", + "proc-macro2", + "quote", + "rstml", + "server_fn_macro", + "syn 2.0.114", + "uuid", +] + +[[package]] +name = "leptos_server" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66985242812ec95e224fb48effe651ba02728beca92c461a9464c811a71aab11" +dependencies = [ + "any_spawner", + "base64 0.22.1", + "codee", + "futures", + "hydration_context", + "or_poisoned", + "reactive_graph", + "send_wrapper", + "serde", + "serde_json", + "server_fn", + "tachys", +] + [[package]] name = "libc" version = "0.2.180" @@ -3907,6 +4400,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linear-map" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" + [[package]] name = "link-cplusplus" version = "1.0.12" @@ -4016,6 +4515,29 @@ dependencies = [ "libc", ] +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + [[package]] name = "matchers" version = "0.2.0" @@ -4286,6 +4808,12 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "next_tuple" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28" + [[package]] name = "no-std-compat" version = "0.2.0" @@ -4518,6 +5046,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "oco_ref" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0423ff9973dea4d6bd075934fdda86ebb8c05bdf9d6b0507067d4a1226371d" +dependencies = [ + "serde", + "thiserror 2.0.18", +] + [[package]] name = "octets" version = "0.2.0" @@ -4554,6 +5092,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "or_poisoned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd" + [[package]] name = "ordered-float" version = "4.6.0" @@ -4626,6 +5170,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pbkdf2" version = "0.11.0" @@ -4660,6 +5210,26 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -4748,6 +5318,19 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -4831,6 +5414,39 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -4840,6 +5456,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "version_check", + "yansi", +] + [[package]] name = "profiling" version = "1.0.17" @@ -4859,6 +5488,13 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "protocol" +version = "0.1.0" +dependencies = [ + "serde", +] + [[package]] name = "pulp" version = "0.22.2" @@ -5033,6 +5669,28 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quote-use" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" +dependencies = [ + "quote", + "quote-use-macros", +] + +[[package]] +name = "quote-use-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "r-efi" version = "5.3.0" @@ -5315,6 +5973,55 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "reactive_graph" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a0ccddbc11a648bd09761801dac9e3f246ef7641130987d6120fced22515e6" +dependencies = [ + "any_spawner", + "async-lock", + "futures", + "guardian", + "hydration_context", + "or_poisoned", + "pin-project-lite", + "rustc-hash 2.1.1", + "send_wrapper", + "serde", + "slotmap", + "thiserror 2.0.18", + "web-sys", +] + +[[package]] +name = "reactive_stores" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aadc7c19e3a360bf19cd595d2dc8b58ce67b9240b95a103fbc1317a8ff194237" +dependencies = [ + "guardian", + "itertools 0.14.0", + "or_poisoned", + "paste", + "reactive_graph", + "reactive_stores_macro", + "rustc-hash 2.1.1", +] + +[[package]] +name = "reactive_stores_macro" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221095cb028dc51fbc2833743ea8b1a585da1a2af19b440b3528027495bf1f2d" +dependencies = [ + "convert_case 0.7.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "reborrow" version = "0.5.5" @@ -5528,6 +6235,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "rstml" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56" +dependencies = [ + "derive-where", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.114", + "syn_derive", + "thiserror 2.0.18", +] + [[package]] name = "rusqlite" version = "0.37.0" @@ -5743,6 +6465,15 @@ version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + [[package]] name = "seq-macro" version = "0.3.6" @@ -5813,6 +6544,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_qs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + [[package]] name = "serde_rusqlite" version = "0.40.1" @@ -5853,6 +6595,60 @@ dependencies = [ "serde", ] +[[package]] +name = "server_fn" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d05a9e3fd8d7404985418db38c6617cc793a1a27f398d4fbc9dfe8e41b804e6" +dependencies = [ + "bytes", + "const_format", + "dashmap", + "futures", + "gloo-net", + "http", + "js-sys", + "once_cell", + "pin-project-lite", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "thiserror 2.0.18", + "throw_error", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504b35e883267b3206317b46d02952ed7b8bf0e11b2e209e2eb453b609a5e052" +dependencies = [ + "const_format", + "convert_case 0.6.0", + "proc-macro2", + "quote", + "syn 2.0.114", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb8b274f568c94226a8045668554aace8142a59b8bca5414ac5a79627c825568" +dependencies = [ + "server_fn_macro", + "syn 2.0.114", +] + [[package]] name = "sha1" version = "0.10.6" @@ -6013,6 +6809,15 @@ dependencies = [ "trictrac-store", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spin" version = "0.10.0" @@ -6136,6 +6941,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -6224,6 +7041,40 @@ dependencies = [ "winapi", ] +[[package]] +name = "tachys" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66c3b70c32844a6f1e2943c72a33ebb777ad6acbeb20d1329d62e3a7806d6ec" +dependencies = [ + "any_spawner", + "async-trait", + "const_str_slice_concat", + "drain_filter_polyfill", + "dyn-clone", + "either_of", + "futures", + "html-escape", + "indexmap", + "itertools 0.14.0", + "js-sys", + "linear-map", + "next_tuple", + "oco_ref", + "once_cell", + "or_poisoned", + "parking_lot", + "paste", + "reactive_graph", + "reactive_stores", + "rustc-hash 2.1.1", + "send_wrapper", + "slotmap", + "throw_error", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "tar" version = "0.4.44" @@ -6355,6 +7206,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "throw_error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ef8bf264c6ae02a065a4a16553283f0656bd6266fc1fcb09fd2e6b5e91427b" +dependencies = [ + "pin-project-lite", +] + [[package]] name = "tiff" version = "0.10.3" @@ -6493,7 +7353,7 @@ dependencies = [ "futures-util", "log", "tokio", - "tungstenite", + "tungstenite 0.28.0", ] [[package]] @@ -6882,6 +7742,24 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + [[package]] name = "tungstenite" version = "0.28.0" @@ -6914,6 +7792,26 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" +[[package]] +name = "typed-builder" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9d30e3a08026c78f246b173243cf07b3696d274debd26680773b6773c2afc7" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c36781cc0e46a83726d9879608e4cf6c2505237e263a8eb8c24502989cfdb28" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "typed-path" version = "0.12.2" @@ -7046,6 +7944,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -7215,6 +8119,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.85" @@ -7844,6 +8761,12 @@ version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "y4m" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 4c2eb15..72e3f08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,4 @@ [workspace] resolver = "2" -members = ["client_cli", "bot", "store", "spiel_bot"] +members = ["client_cli", "bot", "store", "spiel_bot", "client_web"] diff --git a/client_web/Cargo.toml b/client_web/Cargo.toml new file mode 100644 index 0000000..51a35dc --- /dev/null +++ b/client_web/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "client_web" +version = "0.1.0" +edition = "2021" + +[dependencies] +trictrac-store = { path = "../store" } +backbone-lib = { path = "../../forks/multiplayer/backbone-lib" } +leptos = { version = "0.7", features = ["csr"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1" +futures = "0.3" +gloo-storage = "0.3" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen-futures = "0.4" diff --git a/client_web/Trunk.toml b/client_web/Trunk.toml new file mode 100644 index 0000000..57a2aaa --- /dev/null +++ b/client_web/Trunk.toml @@ -0,0 +1,2 @@ +[serve] +port = 9092 diff --git a/client_web/assets/style.css b/client_web/assets/style.css new file mode 100644 index 0000000..74d62b0 --- /dev/null +++ b/client_web/assets/style.css @@ -0,0 +1,9 @@ +/* Trictrac web client — placeholder styles */ +body { + font-family: sans-serif; + display: flex; + justify-content: center; + background: #f4f0e8; + margin: 0; + padding: 1rem; +} diff --git a/client_web/index.html b/client_web/index.html new file mode 100644 index 0000000..b661d76 --- /dev/null +++ b/client_web/index.html @@ -0,0 +1,11 @@ + + + + + + Trictrac + + + + + diff --git a/client_web/src/app.rs b/client_web/src/app.rs new file mode 100644 index 0000000..c27cd8d --- /dev/null +++ b/client_web/src/app.rs @@ -0,0 +1,247 @@ +use futures::channel::mpsc; +use futures::{FutureExt, StreamExt}; +use gloo_storage::{LocalStorage, Storage}; +use leptos::prelude::*; +use leptos::task::spawn_local; +use serde::{Deserialize, Serialize}; + +use backbone_lib::session::{ConnectError, GameSession, RoomConfig, RoomRole, SessionEvent}; +use backbone_lib::traits::ViewStateUpdate; + +use crate::components::{ConnectingScreen, GameScreen, LoginScreen}; +use crate::trictrac::backend::TrictracBackend; +use crate::trictrac::types::{GameDelta, PlayerAction, ViewState}; + +const RELAY_URL: &str = "ws://127.0.0.1:8080/ws"; +const GAME_ID: &str = "trictrac"; +const STORAGE_KEY: &str = "trictrac_session"; + +/// The state the UI needs to render the game screen. +#[derive(Clone, PartialEq)] +pub struct GameUiState { + pub view_state: ViewState, + /// 0 = host, 1 = guest + pub player_id: u16, +} + +/// Which screen is currently shown. +#[derive(Clone, PartialEq)] +pub enum Screen { + Login { error: Option }, + Connecting, + Playing(GameUiState), +} + +/// Commands sent from UI event handlers into the network task. +pub enum NetCommand { + CreateRoom { room: String }, + JoinRoom { room: String }, + Reconnect { + relay_url: String, + game_id: String, + room_id: String, + token: u64, + host_state: Option>, + }, + Action(PlayerAction), + Disconnect, +} + +/// Stored in localStorage to reconnect after a page refresh. +#[derive(Serialize, Deserialize)] +struct StoredSession { + relay_url: String, + game_id: String, + room_id: String, + token: u64, + #[serde(default)] + is_host: bool, + #[serde(default)] + view_state: Option, +} + +fn save_session(session: &StoredSession) { + LocalStorage::set(STORAGE_KEY, session).ok(); +} + +fn load_session() -> Option { + LocalStorage::get::(STORAGE_KEY).ok() +} + +fn clear_session() { + LocalStorage::delete(STORAGE_KEY); +} + +#[component] +pub fn App() -> impl IntoView { + let stored = load_session(); + let initial_screen = if stored.is_some() { + Screen::Connecting + } else { + Screen::Login { error: None } + }; + let screen = RwSignal::new(initial_screen); + + let (cmd_tx, mut cmd_rx) = mpsc::unbounded::(); + provide_context(cmd_tx.clone()); + + if let Some(s) = stored { + let host_state = s + .view_state + .as_ref() + .and_then(|vs| serde_json::to_vec(vs).ok()); + cmd_tx + .unbounded_send(NetCommand::Reconnect { + relay_url: s.relay_url, + game_id: s.game_id, + room_id: s.room_id, + token: s.token, + host_state, + }) + .ok(); + } + + spawn_local(async move { + loop { + // Wait for a connect/reconnect command. + let (config, is_reconnect) = loop { + match cmd_rx.next().await { + Some(NetCommand::CreateRoom { room }) => { + break ( + RoomConfig { + relay_url: RELAY_URL.to_string(), + game_id: GAME_ID.to_string(), + room_id: room, + rule_variation: 0, + role: RoomRole::Create, + reconnect_token: None, + host_state: None, + }, + false, + ); + } + Some(NetCommand::JoinRoom { room }) => { + break ( + RoomConfig { + relay_url: RELAY_URL.to_string(), + game_id: GAME_ID.to_string(), + room_id: room, + rule_variation: 0, + role: RoomRole::Join, + reconnect_token: None, + host_state: None, + }, + false, + ); + } + Some(NetCommand::Reconnect { + relay_url, + game_id, + room_id, + token, + host_state, + }) => { + break ( + RoomConfig { + relay_url, + game_id, + room_id, + rule_variation: 0, + role: RoomRole::Join, + reconnect_token: Some(token), + host_state, + }, + true, + ); + } + _ => {} // Ignore game commands while disconnected. + } + }; + + screen.set(Screen::Connecting); + + let room_id_for_storage = config.room_id.clone(); + let mut session: GameSession = + match GameSession::connect::(config).await { + Ok(s) => s, + Err(ConnectError::WebSocket(e) | ConnectError::Handshake(e)) => { + if is_reconnect { + clear_session(); + } + screen.set(Screen::Login { error: Some(e) }); + continue; + } + }; + + if !session.is_host { + save_session(&StoredSession { + relay_url: RELAY_URL.to_string(), + game_id: GAME_ID.to_string(), + room_id: room_id_for_storage.clone(), + token: session.reconnect_token, + is_host: false, + view_state: None, + }); + } + + let is_host = session.is_host; + let player_id = session.player_id; + let reconnect_token = session.reconnect_token; + let mut vs = ViewState::default_with_names("Host", "Guest"); + + loop { + futures::select! { + cmd = cmd_rx.next().fuse() => match cmd { + Some(NetCommand::Action(action)) => { + session.send_action(action); + } + _ => { + clear_session(); + session.disconnect(); + screen.set(Screen::Login { error: None }); + break; + } + }, + event = session.next_event().fuse() => match event { + Some(SessionEvent::Update(u)) => { + match u { + ViewStateUpdate::Full(state) => vs = state, + ViewStateUpdate::Incremental(delta) => vs.apply_delta(&delta), + } + if is_host { + save_session(&StoredSession { + relay_url: RELAY_URL.to_string(), + game_id: GAME_ID.to_string(), + room_id: room_id_for_storage.clone(), + token: reconnect_token, + is_host: true, + view_state: Some(vs.clone()), + }); + } + screen.set(Screen::Playing(GameUiState { + view_state: vs.clone(), + player_id, + })); + } + Some(SessionEvent::Disconnected(reason)) => { + screen.set(Screen::Login { error: reason }); + break; + } + None => { + screen.set(Screen::Login { error: None }); + break; + } + } + } + } + } + }); + + view! { + {move || match screen.get() { + Screen::Login { error } => view! { }.into_any(), + Screen::Connecting => view! { }.into_any(), + Screen::Playing(state) => view! { }.into_any(), + }} + } +} diff --git a/client_web/src/components/connecting_screen.rs b/client_web/src/components/connecting_screen.rs new file mode 100644 index 0000000..15df805 --- /dev/null +++ b/client_web/src/components/connecting_screen.rs @@ -0,0 +1,6 @@ +use leptos::prelude::*; + +#[component] +pub fn ConnectingScreen() -> impl IntoView { + view! {

"Connecting…"

} +} diff --git a/client_web/src/components/game_screen.rs b/client_web/src/components/game_screen.rs new file mode 100644 index 0000000..81915b8 --- /dev/null +++ b/client_web/src/components/game_screen.rs @@ -0,0 +1,25 @@ +use leptos::prelude::*; + +use crate::app::GameUiState; +use crate::trictrac::types::SerStage; + +#[component] +pub fn GameScreen(state: GameUiState) -> impl IntoView { + let status = match state.view_state.stage { + SerStage::Ended => "Game over", + SerStage::PreGame => "Waiting for players…", + SerStage::InGame => match state.view_state.active_mp_player { + Some(id) if id == state.player_id => "Your turn", + Some(_) => "Opponent's turn", + None => "…", + }, + }; + + view! { +
+

{status}

+ // Board and score panel will be added in a subsequent step. +

"Board placeholder"

+
+ } +} diff --git a/client_web/src/components/login_screen.rs b/client_web/src/components/login_screen.rs new file mode 100644 index 0000000..7657e18 --- /dev/null +++ b/client_web/src/components/login_screen.rs @@ -0,0 +1,54 @@ +use futures::channel::mpsc::UnboundedSender; +use leptos::prelude::*; + +use crate::app::NetCommand; + +#[component] +pub fn LoginScreen(error: Option) -> impl IntoView { + let (room_name, set_room_name) = signal(String::new()); + + let cmd_tx = use_context::>() + .expect("UnboundedSender not found in context"); + + let cmd_tx_create = cmd_tx.clone(); + let cmd_tx_join = cmd_tx; + + view! { + + } +} diff --git a/client_web/src/components/mod.rs b/client_web/src/components/mod.rs new file mode 100644 index 0000000..fbc7ee2 --- /dev/null +++ b/client_web/src/components/mod.rs @@ -0,0 +1,7 @@ +mod connecting_screen; +mod game_screen; +mod login_screen; + +pub use connecting_screen::ConnectingScreen; +pub use game_screen::GameScreen; +pub use login_screen::LoginScreen; diff --git a/client_web/src/main.rs b/client_web/src/main.rs new file mode 100644 index 0000000..b4cf4ab --- /dev/null +++ b/client_web/src/main.rs @@ -0,0 +1,10 @@ +mod app; +mod components; +mod trictrac; + +use app::App; +use leptos::prelude::*; + +fn main() { + mount_to_body(|| view! { }) +} diff --git a/client_web/src/trictrac/backend.rs b/client_web/src/trictrac/backend.rs new file mode 100644 index 0000000..c2fd8fa --- /dev/null +++ b/client_web/src/trictrac/backend.rs @@ -0,0 +1,197 @@ +use backbone_lib::traits::{BackEndArchitecture, BackendCommand}; +use trictrac_store::{CheckerMove, DiceRoller, GameEvent, GameState, TurnStage}; + +use crate::trictrac::types::{GameDelta, PlayerAction, ViewState}; + +// Store PlayerId (u64) values used for the two players. +const HOST_PLAYER_ID: u64 = 1; +const GUEST_PLAYER_ID: u64 = 2; + +pub struct TrictracBackend { + game: GameState, + dice_roller: DiceRoller, + commands: Vec>, + view_state: ViewState, + /// Arrival flags: have host (index 0) and guest (index 1) joined? + arrived: [bool; 2], + /// First move of the current pair, waiting for the second. + pending_first_move: Option, +} + +impl TrictracBackend { + fn sync_view_state(&mut self) { + self.view_state = + ViewState::from_game_state(&self.game, HOST_PLAYER_ID, GUEST_PLAYER_ID); + } + + fn broadcast_state(&mut self) { + self.sync_view_state(); + let delta = GameDelta { state: self.view_state.clone() }; + self.commands.push(BackendCommand::Delta(delta)); + } + + /// Roll dice using the store's DiceRoller and fire Roll + RollResult events. + fn do_roll(&mut self) { + let dice = self.dice_roller.roll(); + let player_id = self.game.active_player_id; + let _ = self.game.consume(&GameEvent::Roll { player_id }); + let _ = self.game.consume(&GameEvent::RollResult { player_id, dice }); + + // Drive automatic stages that require no player input. + self.drive_automatic_stages(); + } + + /// Advance through stages that can be resolved without player input + /// (MarkPoints, MarkAdvPoints). + fn drive_automatic_stages(&mut self) { + loop { + let player_id = self.game.active_player_id; + match self.game.turn_stage { + TurnStage::MarkPoints | TurnStage::MarkAdvPoints => { + let _ = self.game.consume(&GameEvent::Mark { + player_id, + points: self.game.dice_points.0.max(self.game.dice_points.1), + }); + } + _ => break, + } + } + } +} + +impl BackEndArchitecture for TrictracBackend { + fn new(_rule_variation: u16) -> Self { + let mut game = GameState::new(false); + game.init_player("Host"); + game.init_player("Guest"); + + let view_state = + ViewState::from_game_state(&game, HOST_PLAYER_ID, GUEST_PLAYER_ID); + + TrictracBackend { + game, + dice_roller: DiceRoller::default(), + commands: Vec::new(), + view_state, + arrived: [false; 2], + pending_first_move: None, + } + } + + fn from_bytes(_rule_variation: u16, bytes: &[u8]) -> Option { + let view_state: ViewState = serde_json::from_slice(bytes).ok()?; + // Reconstruct a fresh game; full state restore is not yet implemented. + let mut backend = Self::new(_rule_variation); + backend.view_state = view_state; + Some(backend) + } + + fn player_arrival(&mut self, mp_player: u16) { + if mp_player > 1 { + self.commands.push(BackendCommand::KickPlayer { player: mp_player }); + return; + } + self.arrived[mp_player as usize] = true; + + // Cancel any reconnect timer for this player. + self.commands.push(BackendCommand::CancelTimer { timer_id: mp_player }); + + // Start the game once both players have arrived. + if self.arrived[0] && self.arrived[1] && self.game.stage == trictrac_store::Stage::PreGame + { + let _ = self.game.consume(&GameEvent::BeginGame { goes_first: HOST_PLAYER_ID }); + self.commands.push(BackendCommand::ResetViewState); + } else { + self.broadcast_state(); + } + } + + fn player_departure(&mut self, mp_player: u16) { + if mp_player > 1 { + return; + } + self.arrived[mp_player as usize] = false; + // Give 60 seconds to reconnect before terminating the room. + self.commands.push(BackendCommand::SetTimer { + timer_id: mp_player, + duration: 60.0, + }); + } + + fn inform_rpc(&mut self, mp_player: u16, action: PlayerAction) { + if self.game.stage == trictrac_store::Stage::Ended { + return; + } + + let store_id = if mp_player == 0 { HOST_PLAYER_ID } else { GUEST_PLAYER_ID }; + + // Only the active player may act (except during Chance-like waiting stages). + if self.game.active_player_id != store_id { + return; + } + + match action { + PlayerAction::Roll => { + if self.game.turn_stage == TurnStage::RollDice { + self.do_roll(); + } + } + PlayerAction::Move { from, to } => { + if self.game.turn_stage != TurnStage::Move { + return; + } + let Ok(cmove) = CheckerMove::new(from as usize, to as usize) else { + return; + }; + if let Some(first) = self.pending_first_move.take() { + let event = GameEvent::Move { + player_id: store_id, + moves: (first, cmove), + }; + if self.game.validate(&event) { + let _ = self.game.consume(&event); + self.drive_automatic_stages(); + } + // Whether valid or not, clear pending so the player can retry. + } else { + self.pending_first_move = Some(cmove); + // No state broadcast yet — wait for the second move. + return; + } + } + PlayerAction::Go => { + if self.game.turn_stage == TurnStage::HoldOrGoChoice { + let _ = self.game.consume(&GameEvent::Go { player_id: store_id }); + } + } + PlayerAction::Mark => { + if matches!( + self.game.turn_stage, + TurnStage::MarkPoints | TurnStage::MarkAdvPoints + ) { + self.drive_automatic_stages(); + } + } + } + + self.broadcast_state(); + } + + fn timer_triggered(&mut self, timer_id: u16) { + match timer_id { + 0 | 1 => { + // Reconnect grace period expired for host (0) or guest (1). + self.commands.push(BackendCommand::TerminateRoom); + } + _ => {} + } + } + + fn get_view_state(&self) -> &ViewState { + &self.view_state + } + + fn drain_commands(&mut self) -> Vec> { + std::mem::take(&mut self.commands) + } +} diff --git a/client_web/src/trictrac/mod.rs b/client_web/src/trictrac/mod.rs new file mode 100644 index 0000000..e59217a --- /dev/null +++ b/client_web/src/trictrac/mod.rs @@ -0,0 +1,2 @@ +pub mod backend; +pub mod types; diff --git a/client_web/src/trictrac/types.rs b/client_web/src/trictrac/types.rs new file mode 100644 index 0000000..3c91c1b --- /dev/null +++ b/client_web/src/trictrac/types.rs @@ -0,0 +1,143 @@ +use serde::{Deserialize, Serialize}; +use trictrac_store::{GameState, Stage, TurnStage}; + +// ── Actions sent by a player to the host backend ───────────────────────────── + +#[derive(Clone, Serialize, Deserialize)] +pub enum PlayerAction { + /// Active player requests a dice roll. + Roll, + /// Move one checker from `from` to `to` (field numbers 1–24, 0 = exit). + Move { from: u8, to: u8 }, + /// Choose to "go" (advance) during HoldOrGoChoice. + Go, + /// Acknowledge point marking (hold / advance points). + Mark, +} + +// ── Incremental state update broadcast to all clients ──────────────────────── + +/// Carries a full state snapshot; `apply_delta` replaces the local state. +/// Simple and correct; can be refined to true diffs later. +#[derive(Clone, Serialize, Deserialize)] +pub struct GameDelta { + pub state: ViewState, +} + +// ── Full game snapshot ──────────────────────────────────────────────────────── + +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub struct ViewState { + /// Board positions: index i = field i+1. Positive = white, negative = black. + pub board: [i8; 24], + pub stage: SerStage, + pub turn_stage: SerTurnStage, + /// Which multiplayer player_id (0 = host, 1 = guest) is the active player. + pub active_mp_player: Option, + /// Scores indexed by multiplayer player_id (0 = host, 1 = guest). + pub scores: [PlayerScore; 2], + /// Last rolled dice values. + pub dice: (u8, u8), +} + +impl ViewState { + pub fn default_with_names(host_name: &str, guest_name: &str) -> Self { + ViewState { + board: [0i8; 24], + stage: SerStage::PreGame, + turn_stage: SerTurnStage::RollDice, + active_mp_player: None, + scores: [ + PlayerScore { name: host_name.to_string(), points: 0, holes: 0 }, + PlayerScore { name: guest_name.to_string(), points: 0, holes: 0 }, + ], + dice: (0, 0), + } + } + + pub fn apply_delta(&mut self, delta: &GameDelta) { + *self = delta.state.clone(); + } + + /// Convert a store `GameState` to a `ViewState`. + /// `host_store_id` and `guest_store_id` are the trictrac `PlayerId`s assigned + /// to the host (mp player 0) and guest (mp player 1) respectively. + pub fn from_game_state( + gs: &GameState, + host_store_id: u64, + guest_store_id: u64, + ) -> Self { + let board_vec = gs.board.to_vec(); + let board: [i8; 24] = board_vec.try_into().expect("board is always 24 fields"); + + let stage = match gs.stage { + Stage::PreGame => SerStage::PreGame, + Stage::InGame => SerStage::InGame, + Stage::Ended => SerStage::Ended, + }; + let turn_stage = match gs.turn_stage { + TurnStage::RollDice => SerTurnStage::RollDice, + TurnStage::RollWaiting => SerTurnStage::RollWaiting, + TurnStage::MarkPoints => SerTurnStage::MarkPoints, + TurnStage::HoldOrGoChoice => SerTurnStage::HoldOrGoChoice, + TurnStage::Move => SerTurnStage::Move, + TurnStage::MarkAdvPoints => SerTurnStage::MarkAdvPoints, + }; + + let active_mp_player = if gs.active_player_id == host_store_id { + Some(0) + } else if gs.active_player_id == guest_store_id { + Some(1) + } else { + None + }; + + let score_for = |store_id: u64| -> PlayerScore { + gs.players + .get(&store_id) + .map(|p| PlayerScore { + name: p.name.clone(), + points: p.points, + holes: p.holes, + }) + .unwrap_or_else(|| PlayerScore { name: String::new(), points: 0, holes: 0 }) + }; + + ViewState { + board, + stage, + turn_stage, + active_mp_player, + scores: [score_for(host_store_id), score_for(guest_store_id)], + dice: (gs.dice.values.0, gs.dice.values.1), + } + } +} + +// ── Score snapshot ──────────────────────────────────────────────────────────── + +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub struct PlayerScore { + pub name: String, + pub points: u8, + pub holes: u8, +} + +// ── Serialisable mirrors of store enums ────────────────────────────────────── + +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub enum SerStage { + PreGame, + InGame, + Ended, +} + +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub enum SerTurnStage { + RollDice, + RollWaiting, + MarkPoints, + HoldOrGoChoice, + Move, + MarkAdvPoints, +}