diff --git a/.gitignore b/.gitignore index 3736e04..fa83e0e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,3 @@ profile.json bot/models client_web/dist var - -deploy -clients/**/dist diff --git a/Cargo.lock b/Cargo.lock index 8ce19af..2141f27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,7 +23,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common 0.1.7", + "crypto-common", "generic-array", ] @@ -191,18 +191,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures 0.2.17", - "password-hash 0.5.0", -] - [[package]] name = "arimaa_engine_step" version = "1.0.1" @@ -390,7 +378,7 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", - "http 1.4.0", + "http", "http-body", "http-body-util", "hyper", @@ -423,7 +411,7 @@ checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http", "http-body", "http-body-util", "mime", @@ -434,25 +422,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum-login" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964ea6eb764a227baa8c3368e45c94d23b6863cc7b880c6c9e341c143c5a5ff7" -dependencies = [ - "axum", - "form_urlencoded", - "serde", - "subtle", - "thiserror 2.0.18", - "tower-cookies", - "tower-layer", - "tower-service", - "tower-sessions", - "tracing", - "urlencoding", -] - [[package]] name = "backbone-lib" version = "0.1.0" @@ -585,15 +554,6 @@ dependencies = [ "core2", ] -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest 0.10.7", -] - [[package]] name = "block" version = "0.1.6" @@ -609,15 +569,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-buffer" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" -dependencies = [ - "hybrid-array", -] - [[package]] name = "board-game" version = "0.8.2" @@ -1408,7 +1359,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common 0.1.7", + "crypto-common", "inout", "zeroize", ] @@ -1456,7 +1407,6 @@ dependencies = [ "backbone-lib", "futures", "getrandom 0.3.4", - "gloo-net 0.5.0", "gloo-storage", "gloo-timers", "leptos", @@ -1478,12 +1428,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cmov" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" - [[package]] name = "cobs" version = "0.3.0" @@ -1604,12 +1548,6 @@ dependencies = [ "toml 0.8.23", ] -[[package]] -name = "const-oid" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" - [[package]] name = "const-random" version = "0.1.18" @@ -1905,15 +1843,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-common" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" -dependencies = [ - "hybrid-array", -] - [[package]] name = "csv" version = "1.4.0" @@ -1935,15 +1864,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ctutils" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" -dependencies = [ - "cmov", -] - [[package]] name = "cubecl" version = "0.9.0" @@ -2492,41 +2412,6 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" -[[package]] -name = "deadpool" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" -dependencies = [ - "deadpool-runtime", - "lazy_static", - "num_cpus", - "tokio", -] - -[[package]] -name = "deadpool-postgres" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d697d376cbfa018c23eb4caab1fd1883dd9c906a8c034e8d9a3cb06a7e0bef9" -dependencies = [ - "async-trait", - "deadpool", - "getrandom 0.2.17", - "tokio", - "tokio-postgres", - "tracing", -] - -[[package]] -name = "deadpool-runtime" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" -dependencies = [ - "tokio", -] - [[package]] name = "decorum" version = "0.3.1" @@ -2555,7 +2440,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", - "serde_core", ] [[package]] @@ -2632,23 +2516,11 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", + "block-buffer", + "crypto-common", "subtle", ] -[[package]] -name = "digest" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" -dependencies = [ - "block-buffer 0.12.0", - "const-oid", - "crypto-common 0.2.1", - "ctutils", -] - [[package]] name = "directories" version = "6.0.0" @@ -2996,12 +2868,6 @@ dependencies = [ "synstructure 0.12.6", ] -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - [[package]] name = "fallible-iterator" version = "0.3.0" @@ -3426,7 +3292,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -3569,27 +3435,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "gloo-net" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" -dependencies = [ - "futures-channel", - "futures-core", - "futures-sink", - "gloo-utils", - "http 0.2.12", - "js-sys", - "pin-project", - "serde", - "serde_json", - "thiserror 1.0.69", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "gloo-net" version = "0.6.0" @@ -3600,7 +3445,7 @@ dependencies = [ "futures-core", "futures-sink", "gloo-utils", - "http 1.4.0", + "http", "js-sys", "pin-project", "serde", @@ -3757,7 +3602,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.4.0", + "http", "indexmap", "slab", "tokio", @@ -3883,16 +3728,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "hmac" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" -dependencies = [ - "digest 0.11.2", + "digest", ] [[package]] @@ -3904,17 +3740,6 @@ dependencies = [ "utf8-width", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.4.0" @@ -3932,7 +3757,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http", ] [[package]] @@ -3943,17 +3768,11 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", + "http", "http-body", "pin-project-lite", ] -[[package]] -name = "http-range-header" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" - [[package]] name = "httparse" version = "1.10.1" @@ -3972,15 +3791,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" -[[package]] -name = "hybrid-array" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" -dependencies = [ - "typenum", -] - [[package]] name = "hydration_context" version = "0.2.1" @@ -4006,7 +3816,7 @@ dependencies = [ "futures-channel", "futures-core", "h2", - "http 1.4.0", + "http", "http-body", "httparse", "httpdate", @@ -4023,7 +3833,7 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.4.0", + "http", "hyper", "hyper-util", "rustls", @@ -4044,7 +3854,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.4.0", + "http", "http-body", "hyper", "ipnet", @@ -4968,42 +4778,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "leptos_router" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4168ead6a9715daba953aa842795cb2ad81b6e011a15745bd3d1baf86f76de95" -dependencies = [ - "any_spawner", - "either_of", - "futures", - "gloo-net 0.6.0", - "js-sys", - "leptos", - "leptos_router_macro", - "once_cell", - "or_poisoned", - "reactive_graph", - "send_wrapper", - "tachys", - "thiserror 2.0.18", - "url", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "leptos_router_macro" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31197af38d209ffc5d9f89715381c415a1570176f8d23455fbe00d148e79640" -dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "leptos_server" version = "0.7.8" @@ -5143,7 +4917,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", - "serde", ] [[package]] @@ -5274,16 +5047,6 @@ dependencies = [ "rayon", ] -[[package]] -name = "md-5" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" -dependencies = [ - "cfg-if", - "digest 0.11.2", -] - [[package]] name = "md5" version = "0.8.0" @@ -5349,16 +5112,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -5383,7 +5136,7 @@ checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.61.2", ] @@ -5740,15 +5493,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "objc2-system-configuration" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" -dependencies = [ - "objc2-core-foundation", -] - [[package]] name = "object" version = "0.37.3" @@ -5870,17 +5614,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "paste" version = "1.0.15" @@ -5905,10 +5638,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", - "password-hash 0.4.2", - "sha2 0.10.9", + "digest", + "hmac", + "password-hash", + "sha2", ] [[package]] @@ -5957,7 +5690,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", - "sha2 0.10.9", + "sha2", ] [[package]] @@ -5970,25 +5703,6 @@ dependencies = [ "indexmap", ] -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_shared", - "serde", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - [[package]] name = "pico-args" version = "0.5.0" @@ -6116,35 +5830,6 @@ dependencies = [ "serde", ] -[[package]] -name = "postgres-protocol" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56201207dac53e2f38e848e31b4b91616a6bb6e0c7205b77718994a7f49e70fc" -dependencies = [ - "base64 0.22.1", - "byteorder", - "bytes", - "fallible-iterator 0.2.0", - "hmac 0.13.0", - "md-5", - "memchr", - "rand 0.10.1", - "sha2 0.11.0", - "stringprep", -] - -[[package]] -name = "postgres-types" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc729a129e682e8d24170cd30ae1aa01b336b096cbb56df6d534ffec133d186" -dependencies = [ - "bytes", - "fallible-iterator 0.2.0", - "postgres-protocol", -] - [[package]] name = "potential_utf" version = "0.1.5" @@ -6872,31 +6557,6 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" -[[package]] -name = "relay-server" -version = "0.1.0" -dependencies = [ - "argon2", - "axum", - "axum-login", - "bytes", - "deadpool-postgres", - "futures-util", - "postcard", - "protocol", - "rand 0.8.5", - "serde", - "serde_json", - "thiserror 1.0.69", - "time", - "tokio", - "tokio-postgres", - "tower-http", - "tower-sessions", - "tracing", - "tracing-subscriber", -] - [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -6937,7 +6597,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http 1.4.0", + "http", "http-body", "http-body-util", "hyper", @@ -7062,7 +6722,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" dependencies = [ "bitflags 2.11.0", - "fallible-iterator 0.3.0", + "fallible-iterator", "fallible-streaming-iterator", "hashlink", "libsqlite3-sys", @@ -7417,8 +7077,8 @@ dependencies = [ "const_format", "dashmap", "futures", - "gloo-net 0.6.0", - "http 1.4.0", + "gloo-net", + "http", "js-sys", "once_cell", "pin-project-lite", @@ -7469,7 +7129,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest 0.10.7", + "digest", ] [[package]] @@ -7480,18 +7140,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.2", + "digest", ] [[package]] @@ -7578,12 +7227,6 @@ dependencies = [ "quote", ] -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - [[package]] name = "slab" version = "0.4.12" @@ -7690,17 +7333,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - [[package]] name = "strsim" version = "0.11.1" @@ -8156,7 +7788,6 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -8176,32 +7807,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "tokio-postgres" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd8df5ef180f6364759a6f00f7aadda4fbbac86cdee37480826a6ff9f3574ce" -dependencies = [ - "async-trait", - "byteorder", - "bytes", - "fallible-iterator 0.2.0", - "futures-channel", - "futures-util", - "log", - "parking_lot", - "percent-encoding", - "phf", - "pin-project-lite", - "postgres-protocol", - "postgres-types", - "rand 0.10.1", - "socket2", - "tokio", - "tokio-util", - "whoami", -] - [[package]] name = "tokio-rustls" version = "0.26.4" @@ -8382,22 +7987,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tower-cookies" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36" -dependencies = [ - "axum-core", - "cookie", - "futures-util", - "http 1.4.0", - "parking_lot", - "pin-project-lite", - "tower-layer", - "tower-service", -] - [[package]] name = "tower-http" version = "0.6.8" @@ -8406,24 +7995,14 @@ checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags 2.11.0", "bytes", - "futures-core", "futures-util", - "http 1.4.0", + "http", "http-body", - "http-body-util", - "http-range-header", - "httpdate", "iri-string", - "mime", - "mime_guess", - "percent-encoding", "pin-project-lite", - "tokio", - "tokio-util", "tower", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -8438,57 +8017,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" -[[package]] -name = "tower-sessions" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3" -dependencies = [ - "async-trait", - "http 1.4.0", - "time", - "tokio", - "tower-cookies", - "tower-layer", - "tower-service", - "tower-sessions-core", - "tower-sessions-memory-store", - "tracing", -] - -[[package]] -name = "tower-sessions-core" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b" -dependencies = [ - "async-trait", - "axum-core", - "base64 0.22.1", - "futures", - "http 1.4.0", - "parking_lot", - "rand 0.8.5", - "serde", - "serde_json", - "thiserror 2.0.18", - "time", - "tokio", - "tracing", -] - -[[package]] -name = "tower-sessions-memory-store" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb05909f2e1420135a831dd5df9f5596d69196d0a64c3499ca474c4bd3d33242" -dependencies = [ - "async-trait", - "time", - "tokio", - "tower-sessions-core", -] - [[package]] name = "tracel-llvm" version = "20.1.4-7" @@ -8514,7 +8042,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2 0.10.9", + "sha2", "tar", "walkdir", ] @@ -8700,29 +8228,6 @@ dependencies = [ "transpose", ] -[[package]] -name = "trictrac-web" -version = "0.1.0" -dependencies = [ - "backbone-lib", - "futures", - "getrandom 0.3.4", - "gloo-net 0.5.0", - "gloo-storage", - "gloo-timers", - "js-sys", - "leptos", - "leptos_i18n", - "leptos_router", - "rand 0.9.3", - "serde", - "serde_json", - "trictrac-store", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "try-lock" version = "0.2.5" @@ -8738,7 +8243,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.4.0", + "http", "httparse", "log", "rand 0.8.5", @@ -8755,7 +8260,7 @@ checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", - "http 1.4.0", + "http", "httparse", "log", "rand 0.9.3", @@ -8817,18 +8322,6 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - [[package]] name = "unicode-ident" version = "1.0.24" @@ -8844,12 +8337,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - [[package]] name = "unicode-segmentation" version = "1.13.2" @@ -8903,7 +8390,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common 0.1.7", + "crypto-common", "subtle", ] @@ -8955,12 +8442,6 @@ dependencies = [ "serde", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "utf-8" version = "0.7.6" @@ -9080,15 +8561,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -9107,15 +8579,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasite" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" -dependencies = [ - "wasi 0.14.7+wasi-0.2.4", -] - [[package]] name = "wasm-bindgen" version = "0.2.118" @@ -9238,21 +8701,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-user-portal" -version = "0.1.0" -dependencies = [ - "gloo-net 0.5.0", - "js-sys", - "leptos", - "leptos_router", - "serde", - "serde_json", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "webpki-roots" version = "0.26.11" @@ -9426,19 +8874,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "whoami" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a5b12f9df4f978d2cfdb1bd3bac52433f44393342d7ee9c25f5a1c14c0f45d" -dependencies = [ - "libc", - "libredox", - "objc2-system-configuration", - "wasite", - "web-sys", -] - [[package]] name = "wide" version = "0.7.33" @@ -10172,7 +9607,7 @@ dependencies = [ "crc32fast", "crossbeam-utils", "flate2", - "hmac 0.12.1", + "hmac", "pbkdf2", "sha1", "time", diff --git a/Cargo.toml b/Cargo.toml index 94a1c6b..72e3f08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,31 +1,4 @@ [workspace] resolver = "2" -members = [ - "store", - "clients/cli", - "clients/backbone-lib", - "clients/web", - "clients/web-game", - "clients/web-user-portal", - "server/protocol", - "server/relay-server", - "bot", - "spiel_bot", -] - -default-members = [ - "store", - "clients/cli", - "clients/backbone-lib", - "server/protocol", - "server/relay-server", - "bot", - "spiel_bot", -] - -# For the server we will need opt-level='3' -[profile.release] -opt-level = 'z' # Minimum space -lto = "fat" # Aggressive Link Time Optimization -codegen-units = 1 +members = ["client_cli", "bot", "store", "spiel_bot", "client_web"] diff --git a/README.md b/README.md index 4e7789f..e74fb69 100644 --- a/README.md +++ b/README.md @@ -2,133 +2,40 @@ This is a game of [Trictrac](https://en.wikipedia.org/wiki/Trictrac) rust implementation. -The project is still on its early stages. +The project is on its early stages. +Rules (without "schools") are implemented, as well as a rudimentary terminal interface which allow you to play against a bot which plays randomly. + +Training of AI bots is the work in progress. ## Usage -Install [devenv](https://devenv.sh/getting-started/), start a devenv shell `devenv shell`, and run the following commands. - -```bash -# Run the relay server -just build-relay -just run-relay # listens on :8080 - -# Run the game (separate terminal) -just dev-game -``` - -Open two browser windows at `http://127.0.0.1:9091`. In one, create a room; in the other, join with the same room name. - -Playing with the cli against the 'random' bot: `cargo run --bin=client_cli -- --bot random` +`cargo run --bin=client_cli -- --bot random` ## Roadmap - [x] rules - [x] command line interface - [x] basic bot (random play) -- [ ] web client (in progress) -- [ ] network game (in progress) - [ ] AI bot +- [ ] network game +- [ ] web client ## Code structure - game rules and game state are implemented in the _store/_ folder. -- the command-line application is implemented in _clients/cli/_; it allows you to play against a bot, or to have two bots play against each other +- the command-line application is implemented in _client_cli/_; it allows you to play against a bot, or to have two bots play against each other - the bots algorithms and the training of their models are implemented in the _bot/_ and _spiel_bot_ folders. ### _store_ package The game state is defined by the `GameState` struct in _store/src/game.rs_. The `to_string_id()` method allows this state to be encoded compactly in a string (without the played moves history). For a more readable textual representation, the `fmt::Display` trait is implemented. -### _clients/cli_ package +### _client_cli_ package -`clients/cli/src/game_runner.rs` contains the logic to make two bots play against each other. +`client_cli/src/game_runner.rs` contains the logic to make two bots play against each other. ### _bot_ package - `bot/src/strategy/default.rs` contains the code for a basic bot strategy: it determines the list of valid moves (using the `get_possible_moves_sequences` method of `store::MoveRules`) and simply executes the first move in the list. - `bot/src/strategy/dqnburn.rs` is another bot strategy that uses a reinforcement learning trained model with the DQN algorithm via the burn library (). - `bot/scripts/trains.sh` allows you to train agents using different algorithms (DQN, PPO, SAC). - -### multiplayer game - -Pagckages "clients/backbone-lib", "clients/web-game", "server/protocol", "server/relay-server" are a Leptos-optimized adaptation of the macroquad-based [Carbonfreezer/multiplayer](https://github.com/Carbonfreezer/multiplayer) project. It is a multiplayer game system in Rust targeting browser-based board games compiled as WASM. The original project used Macroquad with a polling-based transport layer; this version replaces that with an async session API built for [Leptos](https://leptos.dev/). - -The system consists of: - -- A **relay server** (Axum/Tokio) that routes messages between players and manages rooms, without knowing anything about game rules. -- A **backbone library** that handles WebSocket connection, handshake, and message routing, exposing an async API to the game frontend. -- Game-specific **backend logic** implementing the `BackEndArchitecture` trait, which runs only on the hosting client. -- A **Leptos frontend** that connects to a session and reacts to state updates. - -There is no dedicated game server. One of the players acts as the host: their browser runs the game backend locally. The relay server only forwards messages — it never touches game state. - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Host Client │ -│ ┌─────────────┐ ┌──────────────────┐ ┌────────────┐ │ -│ │ Leptos UI │◄──►│ GameSession │◄──►│ Backend │ │ -│ └─────────────┘ └────────┬─────────┘ └────────────┘ │ -└───────────────────────────── │ ────────────────────────────┘ - │ WebSocket - ┌──────▼──────┐ - │ Relay Server│ - └──────┬──────┘ - │ WebSocket -┌───────────────────────────────│────────────────────────────┐ -│ ┌─────────────┐ ┌─────────▼────────┐ │ -│ │ Leptos UI │◄──►│ GameSession │ (no backend) │ -│ └─────────────┘ └──────────────────┘ │ -│ Remote Client │ -└─────────────────────────────────────────────────────────────┘ -``` - -#### Data flow - -- **Actions** (e.g. "place stone at B3") flow from the UI to the host backend via `GameSession::send_action()`. -- **State updates** flow back as `ViewStateUpdate::Full` (full snapshot, on join or reset) or `ViewStateUpdate::Incremental` (delta, for animations). -- **Timers** are managed by the host's background task (wall-clock, no polling required from the game). - -#### backbone-lib session API - -The key design choice: `backbone-lib` owns a background async task per session. The Leptos app never drives a loop — it just awaits on events. - -#### Workspace - -**server/protocol** - -Shared message-type constants and the `JoinRequest` struct used during the WebSocket handshake. - -**server/relay-server** - -Listens on port 8080. Loads `GameConfig.json` on startup to know which games exist and their player limits: - -```json -[{ "name": "trictrac", "max_players": 10 }] -``` - -Games can be added at runtime via the `/reload` endpoint. `/enlist` lists active rooms. A watchdog cleans up inactive rooms every 20 minutes. - -For production, put it behind a reverse proxy with SSL (the browser requires `wss://` on HTTPS pages). Example Caddy config: - -``` -your-domain.com { - handle_path /api/* { - reverse_proxy localhost:8080 - } - file_server -} -``` - -**clients/backbone-lib** - -Modules: - -| Module | Purpose | -| ---------- | ---------------------------------------------------------------------------------------------------------- | -| `session` | `GameSession`, `connect()`, `SessionEvent`, `RoomConfig` | -| `host` | Background async task for the hosting client (drives `BackEndArchitecture`, manages timers) | -| `client` | Background async task for non-hosting clients | -| `protocol` | Wire encoding/decoding helpers (postcard + message-type bytes) | -| `platform` | `spawn_task` / `sleep_ms` abstractions (WASM: `spawn_local` + gloo-timers; native: thread + thread::sleep) | -| `traits` | `BackEndArchitecture`, `BackendCommand`, `ViewStateUpdate`, `SerializationCap` | diff --git a/clients/cli/Cargo.toml b/client_cli/Cargo.toml similarity index 71% rename from clients/cli/Cargo.toml rename to client_cli/Cargo.toml index 0149b1b..52318cb 100644 --- a/clients/cli/Cargo.toml +++ b/client_cli/Cargo.toml @@ -13,9 +13,9 @@ bincode = "1.3.3" pico-args = "0.5.0" pretty_assertions = "1.4.0" renet = "0.0.13" -trictrac-store = { path = "../../store" } -trictrac-bot = { path = "../../bot" } -spiel_bot = { path = "../../spiel_bot" } +trictrac-store = { path = "../store" } +trictrac-bot = { path = "../bot" } +spiel_bot = { path = "../spiel_bot" } itertools = "0.13.0" env_logger = "0.11.6" log = "0.4.20" diff --git a/clients/cli/src/app.rs b/client_cli/src/app.rs similarity index 100% rename from clients/cli/src/app.rs rename to client_cli/src/app.rs diff --git a/clients/cli/src/game_runner.rs b/client_cli/src/game_runner.rs similarity index 100% rename from clients/cli/src/game_runner.rs rename to client_cli/src/game_runner.rs diff --git a/clients/cli/src/main.rs b/client_cli/src/main.rs similarity index 100% rename from clients/cli/src/main.rs rename to client_cli/src/main.rs diff --git a/clients/web-game/Cargo.toml b/client_web/Cargo.toml similarity index 86% rename from clients/web-game/Cargo.toml rename to client_web/Cargo.toml index 578be7c..10d91b8 100644 --- a/clients/web-game/Cargo.toml +++ b/client_web/Cargo.toml @@ -9,8 +9,8 @@ locales = ["en", "fr"] [dependencies] leptos_i18n = { version = "0.5", features = ["csr", "interpolate_display"] } -trictrac-store = { path = "../../store" } -backbone-lib = { path = "../backbone-lib" } +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" @@ -20,13 +20,11 @@ gloo-storage = "0.3" [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = "0.4" -gloo-net = { version = "0.5", features = ["http"] } gloo-timers = { version = "0.3", features = ["futures"] } # getrandom 0.3 requires an explicit WASM backend; "wasm_js" uses window.crypto.getRandomValues. # Must be a direct dependency (not just transitive) for the feature to take effect. getrandom = { version = "0.3", features = ["wasm_js"] } web-sys = { version = "0.3", features = [ - "RequestCredentials", "AudioContext", "AudioParam", "AudioNode", diff --git a/clients/web-user-portal/Trunk.toml b/client_web/Trunk.toml similarity index 100% rename from clients/web-user-portal/Trunk.toml rename to client_web/Trunk.toml diff --git a/clients/web-game/assets/diceroll.mp3 b/client_web/assets/diceroll.mp3 similarity index 100% rename from clients/web-game/assets/diceroll.mp3 rename to client_web/assets/diceroll.mp3 diff --git a/clients/web-game/assets/style.css b/client_web/assets/style.css similarity index 98% rename from clients/web-game/assets/style.css rename to client_web/assets/style.css index 341be19..3034f38 100644 --- a/clients/web-game/assets/style.css +++ b/client_web/assets/style.css @@ -1194,20 +1194,3 @@ body { color: var(--ui-red-accent); font-style: italic; } - - -.auth-badge { - font-size: 0.8rem; - text-align: center; - padding: 0.35rem 0.6rem; - border-radius: 5px; -} -.auth-badge--in { background: rgba(96,165,250,0.15); color: #93c5fd; } -.auth-badge--out { background: rgba(148,163,184,0.1); color: #64748b; } -.auth-badge a { color: #60a5fa; } - -.playing-as { - font-size: 0.8rem; - color: #64748b; - text-align: center; -} diff --git a/clients/web-game/index.html b/client_web/index.html similarity index 100% rename from clients/web-game/index.html rename to client_web/index.html diff --git a/clients/web-game/locales/en.json b/client_web/locales/en.json similarity index 100% rename from clients/web-game/locales/en.json rename to client_web/locales/en.json diff --git a/clients/web-game/locales/fr.json b/client_web/locales/fr.json similarity index 100% rename from clients/web-game/locales/fr.json rename to client_web/locales/fr.json diff --git a/clients/web-game/src/app.rs b/client_web/src/app.rs similarity index 90% rename from clients/web-game/src/app.rs rename to client_web/src/app.rs index ce355f4..196a43a 100644 --- a/clients/web-game/src/app.rs +++ b/client_web/src/app.rs @@ -19,17 +19,10 @@ use trictrac_store::CheckerMove; use std::collections::VecDeque; -const RELAY_URL: &str = "ws://localhost:8080/ws"; +const RELAY_URL: &str = "ws://127.0.0.1:8080/ws"; const GAME_ID: &str = "trictrac"; const STORAGE_KEY: &str = "trictrac_session"; -// In debug builds trunk serves on 9091, relay is on 8080. -// In release the game is served by the relay itself — use relative paths. -#[cfg(debug_assertions)] -const HTTP_BASE: &str = "http://localhost:8080"; -#[cfg(not(debug_assertions))] -const HTTP_BASE: &str = ""; - /// The state the UI needs to render the game screen. #[derive(Clone, PartialEq)] pub struct GameUiState { @@ -100,11 +93,6 @@ struct StoredSession { view_state: Option, } -#[derive(Deserialize)] -struct MeResponse { - username: String, -} - fn save_session(session: &StoredSession) { LocalStorage::set(STORAGE_KEY, session).ok(); } @@ -117,31 +105,6 @@ fn clear_session() { LocalStorage::delete(STORAGE_KEY); } -/// Fire-and-forget: tell the relay server who won. Only called by the host. -async fn submit_game_result(room_code: String, game_state: ViewState) { - let [score_pl1, score_pl2] = game_state.scores; - let result_str = format!("{:?} - {:?}", score_pl1.holes, score_pl2.holes); - let outcomes = if score_pl1.holes < score_pl2.holes { - [("0", "loss"), ("1", "win")] - } else if score_pl2.holes < score_pl1.holes { - [("0", "win"), ("1", "loss")] - } else { - [("0", "draw"), ("1", "draw")] - }; - let body = serde_json::json!({ - "room_code": room_code, - "game_id": GAME_ID, - "result": result_str, - "outcomes": std::collections::HashMap::from(outcomes), - }); - let _ = gloo_net::http::Request::post(&format!("{HTTP_BASE}/games/result")) - .credentials(web_sys::RequestCredentials::Include) - .json(&body) - .unwrap() - .send() - .await; -} - #[component] pub fn App() -> impl IntoView { let stored = load_session(); @@ -152,23 +115,6 @@ pub fn App() -> impl IntoView { }; let screen = RwSignal::new(initial_screen); - // Auth: fetch once and expose to all child components via context. - let auth_username: RwSignal> = RwSignal::new(None); - provide_context(auth_username); - spawn_local(async move { - if let Ok(resp) = gloo_net::http::Request::get(&format!("{HTTP_BASE}/auth/me")) - .credentials(web_sys::RequestCredentials::Include) - .send() - .await - { - if resp.status() == 200 { - if let Ok(me) = resp.json::().await { - auth_username.set(Some(me.username)); - } - } - } - }); - let (cmd_tx, mut cmd_rx) = mpsc::unbounded::(); let pending: RwSignal> = RwSignal::new(VecDeque::new()); provide_context(pending); @@ -292,7 +238,6 @@ pub fn App() -> impl IntoView { let player_id = session.player_id; let reconnect_token = session.reconnect_token; let mut vs = ViewState::default_with_names("Blancs", "Noirs"); - let mut result_submitted = false; loop { futures::select! { @@ -315,15 +260,6 @@ pub fn App() -> impl IntoView { ViewStateUpdate::Full(state) => vs = state, ViewStateUpdate::Incremental(delta) => vs.apply_delta(&delta), } - - // Host reports outcomes once per terminal game state. - if is_host && !result_submitted && vs.stage == SerStage::Ended { - result_submitted = true; - let room = room_id_for_storage.clone(); - let gs = vs.clone(); - spawn_local(submit_game_result(room, gs)); - } - if is_host { save_session(&StoredSession { relay_url: RELAY_URL.to_string(), @@ -487,11 +423,7 @@ async fn run_local_bot_game( /// Returns the checker moves to animate when the board changed between two ViewStates. /// Returns `None` when the board is unchanged or no real moves were recorded. /// `own_move`: when true, m1 was already shown via staged-moves UI, so only animate m2. -fn compute_last_moves( - prev: &ViewState, - next: &ViewState, - own_move: bool, -) -> Option<(CheckerMove, CheckerMove)> { +fn compute_last_moves(prev: &ViewState, next: &ViewState, own_move: bool) -> Option<(CheckerMove, CheckerMove)> { if prev.board == next.board { return None; } @@ -504,9 +436,7 @@ fn compute_last_moves( } if own_move { // m1 was already shown via the staged-moves overlay; only animate m2. - if m2 == CheckerMove::default() { - return None; - } + if m2 == CheckerMove::default() { return None; } return Some((m2, CheckerMove::default())); } Some((m1, m2)) diff --git a/clients/web-game/src/components/board.rs b/client_web/src/components/board.rs similarity index 100% rename from clients/web-game/src/components/board.rs rename to client_web/src/components/board.rs diff --git a/clients/web-game/src/components/connecting_screen.rs b/client_web/src/components/connecting_screen.rs similarity index 100% rename from clients/web-game/src/components/connecting_screen.rs rename to client_web/src/components/connecting_screen.rs diff --git a/clients/web-game/src/components/die.rs b/client_web/src/components/die.rs similarity index 100% rename from clients/web-game/src/components/die.rs rename to client_web/src/components/die.rs diff --git a/clients/web-game/src/components/game_screen.rs b/client_web/src/components/game_screen.rs similarity index 98% rename from clients/web-game/src/components/game_screen.rs rename to client_web/src/components/game_screen.rs index 2493680..909e266 100644 --- a/clients/web-game/src/components/game_screen.rs +++ b/client_web/src/components/game_screen.rs @@ -18,8 +18,6 @@ use super::scoring::ScoringPanel; pub fn GameScreen(state: GameUiState) -> impl IntoView { let i18n = use_i18n(); - let auth_username = - use_context::>>().expect("auth_username not found in context"); let vs = state.view_state.clone(); let player_id = state.player_id; let is_my_turn = vs.active_mp_player == Some(player_id); @@ -242,11 +240,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { on:click=move |_| i18n.set_locale(Locale::fr) >"FR" - - {move || auth_username.get().map(|u| view! { -

"Playing as " {u}

- })} - ) -> impl IntoView { let i18n = use_i18n(); @@ -16,8 +11,6 @@ pub fn LoginScreen(error: Option) -> impl IntoView { let cmd_tx = use_context::>() .expect("UnboundedSender not found in context"); - let auth_username = - use_context::>>().expect("auth_username not found in context"); let cmd_tx_create = cmd_tx.clone(); let cmd_tx_join = cmd_tx.clone(); @@ -54,19 +47,6 @@ pub fn LoginScreen(error: Option) -> impl IntoView { {error.map(|err| view! {

{err}

})} - // Auth status badge - {move || match auth_username.get() { - Some(u) => view! { -

"✓ Logged in as " {u}

- }.into_any(), - None => view! { -

- "Not logged in — games won't be tracked. " - "Create account" -

- }.into_any(), - }} - ( - mut ws_sender: WsSender, - ws_receiver: WsReceiver, - mut action_rx: UnboundedReceiver>, - event_tx: UnboundedSender>, -) where - A: SerializationCap, - D: SerializationCap, - VS: SerializationCap, -{ - loop { - // 1. Drain outbound actions. - loop { - match action_rx.try_next() { - Ok(Some(BackendMsg::Action(action))) => { - send_rpc(&mut ws_sender, &action); - } - Ok(Some(BackendMsg::Disconnect)) => { - send_disconnect(&mut ws_sender, false); - event_tx - .unbounded_send(SessionEvent::Disconnected(None)) - .ok(); - return; - } - Ok(None) => { - send_disconnect(&mut ws_sender, false); - return; - } - Err(_) => break, - } - } - - // 2. Drain inbound state updates. - loop { - match ws_receiver.try_recv() { - Some(WsEvent::Message(WsMessage::Binary(data))) => { - match parse_client_update::(data) { - Ok(updates) => { - for u in updates { - event_tx - .unbounded_send(SessionEvent::Update(u)) - .ok(); - } - } - Err(e) => { - event_tx - .unbounded_send(SessionEvent::Disconnected(Some(e))) - .ok(); - return; - } - } - } - Some(WsEvent::Closed) => { - event_tx - .unbounded_send(SessionEvent::Disconnected(Some( - "Connection closed".to_string(), - ))) - .ok(); - return; - } - Some(WsEvent::Error(e)) => { - event_tx - .unbounded_send(SessionEvent::Disconnected(Some(e))) - .ok(); - return; - } - Some(_) => continue, - None => break, - } - } - - sleep_ms(2).await; - } -} diff --git a/clients/backbone-lib/src/host.rs b/clients/backbone-lib/src/host.rs deleted file mode 100644 index c78e228..0000000 --- a/clients/backbone-lib/src/host.rs +++ /dev/null @@ -1,211 +0,0 @@ -//! Background task for the host (game server) side of a session. - -use std::collections::HashSet; - -use ewebsock::{WsEvent, WsMessage, WsReceiver, WsSender}; -use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; -use web_time::{Duration, Instant}; - -use crate::platform::sleep_ms; -use crate::protocol::{ - ToServerCommand, parse_server_command, send_delta, send_disconnect, send_full_state, - send_kick, send_reset, -}; -use crate::session::{BackendMsg, SessionEvent}; -use crate::traits::{BackEndArchitecture, BackendCommand, SerializationCap, ViewStateUpdate}; - -struct Timer { - id: u16, - fire_at: Instant, -} - -pub(crate) async fn host_loop( - mut ws_sender: WsSender, - ws_receiver: WsReceiver, - mut action_rx: UnboundedReceiver>, - event_tx: UnboundedSender>, - rule_variation: u16, - host_state: Option>, -) where - A: SerializationCap, - D: SerializationCap + Clone, - VS: SerializationCap + Clone, - Backend: BackEndArchitecture, -{ - let mut backend = host_state - .as_deref() - .and_then(|b| Backend::from_bytes(rule_variation, b)) - .unwrap_or_else(|| Backend::new(rule_variation)); - backend.player_arrival(0); - - // Push initial state to UI immediately. - let initial = backend.get_view_state().clone(); - event_tx - .unbounded_send(SessionEvent::Update(ViewStateUpdate::Full(initial))) - .ok(); - - let mut timers: Vec = Vec::new(); - let mut cancelled_timers: HashSet = HashSet::new(); - let mut remote_player_count: u16 = 0; - - loop { - let mut client_joined = false; - - // 1. Drain local actions / detect session drop or disconnect request. - loop { - match action_rx.try_next() { - Ok(Some(BackendMsg::Action(action))) => { - backend.inform_rpc(0, action); - } - Ok(Some(BackendMsg::Disconnect)) => { - send_disconnect(&mut ws_sender, true); - event_tx - .unbounded_send(SessionEvent::Disconnected(None)) - .ok(); - return; - } - Ok(None) => { - // All senders dropped — session was dropped without calling disconnect(). - send_disconnect(&mut ws_sender, true); - return; - } - Err(_) => break, // Channel empty; nothing pending. - } - } - - // 2. Drain WebSocket events from the relay. - loop { - match ws_receiver.try_recv() { - Some(WsEvent::Message(WsMessage::Binary(data))) => { - match parse_server_command::(data) { - ToServerCommand::ClientJoin(id) => { - backend.player_arrival(id); - remote_player_count += 1; - client_joined = true; - } - ToServerCommand::ClientLeft(id) => { - backend.player_departure(id); - remote_player_count = remote_player_count.saturating_sub(1); - } - ToServerCommand::Rpc(id, payload) => { - backend.inform_rpc(id, payload); - } - ToServerCommand::Error(e) => { - event_tx - .unbounded_send(SessionEvent::Disconnected(Some(e))) - .ok(); - return; - } - } - } - Some(WsEvent::Closed) => { - event_tx - .unbounded_send(SessionEvent::Disconnected(Some( - "Connection closed".to_string(), - ))) - .ok(); - return; - } - Some(WsEvent::Error(e)) => { - event_tx - .unbounded_send(SessionEvent::Disconnected(Some(e))) - .ok(); - return; - } - Some(_) => continue, // Ignore Opened / text messages. - None => break, // No more events this iteration. - } - } - - // 3. Fire elapsed timers. - let now = Instant::now(); - let mut fired = Vec::new(); - timers.retain(|t| { - if t.fire_at <= now { - fired.push(t.id); - false - } else { - true - } - }); - for id in fired { - if !cancelled_timers.remove(&id) { - backend.timer_triggered(id); - } - } - - // 4. Drain and process backend commands. - let commands = backend.drain_commands(); - - if commands.is_empty() && !client_joined { - sleep_ms(2).await; - continue; - } - - let mut delta_batch: Vec = Vec::new(); - let mut reset = false; - - for cmd in commands { - match cmd { - BackendCommand::TerminateRoom => { - send_disconnect(&mut ws_sender, true); - event_tx - .unbounded_send(SessionEvent::Disconnected(None)) - .ok(); - return; - } - BackendCommand::SetTimer { timer_id, duration } => { - // Cancel any existing timer with the same id, then re-arm. - timers.retain(|t| t.id != timer_id); - cancelled_timers.remove(&timer_id); - timers.push(Timer { - id: timer_id, - fire_at: Instant::now() + Duration::from_secs_f32(duration), - }); - } - BackendCommand::CancelTimer { timer_id } => { - cancelled_timers.insert(timer_id); - } - BackendCommand::KickPlayer { player } => { - if remote_player_count > 0 { - send_kick(&mut ws_sender, player); - } - } - BackendCommand::ResetViewState => { - reset = true; - } - BackendCommand::Delta(d) => { - delta_batch.push(d); - } - } - } - - if reset { - // Reset supersedes all pending deltas: send fresh full state. - let state = backend.get_view_state().clone(); - if remote_player_count > 0 { - send_reset(&mut ws_sender, &state); - } - event_tx - .unbounded_send(SessionEvent::Update(ViewStateUpdate::Full(state))) - .ok(); - } else { - // Broadcast deltas, then notify local UI. - if remote_player_count > 0 && !delta_batch.is_empty() { - send_delta(&mut ws_sender, &delta_batch); - } - for d in delta_batch { - event_tx - .unbounded_send(SessionEvent::Update(ViewStateUpdate::Incremental(d))) - .ok(); - } - } - - // Send full state to clients that joined this iteration. - if client_joined { - send_full_state(&mut ws_sender, backend.get_view_state()); - } - - sleep_ms(2).await; - } -} diff --git a/clients/backbone-lib/src/lib.rs b/clients/backbone-lib/src/lib.rs deleted file mode 100644 index d67a96c..0000000 --- a/clients/backbone-lib/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod session; -pub mod traits; - -mod client; -mod host; -mod platform; -mod protocol; - -pub use session::{ConnectError, GameSession, RoomConfig, RoomRole, SessionEvent}; -pub use traits::{BackEndArchitecture, BackendCommand, SerializationCap, ViewStateUpdate}; diff --git a/clients/backbone-lib/src/platform.rs b/clients/backbone-lib/src/platform.rs deleted file mode 100644 index 92f2414..0000000 --- a/clients/backbone-lib/src/platform.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::future::Future; - -/// Spawns a background task. -/// - WASM: uses `wasm_bindgen_futures::spawn_local` (no Send required) -/// - Native: spawns an OS thread running `futures::executor::block_on` -#[cfg(target_arch = "wasm32")] -pub fn spawn_task(fut: F) -where - F: Future + 'static, -{ - wasm_bindgen_futures::spawn_local(fut); -} - -#[cfg(not(target_arch = "wasm32"))] -pub fn spawn_task(fut: F) -where - F: Future + Send + 'static, -{ - std::thread::spawn(move || { - futures::executor::block_on(fut); - }); -} - -/// Yields for approximately `ms` milliseconds. -/// - WASM: non-blocking yield via browser timer -/// - Native: blocks the current thread (safe on a dedicated background thread) -#[cfg(target_arch = "wasm32")] -pub async fn sleep_ms(ms: u32) { - gloo_timers::future::TimeoutFuture::new(ms).await; -} - -#[cfg(not(target_arch = "wasm32"))] -pub async fn sleep_ms(ms: u32) { - std::thread::sleep(std::time::Duration::from_millis(ms as u64)); -} - -/// Platform-agnostic bound for types that can be moved into a background task. -/// - WASM: only requires `'static` (single-threaded, no Send needed) -/// - Native: requires `Send + 'static` -#[cfg(target_arch = "wasm32")] -pub trait TaskBound: 'static {} -#[cfg(target_arch = "wasm32")] -impl TaskBound for T {} - -#[cfg(not(target_arch = "wasm32"))] -pub trait TaskBound: Send + 'static {} -#[cfg(not(target_arch = "wasm32"))] -impl TaskBound for T {} diff --git a/clients/backbone-lib/src/protocol.rs b/clients/backbone-lib/src/protocol.rs deleted file mode 100644 index 65f972a..0000000 --- a/clients/backbone-lib/src/protocol.rs +++ /dev/null @@ -1,159 +0,0 @@ -//! Wire protocol encoding/decoding helpers. -//! -//! Translates between raw WebSocket binary frames and typed Rust values using -//! postcard serialization and the message-type constants from the `protocol` crate. - -use crate::traits::{SerializationCap, ViewStateUpdate}; -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use ewebsock::{WsMessage, WsSender}; -use postcard::{from_bytes, take_from_bytes, to_stdvec}; -use protocol::{ - CLIENT_DISCONNECTS, CLIENT_DISCONNECTS_SELF, CLIENT_GETS_KICKED, CLIENT_ID_SIZE, DELTA_UPDATE, - FULL_UPDATE, HAND_SHAKE_RESPONSE, JoinRequest, NEW_CLIENT, RESET, SERVER_DISCONNECTS, - SERVER_ERROR, SERVER_RPC, -}; - -// --------------------------------------------------------------------------- -// Inbound command types (relay → host) -// --------------------------------------------------------------------------- - -pub enum ToServerCommand { - ClientJoin(u16), - ClientLeft(u16), - Rpc(u16, A), - Error(String), -} - -// --------------------------------------------------------------------------- -// Send helpers -// --------------------------------------------------------------------------- - -fn send_binary(sender: &mut WsSender, data: &[u8]) { - sender.send(WsMessage::Binary(data.to_vec())); -} - -pub fn send_join_request(sender: &mut WsSender, req: &JoinRequest) -> Result<(), String> { - let bytes = to_stdvec(req).map_err(|e| e.to_string())?; - send_binary(sender, &bytes); - Ok(()) -} - -pub fn send_rpc(sender: &mut WsSender, action: &A) { - let raw = to_stdvec(action).expect("Failed to serialize RPC"); - let mut buf = BytesMut::with_capacity(1 + raw.len()); - buf.put_u8(SERVER_RPC); - buf.put_slice(&raw); - send_binary(sender, &buf); -} - -pub fn send_delta(sender: &mut WsSender, deltas: &[D]) { - let serialized: Vec = deltas - .iter() - .flat_map(|d| to_stdvec(d).expect("Failed to serialize delta")) - .collect(); - let mut buf = BytesMut::with_capacity(1 + serialized.len()); - buf.put_u8(DELTA_UPDATE); - buf.put_slice(&serialized); - send_binary(sender, &buf); -} - -pub fn send_full_state(sender: &mut WsSender, state: &VS) { - let serialized = to_stdvec(state).expect("Failed to serialize full state"); - let mut buf = BytesMut::with_capacity(1 + serialized.len()); - buf.put_u8(FULL_UPDATE); - buf.put_slice(&serialized); - send_binary(sender, &buf); -} - -pub fn send_reset(sender: &mut WsSender, state: &VS) { - let serialized = to_stdvec(state).expect("Failed to serialize reset state"); - let mut buf = BytesMut::with_capacity(1 + serialized.len()); - buf.put_u8(RESET); - buf.put_slice(&serialized); - send_binary(sender, &buf); -} - -pub fn send_kick(sender: &mut WsSender, player_id: u16) { - let mut buf = BytesMut::with_capacity(1 + CLIENT_ID_SIZE); - buf.put_u8(CLIENT_GETS_KICKED); - buf.put_u16(player_id); - send_binary(sender, &buf); -} - -pub fn send_disconnect(sender: &mut WsSender, as_host: bool) { - let msg = if as_host { - SERVER_DISCONNECTS - } else { - CLIENT_DISCONNECTS_SELF - }; - send_binary(sender, &[msg]); -} - -// --------------------------------------------------------------------------- -// Receive / parse helpers -// --------------------------------------------------------------------------- - -/// Parses the relay's handshake response. -/// -/// Returns `(player_id, rule_variation, reconnect_token)`. -pub fn parse_handshake_response(data: Vec) -> Result<(u16, u16, u64), String> { - let mut bytes = Bytes::from(data); - let msg = bytes.get_u8(); - match msg { - SERVER_ERROR => Err(String::from_utf8_lossy(&bytes).to_string()), - HAND_SHAKE_RESPONSE => { - let player_id = bytes.get_u16(); - let rule_variation = bytes.get_u16(); - let token = bytes.get_u64(); - Ok((player_id, rule_variation, token)) - } - other => Err(format!("Unexpected handshake message id: {other}")), - } -} - -pub fn parse_server_command(data: Vec) -> ToServerCommand { - let mut bytes = Bytes::from(data); - let msg = bytes.get_u8(); - match msg { - SERVER_ERROR => ToServerCommand::Error(String::from_utf8_lossy(&bytes).to_string()), - NEW_CLIENT => ToServerCommand::ClientJoin(bytes.get_u16()), - CLIENT_DISCONNECTS => ToServerCommand::ClientLeft(bytes.get_u16()), - SERVER_RPC => { - let client_id = bytes.get_u16(); - let payload: A = - from_bytes(bytes.chunk()).expect("Failed to deserialize server RPC payload"); - ToServerCommand::Rpc(client_id, payload) - } - other => ToServerCommand::Error(format!("Unknown server message id: {other}")), - } -} - -pub fn parse_client_update( - data: Vec, -) -> Result>, String> -where - VS: SerializationCap, - D: SerializationCap, -{ - let mut bytes = Bytes::from(data); - let msg = bytes.get_u8(); - match msg { - SERVER_ERROR => Err(String::from_utf8_lossy(&bytes).to_string()), - DELTA_UPDATE => { - let mut result = Vec::new(); - let mut remaining: &[u8] = &bytes; - while !remaining.is_empty() { - let (delta, rest): (D, &[u8]) = - take_from_bytes(remaining).map_err(|e| e.to_string())?; - remaining = rest; - result.push(ViewStateUpdate::Incremental(delta)); - } - Ok(result) - } - FULL_UPDATE | RESET => { - let state: VS = from_bytes(&bytes).map_err(|e| e.to_string())?; - Ok(vec![ViewStateUpdate::Full(state)]) - } - other => Err(format!("Unknown client message id: {other}")), - } -} diff --git a/clients/backbone-lib/src/session.rs b/clients/backbone-lib/src/session.rs deleted file mode 100644 index 24314f7..0000000 --- a/clients/backbone-lib/src/session.rs +++ /dev/null @@ -1,266 +0,0 @@ -//! The public-facing session API. -//! -//! # Usage -//! -//! ```ignore -//! // Connect (async, returns after handshake completes) -//! let mut session: GameSession = -//! GameSession::connect::(RoomConfig { -//! relay_url: "ws://localhost:8080/ws".to_string(), -//! game_id: "my-game".to_string(), -//! room_id: "room-42".to_string(), -//! rule_variation: 0, -//! role: RoomRole::Create, -//! reconnect_token: None, -//! }) -//! .await?; -//! -//! // In a loop (e.g. Dioxus coroutine with futures::select!): -//! loop { -//! futures::select! { -//! cmd = ui_rx.next().fuse() => session.send_action(cmd), -//! event = session.next_event().fuse() => match event { -//! Some(SessionEvent::Update(u)) => view_state.apply(u), -//! Some(SessionEvent::Disconnected(reason)) | None => break, -//! } -//! } -//! } -//! ``` - -use ewebsock::{WsEvent, WsMessage}; -use futures::StreamExt; -use futures::channel::mpsc::{self, UnboundedReceiver, UnboundedSender}; -use protocol::JoinRequest; - -use crate::client::client_loop; -use crate::host::host_loop; -use crate::platform::{TaskBound, sleep_ms, spawn_task}; -use crate::protocol::{parse_handshake_response, send_join_request}; -use crate::traits::{BackEndArchitecture, SerializationCap, ViewStateUpdate}; - -// --------------------------------------------------------------------------- -// Public configuration types -// --------------------------------------------------------------------------- - -/// Whether to create a new room (host) or join an existing one (client). -pub enum RoomRole { - Create, - Join, -} - -/// Configuration required to connect to a game session. -pub struct RoomConfig { - /// WebSocket URL of the relay server (e.g. `"ws://localhost:8080/ws"`). - pub relay_url: String, - /// Game identifier registered on the relay (e.g. `"tic-tac-toe"`). - pub game_id: String, - /// Room identifier shared between host and clients. - pub room_id: String, - /// Game mode/variant. Only used when `role` is `Create`. - pub rule_variation: u16, - pub role: RoomRole, - /// If `Some`, attempt to reconnect to an existing session instead of creating/joining fresh. - /// The value is the token returned by a previous successful handshake. - pub reconnect_token: Option, - /// Serialized backend state for host reconnect. - /// - /// Produced by the app layer (e.g. `serde_json::to_vec(&view_state)`) and stored in - /// localStorage. Passed to [`BackEndArchitecture::from_bytes`] when the host - /// reconnects so the game can resume from the last known state. - /// Ignored for non-host reconnects and normal connections. - pub host_state: Option>, -} - -/// Error returned by [`GameSession::connect`]. -#[derive(Debug)] -pub enum ConnectError { - WebSocket(String), - Handshake(String), -} - -impl std::fmt::Display for ConnectError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ConnectError::WebSocket(e) => write!(f, "WebSocket error: {e}"), - ConnectError::Handshake(e) => write!(f, "Handshake error: {e}"), - } - } -} - -// --------------------------------------------------------------------------- -// Internal message type (UI → background task) -// --------------------------------------------------------------------------- - -pub(crate) enum BackendMsg { - Action(A), - Disconnect, -} - -// --------------------------------------------------------------------------- -// Session event (background task → UI) -// --------------------------------------------------------------------------- - -/// Events emitted by the session to the UI. -pub enum SessionEvent { - /// A state update arrived from the host backend. - Update(ViewStateUpdate), - /// The session ended. `None` = clean disconnect, `Some(reason)` = error. - Disconnected(Option), -} - -// --------------------------------------------------------------------------- -// GameSession -// --------------------------------------------------------------------------- - -/// A connected game session. -/// -/// Created by [`GameSession::connect`]. Holds channels to the background task -/// that owns the WebSocket connection and (on host) the game backend. -pub struct GameSession { - /// The player ID assigned by the relay server. Always `0` for the host. - pub player_id: u16, - /// The game mode/variant selected by the host. - pub rule_variation: u16, - /// `true` if this client is hosting the game (runs the backend). - pub is_host: bool, - /// Token to persist in localStorage for reconnect on page refresh. - /// Only meaningful for non-host players (player_id > 0). - pub reconnect_token: u64, - action_tx: UnboundedSender>, - event_rx: UnboundedReceiver>, -} - -impl GameSession -where - A: SerializationCap + TaskBound, - D: SerializationCap + Clone + TaskBound, - VS: SerializationCap + Clone + TaskBound, -{ - /// Connects to the relay server and performs the handshake. - /// - /// Returns after the relay confirms the player ID and rule variation. - /// Spawns a background task that drives the WebSocket connection for the - /// lifetime of the session. - /// - /// # Errors - /// Returns `Err` if the WebSocket cannot be opened or the handshake fails. - pub async fn connect(config: RoomConfig) -> Result - where - Backend: BackEndArchitecture + TaskBound, - { - let create_room = matches!(config.role, RoomRole::Create); - - // 1. Open WebSocket. - let (mut ws_sender, ws_receiver) = - ewebsock::connect(&config.relay_url, ewebsock::Options::default()) - .map_err(|e| ConnectError::WebSocket(e.to_string()))?; - - // 2. Wait for the Opened event (WASM WebSocket is async). - loop { - match ws_receiver.try_recv() { - Some(WsEvent::Opened) => break, - Some(WsEvent::Error(e)) => return Err(ConnectError::WebSocket(e)), - Some(WsEvent::Closed) => { - return Err(ConnectError::WebSocket("Connection closed".to_string())); - } - Some(_) => continue, - None => sleep_ms(1).await, - } - } - - // 3. Send the join request. - let req = JoinRequest { - game_id: config.game_id, - room_id: config.room_id, - rule_variation: config.rule_variation, - create_room, - reconnect_token: config.reconnect_token, - }; - send_join_request(&mut ws_sender, &req).map_err(ConnectError::Handshake)?; - - // 4. Wait for the handshake response. - let (player_id, rule_variation, reconnect_token) = loop { - match ws_receiver.try_recv() { - Some(WsEvent::Message(WsMessage::Binary(data))) => { - break parse_handshake_response(data).map_err(ConnectError::Handshake)?; - } - Some(WsEvent::Error(e)) => return Err(ConnectError::Handshake(e)), - Some(WsEvent::Closed) => { - // The relay may have sent a binary error frame just before - // closing. ewebsock can deliver Closed before that frame, - // so drain one more message to catch it. - if let Some(WsEvent::Message(WsMessage::Binary(data))) = - ws_receiver.try_recv() - { - break parse_handshake_response(data) - .map_err(ConnectError::Handshake)?; - } - return Err(ConnectError::Handshake( - "Connection closed during handshake".to_string(), - )); - } - Some(_) => continue, - None => sleep_ms(1).await, - } - }; - - // The relay assigns player_id == 0 exclusively to the host. - let is_host = player_id == 0; - - // 5. Set up channels between the UI and the background task. - let (action_tx, action_rx) = mpsc::unbounded::>(); - let (event_tx, event_rx) = mpsc::unbounded::>(); - - // 6. Spawn the background event loop. - if is_host { - spawn_task(host_loop::( - ws_sender, - ws_receiver, - action_rx, - event_tx, - rule_variation, - config.host_state, - )); - } else { - spawn_task(client_loop::( - ws_sender, - ws_receiver, - action_rx, - event_tx, - )); - } - - Ok(GameSession { - player_id, - rule_variation, - is_host, - reconnect_token, - action_tx, - event_rx, - }) - } - - /// Sends a game action to the backend (fire-and-forget). - pub fn send_action(&self, action: A) { - self.action_tx - .unbounded_send(BackendMsg::Action(action)) - .ok(); - } - - /// Awaits the next session event. - /// - /// Returns `None` if the background task has exited (i.e. the session is - /// over). Normal termination arrives as `Some(SessionEvent::Disconnected(_))` - /// before the channel closes. - pub async fn next_event(&mut self) -> Option> { - self.event_rx.next().await - } - - /// Signals the background task to send a graceful disconnect message and - /// shut down. Consumes the session. - pub fn disconnect(self) { - self.action_tx - .unbounded_send(BackendMsg::Disconnect) - .ok(); - } -} diff --git a/clients/backbone-lib/src/traits.rs b/clients/backbone-lib/src/traits.rs deleted file mode 100644 index 1ec50f7..0000000 --- a/clients/backbone-lib/src/traits.rs +++ /dev/null @@ -1,97 +0,0 @@ -use serde::Serialize; -use serde::de::DeserializeOwned; - -/// Marker trait for types that can be serialized with postcard. -pub trait SerializationCap: Serialize + DeserializeOwned {} -impl SerializationCap for T where T: Serialize + DeserializeOwned {} - -/// State updates delivered to the frontend for rendering. -/// -/// - [`Full`](Self::Full): Immediately set all visual state, no animation. -/// - [`Incremental`](Self::Incremental): Apply with animation/transition. -pub enum ViewStateUpdate { - /// Complete game state snapshot. Received on join or after a reset. - Full(ViewState), - /// Incremental state change for animated transitions. - Incremental(DeltaInformation), -} - -/// Commands emitted by the game backend to control the session. -pub enum BackendCommand -where - DeltaInformation: SerializationCap, -{ - /// Incremental state change to be broadcast to all frontends. - Delta(DeltaInformation), - - /// Signals a complete reset: discard queued deltas, broadcast fresh full state. - ResetViewState, - - /// Forcibly removes a player from the session. - KickPlayer { player: u16 }, - - /// Schedules a callback after `duration` seconds. Overwrites any existing - /// timer with the same `timer_id`. - SetTimer { timer_id: u16, duration: f32 }, - - /// Cancels a previously scheduled timer. No-op if already fired or not set. - CancelTimer { timer_id: u16 }, - - /// Shuts down the entire room and disconnects all players. - TerminateRoom, -} - -/// The contract for game-specific server logic. -/// -/// Implement this on the host side. The session calls these methods in response -/// to network events and drives `drain_commands` to collect outbound messages. -/// -/// # Type Parameters -/// * `ServerRpcPayload` — Actions sent by players (e.g. `PlacePiece { x, y }`) -/// * `DeltaInformation` — Incremental state changes for animations -/// * `ViewState` — Complete game snapshot for syncing new clients -pub trait BackEndArchitecture -where - ServerRpcPayload: SerializationCap, - DeltaInformation: SerializationCap, - ViewState: SerializationCap + Clone, -{ - /// Creates a new game instance. `rule_variation` selects the game mode. - fn new(rule_variation: u16) -> Self; - - /// Attempt to restore a previously running game from serialized bytes. - /// - /// Called when the host reconnects after a page refresh. The bytes are the - /// game-specific snapshot produced by the app layer (via `serde_json` or - /// similar) and stored in localStorage. - /// - /// Return `None` if restoration is not supported or the bytes are invalid — - /// the caller falls back to `new(rule_variation)`. - fn from_bytes(_rule_variation: u16, _bytes: &[u8]) -> Option - where - Self: Sized, - { - None - } - - /// Called when a player connects. Player will receive a full state snapshot - /// automatically after this returns. - fn player_arrival(&mut self, player: u16); - - /// Called when a player disconnects. - fn player_departure(&mut self, player: u16); - - /// Called when a player sends a game action. - fn inform_rpc(&mut self, player: u16, payload: ServerRpcPayload); - - /// Called when a previously scheduled timer fires. - fn timer_triggered(&mut self, timer_id: u16); - - /// Returns the complete current game state. - fn get_view_state(&self) -> &ViewState; - - /// Collects and clears all pending commands since the last drain. - /// - /// Implement with `std::mem::take(&mut self.command_list)`. - fn drain_commands(&mut self) -> Vec>; -} diff --git a/clients/web-game/Trunk.toml b/clients/web-game/Trunk.toml deleted file mode 100644 index bae5297..0000000 --- a/clients/web-game/Trunk.toml +++ /dev/null @@ -1,2 +0,0 @@ -[serve] -port = 9091 diff --git a/clients/web-game/src/sound.rs b/clients/web-game/src/sound.rs deleted file mode 100644 index 5637ccd..0000000 --- a/clients/web-game/src/sound.rs +++ /dev/null @@ -1,182 +0,0 @@ -//! Synthesised sound effects using the Web Audio API. -//! -//! All public functions are no-ops on non-WASM targets so callers need no -//! `#[cfg]` guards themselves. - -#[cfg(target_arch = "wasm32")] -mod inner { - use std::cell::RefCell; - use web_sys::{AudioContext, OscillatorType}; - - thread_local! { - static CTX: RefCell> = const { RefCell::new(None) }; - } - - fn with_ctx(f: F) { - CTX.with(|cell| { - let mut opt = cell.borrow_mut(); - if opt.is_none() { - *opt = AudioContext::new().ok(); - } - if let Some(ctx) = opt.as_ref() { - f(ctx); - } - }); - } - - /// Schedule a single oscillator tone with an exponential gain decay. - /// - /// - `start_offset`: seconds from `ctx.current_time()` when the tone starts - /// - `duration`: how long (in seconds) until gain reaches ~0 - fn play_tone( - ctx: &AudioContext, - freq: f32, - gain: f32, - duration: f64, - start_offset: f64, - wave: OscillatorType, - ) { - let t0 = ctx.current_time() + start_offset; - let t1 = t0 + duration; - - let Ok(osc) = ctx.create_oscillator() else { - return; - }; - let Ok(gain_node) = ctx.create_gain() else { - return; - }; - - osc.set_type(wave); - osc.frequency().set_value(freq); - - let gain_param = gain_node.gain(); - let _ = gain_param.set_value_at_time(gain, t0); - // exponential_ramp requires a positive target; 0.001 is inaudible - let _ = gain_param.exponential_ramp_to_value_at_time(0.001, t1); - - let dest = ctx.destination(); - let _ = osc.connect_with_audio_node(&gain_node); - let _ = gain_node.connect_with_audio_node(&dest); - - let _ = osc.start_with_when(t0); - let _ = osc.stop_with_when(t1); - } - - /// Short wooden clack: sine fundamental + triangle body resonance, ~80 ms. - pub fn play_checker_move() { - with_ctx(|ctx| { - // Sine at 300 Hz for the clean attack click - play_tone(ctx, 300.0, 0.55, 0.080, 0.000, OscillatorType::Sine); - // Triangle at 150 Hz for the woody body resonance - play_tone(ctx, 150.0, 0.35, 0.070, 0.005, OscillatorType::Triangle); - // Sub at 80 Hz for weight - play_tone(ctx, 80.0, 0.20, 0.060, 0.008, OscillatorType::Triangle); - }); - } - - /// Cinematic dice roll: ~500 ms of rolling texture + 5 impact transients. - /// - /// Two layers: - /// - A dense series of detuned sawtooth bursts that thin out over time, - /// modelling the continuous scrape/rattle of dice tumbling. - /// - Five percussive impacts (square clicks + triangle thuds) whose - /// inter-arrival gap shrinks as the dice decelerate and settle. - pub fn play_dice_roll_cinematic() { - with_ctx(|ctx| { - // ── Continuous rolling texture ───────────────────────────────── - // 16 steps over 440 ms; each step is two detuned sawtooth waves - // (the interference between them produces a noise-like texture). - // Gain fades by ~55 % from first to last step. - const N: u32 = 16; - for i in 0..N { - let t = i as f64 * 0.028; - let g = 0.017 * (1.0 - i as f32 / N as f32 * 0.55); - // Quasi-random frequencies so each step sounds different. - let f1 = 310.0 + (i as f32 * 29.3 % 280.0); - let f2 = 480.0 + (i as f32 * 43.7 % 220.0); - play_tone(ctx, f1, g, 0.028, t, OscillatorType::Sawtooth); - play_tone(ctx, f2, g * 0.70, 0.028, t, OscillatorType::Sawtooth); - } - - // ── Impact transients ────────────────────────────────────────── - // Gaps narrow toward the end (0.13 → 0.11 → 0.10 → 0.08 s), - // mimicking dice decelerating and settling. - let impacts: &[(f64, f32)] = &[(0.00, 1.00), (0.13, 0.8), (0.24, 0.54), (0.34, 0.30)]; - for &(t_off, amp) in impacts { - // Hard click: bright square partials → percussive attack - for &freq in &[700.0f32, 1_050.0, 1_500.0] { - play_tone(ctx, freq, amp * 0.03, 0.022, t_off, OscillatorType::Square); - } - // Woody body thud: two low triangle partials - play_tone( - ctx, - 130.0, - amp * 0.05, - 0.070, - t_off, - OscillatorType::Triangle, - ); - play_tone( - ctx, - 68.0, - amp * 0.07, - 0.090, - t_off, - OscillatorType::Triangle, - ); - } - }); - } - - /// Play the pre-recorded dice-roll MP3 asset. - pub fn play_dice_roll() { - if let Ok(audio) = web_sys::HtmlAudioElement::new_with_src("/diceroll.mp3") { - audio.set_volume(0.2); - let _ = audio.play(); - } - } - - /// Ascending three-note chime (C5 – E5 – G5). - pub fn play_points_scored() { - with_ctx(|ctx| { - let notes: [(f32, f64); 3] = [(523.25, 0.0), (659.25, 0.14), (783.99, 0.28)]; - for (freq, offset) in notes { - play_tone(ctx, freq, 0.28, 0.30, offset, OscillatorType::Sine); - } - }); - } - - /// Triumphant four-note fanfare (C5 – E5 – G5 – C6). - pub fn play_hole_scored() { - with_ctx(|ctx| { - let notes: [(f32, f64, f64); 4] = [ - (523.25, 0.0, 0.35), - (659.25, 0.17, 0.35), - (783.99, 0.34, 0.35), - (1046.5, 0.51, 0.55), - ]; - for (freq, offset, dur) in notes { - play_tone(ctx, freq, 0.32, dur, offset, OscillatorType::Sine); - } - }); - } -} - -// ── Public API: WASM delegates to `inner`, other targets are no-ops ─────────── - -#[cfg(target_arch = "wasm32")] -pub use inner::{ - play_checker_move, play_dice_roll, play_dice_roll_cinematic, play_hole_scored, - play_points_scored, -}; - -#[cfg(not(target_arch = "wasm32"))] -pub fn play_checker_move() {} -#[cfg(not(target_arch = "wasm32"))] -pub fn play_dice_roll() {} -#[cfg(not(target_arch = "wasm32"))] -pub fn play_dice_roll_cinematic() {} -#[cfg(not(target_arch = "wasm32"))] -pub fn play_points_scored() {} -#[cfg(not(target_arch = "wasm32"))] -pub fn play_hole_scored() {} diff --git a/clients/web-user-portal/Cargo.toml b/clients/web-user-portal/Cargo.toml deleted file mode 100644 index 6afa767..0000000 --- a/clients/web-user-portal/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "web-user-portal" -version = "0.1.0" -edition = "2024" - -[dependencies] -leptos = { version = "0.7", features = ["csr"] } -leptos_router = { version = "0.7" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1" - -[target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen = "0.2" -wasm-bindgen-futures = "0.4" -gloo-net = { version = "0.5", features = ["http"] } -js-sys = "0.3" -web-sys = { version = "0.3", features = ["RequestCredentials"] } diff --git a/clients/web-user-portal/assets/style.css b/clients/web-user-portal/assets/style.css deleted file mode 100644 index 3e7462a..0000000 --- a/clients/web-user-portal/assets/style.css +++ /dev/null @@ -1,103 +0,0 @@ -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } - -body { - font-family: system-ui, sans-serif; - background: #f5f5f5; - color: #1a1a1a; - min-height: 100vh; -} - -nav { - background: #1a1a2e; - color: #fff; - padding: 0.75rem 1.5rem; - display: flex; - align-items: center; - gap: 1.5rem; -} -nav a { color: #ccc; text-decoration: none; } -nav a:hover { color: #fff; } -nav .brand { font-weight: 700; font-size: 1.1rem; color: #fff; } -nav .spacer { flex: 1; } - -main { max-width: 800px; margin: 2rem auto; padding: 0 1rem; } - -h1 { font-size: 1.6rem; margin-bottom: 1rem; } -h2 { font-size: 1.2rem; margin-bottom: 0.75rem; } - -.card { - background: #fff; - border-radius: 8px; - padding: 1.5rem; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); - margin-bottom: 1.5rem; -} - -.tabs { display: flex; gap: 0; margin-bottom: 1.5rem; } -.tab-btn { - padding: 0.5rem 1.25rem; - border: 1px solid #ddd; - background: #f5f5f5; - cursor: pointer; - font-size: 0.95rem; -} -.tab-btn:first-child { border-radius: 6px 0 0 6px; } -.tab-btn:last-child { border-radius: 0 6px 6px 0; border-left: none; } -.tab-btn.active { background: #1a1a2e; color: #fff; border-color: #1a1a2e; } - -label { display: block; font-size: 0.85rem; margin-bottom: 0.25rem; color: #555; } -input[type=text], input[type=email], input[type=password] { - width: 100%; - padding: 0.5rem 0.75rem; - border: 1px solid #ddd; - border-radius: 5px; - font-size: 0.95rem; - margin-bottom: 0.75rem; -} -input:focus { outline: none; border-color: #1a1a2e; } - -button[type=submit], .btn { - padding: 0.5rem 1.25rem; - background: #1a1a2e; - color: #fff; - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 0.95rem; -} -button[type=submit]:hover, .btn:hover { background: #2d2d5e; } -button[type=submit]:disabled { opacity: 0.6; cursor: not-allowed; } - -.error { color: #c0392b; font-size: 0.875rem; margin-top: 0.5rem; } -.success { color: #27ae60; font-size: 0.875rem; margin-top: 0.5rem; } - -.stats-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 1rem; - margin-bottom: 1.5rem; -} -.stat-box { - background: #fff; - border-radius: 8px; - padding: 1rem; - text-align: center; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); -} -.stat-box .value { font-size: 2rem; font-weight: 700; } -.stat-box .label { font-size: 0.8rem; color: #777; margin-top: 0.25rem; } - -table { width: 100%; border-collapse: collapse; font-size: 0.9rem; } -th { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 2px solid #eee; color: #555; } -td { padding: 0.5rem 0.75rem; border-bottom: 1px solid #f0f0f0; } -tr:last-child td { border-bottom: none; } -tr:hover td { background: #fafafa; } -a { color: #2c5cc5; text-decoration: none; } -a:hover { text-decoration: underline; } - -.outcome-win { color: #27ae60; font-weight: 600; } -.outcome-loss { color: #c0392b; font-weight: 600; } -.outcome-draw { color: #e67e22; font-weight: 600; } - -.loading { color: #777; padding: 1rem 0; } -.empty { color: #aaa; font-style: italic; padding: 1rem 0; } diff --git a/clients/web-user-portal/index.html b/clients/web-user-portal/index.html deleted file mode 100644 index 135091c..0000000 --- a/clients/web-user-portal/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Player Portal - - - - - diff --git a/clients/web-user-portal/src/api.rs b/clients/web-user-portal/src/api.rs deleted file mode 100644 index b6dced9..0000000 --- a/clients/web-user-portal/src/api.rs +++ /dev/null @@ -1,191 +0,0 @@ -use serde::{Deserialize, Serialize}; - -// In debug builds trunk serves on 9092 while the relay is on 8080 — use full URL. -// In release builds the portal is served by the relay itself — use relative paths. -#[cfg(debug_assertions)] -const BASE: &str = "http://localhost:8080"; -#[cfg(not(debug_assertions))] -const BASE: &str = ""; - -fn url(path: &str) -> String { - format!("{BASE}{path}") -} - -// ── Response types ──────────────────────────────────────────────────────────── - -#[derive(Clone, Debug, Deserialize)] -pub struct MeResponse { - pub id: i64, - pub username: String, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct UserProfile { - pub id: i64, - pub username: String, - pub created_at: i64, - pub total_games: i64, - pub wins: i64, - pub losses: i64, - pub draws: i64, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct GameSummary { - pub id: i64, - pub game_id: String, - pub room_code: String, - pub started_at: i64, - pub ended_at: Option, - pub result: Option, - pub outcome: Option, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct GamesResponse { - pub games: Vec, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct Participant { - pub player_id: i64, - pub outcome: Option, - pub username: Option, -} - -#[derive(Clone, Debug, Deserialize)] -pub struct GameDetail { - pub id: i64, - pub game_id: String, - pub room_code: String, - pub started_at: i64, - pub ended_at: Option, - pub result: Option, - pub participants: Vec, -} - -// ── Request bodies ──────────────────────────────────────────────────────────── - -#[derive(Serialize)] -pub struct RegisterBody<'a> { - pub username: &'a str, - pub email: &'a str, - pub password: &'a str, -} - -#[derive(Serialize)] -pub struct LoginBody<'a> { - pub username: &'a str, - pub password: &'a str, -} - -// ── Fetch helpers ───────────────────────────────────────────────────────────── - -pub async fn get_me() -> Result { - let resp = gloo_net::http::Request::get(&url("/auth/me")) - .credentials(web_sys::RequestCredentials::Include) - .send() - .await - .map_err(|e| e.to_string())?; - if resp.status() == 200 { - resp.json::().await.map_err(|e| e.to_string()) - } else { - Err(format!("status {}", resp.status())) - } -} - -pub async fn post_login(username: &str, password: &str) -> Result { - let body = LoginBody { username, password }; - let resp = gloo_net::http::Request::post(&url("/auth/login")) - .credentials(web_sys::RequestCredentials::Include) - .json(&body) - .map_err(|e| e.to_string())? - .send() - .await - .map_err(|e| e.to_string())?; - if resp.status() == 200 { - resp.json::().await.map_err(|e| e.to_string()) - } else { - let text = resp.text().await.unwrap_or_default(); - Err(text) - } -} - -pub async fn post_register(username: &str, email: &str, password: &str) -> Result { - let body = RegisterBody { username, email, password }; - let resp = gloo_net::http::Request::post(&url("/auth/register")) - .credentials(web_sys::RequestCredentials::Include) - .json(&body) - .map_err(|e| e.to_string())? - .send() - .await - .map_err(|e| e.to_string())?; - if resp.status() == 201 { - resp.json::().await.map_err(|e| e.to_string()) - } else { - let text = resp.text().await.unwrap_or_default(); - Err(text) - } -} - -pub async fn post_logout() -> Result<(), String> { - let resp = gloo_net::http::Request::post(&url("/auth/logout")) - .credentials(web_sys::RequestCredentials::Include) - .send() - .await - .map_err(|e| e.to_string())?; - if resp.status() == 204 { - Ok(()) - } else { - Err(format!("status {}", resp.status())) - } -} - -pub async fn get_user_profile(username: &str) -> Result { - let resp = gloo_net::http::Request::get(&url(&format!("/users/{username}"))) - .credentials(web_sys::RequestCredentials::Include) - .send() - .await - .map_err(|e| e.to_string())?; - if resp.status() == 200 { - resp.json::().await.map_err(|e| e.to_string()) - } else { - Err(format!("status {}", resp.status())) - } -} - -pub async fn get_user_games(username: &str, page: i64) -> Result { - let resp = gloo_net::http::Request::get(&url(&format!("/users/{username}/games?page={page}&per_page=20"))) - .credentials(web_sys::RequestCredentials::Include) - .send() - .await - .map_err(|e| e.to_string())?; - if resp.status() == 200 { - resp.json::().await.map_err(|e| e.to_string()) - } else { - Err(format!("status {}", resp.status())) - } -} - -pub async fn get_game_detail(id: i64) -> Result { - let resp = gloo_net::http::Request::get(&url(&format!("/games/{id}"))) - .credentials(web_sys::RequestCredentials::Include) - .send() - .await - .map_err(|e| e.to_string())?; - if resp.status() == 200 { - resp.json::().await.map_err(|e| e.to_string()) - } else { - Err(format!("status {}", resp.status())) - } -} - -// ── Utilities ───────────────────────────────────────────────────────────────── - -pub fn format_ts(ts: i64) -> String { - let ms = (ts * 1000) as f64; - let date = js_sys::Date::new(&wasm_bindgen::JsValue::from_f64(ms)); - date.to_locale_string("en-GB", &wasm_bindgen::JsValue::UNDEFINED) - .as_string() - .unwrap_or_default() -} diff --git a/clients/web-user-portal/src/app.rs b/clients/web-user-portal/src/app.rs deleted file mode 100644 index 92a121a..0000000 --- a/clients/web-user-portal/src/app.rs +++ /dev/null @@ -1,67 +0,0 @@ -use leptos::prelude::*; -use leptos_router::{components::{Route, Router, Routes, A}, path}; - -use crate::api::{self, MeResponse}; -use crate::pages::{home::HomePage, profile::ProfilePage, game::GamePage}; - -#[derive(Clone, Debug)] -pub struct AuthState { - pub user: RwSignal>, -} - -#[component] -pub fn App() -> impl IntoView { - let user = RwSignal::new(None::); - provide_context(AuthState { user }); - - // Probe session on load. - let auth = use_context::().unwrap(); - let _ = LocalResource::new(move || async move { - if let Ok(me) = api::get_me().await { - auth.user.set(Some(me)); - } - }); - - view! { - -