diff --git a/.gitignore b/.gitignore index a1157c1..f06eefd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,14 @@ # Generated by Cargo # will have compiled files and executables /target/ +/target # These are backup files generated by rustfmt **/*.rs.bk + .devenv* devenv.local.nix - -# Added by cargo - -/target +# generated by samply rust profiler +profile.json +bot/models diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 120000 index 3cecf95..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1 +0,0 @@ -/nix/store/qp1vc91wm7s7vz337hpv6zimpc4gcvf0-pre-commit-config.json \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 7ff8537..d0f6183 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,90 +1,21 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 - -[[package]] -name = "ab_glyph" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1061f3ff92c2f65800df1f12fc7b4ff44ee14783104187dd04dfee6f11b0fd2" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" - -[[package]] -name = "accesskit" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76eb1adf08c5bcaa8490b9851fd53cca27fa9880076f178ea9d29f05196728a8" - -[[package]] -name = "accesskit_consumer" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04bb4d9e4772fe0d47df57d0d5dbe5d85dd05e2f37ae1ddb6b105e76be58fb00" -dependencies = [ - "accesskit", -] - -[[package]] -name = "accesskit_macos" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134d0acf6acb667c89d3332999b1a5df4edbc8d6113910f392ebb73f2b03bb56" -dependencies = [ - "accesskit", - "accesskit_consumer", - "objc2", - "once_cell", -] - -[[package]] -name = "accesskit_windows" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eac0a7f2d7cd7a93b938af401d3d8e8b7094217989a7c25c55a953023436e31" -dependencies = [ - "accesskit", - "accesskit_consumer", - "arrayvec", - "once_cell", - "paste", - "windows 0.48.0", -] - -[[package]] -name = "accesskit_winit" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "825d23acee1bd6d25cbaa3ca6ed6e73faf24122a774ec33d52c5c86c6ab423c0" -dependencies = [ - "accesskit", - "accesskit_macos", - "accesskit_windows", - "winit", -] +version = 4 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -97,13 +28,23 @@ dependencies = [ ] [[package]] -name = "ahash" -version = "0.8.6" +name = "aes" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom", "once_cell", "version_check", "zerocopy", @@ -118,64 +59,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "allocator-api2" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" -[[package]] -name = "alsa" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2562ad8dcf0f789f65c6fdaad8a8a9708ed6b488e649da28c01656ad66b8b47" -dependencies = [ - "alsa-sys", - "bitflags 1.3.2", - "libc", - "nix 0.24.3", -] - -[[package]] -name = "alsa-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "android-activity" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64529721f27c2314ced0890ce45e469574a73e5e6fdd6e9da1860eb29285f5e0" -dependencies = [ - "android-properties", - "bitflags 1.3.2", - "cc", - "jni-sys", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "num_enum 0.6.1", -] - -[[package]] -name = "android-properties" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" - -[[package]] -name = "android_log-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -186,10 +84,59 @@ dependencies = [ ] [[package]] -name = "anyhow" -version = "1.0.75" +name = "anstream" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" [[package]] name = "approx" @@ -201,59 +148,75 @@ dependencies = [ ] [[package]] -name = "arrayvec" -version = "0.7.4" +name = "arbitrary" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "arimaa_engine_step" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c6726d7896a539a62e157b05fa4b7308ffb7872f2b4a2a592d5adb19837861" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "log", + "regex", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "ash" -version = "0.37.3+1.3.251" +version = "0.38.0+1.3.281" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" dependencies = [ - "libloading 0.7.4", + "libloading", ] [[package]] name = "async-channel" -version = "1.9.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", - "event-listener", + "event-listener-strategy", "futures-core", + "pin-project-lite", ] [[package]] -name = "async-executor" -version = "1.6.0" +name = "atomic_float" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0c4a4f319e45986f347ee47fef8bf5e81c9abc3f6f58dc2391439f30df65f0" -dependencies = [ - "async-lock", - "async-task", - "concurrent-queue", - "fastrand 2.0.1", - "futures-lite", - "slab", -] - -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener", -] - -[[package]] -name = "async-task" -version = "4.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" +checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" [[package]] name = "autocfg" @@ -262,26 +225,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] -name = "backtrace" -version = "0.3.69" +name = "av1-grain" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +dependencies = [ + "anyhow", + "arrayvec 0.7.6", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" +dependencies = [ + "arrayvec 0.7.6", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.7" @@ -289,741 +269,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] -name = "bevy" -version = "0.11.3" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c6d3ec4f89e85294dc97334c5b271ddc301fdf67ac9bb994fe44d9273e6ed7" -dependencies = [ - "bevy_internal", -] +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "bevy_a11y" -version = "0.11.3" +name = "base64ct" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "132c9e35a77c5395951f6d25fa2c52ee92296353426df4f961e60f3ff47e2e42" -dependencies = [ - "accesskit", - "bevy_app", - "bevy_derive", - "bevy_ecs", -] - -[[package]] -name = "bevy_animation" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f44eae3f1c35a87e38ad146f72317f19ce7616dad8bbdfb88ee752c1282d28c5" -dependencies = [ - "bevy_app", - "bevy_asset", - "bevy_core", - "bevy_ecs", - "bevy_hierarchy", - "bevy_math", - "bevy_reflect", - "bevy_render", - "bevy_time", - "bevy_transform", - "bevy_utils", -] - -[[package]] -name = "bevy_app" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f557a7d59e1e16892d7544fc37316506ee598cb5310ef0365125a30783c11531" -dependencies = [ - "bevy_derive", - "bevy_ecs", - "bevy_reflect", - "bevy_tasks", - "bevy_utils", - "downcast-rs", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "bevy_asset" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9714af523da4cdf58c42a317e5ed40349708ad954a18533991fd64c8ae0a6f68" -dependencies = [ - "anyhow", - "async-channel", - "bevy_app", - "bevy_diagnostic", - "bevy_ecs", - "bevy_log", - "bevy_reflect", - "bevy_tasks", - "bevy_utils", - "bevy_winit", - "crossbeam-channel", - "downcast-rs", - "fastrand 1.9.0", - "js-sys", - "notify", - "parking_lot", - "serde", - "thiserror", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "bevy_audio" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de308bd63a2f7a0b77ffeb7cf00cc185ec01393c5db2091fe03964f97152749" -dependencies = [ - "anyhow", - "bevy_app", - "bevy_asset", - "bevy_derive", - "bevy_ecs", - "bevy_math", - "bevy_reflect", - "bevy_transform", - "bevy_utils", - "oboe", - "parking_lot", - "rodio", -] - -[[package]] -name = "bevy_core" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d5272321be5fcf5ce2fb16023bc825bb10dfcb71611117296537181ce950f48" -dependencies = [ - "bevy_app", - "bevy_ecs", - "bevy_math", - "bevy_reflect", - "bevy_tasks", - "bevy_utils", - "bytemuck", -] - -[[package]] -name = "bevy_core_pipeline" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67382fa9c96ce4f4e5833ed7cedd9886844a8f3284b4a717bd4ac738dcdea0c3" -dependencies = [ - "bevy_app", - "bevy_asset", - "bevy_core", - "bevy_derive", - "bevy_ecs", - "bevy_math", - "bevy_reflect", - "bevy_render", - "bevy_transform", - "bevy_utils", - "bitflags 2.4.1", - "radsort", - "serde", -] - -[[package]] -name = "bevy_derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44e4e2784a81430199e4157e02903a987a32127c773985506f020e7d501b62e" -dependencies = [ - "bevy_macro_utils", - "quote", - "syn 2.0.38", -] - -[[package]] -name = "bevy_diagnostic" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6babb230dc383c98fdfc9603e3a7a2a49e1e2879dbe8291059ef37dca897932e" -dependencies = [ - "bevy_app", - "bevy_core", - "bevy_ecs", - "bevy_log", - "bevy_time", - "bevy_utils", - "sysinfo", -] - -[[package]] -name = "bevy_ecs" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266144b36df7e834d5198049e037ecdf2a2310a76ce39ed937d1b0a6a2c4e8c6" -dependencies = [ - "async-channel", - "bevy_ecs_macros", - "bevy_ptr", - "bevy_reflect", - "bevy_tasks", - "bevy_utils", - "downcast-rs", - "event-listener", - "fixedbitset", - "rustc-hash", - "serde", - "thiserror", - "thread_local", -] - -[[package]] -name = "bevy_ecs_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7157a9c3be038d5008ee3f114feb6cf6b39c1d3d32ee21a7cacb8f81fccdfa80" -dependencies = [ - "bevy_macro_utils", - "proc-macro2", - "quote", - "syn 2.0.38", -] - -[[package]] -name = "bevy_encase_derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ac0f55ad6bca1be7b0f35bbd5fc95ed3d31e4e9db158fee8e5327f59006001" -dependencies = [ - "bevy_macro_utils", - "encase_derive_impl", -] - -[[package]] -name = "bevy_gilrs" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65f4d79c55829f8016014593a42453f61a564ffb06ef79460d25696ccdfac67b" -dependencies = [ - "bevy_app", - "bevy_ecs", - "bevy_input", - "bevy_log", - "bevy_time", - "bevy_utils", - "gilrs", - "thiserror", -] - -[[package]] -name = "bevy_gizmos" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e286a3e7276431963f4aa29165ea5429fa7dbbc6d5c5ba0c531e7dd44ecc88a2" -dependencies = [ - "bevy_app", - "bevy_asset", - "bevy_core", - "bevy_core_pipeline", - "bevy_ecs", - "bevy_math", - "bevy_pbr", - "bevy_reflect", - "bevy_render", - "bevy_sprite", - "bevy_transform", - "bevy_utils", -] - -[[package]] -name = "bevy_gltf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f07494a733dca032e71a20f4b1f423de765da49cbff34406ae6cd813f9b50c41" -dependencies = [ - "anyhow", - "base64 0.13.1", - "bevy_animation", - "bevy_app", - "bevy_asset", - "bevy_core", - "bevy_core_pipeline", - "bevy_ecs", - "bevy_hierarchy", - "bevy_log", - "bevy_math", - "bevy_pbr", - "bevy_reflect", - "bevy_render", - "bevy_scene", - "bevy_tasks", - "bevy_transform", - "bevy_utils", - "gltf", - "percent-encoding", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "bevy_hierarchy" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "103f8f58416ac6799b8c7f0b418f1fac9eba44fa924df3b0e16b09256b897e3d" -dependencies = [ - "bevy_app", - "bevy_core", - "bevy_ecs", - "bevy_log", - "bevy_reflect", - "bevy_utils", - "smallvec", -] - -[[package]] -name = "bevy_input" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbd935401101ac8003f3c3aea70788c65ad03f7a32716a10608bedda7a648bc" -dependencies = [ - "bevy_app", - "bevy_ecs", - "bevy_math", - "bevy_reflect", - "bevy_utils", - "thiserror", -] - -[[package]] -name = "bevy_internal" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0e35a9b2bd29aa784b3cc416bcbf2a298f69f00ca51fd042ea39d9af7fad37e" -dependencies = [ - "bevy_a11y", - "bevy_animation", - "bevy_app", - "bevy_asset", - "bevy_audio", - "bevy_core", - "bevy_core_pipeline", - "bevy_derive", - "bevy_diagnostic", - "bevy_ecs", - "bevy_gilrs", - "bevy_gizmos", - "bevy_gltf", - "bevy_hierarchy", - "bevy_input", - "bevy_log", - "bevy_math", - "bevy_pbr", - "bevy_ptr", - "bevy_reflect", - "bevy_render", - "bevy_scene", - "bevy_sprite", - "bevy_tasks", - "bevy_text", - "bevy_time", - "bevy_transform", - "bevy_ui", - "bevy_utils", - "bevy_window", - "bevy_winit", -] - -[[package]] -name = "bevy_log" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07dcc615ff4f617b06c3f9522fca3c55d56f9644db293318f8ab68fcdea5d4fe" -dependencies = [ - "android_log-sys", - "bevy_app", - "bevy_ecs", - "bevy_utils", - "console_error_panic_hook", - "tracing-log", - "tracing-subscriber", - "tracing-wasm", -] - -[[package]] -name = "bevy_macro_utils" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ddc18d489b4e57832d4958cde7cd2f349f0ad91e5892ac9e2f2ee16546b981" -dependencies = [ - "quote", - "rustc-hash", - "syn 2.0.38", - "toml_edit", -] - -[[package]] -name = "bevy_math" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78286a81fead796dc4b45ab14f4f02fe29a94423d3587bcfef872b2a8e0a474b" -dependencies = [ - "glam", - "serde", -] - -[[package]] -name = "bevy_mikktspace" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cfc2a21ea47970a9b1f0f4735af3256a8f204815bd756110051d10f9d909497" -dependencies = [ - "glam", -] - -[[package]] -name = "bevy_pbr" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63ca796a619e61cd43a0a3b11fde54644f7f0732a1fba1eef5d406248c6eba85" -dependencies = [ - "bevy_app", - "bevy_asset", - "bevy_core_pipeline", - "bevy_derive", - "bevy_ecs", - "bevy_math", - "bevy_reflect", - "bevy_render", - "bevy_transform", - "bevy_utils", - "bevy_window", - "bitflags 2.4.1", - "bytemuck", - "naga_oil", - "radsort", -] - -[[package]] -name = "bevy_ptr" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c7586401a46f7d8e436028225c1df5288f2e0082d066b247a82466fea155c6" - -[[package]] -name = "bevy_reflect" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0778197a1eb3e095a71417c74b7152ede02975cdc95b5ea4ddc5251ed00a2eb5" -dependencies = [ - "bevy_math", - "bevy_ptr", - "bevy_reflect_derive", - "bevy_utils", - "downcast-rs", - "erased-serde", - "glam", - "once_cell", - "parking_lot", - "serde", - "smallvec", - "smol_str", - "thiserror", -] - -[[package]] -name = "bevy_reflect_derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "342a4b2d09db22c48607d23ad59a056aff1ee004549050a51d490d375ba29528" -dependencies = [ - "bevy_macro_utils", - "bit-set", - "proc-macro2", - "quote", - "syn 2.0.38", - "uuid", -] - -[[package]] -name = "bevy_render" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39df4824b760928c27afc7b00fb649c7a63c9d76661ab014ff5c86537ee906cb" -dependencies = [ - "anyhow", - "async-channel", - "bevy_app", - "bevy_asset", - "bevy_core", - "bevy_derive", - "bevy_ecs", - "bevy_encase_derive", - "bevy_hierarchy", - "bevy_log", - "bevy_math", - "bevy_mikktspace", - "bevy_reflect", - "bevy_render_macros", - "bevy_tasks", - "bevy_time", - "bevy_transform", - "bevy_utils", - "bevy_window", - "bitflags 2.4.1", - "bytemuck", - "codespan-reporting", - "downcast-rs", - "encase", - "futures-lite", - "hexasphere", - "image", - "js-sys", - "ktx2", - "naga", - "naga_oil", - "parking_lot", - "regex", - "ruzstd", - "serde", - "smallvec", - "thiserror", - "thread_local", - "wasm-bindgen", - "web-sys", - "wgpu", - "wgpu-hal", -] - -[[package]] -name = "bevy_render_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd08c740aac73363e32fb45af869b10cec65bcb76fe3e6cd0f8f7eebf4c36c9" -dependencies = [ - "bevy_macro_utils", - "proc-macro2", - "quote", - "syn 2.0.38", -] - -[[package]] -name = "bevy_renet" -version = "0.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b2082138eac1ff62f90bf31774c439bdeace11e64b70c3454ebd83b38b1eb" -dependencies = [ - "bevy", - "renet", -] - -[[package]] -name = "bevy_scene" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd47e1263506153bef3a8be97fe2d856f206d315668c4f97510ca6cc181d9681" -dependencies = [ - "anyhow", - "bevy_app", - "bevy_asset", - "bevy_derive", - "bevy_ecs", - "bevy_hierarchy", - "bevy_reflect", - "bevy_render", - "bevy_transform", - "bevy_utils", - "ron", - "serde", - "thiserror", - "uuid", -] - -[[package]] -name = "bevy_sprite" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a8ca824fad75c6ef74cfbbba0a4ce3ccc435fa23d6bf3f003f260548813397" -dependencies = [ - "bevy_app", - "bevy_asset", - "bevy_core_pipeline", - "bevy_derive", - "bevy_ecs", - "bevy_log", - "bevy_math", - "bevy_reflect", - "bevy_render", - "bevy_transform", - "bevy_utils", - "bitflags 2.4.1", - "bytemuck", - "fixedbitset", - "guillotiere", - "rectangle-pack", - "thiserror", -] - -[[package]] -name = "bevy_tasks" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c73bbb847c83990d3927005090df52f8ac49332e1643d2ad9aac3cd2974e66bf" -dependencies = [ - "async-channel", - "async-executor", - "async-task", - "concurrent-queue", - "futures-lite", - "wasm-bindgen-futures", -] - -[[package]] -name = "bevy_text" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692288ab7b0a9f8b38058964c52789fc6bcb63703b23de51cce90ec41bfca355" -dependencies = [ - "ab_glyph", - "anyhow", - "bevy_app", - "bevy_asset", - "bevy_ecs", - "bevy_math", - "bevy_reflect", - "bevy_render", - "bevy_sprite", - "bevy_transform", - "bevy_utils", - "bevy_window", - "glyph_brush_layout", - "serde", - "thiserror", -] - -[[package]] -name = "bevy_time" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d58d6dbae9c8225d8c0e0f04d2c5dbb71d22adc01ecd5ab3cebc364139e4a6d" -dependencies = [ - "bevy_app", - "bevy_ecs", - "bevy_reflect", - "bevy_utils", - "crossbeam-channel", - "thiserror", -] - -[[package]] -name = "bevy_transform" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9b0ac0149a57cd846cb357a35fc99286f9848e53d4481954608ac9552ed2d4" -dependencies = [ - "bevy_app", - "bevy_ecs", - "bevy_hierarchy", - "bevy_math", - "bevy_reflect", -] - -[[package]] -name = "bevy_ui" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b6d295a755e5b79e869a09e087029d72974562a521ec7ccfba7141fa948a32" -dependencies = [ - "bevy_a11y", - "bevy_app", - "bevy_asset", - "bevy_core_pipeline", - "bevy_derive", - "bevy_ecs", - "bevy_hierarchy", - "bevy_input", - "bevy_log", - "bevy_math", - "bevy_reflect", - "bevy_render", - "bevy_sprite", - "bevy_text", - "bevy_transform", - "bevy_utils", - "bevy_window", - "bytemuck", - "serde", - "smallvec", - "taffy", - "thiserror", -] - -[[package]] -name = "bevy_utils" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d9484e32434ea84dc548cff246ce0c6f756c1336f5ea03f24ac120a48595c7" -dependencies = [ - "ahash", - "bevy_utils_proc_macros", - "getrandom", - "hashbrown 0.14.2", - "instant", - "petgraph", - "thiserror", - "tracing", - "uuid", -] - -[[package]] -name = "bevy_utils_proc_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5391b242c36f556db01d5891444730c83aa9dd648b6a8fd2b755d22cb3bddb57" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.38", -] - -[[package]] -name = "bevy_window" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd584c0da7c4ada6557b09f57f30fb7cff21ccedc641473fc391574b4c9b7944" -dependencies = [ - "bevy_app", - "bevy_ecs", - "bevy_input", - "bevy_math", - "bevy_reflect", - "bevy_utils", - "raw-window-handle", -] - -[[package]] -name = "bevy_winit" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdc044abdb95790c20053e6326760f0a2985f0dcd78613d397bf35f16039d53" -dependencies = [ - "accesskit_winit", - "approx", - "bevy_a11y", - "bevy_app", - "bevy_derive", - "bevy_ecs", - "bevy_hierarchy", - "bevy_input", - "bevy_math", - "bevy_tasks", - "bevy_utils", - "bevy_window", - "crossbeam-channel", - "raw-window-handle", - "wasm-bindgen", - "web-sys", - "winit", -] +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bincode" @@ -1035,39 +290,35 @@ dependencies = [ ] [[package]] -name = "bindgen" -version = "0.68.1" +name = "bincode" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" dependencies = [ - "bitflags 2.4.1", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.38", + "serde", + "unty", ] [[package]] name = "bit-set" -version = "0.5.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" [[package]] name = "bitflags" @@ -1077,13 +328,19 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" dependencies = [ "serde", ] +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + [[package]] name = "block" version = "0.1.6" @@ -1091,48 +348,442 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] -name = "block-sys" -version = "0.1.0-beta.1" +name = "block-buffer" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa55741ee90902547802152aaf3f8e5248aab7e21468089560d4c8840561146" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "objc-sys", + "generic-array", ] [[package]] -name = "block2" -version = "0.2.0-alpha.6" +name = "board-game" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dd9e63c1744f755c2f60332b88de39d341e5e86239014ad839bd71c106dec42" +checksum = "647fc8459363368aae04df3d21da37094430c57dd993d09be2792133d5365e3e" dependencies = [ - "block-sys", - "objc2-encode", + "arimaa_engine_step", + "cast_trait", + "chess", + "decorum", + "internal-iterator", + "itertools 0.10.5", + "lazy_static", + "nohash-hasher", + "nom", + "num-traits", + "once_cell", + "rand 0.8.5", + "rand_xoshiro", + "rayon", + "static_assertions", ] +[[package]] +name = "bot" +version = "0.1.0" +dependencies = [ + "board-game", + "burn", + "burn-rl", + "confy", + "env_logger 0.10.0", + "internal-iterator", + "log", + "pretty_assertions", + "rand 0.8.5", + "serde", + "serde_json", + "store", +] + +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" + +[[package]] +name = "burn" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec639306f45bd663957465e840cfb07bcd2ae18f7c045dd9aba8cb7a69c0654a" +dependencies = [ + "burn-autodiff", + "burn-candle", + "burn-core", + "burn-cuda", + "burn-ndarray", + "burn-rocm", + "burn-router", + "burn-tch", + "burn-train", + "burn-wgpu", +] + +[[package]] +name = "burn-autodiff" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a178966322ab7ce71405f1324cdc14f79256d85a47138bbd2c8c4f0056148601" +dependencies = [ + "burn-common", + "burn-tensor", + "derive-new 0.7.0", + "hashbrown 0.15.4", + "log", + "num-traits", + "portable-atomic", + "spin 0.10.0", +] + +[[package]] +name = "burn-candle" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed0981b3c1d07e9df0f5bef1042921b6db6e88b5d91916fa5dbdd7f0ca921c3" +dependencies = [ + "burn-tensor", + "candle-core", + "derive-new 0.7.0", + "half", +] + +[[package]] +name = "burn-common" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c3fae76798ea4dd14e6290b6753eb6235ac28c6ceaf6da35ff8396775d5494d" +dependencies = [ + "cubecl-common", + "rayon", + "serde", +] + +[[package]] +name = "burn-core" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2afa81c868c1a9b3fad25c31176945d0cc5181ba7b77c0456bc05cf57fca975c" +dependencies = [ + "ahash", + "bincode 2.0.1", + "burn-common", + "burn-dataset", + "burn-derive", + "burn-tensor", + "data-encoding", + "derive-new 0.7.0", + "flate2", + "half", + "hashbrown 0.15.4", + "log", + "num-traits", + "portable-atomic-util", + "rand 0.9.1", + "rmp-serde", + "serde", + "serde_json", + "spin 0.10.0", + "uuid", +] + +[[package]] +name = "burn-cubecl" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c547cbe414274ab4022abcc85993e1e41aa7cdccc92395ba5658acfdac285e07" +dependencies = [ + "burn-common", + "burn-cubecl-fusion", + "burn-fusion", + "burn-ir", + "burn-tensor", + "bytemuck", + "cubecl", + "cubecl-std", + "derive-new 0.7.0", + "futures-lite", + "half", + "hashbrown 0.15.4", + "log", + "num-traits", + "rand 0.9.1", + "serde", + "spin 0.10.0", + "text_placeholder", +] + +[[package]] +name = "burn-cubecl-fusion" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c98a5cf4d17f2db71f68c44bda21bb85fe8fe06166ea83fdd2eaff557da7fc86" +dependencies = [ + "burn-common", + "burn-fusion", + "burn-ir", + "burn-tensor", + "cubecl", + "derive-new 0.7.0", + "half", + "serde", +] + +[[package]] +name = "burn-cuda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "995bd0b3f52a4cfe0cfe47c16b40b3fd33285d17a086dd583e5b432074857e02" +dependencies = [ + "burn-cubecl", + "burn-fusion", + "burn-tensor", + "bytemuck", + "cubecl", + "derive-new 0.7.0", + "half", + "log", +] + +[[package]] +name = "burn-dataset" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136c784dfc474c822f34d69e865f88a5675e9de9803ef38cee4ce14cdba34d54" +dependencies = [ + "csv", + "derive-new 0.7.0", + "dirs 6.0.0", + "gix-tempfile", + "image", + "r2d2", + "r2d2_sqlite", + "rand 0.9.1", + "rmp-serde", + "rusqlite", + "sanitize-filename 0.6.0", + "serde", + "serde_json", + "serde_rusqlite", + "strum 0.27.1", + "tempfile", + "thiserror 2.0.12", +] + +[[package]] +name = "burn-derive" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12e9f07ccc658ef072bce2e996f0c38c80ee4c241598b6557afe1877dd87ae98" +dependencies = [ + "derive-new 0.7.0", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "burn-fusion" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9ed8e016795ec066c1028fc0d395a00bd5459922b4fa0eeae839b4d4ec58c45" +dependencies = [ + "burn-common", + "burn-ir", + "burn-tensor", + "derive-new 0.7.0", + "half", + "hashbrown 0.15.4", + "log", + "serde", + "spin 0.10.0", +] + +[[package]] +name = "burn-ir" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d63629f2c8b82ee52dbb9c18becded5117c2faf57365dc271a55c16d139cd91a" +dependencies = [ + "burn-tensor", + "hashbrown 0.15.4", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "burn-ndarray" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e883846578e6915e1dbaeeb5bce32cc04cff03e7cb79c5836e1e888bbce974f" +dependencies = [ + "atomic_float", + "burn-autodiff", + "burn-common", + "burn-ir", + "burn-tensor", + "bytemuck", + "derive-new 0.7.0", + "itertools 0.14.0", + "libm", + "macerator", + "matrixmultiply", + "ndarray", + "num-traits", + "paste", + "portable-atomic-util", + "rand 0.9.1", + "seq-macro", + "spin 0.10.0", +] + +[[package]] +name = "burn-rl" +version = "0.1.0" +source = "git+https://github.com/yunjhongwu/burn-rl-examples.git#4d59261f26429f9df16dabd2b183f7fccd27ff9a" +dependencies = [ + "burn", + "gym-rs", + "rand 0.8.5", + "ringbuffer", + "serde", +] + +[[package]] +name = "burn-rocm" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd39d58202558b65b575921b57bff933845e6171296e2b8faf6a9d3610a344c5" +dependencies = [ + "burn-cubecl", + "burn-fusion", + "burn-tensor", + "bytemuck", + "cubecl", + "derive-new 0.7.0", + "half", + "log", +] + +[[package]] +name = "burn-router" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ed8614e180f7a58f77e658bd52e206d2f4a1ee37fcb4665c635ea9da90ea8b" +dependencies = [ + "burn-common", + "burn-ir", + "burn-tensor", + "hashbrown 0.15.4", + "log", + "spin 0.10.0", +] + +[[package]] +name = "burn-tch" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cacd39695f87086360a6457fa912062b2bbc04deacb281361539cf1bf6dcfac5" +dependencies = [ + "burn-tensor", + "cc", + "half", + "libc", + "log", + "tch", + "torch-sys", +] + +[[package]] +name = "burn-tensor" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a70d1562c0d00083939e34daad61dabebb0f8bc8c250d1ef2f5efc31eb93aaf" +dependencies = [ + "burn-common", + "bytemuck", + "colored", + "cubecl", + "derive-new 0.7.0", + "half", + "hashbrown 0.15.4", + "num-traits", + "rand 0.9.1", + "rand_distr", + "serde", + "serde_bytes", +] + +[[package]] +name = "burn-train" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140182cf5f1255d60e1d8c677fa45c6f71018c3c3c66aad093a9e4c3c222cf1c" +dependencies = [ + "async-channel", + "burn-core", + "burn-ndarray", + "derive-new 0.7.0", + "log", + "nvml-wrapper", + "ratatui 0.29.0", + "rstest", + "serde", + "sysinfo", + "systemstat", + "tracing-appender", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "burn-wgpu" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "215bf0e641a27e17bcd3941a11867dcda411c9cb009488c6b6650c8206437c30" +dependencies = [ + "burn-cubecl", + "burn-fusion", + "burn-tensor", + "cubecl", +] [[package]] name = "bytemuck" -version = "1.14.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.5.0" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" +checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.104", ] [[package]] @@ -1141,12 +792,72 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[package]] +name = "bytesize" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659" + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "c_vec" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd7a427adc0135366d99db65b36dae9237130997e560ed61118041fb72be6e8" + +[[package]] +name = "candle-core" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ccf5ee3532e66868516d9b315f73aec9f34ea1a37ae98514534d458915dbf1" +dependencies = [ + "byteorder", + "gemm 0.17.1", + "half", + "memmap2", + "num-traits", + "num_cpus", + "rand 0.9.1", + "rand_distr", + "rayon", + "safetensors 0.4.5", + "thiserror 1.0.69", + "ug", + "yoke", + "zip 1.1.4", +] + [[package]] name = "cassowary" version = "0.3.0" @@ -1154,28 +865,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] -name = "cc" -version = "1.0.83" +name = "cast_trait" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "c4f8d981c476baadf74cd52897866a1d279d3e14e2d5e2d9af045210e0ae6128" + +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" dependencies = [ - "jobserver", - "libc", + "rustversion", ] [[package]] -name = "cesu8" -version = "1.1.0" +name = "cc" +version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +dependencies = [ + "jobserver", + "libc", + "shlex", +] [[package]] -name = "cexpr" -version = "0.6.0" +name = "cfg-expr" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ - "nom", + "smallvec", + "target-lexicon", ] [[package]] @@ -1186,9 +908,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" @@ -1214,6 +936,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chess" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ed299b171ec34f372945ad6726f7bc1d2afd5f59fb8380f64f48e2bab2f0ec8" +dependencies = [ + "arrayvec 0.5.2", + "failure", + "nodrop", + "rand 0.7.3", +] + [[package]] name = "cipher" version = "0.4.4" @@ -1225,23 +959,16 @@ dependencies = [ "zeroize", ] -[[package]] -name = "clang-sys" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" -dependencies = [ - "glob", - "libc", - "libloading 0.7.4", -] - [[package]] name = "client_cli" version = "0.1.0" dependencies = [ "anyhow", - "bincode", + "bincode 1.3.3", + "bot", + "env_logger 0.11.6", + "itertools 0.13.0", + "log", "pico-args", "pretty_assertions", "renet", @@ -1253,21 +980,30 @@ name = "client_tui" version = "0.1.0" dependencies = [ "anyhow", - "bincode", + "bincode 1.3.3", "crossterm", - "ratatui", - "renet", + "ratatui 0.28.1", "store", ] [[package]] -name = "codespan-reporting" -version = "0.11.1" +name = "cmake" +version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" dependencies = [ + "cc", +] + +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "serde", "termcolor", - "unicode-width", + "unicode-width 0.2.0", ] [[package]] @@ -1277,66 +1013,66 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] -name = "com-rs" -version = "0.2.1" +name = "colorchoice" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf43edc576402991846b093a7ca18a3477e0ef9c588cde84964b5d3e43016642" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] -name = "combine" -version = "4.6.6" +name = "colored" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "bytes", - "memchr", + "windows-sys 0.59.0", +] + +[[package]] +name = "compact_str" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", ] [[package]] name = "concurrent-queue" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] -name = "console_error_panic_hook" -version = "0.1.7" +name = "confy" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +checksum = "f29222b549d4e3ded127989d523da9e928918d0d0d7f7c1690b439d0d538bae9" dependencies = [ - "cfg-if", - "wasm-bindgen", + "directories", + "serde", + "thiserror 2.0.12", + "toml", ] [[package]] -name = "const_panic" -version = "0.2.8" +name = "constant_time_eq" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6051f239ecec86fde3410901ab7860d458d160371533842974fc61f96d15879b" - -[[package]] -name = "const_soft_float" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ca1caa64ef4ed453e68bb3db612e51cf1b2f5b871337f0fcab1c8f87cc3dff" - -[[package]] -name = "constgebra" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd23e864550e6dafc1e41ac78ce4f1ccddc8672b40c403524a04ff3f0518420" -dependencies = [ - "const_soft_float", -] +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -1344,79 +1080,21 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - -[[package]] -name = "core-graphics" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics-types" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", "core-foundation", "libc", ] -[[package]] -name = "coreaudio-rs" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" -dependencies = [ - "bitflags 1.3.2", - "core-foundation-sys", - "coreaudio-sys", -] - -[[package]] -name = "coreaudio-sys" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8478e5bdad14dce236b9898ea002eabfa87cbe14f0aa538dbe3b6a4bec4332d" -dependencies = [ - "bindgen", -] - -[[package]] -name = "cpal" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c" -dependencies = [ - "alsa", - "core-foundation-sys", - "coreaudio-rs", - "dasp_sample", - "jni 0.19.0", - "js-sys", - "libc", - "mach2", - "ndk", - "ndk-context", - "oboe", - "once_cell", - "parking_lot", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows 0.46.0", -] - [[package]] name = "cpufeatures" version = "0.2.11" @@ -1428,43 +1106,58 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.8" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" -version = "0.27.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.9.1", "crossterm_winapi", - "libc", "mio", "parking_lot", + "rustix 0.38.37", "signal-hook", "signal-hook-mio", "winapi", @@ -1479,6 +1172,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1486,32 +1185,434 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] [[package]] -name = "d3d12" -version = "0.6.0" +name = "csv" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8f0de2f5a8e7bd4a9eec0e3c781992a4ce1724f68aec7d7a3715344de8b39da" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" dependencies = [ - "bitflags 1.3.2", - "libloading 0.7.4", - "winapi", + "csv-core", + "itoa", + "ryu", + "serde", ] [[package]] -name = "dasp_sample" -version = "0.11.0" +name = "csv-core" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +dependencies = [ + "memchr", +] + +[[package]] +name = "cubecl" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e438056cf7c25b3adde38240b89842e1c924b8e914731c82ad81161d23e6ff" +dependencies = [ + "cubecl-core", + "cubecl-cuda", + "cubecl-hip", + "cubecl-linalg", + "cubecl-reduce", + "cubecl-runtime", + "cubecl-std", + "cubecl-wgpu", + "half", +] + +[[package]] +name = "cubecl-common" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79251bfc7f067ac9038232fe38a317adc2f31cb2fc3800e69fd409ccac7abc1f" +dependencies = [ + "bytemuck", + "derive-new 0.6.0", + "derive_more", + "dirs 5.0.1", + "embassy-futures", + "futures-lite", + "half", + "hashbrown 0.14.5", + "log", + "num-traits", + "portable-atomic", + "rand 0.9.1", + "sanitize-filename 0.5.0", + "serde", + "serde_json", + "spin 0.9.8", +] + +[[package]] +name = "cubecl-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03bf4211cdbd68bb0fb8291e0ed825c13da0d1ac01b7c02dce3cee44a6138be" +dependencies = [ + "bitflags 2.9.1", + "bytemuck", + "cubecl-common", + "cubecl-ir", + "cubecl-macros", + "cubecl-runtime", + "derive-new 0.6.0", + "derive_more", + "half", + "hashbrown 0.14.5", + "log", + "num-traits", + "paste", + "serde", + "serde_json", + "variadics_please", +] + +[[package]] +name = "cubecl-cpp" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5eef85cbcc34be7e25fc9d39edf99ed68559862dbf25c1877ebdf4a9595d31b" +dependencies = [ + "bytemuck", + "cubecl-common", + "cubecl-core", + "cubecl-runtime", + "derive-new 0.6.0", + "half", + "log", +] + +[[package]] +name = "cubecl-cuda" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e091e4e3a3900faff440aec4053805ec4456f94f4acc4afe8e6b27519c6d16" +dependencies = [ + "bytemuck", + "cubecl-common", + "cubecl-core", + "cubecl-cpp", + "cubecl-runtime", + "cudarc", + "derive-new 0.6.0", + "half", + "log", + "serde", +] + +[[package]] +name = "cubecl-hip" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2f8c00207517de61cccdc4ca2724bc1db9dab94840beaf4329e43cead3bc4a" +dependencies = [ + "bytemuck", + "cubecl-common", + "cubecl-core", + "cubecl-cpp", + "cubecl-hip-sys", + "cubecl-runtime", + "derive-new 0.6.0", + "half", + "log", + "paste", +] + +[[package]] +name = "cubecl-hip-sys" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7557762176858fa0357504025f09ae6e979c3547776ff8b6a1025ef0702450" +dependencies = [ + "libc", +] + +[[package]] +name = "cubecl-ir" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e096d77646590f0180ed4ce1aa7df4ecc7219f3c4616e9fe72d93ab63a352855" +dependencies = [ + "cubecl-common", + "cubecl-macros-internal", + "derive_more", + "float-ord", + "fnv", + "half", + "hashbrown 0.14.5", + "num-traits", + "portable-atomic", + "serde", + "variadics_please", +] + +[[package]] +name = "cubecl-linalg" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75aacf86f6004c274e63589aed55c5edcbcdf1b292eaf4ce2c1688c04c41a194" +dependencies = [ + "bytemuck", + "cubecl-common", + "cubecl-core", + "cubecl-reduce", + "cubecl-runtime", + "cubecl-std", + "half", + "serde", +] + +[[package]] +name = "cubecl-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd74622b5c8cb161e3f7fa0b2b751784ef89ab45acfa355f511eb2219dde337e" +dependencies = [ + "cubecl-common", + "darling", + "derive-new 0.6.0", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "cubecl-macros-internal" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a89898212c1eaba0e2f0dffcadc9790b20b75d2ec8836da084370b043be2623" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "cubecl-reduce" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7afbdfe03e7e3ca71f61890ebebc6b4390494204b545e6f6bf51a43755449073" +dependencies = [ + "cubecl-core", + "cubecl-runtime", + "cubecl-std", + "num-traits", + "serde", +] + +[[package]] +name = "cubecl-runtime" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385234520c9e392382737f32ad372b05f345656eb798ba00b72d2722c68b698c" +dependencies = [ + "async-channel", + "bytemuck", + "cfg-if", + "cfg_aliases", + "cubecl-common", + "cubecl-ir", + "derive-new 0.6.0", + "hashbrown 0.14.5", + "log", + "md5", + "serde", + "serde_json", + "spin 0.9.8", + "variadics_please", + "wasm-bindgen-futures", +] + +[[package]] +name = "cubecl-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38868eea6fdc183feb3c46bcf5e666c78e6cf0ddca2c4f3a877785cc0eabd71e" +dependencies = [ + "cubecl-core", + "cubecl-runtime", + "half", + "serde", +] + +[[package]] +name = "cubecl-wgpu" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fa2dcfaa6d75cfbc5ff05cafe99ec4a7fb7c0fa7197917e0fd20f5b90979fe" +dependencies = [ + "async-channel", + "bytemuck", + "cfg-if", + "cfg_aliases", + "cubecl-common", + "cubecl-core", + "cubecl-runtime", + "derive-new 0.6.0", + "derive_more", + "hashbrown 0.14.5", + "log", + "web-time", + "wgpu", +] + +[[package]] +name = "cudarc" +version = "0.13.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486c221362668c63a1636cfa51463b09574433b39029326cff40864b3ba12b6e" +dependencies = [ + "libloading", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.104", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] [[package]] name = "data-encoding" -version = "2.4.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "decorum" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "281759d3c8a14f5c3f0c49363be56810fcd7f910422f97f2db850c2920fde5cf" +dependencies = [ + "num-traits", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive-new" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "derive-new" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "unicode-xid", +] [[package]] name = "diff" @@ -1520,16 +1621,105 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] -name = "dispatch" -version = "0.2.0" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] [[package]] -name = "downcast-rs" -version = "1.2.0" +name = "directories" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + +[[package]] +name = "dyn-stack" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e53799688f5632f364f8fb387488dd05db9fe45db7011be066fc20e7027f8b" +dependencies = [ + "bytemuck", + "reborrow", +] + +[[package]] +name = "dyn-stack" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490bd48eb68fffcfed519b4edbfd82c69cbe741d175b84f0e0cbe8c57cbe0bdd" +dependencies = [ + "bytemuck", +] [[package]] name = "either" @@ -1538,35 +1728,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] -name = "encase" -version = "0.6.1" +name = "embassy-futures" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fce2eeef77fd4a293a54b62aa00ac9daebfbcda4bf8998c5a815635b004aa1c" -dependencies = [ - "const_panic", - "encase_derive", - "glam", - "thiserror", -] +checksum = "1f878075b9794c1e4ac788c95b728f26aa6366d32eeb10c7051389f898f7d067" [[package]] -name = "encase_derive" +name = "enum-as-inner" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e520cde08cbf4f7cc097f61573ec06ce467019803de8ae82fb2823fa1554a0e" -dependencies = [ - "encase_derive_impl", -] - -[[package]] -name = "encase_derive_impl" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fe2568f851fd6144a45fa91cfed8fe5ca8fc0b56ba6797bfc1ed2771b90e37c" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ + "heck", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.104", +] + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", ] [[package]] @@ -1582,98 +1768,156 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "erased-serde" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" -dependencies = [ - "serde", -] - [[package]] name = "errno" -version = "0.3.5" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "euclid" -version = "0.22.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f253bc5c813ca05792837a0ff4b3a580336b224512d48f7eda1d7dd9210787" -dependencies = [ - "num-traits", + "windows-sys 0.59.0", ] [[package]] name = "event-listener" -version = "2.5.3" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ - "instant", + "concurrent-queue", + "parking", + "pin-project-lite", ] [[package]] -name = "fastrand" -version = "2.0.1" +name = "event-listener-strategy" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" -version = "0.3.0" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] -[[package]] -name = "filetime" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.3.5", - "windows-sys 0.48.0", -] - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "flate2" -version = "1.0.28" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", ] +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + [[package]] name = "fnv" version = "1.0.7" @@ -1681,54 +1925,344 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "foreign-types" -version = "0.3.2" +name = "foldhash" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ + "foreign-types-macros", "foreign-types-shared", ] [[package]] -name = "foreign-types-shared" -version = "0.1.1" +name = "foreign-types-macros" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] [[package]] -name = "fsevent-sys" -version = "4.1.0" +name = "foreign-types-shared" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ - "libc", + "percent-encoding", ] [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "1.13.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ - "fastrand 1.9.0", + "fastrand", "futures-core", "futures-io", - "memchr", "parking", "pin-project-lite", - "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gemm" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab24cc62135b40090e31a76a9b2766a501979f3070fa27f689c27ec04377d32" +dependencies = [ + "dyn-stack 0.10.0", + "gemm-c32 0.17.1", + "gemm-c64 0.17.1", + "gemm-common 0.17.1", + "gemm-f16 0.17.1", + "gemm-f32 0.17.1", + "gemm-f64 0.17.1", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 10.7.0", + "seq-macro", +] + +[[package]] +name = "gemm" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451" +dependencies = [ + "dyn-stack 0.13.0", + "gemm-c32 0.18.2", + "gemm-c64 0.18.2", + "gemm-common 0.18.2", + "gemm-f16 0.18.2", + "gemm-f32 0.18.2", + "gemm-f64 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 11.5.0", + "seq-macro", +] + +[[package]] +name = "gemm-c32" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9c030d0b983d1e34a546b86e08f600c11696fde16199f971cd46c12e67512c0" +dependencies = [ + "dyn-stack 0.10.0", + "gemm-common 0.17.1", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 10.7.0", + "seq-macro", +] + +[[package]] +name = "gemm-c32" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847" +dependencies = [ + "dyn-stack 0.13.0", + "gemm-common 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 11.5.0", + "seq-macro", +] + +[[package]] +name = "gemm-c64" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb5f2e79fefb9693d18e1066a557b4546cd334b226beadc68b11a8f9431852a" +dependencies = [ + "dyn-stack 0.10.0", + "gemm-common 0.17.1", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 10.7.0", + "seq-macro", +] + +[[package]] +name = "gemm-c64" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf" +dependencies = [ + "dyn-stack 0.13.0", + "gemm-common 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 11.5.0", + "seq-macro", +] + +[[package]] +name = "gemm-common" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2e7ea062c987abcd8db95db917b4ffb4ecdfd0668471d8dc54734fdff2354e8" +dependencies = [ + "bytemuck", + "dyn-stack 0.10.0", + "half", + "num-complex", + "num-traits", + "once_cell", + "paste", + "pulp 0.18.22", + "raw-cpuid 10.7.0", + "rayon", + "seq-macro", + "sysctl 0.5.5", +] + +[[package]] +name = "gemm-common" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3" +dependencies = [ + "bytemuck", + "dyn-stack 0.13.0", + "half", + "libm", + "num-complex", + "num-traits", + "once_cell", + "paste", + "pulp 0.21.5", + "raw-cpuid 11.5.0", + "rayon", + "seq-macro", + "sysctl 0.6.0", +] + +[[package]] +name = "gemm-f16" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca4c06b9b11952071d317604acb332e924e817bd891bec8dfb494168c7cedd4" +dependencies = [ + "dyn-stack 0.10.0", + "gemm-common 0.17.1", + "gemm-f32 0.17.1", + "half", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 10.7.0", + "rayon", + "seq-macro", +] + +[[package]] +name = "gemm-f16" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109" +dependencies = [ + "dyn-stack 0.13.0", + "gemm-common 0.18.2", + "gemm-f32 0.18.2", + "half", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 11.5.0", + "rayon", + "seq-macro", +] + +[[package]] +name = "gemm-f32" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9a69f51aaefbd9cf12d18faf273d3e982d9d711f60775645ed5c8047b4ae113" +dependencies = [ + "dyn-stack 0.10.0", + "gemm-common 0.17.1", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 10.7.0", + "seq-macro", +] + +[[package]] +name = "gemm-f32" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864" +dependencies = [ + "dyn-stack 0.13.0", + "gemm-common 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 11.5.0", + "seq-macro", +] + +[[package]] +name = "gemm-f64" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa397a48544fadf0b81ec8741e5c0fba0043008113f71f2034def1935645d2b0" +dependencies = [ + "dyn-stack 0.10.0", + "gemm-common 0.17.1", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 10.7.0", + "seq-macro", +] + +[[package]] +name = "gemm-f64" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd" +dependencies = [ + "dyn-stack 0.13.0", + "gemm-common 0.18.2", + "num-complex", + "num-traits", + "paste", + "raw-cpuid 11.5.0", + "seq-macro", ] [[package]] @@ -1743,77 +2277,146 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", - "js-sys", "libc", - "wasi", - "wasm-bindgen", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] -name = "gilrs" -version = "0.10.2" +name = "getrandom" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62fd19844d0eb919aca41d3e4ea0e0b6bf60e1e827558b101c269015b8f5f27a" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ - "fnv", - "gilrs-core", - "log", - "uuid", - "vec_map", + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] -name = "gilrs-core" -version = "0.5.7" +name = "gif" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ccc99e9b8d63ffcaa334c4babfa31f46e156618a11f63efb6e8e6bcb37b830d" +checksum = "fcc37f9a2bfe731e69f1e08d29d91d30604b9ce24bcb2880a961e82d89c6ed89" dependencies = [ - "core-foundation", - "io-kit-sys", - "js-sys", - "libc", - "libudev-sys", - "log", - "nix 0.26.4", - "uuid", - "vec_map", - "wasm-bindgen", - "web-sys", - "windows 0.51.1", + "color_quant", + "weezl", ] [[package]] name = "gimli" -version = "0.28.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] -name = "glam" -version = "0.24.2" +name = "gix-features" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945" +checksum = "56f4399af6ec4fd9db84dd4cf9656c5c785ab492ab40a7c27ea92b4241923fed" dependencies = [ - "bytemuck", - "serde", + "gix-trace", + "gix-utils", + "libc", +] + +[[package]] +name = "gix-fs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a0637149b4ef24d3ea55f81f77231401c8463fae6da27331c987957eb597c7" +dependencies = [ + "bstr", + "fastrand", + "gix-features", + "gix-path", + "gix-utils", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-path" +version = "0.10.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567f65fec4ef10dfab97ae71f26a27fd4d7fe7b8e3f90c8a58551c41ff3fb65b" +dependencies = [ + "bstr", + "gix-trace", + "gix-validate", + "home", + "once_cell", + "thiserror 2.0.12", +] + +[[package]] +name = "gix-tempfile" +version = "17.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c750e8c008453a2dba67a2b0d928b7716e05da31173a3f5e351d5457ad4470aa" +dependencies = [ + "dashmap", + "gix-fs", + "libc", + "once_cell", + "parking_lot", + "signal-hook", + "signal-hook-registry", + "tempfile", +] + +[[package]] +name = "gix-trace" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c396a2036920c69695f760a65e7f2677267ccf483f25046977d87e4cb2665f7" + +[[package]] +name = "gix-utils" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5351af2b172caf41a3728eb4455326d84e0d70fe26fc4de74ab0bd37df4191c5" +dependencies = [ + "fastrand", + "unicode-normalization", +] + +[[package]] +name = "gix-validate" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77b9e00cacde5b51388d28ed746c493b18a6add1f19b5e01d686b3b9ece66d4d" +dependencies = [ + "bstr", + "thiserror 2.0.12", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", ] [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "glow" -version = "0.12.3" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0fe580e4b60a8ab24a868bc08e2f03cbcb20d3d676601fa909386713333728" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" dependencies = [ "js-sys", "slotmap", @@ -1822,130 +2425,111 @@ dependencies = [ ] [[package]] -name = "gltf" -version = "1.3.0" +name = "glutin_wgl_sys" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad2dcfb6dd7a66f9eb3d181a29dcfb22d146b0bcdc2e1ed1713cbf03939a88ea" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" dependencies = [ - "byteorder", - "gltf-json", - "lazy_static", -] - -[[package]] -name = "gltf-derive" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cbcea5dd47e7ad4e9ee6f040384fcd7204bbf671aa4f9e7ca7dfc9bfa1de20" -dependencies = [ - "inflections", - "proc-macro2", - "quote", - "syn 2.0.38", -] - -[[package]] -name = "gltf-json" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5b810806b78dde4b71a95cc0e6fdcab34c4c617da3574df166f9987be97d03" -dependencies = [ - "gltf-derive", - "serde", - "serde_derive", - "serde_json", -] - -[[package]] -name = "glyph_brush_layout" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc32c2334f00ca5ac3695c5009ae35da21da8c62d255b5b96d56e2597a637a38" -dependencies = [ - "ab_glyph", - "approx", - "xi-unicode", + "gl_generator", ] [[package]] name = "gpu-alloc" -version = "0.5.4" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22beaafc29b38204457ea030f6fb7a84c9e4dd1b86e311ba0542533453d87f62" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.1", "gpu-alloc-types", ] [[package]] name = "gpu-alloc-types" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54804d0d6bc9d7f26db4eaec1ad10def69b599315f487d32c334a80d1efe67a5" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.1", ] [[package]] name = "gpu-allocator" -version = "0.22.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce95f9e2e11c2c6fadfce42b5af60005db06576f231f5c92550fdded43c423e8" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" dependencies = [ - "backtrace", "log", - "thiserror", - "winapi", - "windows 0.44.0", + "presser", + "thiserror 1.0.69", + "windows 0.58.0", ] [[package]] name = "gpu-descriptor" -version = "0.2.4" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.9.1", "gpu-descriptor-types", - "hashbrown 0.14.2", + "hashbrown 0.15.4", ] [[package]] name = "gpu-descriptor-types" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.9.1", ] [[package]] -name = "grid" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eec1c01eb1de97451ee0d60de7d81cf1e72aabefb021616027f3d1c3ec1c723c" +name = "gym-rs" +version = "0.3.1" +source = "git+https://github.com/MathisWellmann/gym-rs.git#5283afaa86a3a7c45c46c882cfad459f02539b62" +dependencies = [ + "derivative", + "derive-new 0.7.0", + "log", + "nalgebra", + "num-traits", + "ordered-float 5.0.0", + "rand 0.8.5", + "rand_pcg 0.3.1", + "sdl2", + "serde", +] [[package]] -name = "guillotiere" -version = "0.6.2" +name = "half" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ - "euclid", - "svg_fmt", + "bytemuck", + "cfg-if", + "crunchy", + "num-traits", + "rand 0.9.1", + "rand_distr", + "serde", ] [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -1953,41 +2537,43 @@ dependencies = [ ] [[package]] -name = "hassle-rs" +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", + "serde", +] + +[[package]] +name = "hashlink" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1397650ee315e8891a0df210707f0fc61771b0cc518c3023896064c5407cb3b0" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "bitflags 1.3.2", - "com-rs", - "libc", - "libloading 0.7.4", - "thiserror", - "widestring", - "winapi", + "hashbrown 0.15.4", ] [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] -name = "hexasphere" -version = "9.1.0" +name = "hermit-abi" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cb3df16a7bcb1b5bc092abd55e14f77ca70aea14445026e264586fc62889a10" -dependencies = [ - "constgebra", - "glam", -] +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hexf-parse" @@ -1995,6 +2581,24 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "humantime" version = "2.1.0" @@ -2002,70 +2606,75 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] -name = "image" -version = "0.24.7" +name = "ident_case" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.25.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" dependencies = [ "bytemuck", - "byteorder", + "byteorder-lite", "color_quant", - "num-rational", + "exr", + "gif", + "image-webp", "num-traits", "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", ] [[package]] -name = "indexmap" -version = "1.9.3" +name = "image-webp" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "f6970fe7a5300b4b42e62c52efa0187540a5bef546c60edaf554ef595d2e6f0b" dependencies = [ - "autocfg", - "hashbrown 0.12.3", + "byteorder-lite", + "quick-error", ] [[package]] -name = "indexmap" -version = "2.0.2" +name = "imgref" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.14.2", + "hashbrown 0.15.4", ] [[package]] name = "indoc" -version = "2.0.4" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" - -[[package]] -name = "inflections" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a" - -[[package]] -name = "inotify" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" -dependencies = [ - "bitflags 1.3.2", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "inout" @@ -2077,25 +2686,30 @@ dependencies = [ ] [[package]] -name = "instant" -version = "0.1.12" +name = "instability" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", + "quote", + "syn 2.0.104", ] [[package]] -name = "io-kit-sys" -version = "0.3.0" +name = "internal-iterator" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2d4429acc1deff0fbdece0325b4997bdb02b2c245ab7023fd5deca0f6348de" +checksum = "969ee3fc68ec2e88eb21434ce4d9b7e1600d1ce92ff974560a6c4a304f5124b9" + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ - "core-foundation-sys", - "mach2", + "proc-macro2", + "quote", + "syn 2.0.104", ] [[package]] @@ -2104,16 +2718,49 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi", - "rustix", + "hermit-abi 0.3.9", + "rustix 0.38.37", "windows-sys 0.48.0", ] [[package]] -name = "itertools" -version = "0.11.0" +name = "is_terminal_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] @@ -2124,34 +2771,6 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" -[[package]] -name = "jni" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", -] - -[[package]] -name = "jni" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", -] - [[package]] name = "jni-sys" version = "0.3.0" @@ -2160,126 +2779,129 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.27" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ + "getrandom 0.3.3", "libc", ] [[package]] -name = "js-sys" -version = "0.3.64" +name = "jpeg-decoder" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "khronos-egl" -version = "4.1.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2352bd1d0bceb871cb9d40f24360c8133c11d7486b68b5381c1dd1a32015e3" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading 0.7.4", + "libloading", "pkg-config", ] [[package]] -name = "kqueue" -version = "1.0.8" +name = "khronos_api" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "ktx2" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87d65e08a9ec02e409d27a0139eaa6b9756b4d81fe7cde71f6941a83730ce838" -dependencies = [ - "bitflags 1.3.2", -] +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "lazycell" -version = "1.3.0" +name = "lebe" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - -[[package]] -name = "lewton" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" -dependencies = [ - "byteorder", - "ogg", - "tinyvec", -] +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.149" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] -name = "libloading" -version = "0.7.4" +name = "libfuzzer-sys" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" dependencies = [ - "cfg-if", - "winapi", + "arbitrary", + "cc", ] [[package]] name = "libloading" -version = "0.8.1" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-sys 0.48.0", + "windows-targets 0.52.6", ] [[package]] -name = "libudev-sys" -version = "0.1.4" +name = "libm" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ + "bitflags 2.9.1", "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7" +dependencies = [ + "cc", "pkg-config", + "vcpkg", ] [[package]] name = "linux-raw-sys" -version = "0.4.10" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" @@ -2293,26 +2915,53 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] -name = "lru" -version = "0.12.0" +name = "loop9" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efa59af2ddfad1854ae27d75009d538d0998b4b2fd47083e743ac1a10e46c60" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" dependencies = [ - "hashbrown 0.14.2", + "imgref", ] [[package]] -name = "mach2" -version = "0.4.1" +name = "lru" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "libc", + "hashbrown 0.15.4", +] + +[[package]] +name = "macerator" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bce07f822458c4c303081d133a90610406162e7c8df17434956ac1892faf447b" +dependencies = [ + "bytemuck", + "cfg_aliases", + "half", + "macerator-macros", + "moddef", + "num-traits", + "paste", +] + +[[package]] +name = "macerator-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b955a106dca78c0577269d67a6d56114abb8644b810fc995a22348276bb9dd" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.104", ] [[package]] @@ -2325,32 +2974,85 @@ dependencies = [ ] [[package]] -name = "matchers" -version = "0.1.0" +name = "matrixmultiply" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" dependencies = [ - "regex-automata 0.1.10", + "autocfg", + "num_cpus", + "once_cell", + "rawpointer", + "thread-tree", ] [[package]] -name = "memchr" -version = "2.6.4" +name = "maybe-rayon" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", + "stable_deref_trait", +] + +[[package]] +name = "merge" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10bbef93abb1da61525bbc45eeaff6473a41907d19f8f9aa5168d214e10693e9" +dependencies = [ + "merge_derive", + "num-traits", +] + +[[package]] +name = "merge_derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209d075476da2e63b4b29e72a2ef627b840589588e71400a25e3565c4f849d07" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] [[package]] name = "metal" -version = "0.24.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de11355d1f6781482d027a3b4d4de7825dcedb197bf573e0596d00008402d060" +checksum = "f569fb946490b5743ad69813cb19629130ce9374034abe31614a36402d18f99e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.1", "block", "core-graphics-types", "foreign-types", "log", "objc", + "paste", ] [[package]] @@ -2361,117 +3063,127 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ - "adler", + "adler2", "simd-adler32", ] [[package]] name = "mio" -version = "0.8.9" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi 0.3.9", "libc", "log", - "wasi", - "windows-sys 0.48.0", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", ] +[[package]] +name = "moddef" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e519fd9c6131c1c9a4a67f8bdc4f32eb4105b16c1468adea1b8e68c98c85ec4" + [[package]] name = "naga" -version = "0.12.3" +version = "25.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbcc2e0513220fd2b598e6068608d4462db20322c0e77e47f6f488dfcfc279cb" +checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" dependencies = [ + "arrayvec 0.7.6", "bit-set", - "bitflags 1.3.2", + "bitflags 2.9.1", + "cfg_aliases", "codespan-reporting", + "half", + "hashbrown 0.15.4", "hexf-parse", - "indexmap 1.9.3", + "indexmap", "log", "num-traits", - "pp-rs", + "once_cell", "rustc-hash", "spirv", - "termcolor", - "thiserror", - "unicode-xid", -] - -[[package]] -name = "naga_oil" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be942a5c21c58b9b0bf4d9b99db3634ddb7a916f8e1d1d0b71820cc4150e56b" -dependencies = [ - "bit-set", - "codespan-reporting", - "data-encoding", - "indexmap 1.9.3", - "naga", - "once_cell", - "regex", - "regex-syntax 0.6.29", - "rustc-hash", - "thiserror", - "tracing", + "strum 0.26.3", + "thiserror 2.0.12", "unicode-ident", ] [[package]] -name = "ndk" -version = "0.7.0" +name = "nalgebra" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +checksum = "26aecdf64b707efd1310e3544d709c5c0ac61c13756046aaaba41be5c4f66a3b" dependencies = [ - "bitflags 1.3.2", - "jni-sys", - "ndk-sys", - "num_enum 0.5.11", - "raw-window-handle", - "thiserror", + "approx", + "matrixmultiply", + "nalgebra-macros", + "num-complex", + "num-rational", + "num-traits", + "simba", + "typenum", ] [[package]] -name = "ndk-context" -version = "0.1.1" +name = "nalgebra-macros" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", + "rayon", +] [[package]] name = "ndk-sys" -version = "0.4.1+23.1.7779620" +version = "0.5.0+25.2.9519653" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" dependencies = [ "jni-sys", ] [[package]] -name = "nix" -version = "0.24.3" +name = "new_debug_unreachable" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", -] +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] -name = "nix" -version = "0.26.4" +name = "nodrop" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", -] +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" [[package]] name = "nom" @@ -2484,23 +3196,10 @@ dependencies = [ ] [[package]] -name = "notify" -version = "6.1.1" +name = "noop_proc_macro" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" -dependencies = [ - "bitflags 2.4.1", - "crossbeam-channel", - "filetime", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "walkdir", - "windows-sys 0.48.0", -] +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "ntapi" @@ -2522,31 +3221,70 @@ dependencies = [ ] [[package]] -name = "num-derive" -version = "0.3.3" +name = "num" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", "num-traits", ] [[package]] -name = "num-rational" -version = "0.4.1" +name = "num-bigint" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -2554,54 +3292,87 @@ dependencies = [ ] [[package]] -name = "num-traits" -version = "0.2.17" +name = "num-rational" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi 0.5.2", + "libc", ] [[package]] name = "num_enum" -version = "0.5.11" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ - "num_enum_derive 0.5.11", -] - -[[package]] -name = "num_enum" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" -dependencies = [ - "num_enum_derive 0.6.1", + "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.11" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.104", ] [[package]] -name = "num_enum_derive" -version = "0.6.1" +name = "num_threads" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.38", + "libc", +] + +[[package]] +name = "nvml-wrapper" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9bff0aa1d48904a1385ea2a8b97576fbdcbc9a3cfccd0d31fe978e1c4038c5" +dependencies = [ + "bitflags 2.9.1", + "libloading", + "nvml-wrapper-sys", + "static_assertions", + "thiserror 1.0.69", + "wrapcenum-derive", +] + +[[package]] +name = "nvml-wrapper-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "698d45156f28781a4e79652b6ebe2eaa0589057d588d3aec1333f6466f13fcb5" +dependencies = [ + "libloading", ] [[package]] @@ -2611,96 +3382,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", - "objc_exception", -] - -[[package]] -name = "objc-sys" -version = "0.2.0-beta.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7" - -[[package]] -name = "objc2" -version = "0.3.0-beta.3.patch-leaks.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e01640f9f2cb1220bbe80325e179e532cb3379ebcd1bf2279d703c19fe3a468" -dependencies = [ - "block2", - "objc-sys", - "objc2-encode", -] - -[[package]] -name = "objc2-encode" -version = "2.0.0-pre.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abfcac41015b00a120608fdaa6938c44cb983fee294351cc4bac7638b4e50512" -dependencies = [ - "objc-sys", -] - -[[package]] -name = "objc_exception" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" -dependencies = [ - "cc", ] [[package]] name = "object" -version = "0.32.1" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] -[[package]] -name = "oboe" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0" -dependencies = [ - "jni 0.20.0", - "ndk", - "ndk-context", - "num-derive", - "num-traits", - "oboe-sys", -] - -[[package]] -name = "oboe-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f44155e7fb718d3cfddcf70690b2b51ac4412f347cd9e4fbe511abe9cd7b5f2" -dependencies = [ - "cc", -] - [[package]] name = "octets" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a74f2cda724d43a0a63140af89836d4e7db6138ef67c9f96d3a0f0150d05000" -[[package]] -name = "ogg" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" -dependencies = [ - "byteorder", -] - [[package]] name = "once_cell" -version = "1.18.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "opaque-debug" @@ -2709,12 +3412,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] -name = "orbclient" -version = "0.3.46" +name = "option-ext" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8378ac0dfbd4e7895f2d2c1f1345cab3836910baf3a300b000d04250f0c8428f" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" dependencies = [ - "redox_syscall 0.3.5", + "num-traits", +] + +[[package]] +name = "ordered-float" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2c1f9f56e534ac6a9b8a4600bdf0f530fb393b5f393e7b4d03489c3cf0c3f01" +dependencies = [ + "num-traits", + "rand 0.8.5", + "serde", ] [[package]] @@ -2723,20 +3443,11 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[package]] -name = "owned_ttf_parser" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4" -dependencies = [ - "ttf-parser", -] - [[package]] name = "parking" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" @@ -2756,38 +3467,45 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall", "smallvec", "windows-targets 0.48.5", ] [[package]] -name = "paste" -version = "1.0.14" +name = "password-hash" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] [[package]] -name = "peeking_take_while" -version = "0.1.2" +name = "paste" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" - -[[package]] -name = "petgraph" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" -dependencies = [ - "fixedbitset", - "indexmap 2.0.2", -] +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pico-args" @@ -2797,21 +3515,27 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "png" -version = "0.17.10" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -2832,20 +3556,41 @@ dependencies = [ ] [[package]] -name = "pp-rs" -version = "0.2.1" +name = "portable-atomic" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb458bb7f6e250e6eb79d5026badc10a3ebb8f9a15d1fff0f13d17c71f4d6dee" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" dependencies = [ - "unicode-xid", + "serde", ] +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + [[package]] name = "pretty_assertions" version = "1.4.0" @@ -2857,44 +3602,165 @@ dependencies = [ ] [[package]] -name = "proc-macro-crate" -version = "1.3.1" +name = "prettyplease" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +dependencies = [ + "proc-macro2", + "syn 2.0.104", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ - "once_cell", "toml_edit", ] [[package]] -name = "proc-macro2" -version = "1.0.69" +name = "proc-macro-error" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "profiling" -version = "1.0.11" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89dff0959d98c9758c88826cc002e2c3d0b9dfac4139711d1f30de442f1139b" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.104", +] + +[[package]] +name = "pulp" +version = "0.18.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0a01a0dc67cf4558d279f0c25b0962bd08fc6dec0137699eae304103e882fe6" +dependencies = [ + "bytemuck", + "libm", + "num-complex", + "reborrow", +] + +[[package]] +name = "pulp" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b86df24f0a7ddd5e4b95c94fc9ed8a98f1ca94d3b01bdce2824097e7835907" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "reborrow", + "version_check", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.33" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] -name = "radsort" -version = "0.1.0" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17fd96390ed3feda12e1dfe2645ed587e0bea749e319333f104a33ff62f77a0b" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "r2d2_sqlite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180da684f0a188977d3968f139eb44260192ef8d9a5b7b7cbd01d881e0353179" +dependencies = [ + "r2d2", + "rusqlite", + "uuid", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg 0.2.1", +] [[package]] name = "rand" @@ -2903,8 +3769,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -2914,63 +3801,245 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" + [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", + "serde", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rand_distr" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" +dependencies = [ + "num-traits", + "rand 0.9.1", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", ] [[package]] name = "range-alloc" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" +checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" [[package]] name = "ratatui" -version = "0.24.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ebc917cfb527a566c37ecb94c7e3fd098353516fb4eb6bea17015ade0182425" +checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.9.1", "cassowary", + "compact_str", "crossterm", - "indoc", - "itertools", + "instability", + "itertools 0.13.0", "lru", "paste", - "strum", + "strum 0.26.3", + "strum_macros 0.26.4", "unicode-segmentation", - "unicode-width", + "unicode-truncate", + "unicode-width 0.1.14", +] + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags 2.9.1", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools 0.13.0", + "lru", + "paste", + "strum 0.26.3", + "time", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec 0.7.6", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.12.1", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha 0.3.1", + "simd_helpers", + "system-deps", + "thiserror 1.0.69", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-cpuid" +version = "10.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "raw-cpuid" +version = "11.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" +dependencies = [ + "bitflags 2.9.1", ] [[package]] name = "raw-window-handle" -version = "0.5.2" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] -name = "rectangle-pack" -version = "0.4.2" +name = "rawpointer" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] -name = "redox_syscall" -version = "0.3.5" +name = "rayon" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ - "bitflags 1.3.2", + "either", + "rayon-core", ] +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2980,55 +4049,68 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.12", +] + [[package]] name = "regex" -version = "1.10.2" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.3", - "regex-syntax 0.8.2", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.1.10" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] -name = "regex-syntax" -version = "0.8.2" +name = "relative-path" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "renderdoc-sys" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "renet" @@ -3036,7 +4118,6 @@ version = "0.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "751424a2b0a8640bc41ca8b969250491b1a29e9b872dee4c7c56bcc56575b76e" dependencies = [ - "bevy_ecs", "bytes", "log", "octets", @@ -3054,32 +4135,102 @@ dependencies = [ ] [[package]] -name = "rodio" -version = "0.17.3" +name = "rgb" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b1bb7b48ee48471f55da122c0044fcc7600cfcc85db88240b89cb832935e611" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ - "cpal", - "lewton", + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", ] [[package]] -name = "ron" -version = "0.8.1" +name = "ringbuffer" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +checksum = "3df6368f71f205ff9c33c076d170dd56ebf68e8161c733c0caa07a7a5509ed53" + +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" dependencies = [ - "base64 0.21.7", - "bitflags 2.4.1", + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", "serde", - "serde_derive", +] + +[[package]] +name = "rstest" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.104", + "unicode-ident", +] + +[[package]] +name = "rusqlite" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143" +dependencies = [ + "bitflags 2.9.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", ] [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -3088,16 +4239,72 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] -name = "rustix" -version = "0.38.21" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "bitflags 2.4.1", + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags 2.9.1", "errno", "libc", - "linux-raw-sys", - "windows-sys 0.48.0", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -3106,23 +4313,41 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" -[[package]] -name = "ruzstd" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3ffab8f9715a0d455df4bbb9d21e91135aab3cd3ca187af0cd0c3c3f868fdc" -dependencies = [ - "byteorder", - "thiserror-core", - "twox-hash", -] - [[package]] name = "ryu" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "safetensors" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93279b86b3de76f820a8854dd06cbc33cfa57a417b19c47f6a25280112fb1df" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "safetensors" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44560c11236a6130a46ce36c836a62936dc81ebf8c36a37947423571be0e55b6" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3132,6 +4357,34 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "sanitize-filename" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "sanitize-filename" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc984f4f9ceb736a7bb755c3e3bd17dc56370af2600c9780dcc48c66453da34d" +dependencies = [ + "regex", +] + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3139,36 +4392,124 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "serde" -version = "1.0.190" +name = "sdl2" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +checksum = "3b498da7d14d1ad6c839729bd4ad6fc11d90a57583605f3b4df2cd709a9cd380" +dependencies = [ + "bitflags 1.3.2", + "c_vec", + "lazy_static", + "libc", + "sdl2-sys", +] + +[[package]] +name = "sdl2-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951deab27af08ed9c6068b7b0d05a93c91f0a8eb16b6b816a5e73452a43521d3" +dependencies = [ + "cfg-if", + "cmake", + "libc", + "version-compare 0.1.1", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] -name = "serde_derive" -version = "1.0.190" +name = "serde_bytes" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.104", ] [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] +[[package]] +name = "serde_rusqlite" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e77d42c863496aee3253704aa621691088f718cd8800bd50bcdc19f9bd2a8ea" +dependencies = [ + "rusqlite", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3180,9 +4521,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" @@ -3196,9 +4537,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio", @@ -3207,13 +4548,26 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] +[[package]] +name = "simba" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa" +dependencies = [ + "approx", + "num-complex", + "num-traits", + "paste", + "wide", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -3221,19 +4575,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] -name = "slab" -version = "0.4.9" +name = "simd_helpers" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" dependencies = [ - "autocfg", + "quote", ] [[package]] -name = "slotmap" -version = "1.0.6" +name = "slab" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" dependencies = [ "version_check", ] @@ -3243,29 +4603,42 @@ name = "smallvec" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" dependencies = [ - "serde", + "lock_api", + "portable-atomic", ] [[package]] -name = "smol_str" -version = "0.2.0" +name = "spin" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74212e6bbe9a4352329b2f68ba3130c15a3f26fe88ff22dbdc6cdd58fa85e99c" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" dependencies = [ - "serde", + "lock_api", + "portable-atomic", ] [[package]] name = "spirv" -version = "0.2.0+1.5.4" +version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 1.3.2", - "num-traits", + "bitflags 2.9.1", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -3278,7 +4651,8 @@ version = "0.1.0" dependencies = [ "base64 0.21.7", "log", - "rand", + "merge", + "rand 0.8.5", "serde", "transpose", ] @@ -3290,25 +4664,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" [[package]] -name = "strum" -version = "0.25.0" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +dependencies = [ + "strum_macros 0.27.1", ] [[package]] name = "strum_macros" -version = "0.25.3" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", - "syn 2.0.38", + "syn 2.0.104", +] + +[[package]] +name = "strum_macros" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.104", ] [[package]] @@ -3317,12 +4719,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" -[[package]] -name = "svg_fmt" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb1df15f412ee2e9dfc1c504260fa695c1c3f10fe9f4a6ee2d2184d7d6450e2" - [[package]] name = "syn" version = "1.0.109" @@ -3336,9 +4732,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -3346,95 +4742,260 @@ dependencies = [ ] [[package]] -name = "sysinfo" -version = "0.29.10" +name = "synstructure" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a18d114d420ada3a891e6bc8e96a2023402203296a47cdd65083377dad18ba5" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "sysctl" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" +dependencies = [ + "bitflags 2.9.1", + "byteorder", + "enum-as-inner", + "libc", + "thiserror 1.0.69", + "walkdir", +] + +[[package]] +name = "sysctl" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" +dependencies = [ + "bitflags 2.9.1", + "byteorder", + "enum-as-inner", + "libc", + "thiserror 1.0.69", + "walkdir", +] + +[[package]] +name = "sysinfo" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" dependencies = [ - "cfg-if", "core-foundation-sys", "libc", + "memchr", "ntapi", - "once_cell", + "rayon", + "windows 0.57.0", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare 0.2.0", +] + +[[package]] +name = "systemstat" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668a4db78b439df482c238f559e4ea869017f9e62ef0a059c8bfcd841a4df544" +dependencies = [ + "bytesize", + "lazy_static", + "libc", + "nom", + "time", "winapi", ] [[package]] -name = "taffy" -version = "0.3.17" +name = "target-lexicon" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642c33c68313dd268701fe12e0de45a07e9aa4926c4986d244b511d5fe5c17c2" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tch" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa1ed622c8f13b0c42f8b1afa0e5e9ccccd82ecb6c0e904120722ab52fdc5234" dependencies = [ - "arrayvec", - "grid", - "num-traits", - "slotmap", + "half", + "lazy_static", + "libc", + "ndarray", + "rand 0.8.5", + "safetensors 0.3.3", + "thiserror 1.0.69", + "torch-sys", + "zip 0.6.6", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix 1.0.7", + "windows-sys 0.59.0", ] [[package]] name = "termcolor" -version = "1.3.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] +[[package]] +name = "text_placeholder" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5008f74a09742486ef0047596cf35df2b914e2a8dca5727fcb6ba6842a766b" +dependencies = [ + "hashbrown 0.13.2", + "serde", + "serde_json", +] + [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", ] [[package]] -name = "thiserror-core" -version = "1.0.38" +name = "thiserror" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d97345f6437bb2004cd58819d8a9ef8e36cdd7661c2abc4bbde0a7c40d9f497" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-core-impl", -] - -[[package]] -name = "thiserror-core-impl" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10ac1c5050e43014d16b2f94d0d2ce79e65ffdd8b38d8048f9c8f6a8a6da62ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "thiserror-impl 2.0.12", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.104", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "thread-tree" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbd370cb847953a25954d9f63e14824a36113f8c72eecf6eccef5dc4b45d630" +dependencies = [ + "crossbeam-channel", ] [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", ] [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] @@ -3446,27 +5007,66 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] -name = "toml_datetime" -version = "0.6.5" +name = "toml" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" -version = "0.19.15" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.0.2", + "indexmap", + "serde", + "serde_spanned", "toml_datetime", + "toml_write", "winnow", ] [[package]] -name = "tracing" -version = "0.1.40" +name = "toml_write" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "torch-sys" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef14f5d239e3d60f4919f536a5dfe1d4f71b27b7abf6fe6875fd3a4b22c2dcd5" +dependencies = [ + "anyhow", + "cc", + "libc", + "serde", + "serde_json", + "ureq", + "zip 0.6.6", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -3474,21 +5074,33 @@ dependencies = [ ] [[package]] -name = "tracing-attributes" -version = "0.1.27" +name = "tracing-appender" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror 1.0.69", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.104", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -3496,9 +5108,9 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", @@ -3507,33 +5119,18 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ - "matchers", "nu-ansi-term", - "once_cell", - "regex", "sharded-slab", "smallvec", "thread_local", - "tracing", "tracing-core", "tracing-log", ] -[[package]] -name = "tracing-wasm" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" -dependencies = [ - "tracing", - "tracing-subscriber", - "wasm-bindgen", -] - [[package]] name = "transpose" version = "0.2.2" @@ -3544,46 +5141,18 @@ dependencies = [ "strength_reduce", ] -[[package]] -name = "trictrac-client" -version = "0.1.0" -dependencies = [ - "anyhow", - "bevy", - "bevy_renet", - "bincode", - "renet", - "store", -] - [[package]] name = "trictrac-server" version = "0.1.0" dependencies = [ - "bincode", - "env_logger", + "bincode 1.3.3", + "env_logger 0.10.0", "log", "pico-args", "renet", "store", ] -[[package]] -name = "ttf-parser" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1" - -[[package]] -name = "twox-hash" -version = "1.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" -dependencies = [ - "cfg-if", - "static_assertions", -] - [[package]] name = "typenum" version = "1.17.0" @@ -3591,10 +5160,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] -name = "unicode-ident" -version = "1.0.12" +name = "ug" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "03719c61a91b51541f076dfdba45caacf750b230cefaa4b32d6f5411c3f7f437" +dependencies = [ + "gemm 0.18.2", + "half", + "libloading", + "memmap2", + "num", + "num-traits", + "num_cpus", + "rayon", + "safetensors 0.4.5", + "serde", + "thiserror 1.0.69", + "tracing", + "yoke", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] [[package]] name = "unicode-segmentation" @@ -3603,16 +5208,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] -name = "unicode-width" -version = "0.1.11" +name = "unicode-truncate" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "universal-hash" @@ -3625,44 +5247,122 @@ dependencies = [ ] [[package]] -name = "uuid" -version = "1.5.0" +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "ureq" +version = "2.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" dependencies = [ - "getrandom", + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "rustls-webpki", "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "rand 0.9.1", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", ] [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] -name = "vec_map" -version = "0.8.2" +name = "variadics_please" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "waker-fn" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -3675,47 +5375,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.87" +name = "wasi" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.104", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3723,56 +5434,86 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" - -[[package]] -name = "wayland-scanner" -version = "0.29.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ - "proc-macro2", - "quote", - "xml-rs", + "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "wgpu" -version = "0.16.3" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "480c965c9306872eb6255fa55e4b4953be55a8b64d57e61d7ff840d3dcc051cd" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ - "arrayvec", - "cfg-if", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.1", +] + +[[package]] +name = "webpki-roots" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "weezl" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" + +[[package]] +name = "wgpu" +version = "25.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8fb398f119472be4d80bc3647339f56eb63b2a331f6a3d16e25d8144197dd9" +dependencies = [ + "arrayvec 0.7.6", + "bitflags 2.9.1", + "cfg_aliases", + "document-features", + "hashbrown 0.15.4", "js-sys", "log", "naga", "parking_lot", + "portable-atomic", "profiling", "raw-window-handle", "smallvec", @@ -3787,85 +5528,132 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "0.16.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f478237b4bf0d5b70a39898a66fa67ca3a007d79f2520485b8b0c3dfc46f8c2" +checksum = "f7b882196f8368511d613c6aeec80655160db6646aebddf8328879a88d54e500" dependencies = [ - "arrayvec", + "arrayvec 0.7.6", + "bit-set", "bit-vec", - "bitflags 2.4.1", - "codespan-reporting", + "bitflags 2.9.1", + "cfg_aliases", + "document-features", + "hashbrown 0.15.4", + "indexmap", "log", "naga", + "once_cell", "parking_lot", + "portable-atomic", "profiling", "raw-window-handle", "rustc-hash", "smallvec", - "thiserror", - "web-sys", + "thiserror 2.0.12", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-windows-linux-android", "wgpu-hal", "wgpu-types", ] [[package]] -name = "wgpu-hal" -version = "0.16.2" +name = "wgpu-core-deps-apple" +version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecb3258078e936deee14fd4e0febe1cfe9bbb5ffef165cb60218d2ee5eb4448" +checksum = "cfd488b3239b6b7b185c3b045c39ca6bf8af34467a4c5de4e0b1a564135d093d" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09ad7aceb3818e52539acc679f049d3475775586f3f4e311c30165cf2c00445" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cba5fb5f7f9c98baa7c889d444f63ace25574833df56f5b817985f641af58e46" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "25.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f968767fe4d3d33747bbd1473ccd55bf0f6451f55d733b5597e67b5deab4ad17" dependencies = [ "android_system_properties", - "arrayvec", + "arrayvec 0.7.6", "ash", "bit-set", - "bitflags 2.4.1", + "bitflags 2.9.1", "block", + "bytemuck", + "cfg-if", + "cfg_aliases", "core-graphics-types", - "d3d12", - "foreign-types", "glow", + "glutin_wgl_sys", "gpu-alloc", "gpu-allocator", "gpu-descriptor", - "hassle-rs", + "hashbrown 0.15.4", "js-sys", "khronos-egl", "libc", - "libloading 0.8.1", + "libloading", "log", "metal", "naga", + "ndk-sys", "objc", + "ordered-float 4.6.0", "parking_lot", + "portable-atomic", "profiling", "range-alloc", "raw-window-handle", "renderdoc-sys", - "rustc-hash", "smallvec", - "thiserror", + "thiserror 2.0.12", "wasm-bindgen", "web-sys", "wgpu-types", - "winapi", + "windows 0.58.0", + "windows-core 0.58.0", ] [[package]] name = "wgpu-types" -version = "0.16.1" +version = "25.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c153280bb108c2979eb5c7391cb18c56642dd3c072e55f52065e13e2a1252a" +checksum = "2aa49460c2a8ee8edba3fca54325540d904dd85b2e086ada762767e17d06e8bc" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.9.1", + "bytemuck", "js-sys", + "log", + "thiserror 2.0.12", "web-sys", ] [[package]] -name = "widestring" -version = "1.0.2" +name = "wide" +version = "0.7.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] [[package]] name = "winapi" @@ -3900,81 +5688,119 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.44.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" dependencies = [ - "windows-targets 0.42.2", + "windows-core 0.57.0", + "windows-targets 0.52.6", ] [[package]] name = "windows" -version = "0.46.0" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-targets 0.48.5", -] - -[[package]] -name = "windows" -version = "0.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" -dependencies = [ - "windows-core", - "windows-targets 0.48.5", + "windows-core 0.58.0", + "windows-targets 0.52.6", ] [[package]] name = "windows-core" -version = "0.51.1" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ - "windows-targets 0.48.5", + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", ] [[package]] name = "windows-implement" -version = "0.48.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2ee588991b9e7e6c8338edf3333fbe4da35dc72092643958ebb43f0ab2c49c" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.104", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", ] [[package]] name = "windows-interface" -version = "0.48.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6fb8df20c9bcaa8ad6ab513f7b40104840c8867d5751126e4df3b08388d0cc7" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.104", ] [[package]] -name = "windows-sys" -version = "0.45.0" +name = "windows-interface" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ - "windows-targets 0.42.2", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", ] [[package]] @@ -3987,18 +5813,21 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.42.2" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -4017,10 +5846,20 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] [[package]] name = "windows_aarch64_gnullvm" @@ -4029,10 +5868,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" +name = "windows_aarch64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -4041,10 +5880,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] -name = "windows_i686_gnu" -version = "0.42.2" +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -4053,10 +5892,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] -name = "windows_i686_msvc" -version = "0.42.2" +name = "windows_i686_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -4065,10 +5910,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -4077,10 +5922,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" +name = "windows_x86_64_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -4089,10 +5934,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -4101,66 +5946,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "winit" -version = "0.28.7" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9596d90b45384f5281384ab204224876e8e8bf7d58366d9b795ad99aa9894b94" -dependencies = [ - "android-activity", - "bitflags 1.3.2", - "cfg_aliases", - "core-foundation", - "core-graphics", - "dispatch", - "instant", - "libc", - "log", - "mio", - "ndk", - "objc2", - "once_cell", - "orbclient", - "percent-encoding", - "raw-window-handle", - "redox_syscall 0.3.5", - "wasm-bindgen", - "wayland-scanner", - "web-sys", - "windows-sys 0.45.0", - "x11-dl", -] +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.5.17" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" dependencies = [ "memchr", ] [[package]] -name = "x11-dl" -version = "2.21.0" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "libc", - "once_cell", - "pkg-config", + "bitflags 2.9.1", ] [[package]] -name = "xi-unicode" -version = "0.3.0" +name = "wrapcenum-derive" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" +checksum = "a76ff259533532054cfbaefb115c613203c73707017459206380f03b3b3f266e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.104", +] [[package]] name = "xml-rs" -version = "0.8.19" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" +checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" [[package]] name = "yansi" @@ -4169,23 +5994,68 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] -name = "zerocopy" -version = "0.7.18" +name = "yoke" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7d7c7970ca2215b8c1ccf4d4f354c4733201dfaaba72d44ae5b37472e4901" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "synstructure 0.13.2", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.18" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b27b1bb92570f989aac0ab7e9cbfbacdd65973f7ee920d9f0e71ebac878fd0b" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.104", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "synstructure 0.13.2", ] [[package]] @@ -4193,3 +6063,91 @@ name = "zeroize" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zip" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cc23c04387f4da0374be4533ad1208cbb091d5c11d070dfef13676ad6497164" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "indexmap", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7384255a918371b5af158218d131530f694de9ad3815ebdd0453a940485cb0fa" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index e521f37..6068644 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,4 @@ [workspace] -resolver="2" +resolver = "2" -members = [ - "client", - "client_tui", - "client_cli", - "server", - "store" -] +members = ["client_tui", "client_cli", "bot", "server", "store"] diff --git a/Makefile b/Makefile deleted file mode 100644 index 457653a..0000000 --- a/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -shell: - devenv shell - # nix develop -startserver: - RUST_LOG=trictrac_server cargo run --bin trictrac-server -startclient1: - RUST_LOG=trictrac_client cargo run --bin=trictrac-client Titi -startclient2: - RUST_LOG=trictrac_client cargo run --bin=trictrac-client Titu -startclienttui: - RUST_LOG=trictrac_client cargo run --bin=client_tui Tutu diff --git a/bot/Cargo.toml b/bot/Cargo.toml new file mode 100644 index 0000000..21e0128 --- /dev/null +++ b/bot/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "bot" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[[bin]] +name = "burn_train" +path = "src/burnrl/main.rs" + +[[bin]] +name = "train_dqn_simple" +path = "src/dqn_simple/main.rs" + +[dependencies] +pretty_assertions = "1.4.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +store = { path = "../store" } +rand = "0.8" +env_logger = "0.10" +burn = { version = "0.17", features = ["ndarray", "autodiff"] } +burn-rl = { git = "https://github.com/yunjhongwu/burn-rl-examples.git", package = "burn-rl" } +log = "0.4.20" +confy = "1.0.0" +board-game = "0.8.2" +internal-iterator = "0.2.3" diff --git a/bot/scripts/train.sh b/bot/scripts/train.sh new file mode 100755 index 0000000..d96e690 --- /dev/null +++ b/bot/scripts/train.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash + +ROOT="$(cd "$(dirname "$0")" && pwd)/../.." +LOGS_DIR="$ROOT/bot/models/logs" + +CFG_SIZE=17 +BINBOT=burn_train +# BINBOT=train_ppo_burn +# BINBOT=train_dqn_burn +# BINBOT=train_dqn_burn_big +# BINBOT=train_dqn_burn_before +OPPONENT="random" + +PLOT_EXT="png" + +train() { + ALGO=$1 + cargo build --release --bin=$BINBOT + NAME="$(date +%Y-%m-%d_%H:%M:%S)" + LOGS="$LOGS_DIR/$ALGO/$NAME.out" + mkdir -p "$LOGS_DIR/$ALGO" + LD_LIBRARY_PATH="$ROOT/target/release" "$ROOT/target/release/$BINBOT" $ALGO | tee "$LOGS" +} + +plot() { + ALGO=$1 + NAME=$(ls -rt "$LOGS_DIR/$ALGO" | grep -v png | tail -n 1) + LOGS="$LOGS_DIR/$ALGO/$NAME" + cfgs=$(grep -v "info:" "$LOGS" | head -n $CFG_SIZE) + for cfg in $cfgs; do + eval "$cfg" + done + + tail -n +$((CFG_SIZE + 2)) "$LOGS" | + grep -v "info:" | + awk -F '[ ,]' '{print $5}' | + feedgnuplot --lines --points --unset grid --title "adv = $OPPONENT ; density = $dense_size ; decay = $eps_decay ; max steps = $max_steps" --terminal $PLOT_EXT >"$LOGS_DIR/$ALGO/$NAME.$PLOT_EXT" +} + +if [[ -z "$1" ]]; then + echo "Usage : train [plot] " +elif [ "$1" = "plot" ]; then + if [[ -z "$2" ]]; then + echo "Usage : train [plot] " + else + plot $2 + fi +else + train $1 +fi diff --git a/bot/scripts/trainValid.sh b/bot/scripts/trainValid.sh new file mode 100755 index 0000000..55424a2 --- /dev/null +++ b/bot/scripts/trainValid.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env sh + +ROOT="$(cd "$(dirname "$0")" && pwd)/../.." +LOGS_DIR="$ROOT/bot/models/logs" + +CFG_SIZE=11 +OPPONENT="random" + +PLOT_EXT="png" + +train() { + cargo build --release --bin=train_dqn_burn_valid + NAME="trainValid_$(date +%Y-%m-%d_%H:%M:%S)" + LOGS="$LOGS_DIR/$NAME.out" + mkdir -p "$LOGS_DIR" + LD_LIBRARY_PATH="$ROOT/target/release" "$ROOT/target/release/train_dqn_burn_valid" | tee "$LOGS" +} + +plot() { + NAME=$(ls -rt "$LOGS_DIR" | grep -v "png" | tail -n 1) + LOGS="$LOGS_DIR/$NAME" + cfgs=$(head -n $CFG_SIZE "$LOGS") + for cfg in $cfgs; do + eval "$cfg" + done + + # tail -n +$((CFG_SIZE + 2)) "$LOGS" + tail -n +$((CFG_SIZE + 2)) "$LOGS" | + grep -v "info:" | + awk -F '[ ,]' '{print $5}' | + feedgnuplot --lines --points --unset grid --title "adv = $OPPONENT ; density = $dense_size ; decay = $eps_decay ; max steps = $max_steps" --terminal $PLOT_EXT >"$LOGS_DIR/$OPPONENT-$dense_size-$eps_decay-$max_steps-$NAME.$PLOT_EXT" +} + +avg() { + NAME=$(ls -rt "$LOGS_DIR" | grep -v "png" | tail -n 1) + LOGS="$LOGS_DIR/$NAME" + echo $LOGS + tail -n +$((CFG_SIZE + 2)) "$LOGS" | + grep -v "info:" | + awk -F '[ ,]' '{print $5}' | awk '{ sum += $1; n++ } END { if (n > 0) print sum / n; }' +} + +if [ "$1" = "plot" ]; then + plot +elif [ "$1" = "avg" ]; then + avg +else + train +fi diff --git a/bot/src/burnrl/algos/dqn.rs b/bot/src/burnrl/algos/dqn.rs new file mode 100644 index 0000000..efec37e --- /dev/null +++ b/bot/src/burnrl/algos/dqn.rs @@ -0,0 +1,195 @@ +use crate::burnrl::environment::TrictracEnvironment; +use crate::burnrl::utils::{soft_update_linear, Config}; +use burn::backend::{ndarray::NdArrayDevice, NdArray}; +use burn::module::Module; +use burn::nn::{Linear, LinearConfig}; +use burn::optim::AdamWConfig; +use burn::record::{CompactRecorder, Recorder}; +use burn::tensor::activation::relu; +use burn::tensor::backend::{AutodiffBackend, Backend}; +use burn::tensor::Tensor; +use burn_rl::agent::DQN; +use burn_rl::agent::{DQNModel, DQNTrainingConfig}; +use burn_rl::base::{Action, Agent, ElemType, Environment, Memory, Model, State}; +use std::time::SystemTime; + +#[derive(Module, Debug)] +pub struct Net { + linear_0: Linear, + linear_1: Linear, + linear_2: Linear, +} + +impl Net { + #[allow(unused)] + pub fn new(input_size: usize, dense_size: usize, output_size: usize) -> Self { + Self { + linear_0: LinearConfig::new(input_size, dense_size).init(&Default::default()), + linear_1: LinearConfig::new(dense_size, dense_size).init(&Default::default()), + linear_2: LinearConfig::new(dense_size, output_size).init(&Default::default()), + } + } + + fn consume(self) -> (Linear, Linear, Linear) { + (self.linear_0, self.linear_1, self.linear_2) + } +} + +impl Model, Tensor> for Net { + fn forward(&self, input: Tensor) -> Tensor { + let layer_0_output = relu(self.linear_0.forward(input)); + let layer_1_output = relu(self.linear_1.forward(layer_0_output)); + + relu(self.linear_2.forward(layer_1_output)) + } + + fn infer(&self, input: Tensor) -> Tensor { + self.forward(input) + } +} + +impl DQNModel for Net { + fn soft_update(this: Self, that: &Self, tau: ElemType) -> Self { + let (linear_0, linear_1, linear_2) = this.consume(); + + Self { + linear_0: soft_update_linear(linear_0, &that.linear_0, tau), + linear_1: soft_update_linear(linear_1, &that.linear_1, tau), + linear_2: soft_update_linear(linear_2, &that.linear_2, tau), + } + } +} + +#[allow(unused)] +const MEMORY_SIZE: usize = 8192; + +type MyAgent = DQN>; + +#[allow(unused)] +// pub fn run, B: AutodiffBackend>( +pub fn run< + E: Environment + AsMut, + B: AutodiffBackend, +>( + conf: &Config, + visualized: bool, + // ) -> DQN> { +) -> impl Agent { + let mut env = E::new(visualized); + // env.as_mut().min_steps = conf.min_steps; + env.as_mut().max_steps = conf.max_steps; + + let model = Net::::new( + <::StateType as State>::size(), + conf.dense_size, + <::ActionType as Action>::size(), + ); + + let mut agent = MyAgent::new(model); + + // let config = DQNTrainingConfig::default(); + let config = DQNTrainingConfig { + gamma: conf.gamma, + tau: conf.tau, + learning_rate: conf.learning_rate, + batch_size: conf.batch_size, + clip_grad: Some(burn::grad_clipping::GradientClippingConfig::Value( + conf.clip_grad, + )), + }; + + let mut memory = Memory::::default(); + + let mut optimizer = AdamWConfig::new() + .with_grad_clipping(config.clip_grad.clone()) + .init(); + + let mut policy_net = agent.model().as_ref().unwrap().clone(); + + let mut step = 0_usize; + + for episode in 0..conf.num_episodes { + let mut episode_done = false; + let mut episode_reward: ElemType = 0.0; + let mut episode_duration = 0_usize; + let mut state = env.state(); + let mut now = SystemTime::now(); + + while !episode_done { + let eps_threshold = conf.eps_end + + (conf.eps_start - conf.eps_end) * f64::exp(-(step as f64) / conf.eps_decay); + let action = + DQN::>::react_with_exploration(&policy_net, state, eps_threshold); + let snapshot = env.step(action); + + episode_reward += + <::RewardType as Into>::into(snapshot.reward().clone()); + + memory.push( + state, + *snapshot.state(), + action, + snapshot.reward().clone(), + snapshot.done(), + ); + + if config.batch_size < memory.len() { + policy_net = + agent.train::(policy_net, &memory, &mut optimizer, &config); + } + + step += 1; + episode_duration += 1; + + if snapshot.done() || episode_duration >= conf.max_steps { + let envmut = env.as_mut(); + let goodmoves_ratio = ((envmut.goodmoves_count as f32 / episode_duration as f32) + * 100.0) + .round() as u32; + println!( + "{{\"episode\": {episode}, \"reward\": {episode_reward:.4}, \"steps count\": {episode_duration}, \"epsilon\": {eps_threshold:.3}, \"goodmoves\": {}, \"ratio\": {}%, \"rollpoints\":{}, \"duration\": {}}}", + envmut.goodmoves_count, + goodmoves_ratio, + envmut.pointrolls_count, + now.elapsed().unwrap().as_secs(), + ); + env.reset(); + episode_done = true; + now = SystemTime::now(); + } else { + state = *snapshot.state(); + } + } + } + let valid_agent = agent.valid(); + if let Some(path) = &conf.save_path { + save_model(valid_agent.model().as_ref().unwrap(), path); + } + valid_agent +} + +pub fn save_model(model: &Net>, path: &String) { + let recorder = CompactRecorder::new(); + let model_path = format!("{path}.mpk"); + println!("info: Modèle de validation sauvegardé : {model_path}"); + recorder + .record(model.clone().into_record(), model_path.into()) + .unwrap(); +} + +pub fn load_model(dense_size: usize, path: &String) -> Option>> { + let model_path = format!("{path}.mpk"); + // println!("Chargement du modèle depuis : {model_path}"); + + CompactRecorder::new() + .load(model_path.into(), &NdArrayDevice::default()) + .map(|record| { + Net::new( + ::StateType::size(), + dense_size, + ::ActionType::size(), + ) + .load_record(record) + }) + .ok() +} diff --git a/bot/src/burnrl/algos/dqn_big.rs b/bot/src/burnrl/algos/dqn_big.rs new file mode 100644 index 0000000..7e8951f --- /dev/null +++ b/bot/src/burnrl/algos/dqn_big.rs @@ -0,0 +1,194 @@ +use crate::burnrl::environment_big::TrictracEnvironment; +use crate::burnrl::utils::{soft_update_linear, Config}; +use burn::backend::{ndarray::NdArrayDevice, NdArray}; +use burn::module::Module; +use burn::nn::{Linear, LinearConfig}; +use burn::optim::AdamWConfig; +use burn::record::{CompactRecorder, Recorder}; +use burn::tensor::activation::relu; +use burn::tensor::backend::{AutodiffBackend, Backend}; +use burn::tensor::Tensor; +use burn_rl::agent::DQN; +use burn_rl::agent::{DQNModel, DQNTrainingConfig}; +use burn_rl::base::{Action, Agent, ElemType, Environment, Memory, Model, State}; +use std::time::SystemTime; + +#[derive(Module, Debug)] +pub struct Net { + linear_0: Linear, + linear_1: Linear, + linear_2: Linear, +} + +impl Net { + #[allow(unused)] + pub fn new(input_size: usize, dense_size: usize, output_size: usize) -> Self { + Self { + linear_0: LinearConfig::new(input_size, dense_size).init(&Default::default()), + linear_1: LinearConfig::new(dense_size, dense_size).init(&Default::default()), + linear_2: LinearConfig::new(dense_size, output_size).init(&Default::default()), + } + } + + fn consume(self) -> (Linear, Linear, Linear) { + (self.linear_0, self.linear_1, self.linear_2) + } +} + +impl Model, Tensor> for Net { + fn forward(&self, input: Tensor) -> Tensor { + let layer_0_output = relu(self.linear_0.forward(input)); + let layer_1_output = relu(self.linear_1.forward(layer_0_output)); + + relu(self.linear_2.forward(layer_1_output)) + } + + fn infer(&self, input: Tensor) -> Tensor { + self.forward(input) + } +} + +impl DQNModel for Net { + fn soft_update(this: Self, that: &Self, tau: ElemType) -> Self { + let (linear_0, linear_1, linear_2) = this.consume(); + + Self { + linear_0: soft_update_linear(linear_0, &that.linear_0, tau), + linear_1: soft_update_linear(linear_1, &that.linear_1, tau), + linear_2: soft_update_linear(linear_2, &that.linear_2, tau), + } + } +} + +#[allow(unused)] +const MEMORY_SIZE: usize = 8192; + +type MyAgent = DQN>; + +#[allow(unused)] +// pub fn run, B: AutodiffBackend>( +pub fn run< + E: Environment + AsMut, + B: AutodiffBackend, +>( + conf: &Config, + visualized: bool, + // ) -> DQN> { +) -> impl Agent { + let mut env = E::new(visualized); + env.as_mut().max_steps = conf.max_steps; + + let model = Net::::new( + <::StateType as State>::size(), + conf.dense_size, + <::ActionType as Action>::size(), + ); + + let mut agent = MyAgent::new(model); + + // let config = DQNTrainingConfig::default(); + let config = DQNTrainingConfig { + gamma: conf.gamma, + tau: conf.tau, + learning_rate: conf.learning_rate, + batch_size: conf.batch_size, + clip_grad: Some(burn::grad_clipping::GradientClippingConfig::Value( + conf.clip_grad, + )), + }; + + let mut memory = Memory::::default(); + + let mut optimizer = AdamWConfig::new() + .with_grad_clipping(config.clip_grad.clone()) + .init(); + + let mut policy_net = agent.model().as_ref().unwrap().clone(); + + let mut step = 0_usize; + + for episode in 0..conf.num_episodes { + let mut episode_done = false; + let mut episode_reward: ElemType = 0.0; + let mut episode_duration = 0_usize; + let mut state = env.state(); + let mut now = SystemTime::now(); + + while !episode_done { + let eps_threshold = conf.eps_end + + (conf.eps_start - conf.eps_end) * f64::exp(-(step as f64) / conf.eps_decay); + let action = + DQN::>::react_with_exploration(&policy_net, state, eps_threshold); + let snapshot = env.step(action); + + episode_reward += + <::RewardType as Into>::into(snapshot.reward().clone()); + + memory.push( + state, + *snapshot.state(), + action, + snapshot.reward().clone(), + snapshot.done(), + ); + + if config.batch_size < memory.len() { + policy_net = + agent.train::(policy_net, &memory, &mut optimizer, &config); + } + + step += 1; + episode_duration += 1; + + if snapshot.done() || episode_duration >= conf.max_steps { + let envmut = env.as_mut(); + let goodmoves_ratio = ((envmut.goodmoves_count as f32 / episode_duration as f32) + * 100.0) + .round() as u32; + println!( + "{{\"episode\": {episode}, \"reward\": {episode_reward:.4}, \"steps count\": {episode_duration}, \"epsilon\": {eps_threshold:.3}, \"goodmoves\": {}, \"ratio\": {}%, \"rollpoints\":{}, \"duration\": {}}}", + envmut.goodmoves_count, + goodmoves_ratio, + envmut.pointrolls_count, + now.elapsed().unwrap().as_secs(), + ); + env.reset(); + episode_done = true; + now = SystemTime::now(); + } else { + state = *snapshot.state(); + } + } + } + let valid_agent = agent.valid(); + if let Some(path) = &conf.save_path { + save_model(valid_agent.model().as_ref().unwrap(), path); + } + valid_agent +} + +pub fn save_model(model: &Net>, path: &String) { + let recorder = CompactRecorder::new(); + let model_path = format!("{path}.mpk"); + println!("info: Modèle de validation sauvegardé : {model_path}"); + recorder + .record(model.clone().into_record(), model_path.into()) + .unwrap(); +} + +pub fn load_model(dense_size: usize, path: &String) -> Option>> { + let model_path = format!("{path}.mpk"); + // println!("Chargement du modèle depuis : {model_path}"); + + CompactRecorder::new() + .load(model_path.into(), &NdArrayDevice::default()) + .map(|record| { + Net::new( + ::StateType::size(), + dense_size, + ::ActionType::size(), + ) + .load_record(record) + }) + .ok() +} diff --git a/bot/src/burnrl/algos/dqn_valid.rs b/bot/src/burnrl/algos/dqn_valid.rs new file mode 100644 index 0000000..6198100 --- /dev/null +++ b/bot/src/burnrl/algos/dqn_valid.rs @@ -0,0 +1,189 @@ +use crate::burnrl::environment_valid::TrictracEnvironment; +use crate::burnrl::utils::{soft_update_linear, Config}; +use burn::backend::{ndarray::NdArrayDevice, NdArray}; +use burn::module::Module; +use burn::nn::{Linear, LinearConfig}; +use burn::optim::AdamWConfig; +use burn::record::{CompactRecorder, Recorder}; +use burn::tensor::activation::relu; +use burn::tensor::backend::{AutodiffBackend, Backend}; +use burn::tensor::Tensor; +use burn_rl::agent::DQN; +use burn_rl::agent::{DQNModel, DQNTrainingConfig}; +use burn_rl::base::{Action, Agent, ElemType, Environment, Memory, Model, State}; +use std::time::SystemTime; + +#[derive(Module, Debug)] +pub struct Net { + linear_0: Linear, + linear_1: Linear, + linear_2: Linear, +} + +impl Net { + #[allow(unused)] + pub fn new(input_size: usize, dense_size: usize, output_size: usize) -> Self { + Self { + linear_0: LinearConfig::new(input_size, dense_size).init(&Default::default()), + linear_1: LinearConfig::new(dense_size, dense_size).init(&Default::default()), + linear_2: LinearConfig::new(dense_size, output_size).init(&Default::default()), + } + } + + fn consume(self) -> (Linear, Linear, Linear) { + (self.linear_0, self.linear_1, self.linear_2) + } +} + +impl Model, Tensor> for Net { + fn forward(&self, input: Tensor) -> Tensor { + let layer_0_output = relu(self.linear_0.forward(input)); + let layer_1_output = relu(self.linear_1.forward(layer_0_output)); + + relu(self.linear_2.forward(layer_1_output)) + } + + fn infer(&self, input: Tensor) -> Tensor { + self.forward(input) + } +} + +impl DQNModel for Net { + fn soft_update(this: Self, that: &Self, tau: ElemType) -> Self { + let (linear_0, linear_1, linear_2) = this.consume(); + + Self { + linear_0: soft_update_linear(linear_0, &that.linear_0, tau), + linear_1: soft_update_linear(linear_1, &that.linear_1, tau), + linear_2: soft_update_linear(linear_2, &that.linear_2, tau), + } + } +} + +#[allow(unused)] +const MEMORY_SIZE: usize = 8192; + +type MyAgent = DQN>; + +#[allow(unused)] +// pub fn run, B: AutodiffBackend>( +pub fn run< + E: Environment + AsMut, + B: AutodiffBackend, +>( + conf: &Config, + visualized: bool, + // ) -> DQN> { +) -> impl Agent { + let mut env = E::new(visualized); + env.as_mut().max_steps = conf.max_steps; + + let model = Net::::new( + <::StateType as State>::size(), + conf.dense_size, + <::ActionType as Action>::size(), + ); + + let mut agent = MyAgent::new(model); + + // let config = DQNTrainingConfig::default(); + let config = DQNTrainingConfig { + gamma: conf.gamma, + tau: conf.tau, + learning_rate: conf.learning_rate, + batch_size: conf.batch_size, + clip_grad: Some(burn::grad_clipping::GradientClippingConfig::Value( + conf.clip_grad, + )), + }; + + let mut memory = Memory::::default(); + + let mut optimizer = AdamWConfig::new() + .with_grad_clipping(config.clip_grad.clone()) + .init(); + + let mut policy_net = agent.model().as_ref().unwrap().clone(); + + let mut step = 0_usize; + + for episode in 0..conf.num_episodes { + let mut episode_done = false; + let mut episode_reward: ElemType = 0.0; + let mut episode_duration = 0_usize; + let mut state = env.state(); + let mut now = SystemTime::now(); + + while !episode_done { + let eps_threshold = conf.eps_end + + (conf.eps_start - conf.eps_end) * f64::exp(-(step as f64) / conf.eps_decay); + let action = + DQN::>::react_with_exploration(&policy_net, state, eps_threshold); + let snapshot = env.step(action); + + episode_reward += + <::RewardType as Into>::into(snapshot.reward().clone()); + + memory.push( + state, + *snapshot.state(), + action, + snapshot.reward().clone(), + snapshot.done(), + ); + + if config.batch_size < memory.len() { + policy_net = + agent.train::(policy_net, &memory, &mut optimizer, &config); + } + + step += 1; + episode_duration += 1; + + if snapshot.done() || episode_duration >= conf.max_steps { + let envmut = env.as_mut(); + println!( + "{{\"episode\": {episode}, \"reward\": {episode_reward:.4}, \"steps count\": {episode_duration}, \"epsilon\": {eps_threshold:.3}, \"rollpoints\":{}, \"duration\": {}}}", + envmut.pointrolls_count, + now.elapsed().unwrap().as_secs(), + ); + env.reset(); + episode_done = true; + now = SystemTime::now(); + } else { + state = *snapshot.state(); + } + } + } + let valid_agent = agent.valid(); + if let Some(path) = &conf.save_path { + save_model(valid_agent.model().as_ref().unwrap(), path); + } + valid_agent +} + +pub fn save_model(model: &Net>, path: &String) { + let recorder = CompactRecorder::new(); + let model_path = format!("{path}.mpk"); + println!("info: Modèle de validation sauvegardé : {model_path}"); + recorder + .record(model.clone().into_record(), model_path.into()) + .unwrap(); +} + +pub fn load_model(dense_size: usize, path: &String) -> Option>> { + let model_path = format!("{path}.mpk"); + // println!("Chargement du modèle depuis : {model_path}"); + + CompactRecorder::new() + .load(model_path.into(), &NdArrayDevice::default()) + .map(|record| { + Net::new( + ::StateType::size(), + dense_size, + ::ActionType::size(), + ) + .load_record(record) + }) + .ok() +} diff --git a/bot/src/burnrl/algos/mod.rs b/bot/src/burnrl/algos/mod.rs new file mode 100644 index 0000000..af13327 --- /dev/null +++ b/bot/src/burnrl/algos/mod.rs @@ -0,0 +1,9 @@ +pub mod dqn; +pub mod dqn_big; +pub mod dqn_valid; +pub mod ppo; +pub mod ppo_big; +pub mod ppo_valid; +pub mod sac; +pub mod sac_big; +pub mod sac_valid; diff --git a/bot/src/burnrl/algos/ppo.rs b/bot/src/burnrl/algos/ppo.rs new file mode 100644 index 0000000..df6818c --- /dev/null +++ b/bot/src/burnrl/algos/ppo.rs @@ -0,0 +1,191 @@ +use crate::burnrl::environment::TrictracEnvironment; +use crate::burnrl::utils::Config; +use burn::backend::{ndarray::NdArrayDevice, NdArray}; +use burn::module::Module; +use burn::nn::{Initializer, Linear, LinearConfig}; +use burn::optim::AdamWConfig; +use burn::record::{CompactRecorder, Recorder}; +use burn::tensor::activation::{relu, softmax}; +use burn::tensor::backend::{AutodiffBackend, Backend}; +use burn::tensor::Tensor; +use burn_rl::agent::{PPOModel, PPOOutput, PPOTrainingConfig, PPO}; +use burn_rl::base::{Action, Agent, ElemType, Environment, Memory, Model, State}; +use std::env; +use std::fs; +use std::time::SystemTime; + +#[derive(Module, Debug)] +pub struct Net { + linear: Linear, + linear_actor: Linear, + linear_critic: Linear, +} + +impl Net { + #[allow(unused)] + pub fn new(input_size: usize, dense_size: usize, output_size: usize) -> Self { + let initializer = Initializer::XavierUniform { gain: 1.0 }; + Self { + linear: LinearConfig::new(input_size, dense_size) + .with_initializer(initializer.clone()) + .init(&Default::default()), + linear_actor: LinearConfig::new(dense_size, output_size) + .with_initializer(initializer.clone()) + .init(&Default::default()), + linear_critic: LinearConfig::new(dense_size, 1) + .with_initializer(initializer) + .init(&Default::default()), + } + } +} + +impl Model, PPOOutput, Tensor> for Net { + fn forward(&self, input: Tensor) -> PPOOutput { + let layer_0_output = relu(self.linear.forward(input)); + let policies = softmax(self.linear_actor.forward(layer_0_output.clone()), 1); + let values = self.linear_critic.forward(layer_0_output); + + PPOOutput::::new(policies, values) + } + + fn infer(&self, input: Tensor) -> Tensor { + let layer_0_output = relu(self.linear.forward(input)); + softmax(self.linear_actor.forward(layer_0_output.clone()), 1) + } +} + +impl PPOModel for Net {} +#[allow(unused)] +const MEMORY_SIZE: usize = 512; + +type MyAgent = PPO>; + +#[allow(unused)] +pub fn run< + E: Environment + AsMut, + B: AutodiffBackend, +>( + conf: &Config, + visualized: bool, + // ) -> PPO> { +) -> impl Agent { + let mut env = E::new(visualized); + env.as_mut().max_steps = conf.max_steps; + + let mut model = Net::::new( + <::StateType as State>::size(), + conf.dense_size, + <::ActionType as Action>::size(), + ); + let agent = MyAgent::default(); + let config = PPOTrainingConfig { + gamma: conf.gamma, + lambda: conf.lambda, + epsilon_clip: conf.epsilon_clip, + critic_weight: conf.critic_weight, + entropy_weight: conf.entropy_weight, + learning_rate: conf.learning_rate, + epochs: conf.epochs, + batch_size: conf.batch_size, + clip_grad: Some(burn::grad_clipping::GradientClippingConfig::Value( + conf.clip_grad, + )), + }; + + let mut optimizer = AdamWConfig::new() + .with_grad_clipping(config.clip_grad.clone()) + .init(); + let mut memory = Memory::::default(); + for episode in 0..conf.num_episodes { + let mut episode_done = false; + let mut episode_reward = 0.0; + let mut episode_duration = 0_usize; + let mut now = SystemTime::now(); + + env.reset(); + while !episode_done { + let state = env.state(); + if let Some(action) = MyAgent::::react_with_model(&state, &model) { + let snapshot = env.step(action); + episode_reward += <::RewardType as Into>::into( + snapshot.reward().clone(), + ); + + memory.push( + state, + *snapshot.state(), + action, + snapshot.reward().clone(), + snapshot.done(), + ); + + episode_duration += 1; + episode_done = snapshot.done() || episode_duration >= conf.max_steps; + } + } + println!( + "{{\"episode\": {episode}, \"reward\": {episode_reward:.4}, \"steps count\": {episode_duration}, \"duration\": {}}}", + now.elapsed().unwrap().as_secs(), + ); + + now = SystemTime::now(); + model = MyAgent::train::(model, &memory, &mut optimizer, &config); + memory.clear(); + } + + if let Some(path) = &conf.save_path { + let device = NdArrayDevice::default(); + let recorder = CompactRecorder::new(); + let tmp_path = env::temp_dir().join("tmp_model.mpk"); + + // Save the trained model (backend B) to a temporary file + recorder + .record(model.clone().into_record(), tmp_path.clone()) + .expect("Failed to save temporary model"); + + // Create a new model instance with the target backend (NdArray) + let model_to_save: Net> = Net::new( + <::StateType as State>::size(), + conf.dense_size, + <::ActionType as Action>::size(), + ); + + // Load the record from the temporary file into the new model + let record = recorder + .load(tmp_path.clone(), &device) + .expect("Failed to load temporary model"); + let model_with_loaded_weights = model_to_save.load_record(record); + + // Clean up the temporary file + fs::remove_file(tmp_path).expect("Failed to remove temporary model file"); + + save_model(&model_with_loaded_weights, path); + } + agent.valid(model) +} + +pub fn save_model(model: &Net>, path: &String) { + let recorder = CompactRecorder::new(); + let model_path = format!("{path}.mpk"); + println!("info: Modèle de validation sauvegardé : {model_path}"); + recorder + .record(model.clone().into_record(), model_path.into()) + .unwrap(); +} + +pub fn load_model(dense_size: usize, path: &String) -> Option>> { + let model_path = format!("{path}.mpk"); + // println!("Chargement du modèle depuis : {model_path}"); + + CompactRecorder::new() + .load(model_path.into(), &NdArrayDevice::default()) + .map(|record| { + Net::new( + ::StateType::size(), + dense_size, + ::ActionType::size(), + ) + .load_record(record) + }) + .ok() +} diff --git a/bot/src/burnrl/algos/ppo_big.rs b/bot/src/burnrl/algos/ppo_big.rs new file mode 100644 index 0000000..ab860ee --- /dev/null +++ b/bot/src/burnrl/algos/ppo_big.rs @@ -0,0 +1,191 @@ +use crate::burnrl::environment_big::TrictracEnvironment; +use crate::burnrl::utils::Config; +use burn::backend::{ndarray::NdArrayDevice, NdArray}; +use burn::module::Module; +use burn::nn::{Initializer, Linear, LinearConfig}; +use burn::optim::AdamWConfig; +use burn::record::{CompactRecorder, Recorder}; +use burn::tensor::activation::{relu, softmax}; +use burn::tensor::backend::{AutodiffBackend, Backend}; +use burn::tensor::Tensor; +use burn_rl::agent::{PPOModel, PPOOutput, PPOTrainingConfig, PPO}; +use burn_rl::base::{Action, Agent, ElemType, Environment, Memory, Model, State}; +use std::env; +use std::fs; +use std::time::SystemTime; + +#[derive(Module, Debug)] +pub struct Net { + linear: Linear, + linear_actor: Linear, + linear_critic: Linear, +} + +impl Net { + #[allow(unused)] + pub fn new(input_size: usize, dense_size: usize, output_size: usize) -> Self { + let initializer = Initializer::XavierUniform { gain: 1.0 }; + Self { + linear: LinearConfig::new(input_size, dense_size) + .with_initializer(initializer.clone()) + .init(&Default::default()), + linear_actor: LinearConfig::new(dense_size, output_size) + .with_initializer(initializer.clone()) + .init(&Default::default()), + linear_critic: LinearConfig::new(dense_size, 1) + .with_initializer(initializer) + .init(&Default::default()), + } + } +} + +impl Model, PPOOutput, Tensor> for Net { + fn forward(&self, input: Tensor) -> PPOOutput { + let layer_0_output = relu(self.linear.forward(input)); + let policies = softmax(self.linear_actor.forward(layer_0_output.clone()), 1); + let values = self.linear_critic.forward(layer_0_output); + + PPOOutput::::new(policies, values) + } + + fn infer(&self, input: Tensor) -> Tensor { + let layer_0_output = relu(self.linear.forward(input)); + softmax(self.linear_actor.forward(layer_0_output.clone()), 1) + } +} + +impl PPOModel for Net {} +#[allow(unused)] +const MEMORY_SIZE: usize = 512; + +type MyAgent = PPO>; + +#[allow(unused)] +pub fn run< + E: Environment + AsMut, + B: AutodiffBackend, +>( + conf: &Config, + visualized: bool, + // ) -> PPO> { +) -> impl Agent { + let mut env = E::new(visualized); + env.as_mut().max_steps = conf.max_steps; + + let mut model = Net::::new( + <::StateType as State>::size(), + conf.dense_size, + <::ActionType as Action>::size(), + ); + let agent = MyAgent::default(); + let config = PPOTrainingConfig { + gamma: conf.gamma, + lambda: conf.lambda, + epsilon_clip: conf.epsilon_clip, + critic_weight: conf.critic_weight, + entropy_weight: conf.entropy_weight, + learning_rate: conf.learning_rate, + epochs: conf.epochs, + batch_size: conf.batch_size, + clip_grad: Some(burn::grad_clipping::GradientClippingConfig::Value( + conf.clip_grad, + )), + }; + + let mut optimizer = AdamWConfig::new() + .with_grad_clipping(config.clip_grad.clone()) + .init(); + let mut memory = Memory::::default(); + for episode in 0..conf.num_episodes { + let mut episode_done = false; + let mut episode_reward = 0.0; + let mut episode_duration = 0_usize; + let mut now = SystemTime::now(); + + env.reset(); + while !episode_done { + let state = env.state(); + if let Some(action) = MyAgent::::react_with_model(&state, &model) { + let snapshot = env.step(action); + episode_reward += <::RewardType as Into>::into( + snapshot.reward().clone(), + ); + + memory.push( + state, + *snapshot.state(), + action, + snapshot.reward().clone(), + snapshot.done(), + ); + + episode_duration += 1; + episode_done = snapshot.done() || episode_duration >= conf.max_steps; + } + } + println!( + "{{\"episode\": {episode}, \"reward\": {episode_reward:.4}, \"steps count\": {episode_duration}, \"duration\": {}}}", + now.elapsed().unwrap().as_secs(), + ); + + now = SystemTime::now(); + model = MyAgent::train::(model, &memory, &mut optimizer, &config); + memory.clear(); + } + + if let Some(path) = &conf.save_path { + let device = NdArrayDevice::default(); + let recorder = CompactRecorder::new(); + let tmp_path = env::temp_dir().join("tmp_model.mpk"); + + // Save the trained model (backend B) to a temporary file + recorder + .record(model.clone().into_record(), tmp_path.clone()) + .expect("Failed to save temporary model"); + + // Create a new model instance with the target backend (NdArray) + let model_to_save: Net> = Net::new( + <::StateType as State>::size(), + conf.dense_size, + <::ActionType as Action>::size(), + ); + + // Load the record from the temporary file into the new model + let record = recorder + .load(tmp_path.clone(), &device) + .expect("Failed to load temporary model"); + let model_with_loaded_weights = model_to_save.load_record(record); + + // Clean up the temporary file + fs::remove_file(tmp_path).expect("Failed to remove temporary model file"); + + save_model(&model_with_loaded_weights, path); + } + agent.valid(model) +} + +pub fn save_model(model: &Net>, path: &String) { + let recorder = CompactRecorder::new(); + let model_path = format!("{path}.mpk"); + println!("info: Modèle de validation sauvegardé : {model_path}"); + recorder + .record(model.clone().into_record(), model_path.into()) + .unwrap(); +} + +pub fn load_model(dense_size: usize, path: &String) -> Option>> { + let model_path = format!("{path}.mpk"); + // println!("Chargement du modèle depuis : {model_path}"); + + CompactRecorder::new() + .load(model_path.into(), &NdArrayDevice::default()) + .map(|record| { + Net::new( + ::StateType::size(), + dense_size, + ::ActionType::size(), + ) + .load_record(record) + }) + .ok() +} diff --git a/bot/src/burnrl/algos/ppo_valid.rs b/bot/src/burnrl/algos/ppo_valid.rs new file mode 100644 index 0000000..8a391fb --- /dev/null +++ b/bot/src/burnrl/algos/ppo_valid.rs @@ -0,0 +1,191 @@ +use crate::burnrl::environment_valid::TrictracEnvironment; +use crate::burnrl::utils::Config; +use burn::backend::{ndarray::NdArrayDevice, NdArray}; +use burn::module::Module; +use burn::nn::{Initializer, Linear, LinearConfig}; +use burn::optim::AdamWConfig; +use burn::record::{CompactRecorder, Recorder}; +use burn::tensor::activation::{relu, softmax}; +use burn::tensor::backend::{AutodiffBackend, Backend}; +use burn::tensor::Tensor; +use burn_rl::agent::{PPOModel, PPOOutput, PPOTrainingConfig, PPO}; +use burn_rl::base::{Action, Agent, ElemType, Environment, Memory, Model, State}; +use std::env; +use std::fs; +use std::time::SystemTime; + +#[derive(Module, Debug)] +pub struct Net { + linear: Linear, + linear_actor: Linear, + linear_critic: Linear, +} + +impl Net { + #[allow(unused)] + pub fn new(input_size: usize, dense_size: usize, output_size: usize) -> Self { + let initializer = Initializer::XavierUniform { gain: 1.0 }; + Self { + linear: LinearConfig::new(input_size, dense_size) + .with_initializer(initializer.clone()) + .init(&Default::default()), + linear_actor: LinearConfig::new(dense_size, output_size) + .with_initializer(initializer.clone()) + .init(&Default::default()), + linear_critic: LinearConfig::new(dense_size, 1) + .with_initializer(initializer) + .init(&Default::default()), + } + } +} + +impl Model, PPOOutput, Tensor> for Net { + fn forward(&self, input: Tensor) -> PPOOutput { + let layer_0_output = relu(self.linear.forward(input)); + let policies = softmax(self.linear_actor.forward(layer_0_output.clone()), 1); + let values = self.linear_critic.forward(layer_0_output); + + PPOOutput::::new(policies, values) + } + + fn infer(&self, input: Tensor) -> Tensor { + let layer_0_output = relu(self.linear.forward(input)); + softmax(self.linear_actor.forward(layer_0_output.clone()), 1) + } +} + +impl PPOModel for Net {} +#[allow(unused)] +const MEMORY_SIZE: usize = 512; + +type MyAgent = PPO>; + +#[allow(unused)] +pub fn run< + E: Environment + AsMut, + B: AutodiffBackend, +>( + conf: &Config, + visualized: bool, + // ) -> PPO> { +) -> impl Agent { + let mut env = E::new(visualized); + env.as_mut().max_steps = conf.max_steps; + + let mut model = Net::::new( + <::StateType as State>::size(), + conf.dense_size, + <::ActionType as Action>::size(), + ); + let agent = MyAgent::default(); + let config = PPOTrainingConfig { + gamma: conf.gamma, + lambda: conf.lambda, + epsilon_clip: conf.epsilon_clip, + critic_weight: conf.critic_weight, + entropy_weight: conf.entropy_weight, + learning_rate: conf.learning_rate, + epochs: conf.epochs, + batch_size: conf.batch_size, + clip_grad: Some(burn::grad_clipping::GradientClippingConfig::Value( + conf.clip_grad, + )), + }; + + let mut optimizer = AdamWConfig::new() + .with_grad_clipping(config.clip_grad.clone()) + .init(); + let mut memory = Memory::::default(); + for episode in 0..conf.num_episodes { + let mut episode_done = false; + let mut episode_reward = 0.0; + let mut episode_duration = 0_usize; + let mut now = SystemTime::now(); + + env.reset(); + while !episode_done { + let state = env.state(); + if let Some(action) = MyAgent::::react_with_model(&state, &model) { + let snapshot = env.step(action); + episode_reward += <::RewardType as Into>::into( + snapshot.reward().clone(), + ); + + memory.push( + state, + *snapshot.state(), + action, + snapshot.reward().clone(), + snapshot.done(), + ); + + episode_duration += 1; + episode_done = snapshot.done() || episode_duration >= conf.max_steps; + } + } + println!( + "{{\"episode\": {episode}, \"reward\": {episode_reward:.4}, \"steps count\": {episode_duration}, \"duration\": {}}}", + now.elapsed().unwrap().as_secs(), + ); + + now = SystemTime::now(); + model = MyAgent::train::(model, &memory, &mut optimizer, &config); + memory.clear(); + } + + if let Some(path) = &conf.save_path { + let device = NdArrayDevice::default(); + let recorder = CompactRecorder::new(); + let tmp_path = env::temp_dir().join("tmp_model.mpk"); + + // Save the trained model (backend B) to a temporary file + recorder + .record(model.clone().into_record(), tmp_path.clone()) + .expect("Failed to save temporary model"); + + // Create a new model instance with the target backend (NdArray) + let model_to_save: Net> = Net::new( + <::StateType as State>::size(), + conf.dense_size, + <::ActionType as Action>::size(), + ); + + // Load the record from the temporary file into the new model + let record = recorder + .load(tmp_path.clone(), &device) + .expect("Failed to load temporary model"); + let model_with_loaded_weights = model_to_save.load_record(record); + + // Clean up the temporary file + fs::remove_file(tmp_path).expect("Failed to remove temporary model file"); + + save_model(&model_with_loaded_weights, path); + } + agent.valid(model) +} + +pub fn save_model(model: &Net>, path: &String) { + let recorder = CompactRecorder::new(); + let model_path = format!("{path}.mpk"); + println!("info: Modèle de validation sauvegardé : {model_path}"); + recorder + .record(model.clone().into_record(), model_path.into()) + .unwrap(); +} + +pub fn load_model(dense_size: usize, path: &String) -> Option>> { + let model_path = format!("{path}.mpk"); + // println!("Chargement du modèle depuis : {model_path}"); + + CompactRecorder::new() + .load(model_path.into(), &NdArrayDevice::default()) + .map(|record| { + Net::new( + ::StateType::size(), + dense_size, + ::ActionType::size(), + ) + .load_record(record) + }) + .ok() +} diff --git a/bot/src/burnrl/algos/sac.rs b/bot/src/burnrl/algos/sac.rs new file mode 100644 index 0000000..67db72a --- /dev/null +++ b/bot/src/burnrl/algos/sac.rs @@ -0,0 +1,221 @@ +use crate::burnrl::environment::TrictracEnvironment; +use crate::burnrl::utils::{soft_update_linear, Config}; +use burn::backend::{ndarray::NdArrayDevice, NdArray}; +use burn::module::Module; +use burn::nn::{Linear, LinearConfig}; +use burn::optim::AdamWConfig; +use burn::record::{CompactRecorder, Recorder}; +use burn::tensor::activation::{relu, softmax}; +use burn::tensor::backend::{AutodiffBackend, Backend}; +use burn::tensor::Tensor; +use burn_rl::agent::{SACActor, SACCritic, SACNets, SACOptimizer, SACTrainingConfig, SAC}; +use burn_rl::base::{Action, Agent, ElemType, Environment, Memory, Model, State}; +use std::time::SystemTime; + +#[derive(Module, Debug)] +pub struct Actor { + linear_0: Linear, + linear_1: Linear, + linear_2: Linear, +} + +impl Actor { + pub fn new(input_size: usize, dense_size: usize, output_size: usize) -> Self { + Self { + linear_0: LinearConfig::new(input_size, dense_size).init(&Default::default()), + linear_1: LinearConfig::new(dense_size, dense_size).init(&Default::default()), + linear_2: LinearConfig::new(dense_size, output_size).init(&Default::default()), + } + } +} + +impl Model, Tensor> for Actor { + fn forward(&self, input: Tensor) -> Tensor { + let layer_0_output = relu(self.linear_0.forward(input)); + let layer_1_output = relu(self.linear_1.forward(layer_0_output)); + + softmax(self.linear_2.forward(layer_1_output), 1) + } + + fn infer(&self, input: Tensor) -> Tensor { + self.forward(input) + } +} + +impl SACActor for Actor {} + +#[derive(Module, Debug)] +pub struct Critic { + linear_0: Linear, + linear_1: Linear, + linear_2: Linear, +} + +impl Critic { + pub fn new(input_size: usize, dense_size: usize, output_size: usize) -> Self { + Self { + linear_0: LinearConfig::new(input_size, dense_size).init(&Default::default()), + linear_1: LinearConfig::new(dense_size, dense_size).init(&Default::default()), + linear_2: LinearConfig::new(dense_size, output_size).init(&Default::default()), + } + } + + fn consume(self) -> (Linear, Linear, Linear) { + (self.linear_0, self.linear_1, self.linear_2) + } +} + +impl Model, Tensor> for Critic { + fn forward(&self, input: Tensor) -> Tensor { + let layer_0_output = relu(self.linear_0.forward(input)); + let layer_1_output = relu(self.linear_1.forward(layer_0_output)); + + self.linear_2.forward(layer_1_output) + } + + fn infer(&self, input: Tensor) -> Tensor { + self.forward(input) + } +} + +impl SACCritic for Critic { + fn soft_update(this: Self, that: &Self, tau: ElemType) -> Self { + let (linear_0, linear_1, linear_2) = this.consume(); + + Self { + linear_0: soft_update_linear(linear_0, &that.linear_0, tau), + linear_1: soft_update_linear(linear_1, &that.linear_1, tau), + linear_2: soft_update_linear(linear_2, &that.linear_2, tau), + } + } +} + +#[allow(unused)] +const MEMORY_SIZE: usize = 4096; + +type MyAgent = SAC>; + +#[allow(unused)] +pub fn run< + E: Environment + AsMut, + B: AutodiffBackend, +>( + conf: &Config, + visualized: bool, +) -> impl Agent { + let mut env = E::new(visualized); + env.as_mut().max_steps = conf.max_steps; + let state_dim = <::StateType as State>::size(); + let action_dim = <::ActionType as Action>::size(); + + let actor = Actor::::new(state_dim, conf.dense_size, action_dim); + let critic_1 = Critic::::new(state_dim, conf.dense_size, action_dim); + let critic_2 = Critic::::new(state_dim, conf.dense_size, action_dim); + let mut nets = SACNets::, Critic>::new(actor, critic_1, critic_2); + + let mut agent = MyAgent::default(); + + let config = SACTrainingConfig { + gamma: conf.gamma, + tau: conf.tau, + learning_rate: conf.learning_rate, + min_probability: conf.min_probability, + batch_size: conf.batch_size, + clip_grad: Some(burn::grad_clipping::GradientClippingConfig::Value( + conf.clip_grad, + )), + }; + + let mut memory = Memory::::default(); + + let optimizer_config = AdamWConfig::new().with_grad_clipping(config.clip_grad.clone()); + + let mut optimizer = SACOptimizer::new( + optimizer_config.clone().init(), + optimizer_config.clone().init(), + optimizer_config.clone().init(), + optimizer_config.init(), + ); + + let mut step = 0_usize; + + for episode in 0..conf.num_episodes { + let mut episode_done = false; + let mut episode_reward = 0.0; + let mut episode_duration = 0_usize; + let mut state = env.state(); + let mut now = SystemTime::now(); + + while !episode_done { + if let Some(action) = MyAgent::::react_with_model(&state, &nets.actor) { + let snapshot = env.step(action); + + episode_reward += <::RewardType as Into>::into( + snapshot.reward().clone(), + ); + + memory.push( + state, + *snapshot.state(), + action, + snapshot.reward().clone(), + snapshot.done(), + ); + + if config.batch_size < memory.len() { + nets = agent.train::(nets, &memory, &mut optimizer, &config); + } + + step += 1; + episode_duration += 1; + + if snapshot.done() || episode_duration >= conf.max_steps { + env.reset(); + episode_done = true; + + println!( + "{{\"episode\": {episode}, \"reward\": {episode_reward:.4}, \"steps count\": {episode_duration}, \"duration\": {}}}", + now.elapsed().unwrap().as_secs() + ); + now = SystemTime::now(); + } else { + state = *snapshot.state(); + } + } + } + } + + let valid_agent = agent.valid(nets.actor); + if let Some(path) = &conf.save_path { + if let Some(model) = valid_agent.model() { + save_model(model, path); + } + } + valid_agent +} + +pub fn save_model(model: &Actor>, path: &String) { + let recorder = CompactRecorder::new(); + let model_path = format!("{path}.mpk"); + println!("info: Modèle de validation sauvegardé : {model_path}"); + recorder + .record(model.clone().into_record(), model_path.into()) + .unwrap(); +} + +pub fn load_model(dense_size: usize, path: &String) -> Option>> { + let model_path = format!("{path}.mpk"); + // println!("Chargement du modèle depuis : {model_path}"); + + CompactRecorder::new() + .load(model_path.into(), &NdArrayDevice::default()) + .map(|record| { + Actor::new( + ::StateType::size(), + dense_size, + ::ActionType::size(), + ) + .load_record(record) + }) + .ok() +} \ No newline at end of file diff --git a/bot/src/burnrl/algos/sac_big.rs b/bot/src/burnrl/algos/sac_big.rs new file mode 100644 index 0000000..1361b42 --- /dev/null +++ b/bot/src/burnrl/algos/sac_big.rs @@ -0,0 +1,222 @@ +use crate::burnrl::environment_big::TrictracEnvironment; +use crate::burnrl::utils::{soft_update_linear, Config}; +use burn::backend::{ndarray::NdArrayDevice, NdArray}; +use burn::module::Module; +use burn::nn::{Linear, LinearConfig}; +use burn::optim::AdamWConfig; +use burn::record::{CompactRecorder, Recorder}; +use burn::tensor::activation::{relu, softmax}; +use burn::tensor::backend::{AutodiffBackend, Backend}; +use burn::tensor::Tensor; +use burn_rl::agent::{SACActor, SACCritic, SACNets, SACOptimizer, SACTrainingConfig, SAC}; +use burn_rl::base::{Action, Agent, ElemType, Environment, Memory, Model, State}; +use std::time::SystemTime; + +#[derive(Module, Debug)] +pub struct Actor { + linear_0: Linear, + linear_1: Linear, + linear_2: Linear, +} + +impl Actor { + pub fn new(input_size: usize, dense_size: usize, output_size: usize) -> Self { + Self { + linear_0: LinearConfig::new(input_size, dense_size).init(&Default::default()), + linear_1: LinearConfig::new(dense_size, dense_size).init(&Default::default()), + linear_2: LinearConfig::new(dense_size, output_size).init(&Default::default()), + } + } +} + +impl Model, Tensor> for Actor { + fn forward(&self, input: Tensor) -> Tensor { + let layer_0_output = relu(self.linear_0.forward(input)); + let layer_1_output = relu(self.linear_1.forward(layer_0_output)); + + softmax(self.linear_2.forward(layer_1_output), 1) + } + + fn infer(&self, input: Tensor) -> Tensor { + self.forward(input) + } +} + +impl SACActor for Actor {} + +#[derive(Module, Debug)] +pub struct Critic { + linear_0: Linear, + linear_1: Linear, + linear_2: Linear, +} + +impl Critic { + pub fn new(input_size: usize, dense_size: usize, output_size: usize) -> Self { + Self { + linear_0: LinearConfig::new(input_size, dense_size).init(&Default::default()), + linear_1: LinearConfig::new(dense_size, dense_size).init(&Default::default()), + linear_2: LinearConfig::new(dense_size, output_size).init(&Default::default()), + } + } + + fn consume(self) -> (Linear, Linear, Linear) { + (self.linear_0, self.linear_1, self.linear_2) + } +} + +impl Model, Tensor> for Critic { + fn forward(&self, input: Tensor) -> Tensor { + let layer_0_output = relu(self.linear_0.forward(input)); + let layer_1_output = relu(self.linear_1.forward(layer_0_output)); + + self.linear_2.forward(layer_1_output) + } + + fn infer(&self, input: Tensor) -> Tensor { + self.forward(input) + } +} + +impl SACCritic for Critic { + fn soft_update(this: Self, that: &Self, tau: ElemType) -> Self { + let (linear_0, linear_1, linear_2) = this.consume(); + + Self { + linear_0: soft_update_linear(linear_0, &that.linear_0, tau), + linear_1: soft_update_linear(linear_1, &that.linear_1, tau), + linear_2: soft_update_linear(linear_2, &that.linear_2, tau), + } + } +} + +#[allow(unused)] +const MEMORY_SIZE: usize = 4096; + +type MyAgent = SAC>; + +#[allow(unused)] +pub fn run< + E: Environment + AsMut, + B: AutodiffBackend, +>( + conf: &Config, + visualized: bool, +) -> impl Agent { + let mut env = E::new(visualized); + env.as_mut().max_steps = conf.max_steps; + let state_dim = <::StateType as State>::size(); + let action_dim = <::ActionType as Action>::size(); + + let actor = Actor::::new(state_dim, conf.dense_size, action_dim); + let critic_1 = Critic::::new(state_dim, conf.dense_size, action_dim); + let critic_2 = Critic::::new(state_dim, conf.dense_size, action_dim); + let mut nets = SACNets::, Critic>::new(actor, critic_1, critic_2); + + let mut agent = MyAgent::default(); + + let config = SACTrainingConfig { + gamma: conf.gamma, + tau: conf.tau, + learning_rate: conf.learning_rate, + min_probability: conf.min_probability, + batch_size: conf.batch_size, + clip_grad: Some(burn::grad_clipping::GradientClippingConfig::Value( + conf.clip_grad, + )), + }; + + let mut memory = Memory::::default(); + + let optimizer_config = AdamWConfig::new().with_grad_clipping(config.clip_grad.clone()); + + let mut optimizer = SACOptimizer::new( + optimizer_config.clone().init(), + optimizer_config.clone().init(), + optimizer_config.clone().init(), + optimizer_config.init(), + ); + + let mut step = 0_usize; + + for episode in 0..conf.num_episodes { + let mut episode_done = false; + let mut episode_reward = 0.0; + let mut episode_duration = 0_usize; + let mut state = env.state(); + let mut now = SystemTime::now(); + + while !episode_done { + if let Some(action) = MyAgent::::react_with_model(&state, &nets.actor) { + let snapshot = env.step(action); + + episode_reward += <::RewardType as Into>::into( + snapshot.reward().clone(), + ); + + memory.push( + state, + *snapshot.state(), + action, + snapshot.reward().clone(), + snapshot.done(), + ); + + if config.batch_size < memory.len() { + nets = agent.train::(nets, &memory, &mut optimizer, &config); + } + + step += 1; + episode_duration += 1; + + if snapshot.done() || episode_duration >= conf.max_steps { + env.reset(); + episode_done = true; + + println!( + "{{\"episode\": {episode}, \"reward\": {episode_reward:.4}, \"steps count\": {episode_duration}, \"duration\": {}}}", + now.elapsed().unwrap().as_secs() + ); + now = SystemTime::now(); + } else { + state = *snapshot.state(); + } + } + } + } + + let valid_agent = agent.valid(nets.actor); + if let Some(path) = &conf.save_path { + if let Some(model) = valid_agent.model() { + save_model(model, path); + } + } + valid_agent +} + +pub fn save_model(model: &Actor>, path: &String) { + let recorder = CompactRecorder::new(); + let model_path = format!("{path}.mpk"); + println!("info: Modèle de validation sauvegardé : {model_path}"); + recorder + .record(model.clone().into_record(), model_path.into()) + .unwrap(); +} + +pub fn load_model(dense_size: usize, path: &String) -> Option>> { + let model_path = format!("{path}.mpk"); + // println!("Chargement du modèle depuis : {model_path}"); + + CompactRecorder::new() + .load(model_path.into(), &NdArrayDevice::default()) + .map(|record| { + Actor::new( + ::StateType::size(), + dense_size, + ::ActionType::size(), + ) + .load_record(record) + }) + .ok() +} + diff --git a/bot/src/burnrl/algos/sac_valid.rs b/bot/src/burnrl/algos/sac_valid.rs new file mode 100644 index 0000000..81523c4 --- /dev/null +++ b/bot/src/burnrl/algos/sac_valid.rs @@ -0,0 +1,222 @@ +use crate::burnrl::environment_valid::TrictracEnvironment; +use crate::burnrl::utils::{soft_update_linear, Config}; +use burn::backend::{ndarray::NdArrayDevice, NdArray}; +use burn::module::Module; +use burn::nn::{Linear, LinearConfig}; +use burn::optim::AdamWConfig; +use burn::record::{CompactRecorder, Recorder}; +use burn::tensor::activation::{relu, softmax}; +use burn::tensor::backend::{AutodiffBackend, Backend}; +use burn::tensor::Tensor; +use burn_rl::agent::{SACActor, SACCritic, SACNets, SACOptimizer, SACTrainingConfig, SAC}; +use burn_rl::base::{Action, Agent, ElemType, Environment, Memory, Model, State}; +use std::time::SystemTime; + +#[derive(Module, Debug)] +pub struct Actor { + linear_0: Linear, + linear_1: Linear, + linear_2: Linear, +} + +impl Actor { + pub fn new(input_size: usize, dense_size: usize, output_size: usize) -> Self { + Self { + linear_0: LinearConfig::new(input_size, dense_size).init(&Default::default()), + linear_1: LinearConfig::new(dense_size, dense_size).init(&Default::default()), + linear_2: LinearConfig::new(dense_size, output_size).init(&Default::default()), + } + } +} + +impl Model, Tensor> for Actor { + fn forward(&self, input: Tensor) -> Tensor { + let layer_0_output = relu(self.linear_0.forward(input)); + let layer_1_output = relu(self.linear_1.forward(layer_0_output)); + + softmax(self.linear_2.forward(layer_1_output), 1) + } + + fn infer(&self, input: Tensor) -> Tensor { + self.forward(input) + } +} + +impl SACActor for Actor {} + +#[derive(Module, Debug)] +pub struct Critic { + linear_0: Linear, + linear_1: Linear, + linear_2: Linear, +} + +impl Critic { + pub fn new(input_size: usize, dense_size: usize, output_size: usize) -> Self { + Self { + linear_0: LinearConfig::new(input_size, dense_size).init(&Default::default()), + linear_1: LinearConfig::new(dense_size, dense_size).init(&Default::default()), + linear_2: LinearConfig::new(dense_size, output_size).init(&Default::default()), + } + } + + fn consume(self) -> (Linear, Linear, Linear) { + (self.linear_0, self.linear_1, self.linear_2) + } +} + +impl Model, Tensor> for Critic { + fn forward(&self, input: Tensor) -> Tensor { + let layer_0_output = relu(self.linear_0.forward(input)); + let layer_1_output = relu(self.linear_1.forward(layer_0_output)); + + self.linear_2.forward(layer_1_output) + } + + fn infer(&self, input: Tensor) -> Tensor { + self.forward(input) + } +} + +impl SACCritic for Critic { + fn soft_update(this: Self, that: &Self, tau: ElemType) -> Self { + let (linear_0, linear_1, linear_2) = this.consume(); + + Self { + linear_0: soft_update_linear(linear_0, &that.linear_0, tau), + linear_1: soft_update_linear(linear_1, &that.linear_1, tau), + linear_2: soft_update_linear(linear_2, &that.linear_2, tau), + } + } +} + +#[allow(unused)] +const MEMORY_SIZE: usize = 4096; + +type MyAgent = SAC>; + +#[allow(unused)] +pub fn run< + E: Environment + AsMut, + B: AutodiffBackend, +>( + conf: &Config, + visualized: bool, +) -> impl Agent { + let mut env = E::new(visualized); + env.as_mut().max_steps = conf.max_steps; + let state_dim = <::StateType as State>::size(); + let action_dim = <::ActionType as Action>::size(); + + let actor = Actor::::new(state_dim, conf.dense_size, action_dim); + let critic_1 = Critic::::new(state_dim, conf.dense_size, action_dim); + let critic_2 = Critic::::new(state_dim, conf.dense_size, action_dim); + let mut nets = SACNets::, Critic>::new(actor, critic_1, critic_2); + + let mut agent = MyAgent::default(); + + let config = SACTrainingConfig { + gamma: conf.gamma, + tau: conf.tau, + learning_rate: conf.learning_rate, + min_probability: conf.min_probability, + batch_size: conf.batch_size, + clip_grad: Some(burn::grad_clipping::GradientClippingConfig::Value( + conf.clip_grad, + )), + }; + + let mut memory = Memory::::default(); + + let optimizer_config = AdamWConfig::new().with_grad_clipping(config.clip_grad.clone()); + + let mut optimizer = SACOptimizer::new( + optimizer_config.clone().init(), + optimizer_config.clone().init(), + optimizer_config.clone().init(), + optimizer_config.init(), + ); + + let mut step = 0_usize; + + for episode in 0..conf.num_episodes { + let mut episode_done = false; + let mut episode_reward = 0.0; + let mut episode_duration = 0_usize; + let mut state = env.state(); + let mut now = SystemTime::now(); + + while !episode_done { + if let Some(action) = MyAgent::::react_with_model(&state, &nets.actor) { + let snapshot = env.step(action); + + episode_reward += <::RewardType as Into>::into( + snapshot.reward().clone(), + ); + + memory.push( + state, + *snapshot.state(), + action, + snapshot.reward().clone(), + snapshot.done(), + ); + + if config.batch_size < memory.len() { + nets = agent.train::(nets, &memory, &mut optimizer, &config); + } + + step += 1; + episode_duration += 1; + + if snapshot.done() || episode_duration >= conf.max_steps { + env.reset(); + episode_done = true; + + println!( + "{{\"episode\": {episode}, \"reward\": {episode_reward:.4}, \"steps count\": {episode_duration}, \"duration\": {}}}", + now.elapsed().unwrap().as_secs() + ); + now = SystemTime::now(); + } else { + state = *snapshot.state(); + } + } + } + } + + let valid_agent = agent.valid(nets.actor); + if let Some(path) = &conf.save_path { + if let Some(model) = valid_agent.model() { + save_model(model, path); + } + } + valid_agent +} + +pub fn save_model(model: &Actor>, path: &String) { + let recorder = CompactRecorder::new(); + let model_path = format!("{path}.mpk"); + println!("info: Modèle de validation sauvegardé : {model_path}"); + recorder + .record(model.clone().into_record(), model_path.into()) + .unwrap(); +} + +pub fn load_model(dense_size: usize, path: &String) -> Option>> { + let model_path = format!("{path}.mpk"); + // println!("Chargement du modèle depuis : {model_path}"); + + CompactRecorder::new() + .load(model_path.into(), &NdArrayDevice::default()) + .map(|record| { + Actor::new( + ::StateType::size(), + dense_size, + ::ActionType::size(), + ) + .load_record(record) + }) + .ok() +} + diff --git a/bot/src/burnrl/environment.rs b/bot/src/burnrl/environment.rs new file mode 100644 index 0000000..50daf11 --- /dev/null +++ b/bot/src/burnrl/environment.rs @@ -0,0 +1,424 @@ +use std::io::Write; + +use crate::training_common; +use burn::{prelude::Backend, tensor::Tensor}; +use burn_rl::base::{Action, Environment, Snapshot, State}; +use rand::{thread_rng, Rng}; +use store::{GameEvent, GameState, PlayerId, PointsRules, Stage, TurnStage}; + +const ERROR_REWARD: f32 = -1.12121; +const REWARD_VALID_MOVE: f32 = 1.12121; +const REWARD_RATIO: f32 = 0.01; +const WIN_POINTS: f32 = 1.0; + +/// État du jeu Trictrac pour burn-rl +#[derive(Debug, Clone, Copy)] +pub struct TrictracState { + pub data: [i8; 36], // Représentation vectorielle de l'état du jeu +} + +impl State for TrictracState { + type Data = [i8; 36]; + + fn to_tensor(&self) -> Tensor { + Tensor::from_floats(self.data, &B::Device::default()) + } + + fn size() -> usize { + 36 + } +} + +impl TrictracState { + /// Convertit un GameState en TrictracState + pub fn from_game_state(game_state: &GameState) -> Self { + let state_vec = game_state.to_vec(); + let mut data = [0; 36]; + + // Copier les données en s'assurant qu'on ne dépasse pas la taille + let copy_len = state_vec.len().min(36); + data[..copy_len].copy_from_slice(&state_vec[..copy_len]); + + TrictracState { data } + } +} + +/// Actions possibles dans Trictrac pour burn-rl +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct TrictracAction { + // u32 as required by burn_rl::base::Action type + pub index: u32, +} + +impl Action for TrictracAction { + fn random() -> Self { + use rand::{thread_rng, Rng}; + let mut rng = thread_rng(); + TrictracAction { + index: rng.gen_range(0..Self::size() as u32), + } + } + + fn enumerate() -> Vec { + (0..Self::size() as u32) + .map(|index| TrictracAction { index }) + .collect() + } + + fn size() -> usize { + 514 + } +} + +impl From for TrictracAction { + fn from(index: u32) -> Self { + TrictracAction { index } + } +} + +impl From for u32 { + fn from(action: TrictracAction) -> u32 { + action.index + } +} + +/// Environnement Trictrac pour burn-rl +#[derive(Debug)] +pub struct TrictracEnvironment { + pub game: GameState, + active_player_id: PlayerId, + opponent_id: PlayerId, + current_state: TrictracState, + episode_reward: f32, + pub step_count: usize, + pub best_ratio: f32, + pub max_steps: usize, + pub pointrolls_count: usize, + pub goodmoves_count: usize, + pub goodmoves_ratio: f32, + pub visualized: bool, +} + +impl Environment for TrictracEnvironment { + type StateType = TrictracState; + type ActionType = TrictracAction; + type RewardType = f32; + + fn new(visualized: bool) -> Self { + let mut game = GameState::new(false); + + // Ajouter deux joueurs + game.init_player("DQN Agent"); + game.init_player("Opponent"); + let player1_id = 1; + let player2_id = 2; + + // Commencer la partie + game.consume(&GameEvent::BeginGame { goes_first: 1 }); + + let current_state = TrictracState::from_game_state(&game); + TrictracEnvironment { + game, + active_player_id: player1_id, + opponent_id: player2_id, + current_state, + episode_reward: 0.0, + step_count: 0, + best_ratio: 0.0, + max_steps: 2000, + pointrolls_count: 0, + goodmoves_count: 0, + goodmoves_ratio: 0.0, + visualized, + } + } + + fn state(&self) -> Self::StateType { + self.current_state + } + + fn reset(&mut self) -> Snapshot { + // Réinitialiser le jeu + let history = self.game.history.clone(); + self.game = GameState::new(false); + self.game.init_player("DQN Agent"); + self.game.init_player("Opponent"); + + // Commencer la partie + self.game.consume(&GameEvent::BeginGame { goes_first: 1 }); + + self.current_state = TrictracState::from_game_state(&self.game); + self.episode_reward = 0.0; + self.goodmoves_ratio = if self.step_count == 0 { + 0.0 + } else { + self.goodmoves_count as f32 / self.step_count as f32 + }; + self.best_ratio = self.best_ratio.max(self.goodmoves_ratio); + let _warning = if self.best_ratio > 0.7 && self.goodmoves_ratio < 0.1 { + let path = "bot/models/logs/debug.log"; + if let Ok(mut out) = std::fs::File::create(path) { + write!(out, "{history:?}").expect("could not write history log"); + } + "!!!!" + } else { + "" + }; + // println!( + // "info: correct moves: {} ({}%) {}", + // self.goodmoves_count, + // (100.0 * self.goodmoves_ratio).round() as u32, + // warning + // ); + self.step_count = 0; + self.pointrolls_count = 0; + self.goodmoves_count = 0; + + Snapshot::new(self.current_state, 0.0, false) + } + + fn step(&mut self, action: Self::ActionType) -> Snapshot { + self.step_count += 1; + + // Convertir l'action burn-rl vers une action Trictrac + let trictrac_action = Self::convert_action(action); + + let mut reward = 0.0; + let is_rollpoint; + + // Exécuter l'action si c'est le tour de l'agent DQN + if self.game.active_player_id == self.active_player_id { + if let Some(action) = trictrac_action { + (reward, is_rollpoint) = self.execute_action(action); + if is_rollpoint { + self.pointrolls_count += 1; + } + if reward != ERROR_REWARD { + self.goodmoves_count += 1; + } + } else { + // Action non convertible, pénalité + panic!("action non convertible"); + //reward = -0.5; + } + } + + // Faire jouer l'adversaire (stratégie simple) + while self.game.active_player_id == self.opponent_id && self.game.stage != Stage::Ended { + reward += self.play_opponent_if_needed(); + } + + // Vérifier si la partie est terminée + // let max_steps = self.max_steps; + // let max_steps = self.min_steps + // + (self.max_steps as f32 - self.min_steps) + // * f32::exp((self.goodmoves_ratio - 1.0) / 0.25); + let done = self.game.stage == Stage::Ended || self.game.determine_winner().is_some(); + + if done { + // Récompense finale basée sur le résultat + if let Some(winner_id) = self.game.determine_winner() { + if winner_id == self.active_player_id { + reward += WIN_POINTS; // Victoire + } else { + reward -= WIN_POINTS; // Défaite + } + } + } + let terminated = done || self.step_count >= self.max_steps; + // let terminated = done || self.step_count >= max_steps.round() as usize; + + // Mettre à jour l'état + self.current_state = TrictracState::from_game_state(&self.game); + self.episode_reward += reward; + + if self.visualized && terminated { + println!( + "Episode terminé. Récompense totale: {:.2}, Étapes: {}", + self.episode_reward, self.step_count + ); + } + + Snapshot::new(self.current_state, reward, terminated) + } +} + +impl TrictracEnvironment { + /// Convertit une action burn-rl vers une action Trictrac + pub fn convert_action(action: TrictracAction) -> Option { + training_common::TrictracAction::from_action_index(action.index.try_into().unwrap()) + } + + /// Convertit l'index d'une action au sein des actions valides vers une action Trictrac + #[allow(dead_code)] + fn convert_valid_action_index( + &self, + action: TrictracAction, + game_state: &GameState, + ) -> Option { + use training_common::get_valid_actions; + + // Obtenir les actions valides dans le contexte actuel + let valid_actions = get_valid_actions(game_state); + + if valid_actions.is_empty() { + return None; + } + + // Mapper l'index d'action sur une action valide + let action_index = (action.index as usize) % valid_actions.len(); + Some(valid_actions[action_index].clone()) + } + + /// Exécute une action Trictrac dans le jeu + // fn execute_action( + // &mut self, + // action: training_common::TrictracAction, + // ) -> Result> { + fn execute_action(&mut self, action: training_common::TrictracAction) -> (f32, bool) { + use training_common::TrictracAction; + + let mut reward = 0.0; + let mut is_rollpoint = false; + + // Appliquer l'événement si valide + if let Some(event) = action.to_event(&self.game) { + if self.game.validate(&event) { + self.game.consume(&event); + reward += REWARD_VALID_MOVE; + // Simuler le résultat des dés après un Roll + if matches!(action, TrictracAction::Roll) { + let mut rng = thread_rng(); + let dice_values = (rng.gen_range(1..=6), rng.gen_range(1..=6)); + let dice_event = GameEvent::RollResult { + player_id: self.active_player_id, + dice: store::Dice { + values: dice_values, + }, + }; + if self.game.validate(&dice_event) { + self.game.consume(&dice_event); + let (points, adv_points) = self.game.dice_points; + reward += REWARD_RATIO * (points as f32 - adv_points as f32); + if points > 0 { + is_rollpoint = true; + // println!("info: rolled for {reward}"); + } + // Récompense proportionnelle aux points + } + } + } else { + // Pénalité pour action invalide + // on annule les précédents reward + // et on indique une valeur reconnaissable pour statistiques + reward = ERROR_REWARD; + } + } else { + reward = ERROR_REWARD; + } + + (reward, is_rollpoint) + } + + /// Fait jouer l'adversaire avec une stratégie simple + fn play_opponent_if_needed(&mut self) -> f32 { + let mut reward = 0.0; + + // Si c'est le tour de l'adversaire, jouer automatiquement + if self.game.active_player_id == self.opponent_id && self.game.stage != Stage::Ended { + // Utiliser la stratégie default pour l'adversaire + use crate::BotStrategy; + + let mut strategy = crate::strategy::random::RandomStrategy::default(); + strategy.set_player_id(self.opponent_id); + if let Some(color) = self.game.player_color_by_id(&self.opponent_id) { + strategy.set_color(color); + } + *strategy.get_mut_game() = self.game.clone(); + + // Exécuter l'action selon le turn_stage + let mut calculate_points = false; + let opponent_color = store::Color::Black; + let event = match self.game.turn_stage { + TurnStage::RollDice => GameEvent::Roll { + player_id: self.opponent_id, + }, + TurnStage::RollWaiting => { + let mut rng = thread_rng(); + let dice_values = (rng.gen_range(1..=6), rng.gen_range(1..=6)); + calculate_points = true; + GameEvent::RollResult { + player_id: self.opponent_id, + dice: store::Dice { + values: dice_values, + }, + } + } + TurnStage::MarkPoints => { + let dice_roll_count = self + .game + .players + .get(&self.opponent_id) + .unwrap() + .dice_roll_count; + let points_rules = + PointsRules::new(&opponent_color, &self.game.board, self.game.dice); + GameEvent::Mark { + player_id: self.opponent_id, + points: points_rules.get_points(dice_roll_count).0, + } + } + TurnStage::MarkAdvPoints => { + let opponent_color = store::Color::Black; + let dice_roll_count = self + .game + .players + .get(&self.opponent_id) + .unwrap() + .dice_roll_count; + let points_rules = + PointsRules::new(&opponent_color, &self.game.board, self.game.dice); + // pas de reward : déjà comptabilisé lors du tour de blanc + GameEvent::Mark { + player_id: self.opponent_id, + points: points_rules.get_points(dice_roll_count).1, + } + } + TurnStage::HoldOrGoChoice => { + // Stratégie simple : toujours continuer + GameEvent::Go { + player_id: self.opponent_id, + } + } + TurnStage::Move => GameEvent::Move { + player_id: self.opponent_id, + moves: strategy.choose_move(), + }, + }; + + if self.game.validate(&event) { + self.game.consume(&event); + if calculate_points { + let dice_roll_count = self + .game + .players + .get(&self.opponent_id) + .unwrap() + .dice_roll_count; + let points_rules = + PointsRules::new(&opponent_color, &self.game.board, self.game.dice); + let (points, adv_points) = points_rules.get_points(dice_roll_count); + // Récompense proportionnelle aux points + reward -= REWARD_RATIO * (points as f32 - adv_points as f32); + } + } + } + reward + } +} + +impl AsMut for TrictracEnvironment { + fn as_mut(&mut self) -> &mut Self { + self + } +} diff --git a/bot/src/burnrl/environment_big.rs b/bot/src/burnrl/environment_big.rs new file mode 100644 index 0000000..1bba2bd --- /dev/null +++ b/bot/src/burnrl/environment_big.rs @@ -0,0 +1,469 @@ +use crate::training_common_big; +use burn::{prelude::Backend, tensor::Tensor}; +use burn_rl::base::{Action, Environment, Snapshot, State}; +use rand::{thread_rng, Rng}; +use store::{GameEvent, GameState, PlayerId, PointsRules, Stage, TurnStage}; + +const ERROR_REWARD: f32 = -2.12121; +const REWARD_VALID_MOVE: f32 = 2.12121; +const REWARD_RATIO: f32 = 0.01; +const WIN_POINTS: f32 = 0.1; + +/// État du jeu Trictrac pour burn-rl +#[derive(Debug, Clone, Copy)] +pub struct TrictracState { + pub data: [i8; 36], // Représentation vectorielle de l'état du jeu +} + +impl State for TrictracState { + type Data = [i8; 36]; + + fn to_tensor(&self) -> Tensor { + Tensor::from_floats(self.data, &B::Device::default()) + } + + fn size() -> usize { + 36 + } +} + +impl TrictracState { + /// Convertit un GameState en TrictracState + pub fn from_game_state(game_state: &GameState) -> Self { + let state_vec = game_state.to_vec(); + let mut data = [0; 36]; + + // Copier les données en s'assurant qu'on ne dépasse pas la taille + let copy_len = state_vec.len().min(36); + data[..copy_len].copy_from_slice(&state_vec[..copy_len]); + + TrictracState { data } + } +} + +/// Actions possibles dans Trictrac pour burn-rl +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct TrictracAction { + // u32 as required by burn_rl::base::Action type + pub index: u32, +} + +impl Action for TrictracAction { + fn random() -> Self { + use rand::{thread_rng, Rng}; + let mut rng = thread_rng(); + TrictracAction { + index: rng.gen_range(0..Self::size() as u32), + } + } + + fn enumerate() -> Vec { + (0..Self::size() as u32) + .map(|index| TrictracAction { index }) + .collect() + } + + fn size() -> usize { + 1252 + } +} + +impl From for TrictracAction { + fn from(index: u32) -> Self { + TrictracAction { index } + } +} + +impl From for u32 { + fn from(action: TrictracAction) -> u32 { + action.index + } +} + +/// Environnement Trictrac pour burn-rl +#[derive(Debug)] +pub struct TrictracEnvironment { + pub game: GameState, + active_player_id: PlayerId, + opponent_id: PlayerId, + current_state: TrictracState, + episode_reward: f32, + pub step_count: usize, + pub max_steps: usize, + pub pointrolls_count: usize, + pub goodmoves_count: usize, + pub goodmoves_ratio: f32, + pub visualized: bool, +} + +impl Environment for TrictracEnvironment { + type StateType = TrictracState; + type ActionType = TrictracAction; + type RewardType = f32; + + fn new(visualized: bool) -> Self { + let mut game = GameState::new(false); + + // Ajouter deux joueurs + game.init_player("DQN Agent"); + game.init_player("Opponent"); + let player1_id = 1; + let player2_id = 2; + + // Commencer la partie + game.consume(&GameEvent::BeginGame { goes_first: 1 }); + + let current_state = TrictracState::from_game_state(&game); + TrictracEnvironment { + game, + active_player_id: player1_id, + opponent_id: player2_id, + current_state, + episode_reward: 0.0, + step_count: 0, + max_steps: 2000, + pointrolls_count: 0, + goodmoves_count: 0, + goodmoves_ratio: 0.0, + visualized, + } + } + + fn state(&self) -> Self::StateType { + self.current_state + } + + fn reset(&mut self) -> Snapshot { + // Réinitialiser le jeu + self.game = GameState::new(false); + self.game.init_player("DQN Agent"); + self.game.init_player("Opponent"); + + // Commencer la partie + self.game.consume(&GameEvent::BeginGame { goes_first: 1 }); + + self.current_state = TrictracState::from_game_state(&self.game); + self.episode_reward = 0.0; + self.goodmoves_ratio = if self.step_count == 0 { + 0.0 + } else { + self.goodmoves_count as f32 / self.step_count as f32 + }; + println!( + "info: correct moves: {} ({}%)", + self.goodmoves_count, + (100.0 * self.goodmoves_ratio).round() as u32 + ); + self.step_count = 0; + self.pointrolls_count = 0; + self.goodmoves_count = 0; + + Snapshot::new(self.current_state, 0.0, false) + } + + fn step(&mut self, action: Self::ActionType) -> Snapshot { + self.step_count += 1; + + // Convertir l'action burn-rl vers une action Trictrac + let trictrac_action = Self::convert_action(action); + + let mut reward = 0.0; + let is_rollpoint; + + // Exécuter l'action si c'est le tour de l'agent DQN + if self.game.active_player_id == self.active_player_id { + if let Some(action) = trictrac_action { + (reward, is_rollpoint) = self.execute_action(action); + if is_rollpoint { + self.pointrolls_count += 1; + } + if reward != ERROR_REWARD { + self.goodmoves_count += 1; + // println!("{str_action}"); + } + } else { + // Action non convertible, pénalité + reward = -0.5; + } + } + + // Faire jouer l'adversaire (stratégie simple) + while self.game.active_player_id == self.opponent_id && self.game.stage != Stage::Ended { + // print!(":"); + reward += self.play_opponent_if_needed(); + } + + // Vérifier si la partie est terminée + // let max_steps = self.max_steps + // let max_steps = self.min_steps + // + (self.max_steps as f32 - self.min_steps) + // * f32::exp((self.goodmoves_ratio - 1.0) / 0.25); + let done = self.game.stage == Stage::Ended || self.game.determine_winner().is_some(); + + if done { + // Récompense finale basée sur le résultat + if let Some(winner_id) = self.game.determine_winner() { + if winner_id == self.active_player_id { + reward += WIN_POINTS; // Victoire + } else { + reward -= WIN_POINTS; // Défaite + } + } + } + let terminated = done || self.step_count >= self.max_steps; + + // Mettre à jour l'état + self.current_state = TrictracState::from_game_state(&self.game); + self.episode_reward += reward; + if self.visualized && terminated { + println!( + "Episode terminé. Récompense totale: {:.2}, Étapes: {}", + self.episode_reward, self.step_count + ); + } + + Snapshot::new(self.current_state, reward, terminated) + } +} + +impl TrictracEnvironment { + /// Convertit une action burn-rl vers une action Trictrac + pub fn convert_action(action: TrictracAction) -> Option { + training_common_big::TrictracAction::from_action_index(action.index.try_into().unwrap()) + } + + /// Convertit l'index d'une action au sein des actions valides vers une action Trictrac + #[allow(dead_code)] + fn convert_valid_action_index( + &self, + action: TrictracAction, + game_state: &GameState, + ) -> Option { + use training_common_big::get_valid_actions; + + // Obtenir les actions valides dans le contexte actuel + let valid_actions = get_valid_actions(game_state); + + if valid_actions.is_empty() { + return None; + } + + // Mapper l'index d'action sur une action valide + let action_index = (action.index as usize) % valid_actions.len(); + Some(valid_actions[action_index].clone()) + } + + /// Exécute une action Trictrac dans le jeu + // fn execute_action( + // &mut self, + // action:training_common_big::TrictracAction, + // ) -> Result> { + fn execute_action(&mut self, action: training_common_big::TrictracAction) -> (f32, bool) { + use training_common_big::TrictracAction; + + let mut reward = 0.0; + let mut is_rollpoint = false; + let mut need_roll = false; + + let event = match action { + TrictracAction::Roll => { + // Lancer les dés + need_roll = true; + Some(GameEvent::Roll { + player_id: self.active_player_id, + }) + } + // TrictracAction::Mark => { + // // Marquer des points + // let points = self.game. + // reward += 0.1 * points as f32; + // Some(GameEvent::Mark { + // player_id: self.active_player_id, + // points, + // }) + // } + TrictracAction::Go => { + // Continuer après avoir gagné un trou + Some(GameEvent::Go { + player_id: self.active_player_id, + }) + } + TrictracAction::Move { + dice_order, + from1, + from2, + } => { + // Effectuer un mouvement + let (dice1, dice2) = if dice_order { + (self.game.dice.values.0, self.game.dice.values.1) + } else { + (self.game.dice.values.1, self.game.dice.values.0) + }; + let mut to1 = from1 + dice1 as usize; + let mut to2 = from2 + dice2 as usize; + + // Gestion prise de coin par puissance + let opp_rest_field = 13; + if to1 == opp_rest_field && to2 == opp_rest_field { + to1 -= 1; + to2 -= 1; + } + + let checker_move1 = store::CheckerMove::new(from1, to1).unwrap_or_default(); + let checker_move2 = store::CheckerMove::new(from2, to2).unwrap_or_default(); + + Some(GameEvent::Move { + player_id: self.active_player_id, + moves: (checker_move1, checker_move2), + }) + } + }; + + // Appliquer l'événement si valide + if let Some(event) = event { + if self.game.validate(&event) { + self.game.consume(&event); + reward += REWARD_VALID_MOVE; + // Simuler le résultat des dés après un Roll + // if matches!(action, TrictracAction::Roll) { + if need_roll { + let mut rng = thread_rng(); + let dice_values = (rng.gen_range(1..=6), rng.gen_range(1..=6)); + let dice_event = GameEvent::RollResult { + player_id: self.active_player_id, + dice: store::Dice { + values: dice_values, + }, + }; + // print!("o"); + if self.game.validate(&dice_event) { + self.game.consume(&dice_event); + let (points, adv_points) = self.game.dice_points; + reward += REWARD_RATIO * (points - adv_points) as f32; + if points > 0 { + is_rollpoint = true; + // println!("info: rolled for {reward}"); + } + // Récompense proportionnelle aux points + } + } + } else { + // Pénalité pour action invalide + // on annule les précédents reward + // et on indique une valeur reconnaissable pour statistiques + reward = ERROR_REWARD; + } + } + + (reward, is_rollpoint) + } + + /// Fait jouer l'adversaire avec une stratégie simple + fn play_opponent_if_needed(&mut self) -> f32 { + // print!("z?"); + let mut reward = 0.0; + + // Si c'est le tour de l'adversaire, jouer automatiquement + if self.game.active_player_id == self.opponent_id && self.game.stage != Stage::Ended { + // Utiliser la stratégie default pour l'adversaire + use crate::BotStrategy; + + let mut strategy = crate::strategy::random::RandomStrategy::default(); + strategy.set_player_id(self.opponent_id); + if let Some(color) = self.game.player_color_by_id(&self.opponent_id) { + strategy.set_color(color); + } + *strategy.get_mut_game() = self.game.clone(); + + // Exécuter l'action selon le turn_stage + let mut calculate_points = false; + let opponent_color = store::Color::Black; + let event = match self.game.turn_stage { + TurnStage::RollDice => GameEvent::Roll { + player_id: self.opponent_id, + }, + TurnStage::RollWaiting => { + let mut rng = thread_rng(); + let dice_values = (rng.gen_range(1..=6), rng.gen_range(1..=6)); + calculate_points = true; // comment to replicate burnrl_before + GameEvent::RollResult { + player_id: self.opponent_id, + dice: store::Dice { + values: dice_values, + }, + } + } + TurnStage::MarkPoints => { + panic!("in play_opponent_if_needed > TurnStage::MarkPoints"); + // let dice_roll_count = self + // .game + // .players + // .get(&self.opponent_id) + // .unwrap() + // .dice_roll_count; + // let points_rules = + // PointsRules::new(&opponent_color, &self.game.board, self.game.dice); + // GameEvent::Mark { + // player_id: self.opponent_id, + // points: points_rules.get_points(dice_roll_count).0, + // } + } + TurnStage::MarkAdvPoints => { + let dice_roll_count = self + .game + .players + .get(&self.opponent_id) + .unwrap() + .dice_roll_count; + let points_rules = + PointsRules::new(&opponent_color, &self.game.board, self.game.dice); + // pas de reward : déjà comptabilisé lors du tour de blanc + GameEvent::Mark { + player_id: self.opponent_id, + points: points_rules.get_points(dice_roll_count).1, + } + } + TurnStage::HoldOrGoChoice => { + // Stratégie simple : toujours continuer + GameEvent::Go { + player_id: self.opponent_id, + } + } + TurnStage::Move => GameEvent::Move { + player_id: self.opponent_id, + moves: strategy.choose_move(), + }, + }; + + if self.game.validate(&event) { + self.game.consume(&event); + // print!("."); + if calculate_points { + // print!("x"); + let dice_roll_count = self + .game + .players + .get(&self.opponent_id) + .unwrap() + .dice_roll_count; + let points_rules = + PointsRules::new(&opponent_color, &self.game.board, self.game.dice); + let (points, adv_points) = points_rules.get_points(dice_roll_count); + // Récompense proportionnelle aux points + let adv_reward = REWARD_RATIO * (points - adv_points) as f32; + reward -= adv_reward; + // if adv_reward != 0.0 { + // println!("info: opponent : {adv_reward} -> {reward}"); + // } + } + } + } + reward + } +} + +impl AsMut for TrictracEnvironment { + fn as_mut(&mut self) -> &mut Self { + self + } +} diff --git a/bot/src/burnrl/environment_valid.rs b/bot/src/burnrl/environment_valid.rs new file mode 100644 index 0000000..346044c --- /dev/null +++ b/bot/src/burnrl/environment_valid.rs @@ -0,0 +1,443 @@ +use crate::training_common_big; +use burn::{prelude::Backend, tensor::Tensor}; +use burn_rl::base::{Action, Environment, Snapshot, State}; +use rand::{thread_rng, Rng}; +use store::{GameEvent, GameState, PlayerId, PointsRules, Stage, TurnStage}; + +/// État du jeu Trictrac pour burn-rl +#[derive(Debug, Clone, Copy)] +pub struct TrictracState { + pub data: [i8; 36], // Représentation vectorielle de l'état du jeu +} + +impl State for TrictracState { + type Data = [i8; 36]; + + fn to_tensor(&self) -> Tensor { + Tensor::from_floats(self.data, &B::Device::default()) + } + + fn size() -> usize { + 36 + } +} + +impl TrictracState { + /// Convertit un GameState en TrictracState + pub fn from_game_state(game_state: &GameState) -> Self { + let state_vec = game_state.to_vec(); + let mut data = [0; 36]; + + // Copier les données en s'assurant qu'on ne dépasse pas la taille + let copy_len = state_vec.len().min(36); + data[..copy_len].copy_from_slice(&state_vec[..copy_len]); + + TrictracState { data } + } +} + +/// Actions possibles dans Trictrac pour burn-rl +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct TrictracAction { + // u32 as required by burn_rl::base::Action type + pub index: u32, +} + +impl Action for TrictracAction { + fn random() -> Self { + use rand::{thread_rng, Rng}; + let mut rng = thread_rng(); + TrictracAction { + index: rng.gen_range(0..Self::size() as u32), + } + } + + fn enumerate() -> Vec { + (0..Self::size() as u32) + .map(|index| TrictracAction { index }) + .collect() + } + + fn size() -> usize { + // état avec le plus de choix : mouvement + // choix premier dé : 16 (15 pions + aucun pion), choix deuxième dé 16, x2 ordre dé + 64 + } +} + +impl From for TrictracAction { + fn from(index: u32) -> Self { + TrictracAction { index } + } +} + +impl From for u32 { + fn from(action: TrictracAction) -> u32 { + action.index + } +} + +/// Environnement Trictrac pour burn-rl +#[derive(Debug)] +pub struct TrictracEnvironment { + pub game: GameState, + active_player_id: PlayerId, + opponent_id: PlayerId, + current_state: TrictracState, + episode_reward: f32, + pub step_count: usize, + pub max_steps: usize, + pub pointrolls_count: usize, + pub visualized: bool, +} + +impl Environment for TrictracEnvironment { + type StateType = TrictracState; + type ActionType = TrictracAction; + type RewardType = f32; + + fn new(visualized: bool) -> Self { + let mut game = GameState::new(false); + + // Ajouter deux joueurs + game.init_player("DQN Agent"); + game.init_player("Opponent"); + let player1_id = 1; + let player2_id = 2; + + // Commencer la partie + game.consume(&GameEvent::BeginGame { goes_first: 1 }); + + let current_state = TrictracState::from_game_state(&game); + TrictracEnvironment { + game, + active_player_id: player1_id, + opponent_id: player2_id, + current_state, + episode_reward: 0.0, + step_count: 0, + max_steps: 2000, + pointrolls_count: 0, + visualized, + } + } + + fn state(&self) -> Self::StateType { + self.current_state + } + + fn reset(&mut self) -> Snapshot { + // Réinitialiser le jeu + self.game = GameState::new(false); + self.game.init_player("DQN Agent"); + self.game.init_player("Opponent"); + + // Commencer la partie + self.game.consume(&GameEvent::BeginGame { goes_first: 1 }); + + self.current_state = TrictracState::from_game_state(&self.game); + self.episode_reward = 0.0; + self.step_count = 0; + self.pointrolls_count = 0; + + Snapshot::new(self.current_state, 0.0, false) + } + + fn step(&mut self, action: Self::ActionType) -> Snapshot { + self.step_count += 1; + + // Convertir l'action burn-rl vers une action Trictrac + // let trictrac_action = Self::convert_action(action); + let trictrac_action = self.convert_valid_action_index(action); + let mut reward = 0.0; + let is_rollpoint: bool; + + // Exécuter l'action si c'est le tour de l'agent DQN + if self.game.active_player_id == self.active_player_id { + if let Some(action) = trictrac_action { + (reward, is_rollpoint) = self.execute_action(action); + // if reward != 0.0 { + // println!("info: self rew {reward}"); + // } + if is_rollpoint { + self.pointrolls_count += 1; + } + } else { + // Action non convertible, pénalité + println!("info: action non convertible -> -1 {trictrac_action:?}"); + reward = -1.0; + } + } + + // Faire jouer l'adversaire (stratégie simple) + while self.game.active_player_id == self.opponent_id && self.game.stage != Stage::Ended { + // let op_rew = self.play_opponent_if_needed(); + // if op_rew != 0.0 { + // println!("info: op rew {op_rew}"); + // } + // reward += op_rew; + reward += self.play_opponent_if_needed(); + } + + // Vérifier si la partie est terminée + let done = self.game.stage == Stage::Ended || self.game.determine_winner().is_some(); + + if done { + // Récompense finale basée sur le résultat + if let Some(winner_id) = self.game.determine_winner() { + if winner_id == self.active_player_id { + reward += 100.0; // Victoire + } else { + reward -= 100.0; // Défaite + } + } + } + let terminated = done || self.step_count >= self.max_steps; + + // Mettre à jour l'état + self.current_state = TrictracState::from_game_state(&self.game); + self.episode_reward += reward; + + if self.visualized && terminated { + println!( + "Episode terminé. Récompense totale: {:.2}, Étapes: {}", + self.episode_reward, self.step_count + ); + } + + Snapshot::new(self.current_state, reward, terminated) + } +} + +impl TrictracEnvironment { + const ERROR_REWARD: f32 = -1.12121; + const REWARD_RATIO: f32 = 1.0; + + /// Convertit une action burn-rl vers une action Trictrac + pub fn convert_action(action: TrictracAction) -> Option { + training_common_big::TrictracAction::from_action_index(action.index.try_into().unwrap()) + } + + /// Convertit l'index d'une action au sein des actions valides vers une action Trictrac + fn convert_valid_action_index( + &self, + action: TrictracAction, + ) -> Option { + use training_common_big::get_valid_actions; + + // Obtenir les actions valides dans le contexte actuel + let valid_actions = get_valid_actions(&self.game); + + if valid_actions.is_empty() { + return None; + } + + // Mapper l'index d'action sur une action valide + let action_index = (action.index as usize) % valid_actions.len(); + Some(valid_actions[action_index].clone()) + } + + /// Exécute une action Trictrac dans le jeu + // fn execute_action( + // &mut self, + // action: training_common_big::TrictracAction, + // ) -> Result> { + fn execute_action(&mut self, action: training_common_big::TrictracAction) -> (f32, bool) { + use training_common_big::TrictracAction; + + let mut reward = 0.0; + let mut is_rollpoint = false; + + let event = match action { + TrictracAction::Roll => { + // Lancer les dés + Some(GameEvent::Roll { + player_id: self.active_player_id, + }) + } + // TrictracAction::Mark => { + // // Marquer des points + // let points = self.game. + // reward += 0.1 * points as f32; + // Some(GameEvent::Mark { + // player_id: self.active_player_id, + // points, + // }) + // } + TrictracAction::Go => { + // Continuer après avoir gagné un trou + Some(GameEvent::Go { + player_id: self.active_player_id, + }) + } + TrictracAction::Move { + dice_order, + from1, + from2, + } => { + // Effectuer un mouvement + let (dice1, dice2) = if dice_order { + (self.game.dice.values.0, self.game.dice.values.1) + } else { + (self.game.dice.values.1, self.game.dice.values.0) + }; + let mut to1 = from1 + dice1 as usize; + let mut to2 = from2 + dice2 as usize; + + // Gestion prise de coin par puissance + let opp_rest_field = 13; + if to1 == opp_rest_field && to2 == opp_rest_field { + to1 -= 1; + to2 -= 1; + } + + let checker_move1 = store::CheckerMove::new(from1, to1).unwrap_or_default(); + let checker_move2 = store::CheckerMove::new(from2, to2).unwrap_or_default(); + + Some(GameEvent::Move { + player_id: self.active_player_id, + moves: (checker_move1, checker_move2), + }) + } + }; + + // Appliquer l'événement si valide + if let Some(event) = event { + if self.game.validate(&event) { + self.game.consume(&event); + + // Simuler le résultat des dés après un Roll + if matches!(action, TrictracAction::Roll) { + let mut rng = thread_rng(); + let dice_values = (rng.gen_range(1..=6), rng.gen_range(1..=6)); + let dice_event = GameEvent::RollResult { + player_id: self.active_player_id, + dice: store::Dice { + values: dice_values, + }, + }; + if self.game.validate(&dice_event) { + self.game.consume(&dice_event); + let (points, adv_points) = self.game.dice_points; + reward += Self::REWARD_RATIO * (points - adv_points) as f32; + if points > 0 { + is_rollpoint = true; + // println!("info: rolled for {reward}"); + } + // Récompense proportionnelle aux points + } + } + } else { + // Pénalité pour action invalide + // on annule les précédents reward + // et on indique une valeur reconnaissable pour statistiques + println!("info: action invalide -> err_reward"); + reward = Self::ERROR_REWARD; + } + } + + (reward, is_rollpoint) + } + + /// Fait jouer l'adversaire avec une stratégie simple + fn play_opponent_if_needed(&mut self) -> f32 { + let mut reward = 0.0; + + // Si c'est le tour de l'adversaire, jouer automatiquement + if self.game.active_player_id == self.opponent_id && self.game.stage != Stage::Ended { + // Utiliser la stratégie default pour l'adversaire + use crate::BotStrategy; + + let mut strategy = crate::strategy::random::RandomStrategy::default(); + strategy.set_player_id(self.opponent_id); + if let Some(color) = self.game.player_color_by_id(&self.opponent_id) { + strategy.set_color(color); + } + *strategy.get_mut_game() = self.game.clone(); + + // Exécuter l'action selon le turn_stage + let mut calculate_points = false; + let opponent_color = store::Color::Black; + let event = match self.game.turn_stage { + TurnStage::RollDice => GameEvent::Roll { + player_id: self.opponent_id, + }, + TurnStage::RollWaiting => { + let mut rng = thread_rng(); + let dice_values = (rng.gen_range(1..=6), rng.gen_range(1..=6)); + calculate_points = true; + GameEvent::RollResult { + player_id: self.opponent_id, + dice: store::Dice { + values: dice_values, + }, + } + } + TurnStage::MarkPoints => { + let dice_roll_count = self + .game + .players + .get(&self.opponent_id) + .unwrap() + .dice_roll_count; + let points_rules = + PointsRules::new(&opponent_color, &self.game.board, self.game.dice); + GameEvent::Mark { + player_id: self.opponent_id, + points: points_rules.get_points(dice_roll_count).0, + } + } + TurnStage::MarkAdvPoints => { + let dice_roll_count = self + .game + .players + .get(&self.opponent_id) + .unwrap() + .dice_roll_count; + let points_rules = + PointsRules::new(&opponent_color, &self.game.board, self.game.dice); + let points = points_rules.get_points(dice_roll_count).1; + // pas de reward : déjà comptabilisé lors du tour de blanc + GameEvent::Mark { + player_id: self.opponent_id, + points, + } + } + TurnStage::HoldOrGoChoice => { + // Stratégie simple : toujours continuer + GameEvent::Go { + player_id: self.opponent_id, + } + } + TurnStage::Move => GameEvent::Move { + player_id: self.opponent_id, + moves: strategy.choose_move(), + }, + }; + + if self.game.validate(&event) { + self.game.consume(&event); + if calculate_points { + let dice_roll_count = self + .game + .players + .get(&self.opponent_id) + .unwrap() + .dice_roll_count; + let points_rules = + PointsRules::new(&opponent_color, &self.game.board, self.game.dice); + let (points, adv_points) = points_rules.get_points(dice_roll_count); + reward -= Self::REWARD_RATIO * (points - adv_points) as f32; + // Récompense proportionnelle aux points + } + } + } + reward + } +} + +impl AsMut for TrictracEnvironment { + fn as_mut(&mut self) -> &mut Self { + self + } +} diff --git a/bot/src/burnrl/main.rs b/bot/src/burnrl/main.rs new file mode 100644 index 0000000..f7608a3 --- /dev/null +++ b/bot/src/burnrl/main.rs @@ -0,0 +1,123 @@ +use bot::burnrl::algos::{ + dqn, dqn_big, dqn_valid, ppo, ppo_big, ppo_valid, sac, sac_big, sac_valid, +}; +use bot::burnrl::environment::TrictracEnvironment; +use bot::burnrl::environment_big::TrictracEnvironment as TrictracEnvironmentBig; +use bot::burnrl::environment_valid::TrictracEnvironment as TrictracEnvironmentValid; +use bot::burnrl::utils::{demo_model, Config}; +use burn::backend::{Autodiff, NdArray}; +use burn_rl::base::ElemType; +use std::env; + +type Backend = Autodiff>; + +fn main() { + let args: Vec = env::args().collect(); + let algo = &args[1]; + // let dir_path = &args[2]; + + let path = format!("bot/models/burnrl_{algo}"); + println!( + "info: loading configuration from file {:?}", + confy::get_configuration_file_path("trictrac_bot", None).unwrap() + ); + let mut conf: Config = confy::load("trictrac_bot", None).expect("Could not load config"); + conf.save_path = Some(path.clone()); + println!("{conf}----------"); + + match algo.as_str() { + "dqn" => { + let _agent = dqn::run::(&conf, false); + println!("> Chargement du modèle pour test"); + let loaded_model = dqn::load_model(conf.dense_size, &path); + let loaded_agent: burn_rl::agent::DQN = + burn_rl::agent::DQN::new(loaded_model.unwrap()); + + println!("> Test avec le modèle chargé"); + demo_model(loaded_agent); + } + "dqn_big" => { + let _agent = dqn_big::run::(&conf, false); + println!("> Chargement du modèle pour test"); + let loaded_model = dqn_big::load_model(conf.dense_size, &path); + let loaded_agent: burn_rl::agent::DQN = + burn_rl::agent::DQN::new(loaded_model.unwrap()); + + println!("> Test avec le modèle chargé"); + demo_model(loaded_agent); + } + "dqn_valid" => { + let _agent = dqn_valid::run::(&conf, false); + println!("> Chargement du modèle pour test"); + let loaded_model = dqn_valid::load_model(conf.dense_size, &path); + let loaded_agent: burn_rl::agent::DQN = + burn_rl::agent::DQN::new(loaded_model.unwrap()); + + println!("> Test avec le modèle chargé"); + demo_model(loaded_agent); + } + "sac" => { + let _agent = sac::run::(&conf, false); + println!("> Chargement du modèle pour test"); + let loaded_model = sac::load_model(conf.dense_size, &path); + let loaded_agent: burn_rl::agent::SAC = + burn_rl::agent::SAC::new(loaded_model.unwrap()); + + println!("> Test avec le modèle chargé"); + demo_model(loaded_agent); + } + "sac_big" => { + let _agent = sac_big::run::(&conf, false); + println!("> Chargement du modèle pour test"); + let loaded_model = sac_big::load_model(conf.dense_size, &path); + let loaded_agent: burn_rl::agent::SAC = + burn_rl::agent::SAC::new(loaded_model.unwrap()); + + println!("> Test avec le modèle chargé"); + demo_model(loaded_agent); + } + "sac_valid" => { + let _agent = sac_valid::run::(&conf, false); + println!("> Chargement du modèle pour test"); + let loaded_model = sac_valid::load_model(conf.dense_size, &path); + let loaded_agent: burn_rl::agent::SAC = + burn_rl::agent::SAC::new(loaded_model.unwrap()); + + println!("> Test avec le modèle chargé"); + demo_model(loaded_agent); + } + "ppo" => { + let _agent = ppo::run::(&conf, false); + println!("> Chargement du modèle pour test"); + let loaded_model = ppo::load_model(conf.dense_size, &path); + let loaded_agent: burn_rl::agent::PPO = + burn_rl::agent::PPO::new(loaded_model.unwrap()); + + println!("> Test avec le modèle chargé"); + demo_model(loaded_agent); + } + "ppo_big" => { + let _agent = ppo_big::run::(&conf, false); + println!("> Chargement du modèle pour test"); + let loaded_model = ppo_big::load_model(conf.dense_size, &path); + let loaded_agent: burn_rl::agent::PPO = + burn_rl::agent::PPO::new(loaded_model.unwrap()); + + println!("> Test avec le modèle chargé"); + demo_model(loaded_agent); + } + "ppo_valid" => { + let _agent = ppo_valid::run::(&conf, false); + println!("> Chargement du modèle pour test"); + let loaded_model = ppo_valid::load_model(conf.dense_size, &path); + let loaded_agent: burn_rl::agent::PPO = + burn_rl::agent::PPO::new(loaded_model.unwrap()); + + println!("> Test avec le modèle chargé"); + demo_model(loaded_agent); + } + &_ => { + println!("unknown algo {algo}"); + } + } +} diff --git a/bot/src/burnrl/mod.rs b/bot/src/burnrl/mod.rs new file mode 100644 index 0000000..62bebc8 --- /dev/null +++ b/bot/src/burnrl/mod.rs @@ -0,0 +1,5 @@ +pub mod algos; +pub mod environment; +pub mod environment_big; +pub mod environment_valid; +pub mod utils; diff --git a/bot/src/burnrl/utils.rs b/bot/src/burnrl/utils.rs new file mode 100644 index 0000000..9233819 --- /dev/null +++ b/bot/src/burnrl/utils.rs @@ -0,0 +1,132 @@ +use burn::module::{Param, ParamId}; +use burn::nn::Linear; +use burn::tensor::backend::Backend; +use burn::tensor::Tensor; +use burn_rl::base::{Agent, ElemType, Environment}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Config { + pub save_path: Option, + pub max_steps: usize, // max steps by episode + pub num_episodes: usize, + pub dense_size: usize, // neural network complexity + + // discount factor. Plus élevé = encourage stratégies à long terme + pub gamma: f32, + // soft update rate. Taux de mise à jour du réseau cible. Plus bas = adaptation plus lente moins sensible aux coups de chance + pub tau: f32, + // taille du pas. Bas : plus lent, haut : risque de ne jamais + pub learning_rate: f32, + // nombre d'expériences passées sur lesquelles pour calcul de l'erreur moy. + pub batch_size: usize, + // limite max de correction à apporter au gradient (default 100) + pub clip_grad: f32, + + // ---- for SAC + pub min_probability: f32, + + // ---- for DQN + // epsilon initial value (0.9 => more exploration) + pub eps_start: f64, + pub eps_end: f64, + // eps_decay higher = epsilon decrease slower + // used in : epsilon = eps_end + (eps_start - eps_end) * e^(-step / eps_decay); + // epsilon is updated at the start of each episode + pub eps_decay: f64, + + // ---- for PPO + pub lambda: f32, + pub epsilon_clip: f32, + pub critic_weight: f32, + pub entropy_weight: f32, + pub epochs: usize, +} + +impl Default for Config { + fn default() -> Self { + Self { + save_path: None, + max_steps: 2000, + num_episodes: 1000, + dense_size: 256, + gamma: 0.999, + tau: 0.005, + learning_rate: 0.001, + batch_size: 32, + clip_grad: 100.0, + min_probability: 1e-9, + eps_start: 0.9, + eps_end: 0.05, + eps_decay: 1000.0, + lambda: 0.95, + epsilon_clip: 0.2, + critic_weight: 0.5, + entropy_weight: 0.01, + epochs: 8, + } + } +} + +impl std::fmt::Display for Config { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let mut s = String::new(); + s.push_str(&format!("max_steps={:?}\n", self.max_steps)); + s.push_str(&format!("num_episodes={:?}\n", self.num_episodes)); + s.push_str(&format!("dense_size={:?}\n", self.dense_size)); + s.push_str(&format!("eps_start={:?}\n", self.eps_start)); + s.push_str(&format!("eps_end={:?}\n", self.eps_end)); + s.push_str(&format!("eps_decay={:?}\n", self.eps_decay)); + s.push_str(&format!("gamma={:?}\n", self.gamma)); + s.push_str(&format!("tau={:?}\n", self.tau)); + s.push_str(&format!("learning_rate={:?}\n", self.learning_rate)); + s.push_str(&format!("batch_size={:?}\n", self.batch_size)); + s.push_str(&format!("clip_grad={:?}\n", self.clip_grad)); + s.push_str(&format!("min_probability={:?}\n", self.min_probability)); + s.push_str(&format!("lambda={:?}\n", self.lambda)); + s.push_str(&format!("epsilon_clip={:?}\n", self.epsilon_clip)); + s.push_str(&format!("critic_weight={:?}\n", self.critic_weight)); + s.push_str(&format!("entropy_weight={:?}\n", self.entropy_weight)); + s.push_str(&format!("epochs={:?}\n", self.epochs)); + write!(f, "{s}") + } +} + +pub fn demo_model(agent: impl Agent) { + let mut env = E::new(true); + let mut state = env.state(); + let mut done = false; + while !done { + if let Some(action) = agent.react(&state) { + let snapshot = env.step(action); + state = *snapshot.state(); + done = snapshot.done(); + } + } +} + +fn soft_update_tensor( + this: &Param>, + that: &Param>, + tau: ElemType, +) -> Param> { + let that_weight = that.val(); + let this_weight = this.val(); + let new_weight = this_weight * (1.0 - tau) + that_weight * tau; + + Param::initialized(ParamId::new(), new_weight) +} + +pub fn soft_update_linear( + this: Linear, + that: &Linear, + tau: ElemType, +) -> Linear { + let weight = soft_update_tensor(&this.weight, &that.weight, tau); + let bias = match (&this.bias, &that.bias) { + (Some(this_bias), Some(that_bias)) => Some(soft_update_tensor(this_bias, that_bias, tau)), + _ => None, + }; + + Linear:: { weight, bias } +} diff --git a/bot/src/dqn_simple/dqn_model.rs b/bot/src/dqn_simple/dqn_model.rs new file mode 100644 index 0000000..9c31f44 --- /dev/null +++ b/bot/src/dqn_simple/dqn_model.rs @@ -0,0 +1,153 @@ +use crate::training_common_big::TrictracAction; +use serde::{Deserialize, Serialize}; + +/// Configuration pour l'agent DQN +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DqnConfig { + pub state_size: usize, + pub hidden_size: usize, + pub num_actions: usize, + pub learning_rate: f64, + pub gamma: f64, + pub epsilon: f64, + pub epsilon_decay: f64, + pub epsilon_min: f64, + pub replay_buffer_size: usize, + pub batch_size: usize, +} + +impl Default for DqnConfig { + fn default() -> Self { + Self { + state_size: 36, + hidden_size: 512, // Augmenter la taille pour gérer l'espace d'actions élargi + num_actions: TrictracAction::action_space_size(), + learning_rate: 0.001, + gamma: 0.99, + epsilon: 0.1, + epsilon_decay: 0.995, + epsilon_min: 0.01, + replay_buffer_size: 10000, + batch_size: 32, + } + } +} + +/// Réseau de neurones DQN simplifié (matrice de poids basique) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SimpleNeuralNetwork { + pub weights1: Vec>, + pub biases1: Vec, + pub weights2: Vec>, + pub biases2: Vec, + pub weights3: Vec>, + pub biases3: Vec, +} + +impl SimpleNeuralNetwork { + pub fn new(input_size: usize, hidden_size: usize, output_size: usize) -> Self { + use rand::{thread_rng, Rng}; + let mut rng = thread_rng(); + + // Initialisation aléatoire des poids avec Xavier/Glorot + let scale1 = (2.0 / input_size as f32).sqrt(); + let weights1 = (0..hidden_size) + .map(|_| { + (0..input_size) + .map(|_| rng.gen_range(-scale1..scale1)) + .collect() + }) + .collect(); + let biases1 = vec![0.0; hidden_size]; + + let scale2 = (2.0 / hidden_size as f32).sqrt(); + let weights2 = (0..hidden_size) + .map(|_| { + (0..hidden_size) + .map(|_| rng.gen_range(-scale2..scale2)) + .collect() + }) + .collect(); + let biases2 = vec![0.0; hidden_size]; + + let scale3 = (2.0 / hidden_size as f32).sqrt(); + let weights3 = (0..output_size) + .map(|_| { + (0..hidden_size) + .map(|_| rng.gen_range(-scale3..scale3)) + .collect() + }) + .collect(); + let biases3 = vec![0.0; output_size]; + + Self { + weights1, + biases1, + weights2, + biases2, + weights3, + biases3, + } + } + + pub fn forward(&self, input: &[f32]) -> Vec { + // Première couche + let mut layer1: Vec = self.biases1.clone(); + for (i, neuron_weights) in self.weights1.iter().enumerate() { + for (j, &weight) in neuron_weights.iter().enumerate() { + if j < input.len() { + layer1[i] += input[j] * weight; + } + } + layer1[i] = layer1[i].max(0.0); // ReLU + } + + // Deuxième couche + let mut layer2: Vec = self.biases2.clone(); + for (i, neuron_weights) in self.weights2.iter().enumerate() { + for (j, &weight) in neuron_weights.iter().enumerate() { + if j < layer1.len() { + layer2[i] += layer1[j] * weight; + } + } + layer2[i] = layer2[i].max(0.0); // ReLU + } + + // Couche de sortie + let mut output: Vec = self.biases3.clone(); + for (i, neuron_weights) in self.weights3.iter().enumerate() { + for (j, &weight) in neuron_weights.iter().enumerate() { + if j < layer2.len() { + output[i] += layer2[j] * weight; + } + } + } + + output + } + + pub fn get_best_action(&self, input: &[f32]) -> usize { + let q_values = self.forward(input); + q_values + .iter() + .enumerate() + .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap()) + .map(|(index, _)| index) + .unwrap_or(0) + } + + pub fn save>( + &self, + path: P, + ) -> Result<(), Box> { + let data = serde_json::to_string_pretty(self)?; + std::fs::write(path, data)?; + Ok(()) + } + + pub fn load>(path: P) -> Result> { + let data = std::fs::read_to_string(path)?; + let network = serde_json::from_str(&data)?; + Ok(network) + } +} diff --git a/bot/src/dqn_simple/dqn_trainer.rs b/bot/src/dqn_simple/dqn_trainer.rs new file mode 100644 index 0000000..ed60f5e --- /dev/null +++ b/bot/src/dqn_simple/dqn_trainer.rs @@ -0,0 +1,494 @@ +use crate::{CheckerMove, Color, GameState, PlayerId}; +use rand::prelude::SliceRandom; +use rand::{thread_rng, Rng}; +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; +use store::{GameEvent, MoveRules, PointsRules, Stage, TurnStage}; + +use super::dqn_model::{DqnConfig, SimpleNeuralNetwork}; +use crate::training_common_big::{get_valid_actions, TrictracAction}; + +/// Expérience pour le buffer de replay +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Experience { + pub state: Vec, + pub action: TrictracAction, + pub reward: f32, + pub next_state: Vec, + pub done: bool, +} + +/// Buffer de replay pour stocker les expériences +#[derive(Debug)] +pub struct ReplayBuffer { + buffer: VecDeque, + capacity: usize, +} + +impl ReplayBuffer { + pub fn new(capacity: usize) -> Self { + Self { + buffer: VecDeque::with_capacity(capacity), + capacity, + } + } + + pub fn push(&mut self, experience: Experience) { + if self.buffer.len() >= self.capacity { + self.buffer.pop_front(); + } + self.buffer.push_back(experience); + } + + pub fn sample(&self, batch_size: usize) -> Vec { + let mut rng = thread_rng(); + let len = self.buffer.len(); + if len < batch_size { + return self.buffer.iter().cloned().collect(); + } + + let mut batch = Vec::with_capacity(batch_size); + for _ in 0..batch_size { + let idx = rng.gen_range(0..len); + batch.push(self.buffer[idx].clone()); + } + batch + } + + pub fn is_empty(&self) -> bool { + self.buffer.is_empty() + } + + pub fn len(&self) -> usize { + self.buffer.len() + } +} + +/// Agent DQN pour l'apprentissage par renforcement +#[derive(Debug)] +pub struct DqnAgent { + config: DqnConfig, + model: SimpleNeuralNetwork, + target_model: SimpleNeuralNetwork, + replay_buffer: ReplayBuffer, + epsilon: f64, + step_count: usize, +} + +impl DqnAgent { + pub fn new(config: DqnConfig) -> Self { + let model = + SimpleNeuralNetwork::new(config.state_size, config.hidden_size, config.num_actions); + let target_model = model.clone(); + let replay_buffer = ReplayBuffer::new(config.replay_buffer_size); + let epsilon = config.epsilon; + + Self { + config, + model, + target_model, + replay_buffer, + epsilon, + step_count: 0, + } + } + + pub fn select_action(&mut self, game_state: &GameState, state: &[f32]) -> TrictracAction { + let valid_actions = get_valid_actions(game_state); + + if valid_actions.is_empty() { + // Fallback si aucune action valide + return TrictracAction::Roll; + } + + let mut rng = thread_rng(); + if rng.gen::() < self.epsilon { + // Exploration : action valide aléatoire + valid_actions + .choose(&mut rng) + .cloned() + .unwrap_or(TrictracAction::Roll) + } else { + // Exploitation : meilleure action valide selon le modèle + let q_values = self.model.forward(state); + + let mut best_action = &valid_actions[0]; + let mut best_q_value = f32::NEG_INFINITY; + + for action in &valid_actions { + let action_index = action.to_action_index(); + if action_index < q_values.len() { + let q_value = q_values[action_index]; + if q_value > best_q_value { + best_q_value = q_value; + best_action = action; + } + } + } + + best_action.clone() + } + } + + pub fn store_experience(&mut self, experience: Experience) { + self.replay_buffer.push(experience); + } + + pub fn train(&mut self) { + if self.replay_buffer.len() < self.config.batch_size { + return; + } + + // Pour l'instant, on simule l'entraînement en mettant à jour epsilon + // Dans une implémentation complète, ici on ferait la backpropagation + self.epsilon = (self.epsilon * self.config.epsilon_decay).max(self.config.epsilon_min); + self.step_count += 1; + + // Mise à jour du target model tous les 100 steps + if self.step_count % 100 == 0 { + self.target_model = self.model.clone(); + } + } + + pub fn save_model>( + &self, + path: P, + ) -> Result<(), Box> { + self.model.save(path) + } + + pub fn get_epsilon(&self) -> f64 { + self.epsilon + } + + pub fn get_step_count(&self) -> usize { + self.step_count + } +} + +/// Environnement Trictrac pour l'entraînement +#[derive(Debug)] +pub struct TrictracEnv { + pub game_state: GameState, + pub agent_player_id: PlayerId, + pub opponent_player_id: PlayerId, + pub agent_color: Color, + pub max_steps: usize, + pub current_step: usize, +} + +impl Default for TrictracEnv { + fn default() -> Self { + let mut game_state = GameState::new(false); + game_state.init_player("agent"); + game_state.init_player("opponent"); + + Self { + game_state, + agent_player_id: 1, + opponent_player_id: 2, + agent_color: Color::White, + max_steps: 1000, + current_step: 0, + } + } +} + +impl TrictracEnv { + pub fn reset(&mut self) -> Vec { + self.game_state = GameState::new(false); + self.game_state.init_player("agent"); + self.game_state.init_player("opponent"); + + // Commencer la partie + self.game_state.consume(&GameEvent::BeginGame { + goes_first: self.agent_player_id, + }); + + self.current_step = 0; + self.game_state.to_vec_float() + } + + pub fn step(&mut self, action: TrictracAction) -> (Vec, f32, bool) { + let mut reward = 0.0; + + // Appliquer l'action de l'agent + if self.game_state.active_player_id == self.agent_player_id { + reward += self.apply_agent_action(action); + } + + // Faire jouer l'adversaire (stratégie simple) + while self.game_state.active_player_id == self.opponent_player_id + && self.game_state.stage != Stage::Ended + { + reward += self.play_opponent_turn(); + } + + // Vérifier si la partie est terminée + let done = self.game_state.stage == Stage::Ended + || self.game_state.determine_winner().is_some() + || self.current_step >= self.max_steps; + + // Récompense finale si la partie est terminée + if done { + if let Some(winner) = self.game_state.determine_winner() { + if winner == self.agent_player_id { + reward += 100.0; // Bonus pour gagner + } else { + reward -= 50.0; // Pénalité pour perdre + } + } + } + + self.current_step += 1; + let next_state = self.game_state.to_vec_float(); + (next_state, reward, done) + } + + fn apply_agent_action(&mut self, action: TrictracAction) -> f32 { + let mut reward = 0.0; + + let event = match action { + TrictracAction::Roll => { + // Lancer les dés + reward += 0.1; + Some(GameEvent::Roll { + player_id: self.agent_player_id, + }) + } + // TrictracAction::Mark => { + // // Marquer des points + // let points = self.game_state. + // reward += 0.1 * points as f32; + // Some(GameEvent::Mark { + // player_id: self.agent_player_id, + // points, + // }) + // } + TrictracAction::Go => { + // Continuer après avoir gagné un trou + reward += 0.2; + Some(GameEvent::Go { + player_id: self.agent_player_id, + }) + } + TrictracAction::Move { + dice_order, + from1, + from2, + } => { + // Effectuer un mouvement + let (dice1, dice2) = if dice_order { + (self.game_state.dice.values.0, self.game_state.dice.values.1) + } else { + (self.game_state.dice.values.1, self.game_state.dice.values.0) + }; + let mut to1 = from1 + dice1 as usize; + let mut to2 = from2 + dice2 as usize; + + // Gestion prise de coin par puissance + let opp_rest_field = 13; + if to1 == opp_rest_field && to2 == opp_rest_field { + to1 -= 1; + to2 -= 1; + } + + let checker_move1 = store::CheckerMove::new(from1, to1).unwrap_or_default(); + let checker_move2 = store::CheckerMove::new(from2, to2).unwrap_or_default(); + + reward += 0.2; + Some(GameEvent::Move { + player_id: self.agent_player_id, + moves: (checker_move1, checker_move2), + }) + } + }; + + // Appliquer l'événement si valide + if let Some(event) = event { + if self.game_state.validate(&event) { + self.game_state.consume(&event); + + // Simuler le résultat des dés après un Roll + if matches!(action, TrictracAction::Roll) { + let mut rng = thread_rng(); + let dice_values = (rng.gen_range(1..=6), rng.gen_range(1..=6)); + let dice_event = GameEvent::RollResult { + player_id: self.agent_player_id, + dice: store::Dice { + values: dice_values, + }, + }; + if self.game_state.validate(&dice_event) { + self.game_state.consume(&dice_event); + } + } + } else { + // Pénalité pour action invalide + reward -= 2.0; + } + } + + reward + } + + // TODO : use default bot strategy + fn play_opponent_turn(&mut self) -> f32 { + let mut reward = 0.0; + let event = match self.game_state.turn_stage { + TurnStage::RollDice => GameEvent::Roll { + player_id: self.opponent_player_id, + }, + TurnStage::RollWaiting => { + let mut rng = thread_rng(); + let dice_values = (rng.gen_range(1..=6), rng.gen_range(1..=6)); + GameEvent::RollResult { + player_id: self.opponent_player_id, + dice: store::Dice { + values: dice_values, + }, + } + } + TurnStage::MarkAdvPoints | TurnStage::MarkPoints => { + let opponent_color = self.agent_color.opponent_color(); + let dice_roll_count = self + .game_state + .players + .get(&self.opponent_player_id) + .unwrap() + .dice_roll_count; + let points_rules = PointsRules::new( + &opponent_color, + &self.game_state.board, + self.game_state.dice, + ); + let (points, adv_points) = points_rules.get_points(dice_roll_count); + reward -= 0.3 * (points - adv_points) as f32; // Récompense proportionnelle aux points + + GameEvent::Mark { + player_id: self.opponent_player_id, + points, + } + } + TurnStage::Move => { + let opponent_color = self.agent_color.opponent_color(); + let rules = MoveRules::new( + &opponent_color, + &self.game_state.board, + self.game_state.dice, + ); + let possible_moves = rules.get_possible_moves_sequences(true, vec![]); + + // Stratégie simple : choix aléatoire + let mut rng = thread_rng(); + let choosen_move = *possible_moves + .choose(&mut rng) + .unwrap_or(&(CheckerMove::default(), CheckerMove::default())); + + GameEvent::Move { + player_id: self.opponent_player_id, + moves: if opponent_color == Color::White { + choosen_move + } else { + (choosen_move.0.mirror(), choosen_move.1.mirror()) + }, + } + } + TurnStage::HoldOrGoChoice => { + // Stratégie simple : toujours continuer + GameEvent::Go { + player_id: self.opponent_player_id, + } + } + }; + if self.game_state.validate(&event) { + self.game_state.consume(&event); + } + reward + } +} + +/// Entraîneur pour le modèle DQN +pub struct DqnTrainer { + agent: DqnAgent, + env: TrictracEnv, +} + +impl DqnTrainer { + pub fn new(config: DqnConfig) -> Self { + Self { + agent: DqnAgent::new(config), + env: TrictracEnv::default(), + } + } + + pub fn train_episode(&mut self) -> f32 { + let mut total_reward = 0.0; + let mut state = self.env.reset(); + // let mut step_count = 0; + + loop { + // step_count += 1; + let action = self.agent.select_action(&self.env.game_state, &state); + let (next_state, reward, done) = self.env.step(action.clone()); + total_reward += reward; + + let experience = Experience { + state: state.clone(), + action, + reward, + next_state: next_state.clone(), + done, + }; + self.agent.store_experience(experience); + self.agent.train(); + + if done { + break; + } + // if step_count % 100 == 0 { + // println!("{:?}", next_state); + // } + state = next_state; + } + + total_reward + } + + pub fn train( + &mut self, + episodes: usize, + save_every: usize, + model_path: &str, + ) -> Result<(), Box> { + println!("Démarrage de l'entraînement DQN pour {episodes} épisodes"); + + for episode in 1..=episodes { + let reward = self.train_episode(); + + if episode % 100 == 0 { + println!( + "Épisode {}/{}: Récompense = {:.2}, Epsilon = {:.3}, Steps = {}", + episode, + episodes, + reward, + self.agent.get_epsilon(), + self.agent.get_step_count() + ); + } + + if episode % save_every == 0 { + let save_path = format!("{model_path}_episode_{episode}.json"); + self.agent.save_model(&save_path)?; + println!("Modèle sauvegardé : {save_path}"); + } + } + + // Sauvegarder le modèle final + let final_path = format!("{model_path}_final.json"); + self.agent.save_model(&final_path)?; + println!("Modèle final sauvegardé : {final_path}"); + + Ok(()) + } +} diff --git a/bot/src/dqn_simple/main.rs b/bot/src/dqn_simple/main.rs new file mode 100644 index 0000000..024f895 --- /dev/null +++ b/bot/src/dqn_simple/main.rs @@ -0,0 +1,109 @@ +use bot::dqn_simple::dqn_model::DqnConfig; +use bot::dqn_simple::dqn_trainer::DqnTrainer; +use bot::training_common::TrictracAction; +use std::env; + +fn main() -> Result<(), Box> { + env_logger::init(); + + let args: Vec = env::args().collect(); + + // Paramètres par défaut + let mut episodes = 1000; + let mut model_path = "models/dqn_model".to_string(); + let mut save_every = 100; + + // Parser les arguments de ligne de commande + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--episodes" => { + if i + 1 < args.len() { + episodes = args[i + 1].parse().unwrap_or(1000); + i += 2; + } else { + eprintln!("Erreur : --episodes nécessite une valeur"); + std::process::exit(1); + } + } + "--model-path" => { + if i + 1 < args.len() { + model_path = args[i + 1].clone(); + i += 2; + } else { + eprintln!("Erreur : --model-path nécessite une valeur"); + std::process::exit(1); + } + } + "--save-every" => { + if i + 1 < args.len() { + save_every = args[i + 1].parse().unwrap_or(100); + i += 2; + } else { + eprintln!("Erreur : --save-every nécessite une valeur"); + std::process::exit(1); + } + } + "--help" | "-h" => { + print_help(); + std::process::exit(0); + } + _ => { + eprintln!("Argument inconnu : {}", args[i]); + print_help(); + std::process::exit(1); + } + } + } + + // Créer le dossier models s'il n'existe pas + std::fs::create_dir_all("models")?; + + println!("Configuration d'entraînement DQN :"); + println!(" Épisodes : {episodes}"); + println!(" Chemin du modèle : {model_path}"); + println!(" Sauvegarde tous les {save_every} épisodes"); + println!(); + + // Configuration DQN + let config = DqnConfig { + state_size: 36, // state.to_vec size + hidden_size: 256, + num_actions: TrictracAction::action_space_size(), + learning_rate: 0.001, + gamma: 0.99, + epsilon: 0.9, // Commencer avec plus d'exploration + epsilon_decay: 0.995, + epsilon_min: 0.01, + replay_buffer_size: 10000, + batch_size: 32, + }; + + // Créer et lancer l'entraîneur + let mut trainer = DqnTrainer::new(config); + trainer.train(episodes, save_every, &model_path)?; + + println!("Entraînement terminé avec succès !"); + println!("Pour utiliser le modèle entraîné :"); + println!(" cargo run --bin=client_cli -- --bot dqn:{model_path}_final.json,dummy"); + + Ok(()) +} + +fn print_help() { + println!("Entraîneur DQN pour Trictrac"); + println!(); + println!("USAGE:"); + println!(" cargo run --bin=train_dqn [OPTIONS]"); + println!(); + println!("OPTIONS:"); + println!(" --episodes Nombre d'épisodes d'entraînement (défaut: 1000)"); + println!(" --model-path Chemin de base pour sauvegarder les modèles (défaut: models/dqn_model)"); + println!(" --save-every Sauvegarder le modèle tous les N épisodes (défaut: 100)"); + println!(" -h, --help Afficher cette aide"); + println!(); + println!("EXEMPLES:"); + println!(" cargo run --bin=train_dqn"); + println!(" cargo run --bin=train_dqn -- --episodes 5000 --save-every 500"); + println!(" cargo run --bin=train_dqn -- --model-path models/my_model --episodes 2000"); +} diff --git a/bot/src/dqn_simple/mod.rs b/bot/src/dqn_simple/mod.rs new file mode 100644 index 0000000..8090a29 --- /dev/null +++ b/bot/src/dqn_simple/mod.rs @@ -0,0 +1,2 @@ +pub mod dqn_model; +pub mod dqn_trainer; diff --git a/bot/src/lib.rs b/bot/src/lib.rs new file mode 100644 index 0000000..dab36be --- /dev/null +++ b/bot/src/lib.rs @@ -0,0 +1,191 @@ +pub mod burnrl; +pub mod dqn_simple; +pub mod strategy; +pub mod training_common; +pub mod training_common_big; +pub mod trictrac_board; + +use log::debug; +use store::{CheckerMove, Color, GameEvent, GameState, PlayerId, PointsRules, Stage, TurnStage}; +pub use strategy::default::DefaultStrategy; +pub use strategy::dqn::DqnStrategy; +pub use strategy::dqnburn::DqnBurnStrategy; +pub use strategy::erroneous_moves::ErroneousStrategy; +pub use strategy::random::RandomStrategy; +pub use strategy::stable_baselines3::StableBaselines3Strategy; + +pub trait BotStrategy: std::fmt::Debug { + fn get_game(&self) -> &GameState; + fn get_mut_game(&mut self) -> &mut GameState; + fn calculate_points(&self) -> u8; + fn calculate_adv_points(&self) -> u8; + fn choose_move(&self) -> (CheckerMove, CheckerMove); + fn choose_go(&self) -> bool; + fn set_player_id(&mut self, player_id: PlayerId); + fn set_color(&mut self, color: Color); + fn init_players(&mut self) { + self.get_mut_game().init_player("p1"); + self.get_mut_game().init_player("p2"); + } +} + +#[derive(Debug)] +pub struct Bot { + pub player_id: PlayerId, + strategy: Box, + color: Color, + // schools_enabled: bool, +} + +impl Default for Bot { + fn default() -> Self { + let strategy = DefaultStrategy::default(); + Self { + player_id: 1, + strategy: Box::new(strategy), + color: Color::White, + // schools_enabled: false, + } + } +} + +impl Bot { + /// new initialize a bot + // pub fn new(mut strategy: Box, color: Color, schools_enabled: bool) -> Self { + pub fn new(mut strategy: Box, color: Color) -> Self { + // let game = strategy.get_mut_game(); + strategy.init_players(); + let player_id = match color { + Color::White => 1, + Color::Black => 2, + }; + // strategy.set_player_id(player_id); + // strategy.set_color(color); + Self { + player_id, + strategy, + color, + // schools_enabled: false, + } + } + + pub fn handle_event(&mut self, event: &GameEvent) -> Option { + debug!(">>>> {:?} BOT handle", self.color); + let game = self.strategy.get_mut_game(); + let internal_event = if self.color == Color::Black { + &event.get_mirror() + } else { + event + }; + + let init_player_points = game.who_plays().map(|p| (p.points, p.holes)); + let turn_stage = game.turn_stage; + game.consume(internal_event); + if game.stage == Stage::Ended { + debug!("<<<< end {:?} BOT handle", self.color); + return None; + } + let active_player_id = if self.color == Color::Black { + if game.active_player_id == 1 { + 2 + } else { + 1 + } + } else { + game.active_player_id + }; + if active_player_id == self.player_id { + let player_points = game.who_plays().map(|p| (p.points, p.holes)); + if self.color == Color::Black { + debug!( " input (internal) evt : {internal_event:?}, points : {init_player_points:?}, stage : {turn_stage:?}"); + } + let internal_event = match game.turn_stage { + TurnStage::MarkAdvPoints => Some(GameEvent::Mark { + player_id: 1, + points: self.strategy.calculate_adv_points(), + }), + TurnStage::RollDice => Some(GameEvent::Roll { player_id: 1 }), + TurnStage::MarkPoints => Some(GameEvent::Mark { + player_id: 1, + points: self.strategy.calculate_points(), + }), + TurnStage::Move => Some(GameEvent::Move { + player_id: 1, + moves: self.strategy.choose_move(), + }), + TurnStage::HoldOrGoChoice => { + if self.strategy.choose_go() { + Some(GameEvent::Go { player_id: 1 }) + } else { + Some(GameEvent::Move { + player_id: 1, + moves: self.strategy.choose_move(), + }) + } + } + _ => None, + }; + return if self.color == Color::Black { + debug!(" bot (internal) evt : {internal_event:?} ; points : {player_points:?}"); + debug!("<<<< end {:?} BOT handle", self.color); + internal_event.map(|evt| evt.get_mirror()) + } else { + debug!("<<<< end {:?} BOT handle", self.color); + internal_event + }; + } + debug!("<<<< end {:?} BOT handle", self.color); + None + } + + // Only used in tests below + pub fn get_state(&self) -> &GameState { + self.strategy.get_game() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use store::{Dice, Stage}; + + #[test] + fn test_new() { + let bot = Bot::new(Box::new(DefaultStrategy::default()), Color::Black); + // let bot = Bot::new(Box::new(DefaultStrategy::default()), Color::Black, false); + assert_eq!(bot.get_state().stage, Stage::PreGame); + } + + #[test] + fn test_handle_event() { + let mut bot = Bot::new(Box::new(DefaultStrategy::default()), Color::Black); + // let mut bot = Bot::new(Box::new(DefaultStrategy::default()), Color::Black, false); + let mut event = bot.handle_event(&GameEvent::BeginGame { goes_first: 2 }); + assert_eq!(event, Some(GameEvent::Roll { player_id: 2 })); + assert_eq!(bot.get_state().active_player_id, 1); // bot internal active_player_id for black + event = bot.handle_event(&GameEvent::RollResult { + player_id: 2, + dice: Dice { values: (2, 3) }, + }); + assert_eq!( + event, + Some(GameEvent::Move { + player_id: 2, + moves: ( + CheckerMove::new(24, 21).unwrap(), + CheckerMove::new(24, 22).unwrap() + ) + }) + ); + + event = bot.handle_event(&GameEvent::BeginGame { goes_first: 1 }); + assert_eq!(event, None); + + assert_eq!(bot.get_state().active_player_id, 2); //internal active_player_id + bot.handle_event(&GameEvent::RollResult { + player_id: 1, + dice: Dice { values: (2, 3) }, + }); + assert_eq!(bot.get_state().turn_stage, TurnStage::Move); + } +} diff --git a/bot/src/strategy/client.rs b/bot/src/strategy/client.rs new file mode 100644 index 0000000..891f942 --- /dev/null +++ b/bot/src/strategy/client.rs @@ -0,0 +1,79 @@ +use crate::{BotStrategy, CheckerMove, Color, GameState, PlayerId, PointsRules}; + +#[derive(Debug)] +pub struct ClientStrategy { + pub game: GameState, + pub player_id: PlayerId, + pub color: Color, +} + +impl Default for ClientStrategy { + fn default() -> Self { + let game = GameState::default(); + Self { + game, + player_id: 1, + color: Color::White, + } + } +} + +impl BotStrategy for ClientStrategy { + fn get_game(&self) -> &GameState { + &self.game + } + fn get_mut_game(&mut self) -> &mut GameState { + &mut self.game + } + + fn set_player_id(&mut self, player_id: PlayerId) { + self.player_id = player_id; + } + + fn set_color(&mut self, color: Color) { + self.color = color; + } + + fn calculate_points(&self) -> u8 { + let dice_roll_count = self + .get_game() + .players + .get(&self.player_id) + .unwrap() + .dice_roll_count; + let points_rules = PointsRules::new(&Color::White, &self.game.board, self.game.dice); + points_rules.get_points(dice_roll_count).0 + } + + fn calculate_adv_points(&self) -> u8 { + let dice_roll_count = self + .get_game() + .players + .get(&self.player_id) + .unwrap() + .dice_roll_count; + let points_rules = PointsRules::new(&Color::White, &self.game.board, self.game.dice); + points_rules.get_points(dice_roll_count).1 + } + + fn choose_go(&self) -> bool { + true + } + + fn choose_move(&self) -> (CheckerMove, CheckerMove) { + let (dice1, dice2) = match self.color { + Color::White => (self.game.dice.values.0 as i8, self.game.dice.values.1 as i8), + Color::Black => ( + 0 - self.game.dice.values.0 as i8, + 0 - self.game.dice.values.1 as i8, + ), + }; + + let fields = self.game.board.get_color_fields(self.color); + let first_field = fields.first().unwrap(); + ( + CheckerMove::new(first_field.0, (first_field.0 as i8 + dice1) as usize).unwrap(), + CheckerMove::new(first_field.0, (first_field.0 as i8 + dice2) as usize).unwrap(), + ) + } +} diff --git a/bot/src/strategy/default.rs b/bot/src/strategy/default.rs new file mode 100644 index 0000000..628ce83 --- /dev/null +++ b/bot/src/strategy/default.rs @@ -0,0 +1,86 @@ +use crate::{BotStrategy, CheckerMove, Color, GameState, PlayerId}; +use store::MoveRules; + +#[derive(Debug)] +pub struct DefaultStrategy { + pub game: GameState, + pub player_id: PlayerId, + pub color: Color, +} + +impl Default for DefaultStrategy { + fn default() -> Self { + let game = GameState::default(); + Self { + game, + player_id: 1, + color: Color::White, + } + } +} + +impl BotStrategy for DefaultStrategy { + fn get_game(&self) -> &GameState { + &self.game + } + fn get_mut_game(&mut self) -> &mut GameState { + &mut self.game + } + + fn set_color(&mut self, color: Color) { + self.color = color; + } + + fn set_player_id(&mut self, player_id: PlayerId) { + self.player_id = player_id; + } + + fn calculate_points(&self) -> u8 { + // let dice_roll_count = self + // .get_game() + // .players + // .get(&self.player_id) + // .unwrap() + // .dice_roll_count; + // let points_rules = PointsRules::new(&Color::White, &self.game.board, self.game.dice); + // points_rules.get_points(dice_roll_count).0 + self.game.dice_points.0 + } + + fn calculate_adv_points(&self) -> u8 { + // self.calculate_points() + self.game.dice_points.1 + } + + fn choose_go(&self) -> bool { + true + } + + fn choose_move(&self) -> (CheckerMove, CheckerMove) { + let rules = MoveRules::new(&self.color, &self.game.board, self.game.dice); + let possible_moves = rules.get_possible_moves_sequences(true, vec![]); + let choosen_move = *possible_moves + .first() + .unwrap_or(&(CheckerMove::default(), CheckerMove::default())); + if self.color == Color::White { + choosen_move + } else { + (choosen_move.0.mirror(), choosen_move.1.mirror()) + } + + // let (dice1, dice2) = match self.color { + // Color::White => (self.game.dice.values.0 as i8, self.game.dice.values.1 as i8), + // Color::Black => ( + // 0 - self.game.dice.values.0 as i8, + // 0 - self.game.dice.values.1 as i8, + // ), + // }; + // + // let fields = self.game.board.get_color_fields(self.color); + // let first_field = fields.first().unwrap(); + // ( + // CheckerMove::new(first_field.0, (first_field.0 as i8 + dice1) as usize).unwrap(), + // CheckerMove::new(first_field.0, (first_field.0 as i8 + dice2) as usize).unwrap(), + // ) + } +} diff --git a/bot/src/strategy/dqn.rs b/bot/src/strategy/dqn.rs new file mode 100644 index 0000000..2874195 --- /dev/null +++ b/bot/src/strategy/dqn.rs @@ -0,0 +1,174 @@ +use crate::{BotStrategy, CheckerMove, Color, GameState, PlayerId}; +use log::info; +use std::path::Path; +use store::MoveRules; + +use crate::dqn_simple::dqn_model::SimpleNeuralNetwork; +use crate::training_common_big::{get_valid_actions, sample_valid_action, TrictracAction}; + +/// Stratégie DQN pour le bot - ne fait que charger et utiliser un modèle pré-entraîné +#[derive(Debug)] +pub struct DqnStrategy { + pub game: GameState, + pub player_id: PlayerId, + pub color: Color, + pub model: Option, +} + +impl Default for DqnStrategy { + fn default() -> Self { + Self { + game: GameState::default(), + player_id: 1, + color: Color::White, + model: None, + } + } +} + +impl DqnStrategy { + pub fn new() -> Self { + Self::default() + } + + pub fn new_with_model + std::fmt::Debug>(model_path: P) -> Self { + let mut strategy = Self::new(); + if let Ok(model) = SimpleNeuralNetwork::load(&model_path) { + info!("Loading model {model_path:?}"); + strategy.model = Some(model); + } + strategy + } + + /// Utilise le modèle DQN pour choisir une action valide + fn get_dqn_action(&self) -> Option { + if let Some(ref model) = self.model { + let state = self.game.to_vec_float(); + let valid_actions = get_valid_actions(&self.game); + + if valid_actions.is_empty() { + return None; + } + + // Obtenir les Q-values pour toutes les actions + let q_values = model.forward(&state); + + // Trouver la meilleure action valide + let mut best_action = &valid_actions[0]; + let mut best_q_value = f32::NEG_INFINITY; + + for action in &valid_actions { + let action_index = action.to_action_index(); + if action_index < q_values.len() { + let q_value = q_values[action_index]; + if q_value > best_q_value { + best_q_value = q_value; + best_action = action; + } + } + } + + Some(best_action.clone()) + } else { + // Fallback : action aléatoire valide + sample_valid_action(&self.game) + } + } +} + +impl BotStrategy for DqnStrategy { + fn get_game(&self) -> &GameState { + &self.game + } + + fn get_mut_game(&mut self) -> &mut GameState { + &mut self.game + } + + fn set_color(&mut self, color: Color) { + self.color = color; + } + + fn set_player_id(&mut self, player_id: PlayerId) { + self.player_id = player_id; + } + + fn calculate_points(&self) -> u8 { + self.game.dice_points.0 + } + + fn calculate_adv_points(&self) -> u8 { + self.game.dice_points.1 + } + + fn choose_go(&self) -> bool { + // Utiliser le DQN pour décider si on continue + if let Some(action) = self.get_dqn_action() { + matches!(action, TrictracAction::Go) + } else { + // Fallback : toujours continuer + true + } + } + + fn choose_move(&self) -> (CheckerMove, CheckerMove) { + // Utiliser le DQN pour choisir le mouvement + if let Some(TrictracAction::Move { + dice_order, + from1, + from2, + }) = self.get_dqn_action() + { + let dicevals = self.game.dice.values; + let (mut dice1, mut dice2) = if dice_order { + (dicevals.0, dicevals.1) + } else { + (dicevals.1, dicevals.0) + }; + + if from1 == 0 { + // empty move + dice1 = 0; + } + let mut to1 = from1 + dice1 as usize; + if 24 < to1 { + // sortie + to1 = 0; + } + if from2 == 0 { + // empty move + dice2 = 0; + } + let mut to2 = from2 + dice2 as usize; + if 24 < to2 { + // sortie + to2 = 0; + } + + let checker_move1 = CheckerMove::new(from1, to1).unwrap_or_default(); + let checker_move2 = CheckerMove::new(from2, to2).unwrap_or_default(); + + let chosen_move = if self.color == Color::White { + (checker_move1, checker_move2) + } else { + (checker_move1.mirror(), checker_move2.mirror()) + }; + + return chosen_move; + } + + // Fallback : utiliser la stratégie par défaut + let rules = MoveRules::new(&self.color, &self.game.board, self.game.dice); + let possible_moves = rules.get_possible_moves_sequences(true, vec![]); + + let chosen_move = *possible_moves + .first() + .unwrap_or(&(CheckerMove::default(), CheckerMove::default())); + + if self.color == Color::White { + chosen_move + } else { + (chosen_move.0.mirror(), chosen_move.1.mirror()) + } + } +} diff --git a/bot/src/strategy/dqnburn.rs b/bot/src/strategy/dqnburn.rs new file mode 100644 index 0000000..2fea85e --- /dev/null +++ b/bot/src/strategy/dqnburn.rs @@ -0,0 +1,220 @@ +use burn::backend::NdArray; +use burn::tensor::cast::ToElement; +use burn_rl::base::{ElemType, Model, State}; + +use crate::{BotStrategy, CheckerMove, Color, GameState, PlayerId}; +use log::info; +use store::MoveRules; + +use crate::burnrl::algos::dqn; +use crate::burnrl::environment; +use crate::training_common::{get_valid_action_indices, sample_valid_action, TrictracAction}; + +type DqnBurnNetwork = dqn::Net>; + +/// Stratégie DQN pour le bot - ne fait que charger et utiliser un modèle pré-entraîné +#[derive(Debug)] +pub struct DqnBurnStrategy { + pub game: GameState, + pub player_id: PlayerId, + pub color: Color, + pub model: Option, +} + +impl Default for DqnBurnStrategy { + fn default() -> Self { + Self { + game: GameState::default(), + player_id: 1, + color: Color::White, + model: None, + } + } +} + +impl DqnBurnStrategy { + pub fn new() -> Self { + Self::default() + } + + pub fn new_with_model(model_path: &String) -> Self { + info!("Loading model {model_path:?}"); + let mut strategy = Self::new(); + strategy.model = dqn::load_model(256, model_path); + strategy + } + + /// Utilise le modèle DQN pour choisir une action valide + fn get_dqn_action(&self) -> Option { + if let Some(ref model) = self.model { + let state = environment::TrictracState::from_game_state(&self.game); + let valid_actions_indices = get_valid_action_indices(&self.game); + if valid_actions_indices.is_empty() { + return None; // No valid actions, end of episode + } + + // Obtenir les Q-values pour toutes les actions + let q_values = model.infer(state.to_tensor().unsqueeze()); + + // Set non valid actions q-values to lowest + let mut masked_q_values = q_values.clone(); + let q_values_vec: Vec = q_values.into_data().into_vec().unwrap(); + for (index, q_value) in q_values_vec.iter().enumerate() { + if !valid_actions_indices.contains(&index) { + masked_q_values = masked_q_values.clone().mask_fill( + masked_q_values.clone().equal_elem(*q_value), + f32::NEG_INFINITY, + ); + } + } + // Get best action (highest q-value) + let action_index = masked_q_values.argmax(1).into_scalar().to_u32(); + environment::TrictracEnvironment::convert_action(environment::TrictracAction::from( + action_index, + )) + } else { + // Fallback : action aléatoire valide + sample_valid_action(&self.game) + } + } +} + +impl BotStrategy for DqnBurnStrategy { + fn get_game(&self) -> &GameState { + &self.game + } + + fn get_mut_game(&mut self) -> &mut GameState { + &mut self.game + } + + fn set_color(&mut self, color: Color) { + self.color = color; + } + + fn set_player_id(&mut self, player_id: PlayerId) { + self.player_id = player_id; + } + + fn calculate_points(&self) -> u8 { + self.game.dice_points.0 + } + + fn calculate_adv_points(&self) -> u8 { + self.game.dice_points.1 + } + + fn choose_go(&self) -> bool { + // Utiliser le DQN pour décider si on continue + if let Some(action) = self.get_dqn_action() { + matches!(action, TrictracAction::Go) + } else { + // Fallback : toujours continuer + true + } + } + + fn choose_move(&self) -> (CheckerMove, CheckerMove) { + // Utiliser le DQN pour choisir le mouvement + if let Some(TrictracAction::Move { + dice_order, + checker1, + checker2, + }) = self.get_dqn_action() + { + let dicevals = self.game.dice.values; + let (mut dice1, mut dice2) = if dice_order { + (dicevals.0, dicevals.1) + } else { + (dicevals.1, dicevals.0) + }; + + assert_eq!(self.color, Color::White); + let from1 = self + .game + .board + .get_checker_field(&self.color, checker1 as u8) + .unwrap_or(0); + + if from1 == 0 { + // empty move + dice1 = 0; + } + let mut to1 = from1; + if self.color == Color::White { + to1 += dice1 as usize; + if 24 < to1 { + // sortie + to1 = 0; + } + } else { + let fto1 = to1 as i16 - dice1 as i16; + to1 = if fto1 < 0 { 0 } else { fto1 as usize }; + } + + let checker_move1 = store::CheckerMove::new(from1, to1).unwrap_or_default(); + + let mut tmp_board = self.game.board.clone(); + let move_res = tmp_board.move_checker(&self.color, checker_move1); + if move_res.is_err() { + panic!("could not move {move_res:?}"); + } + let from2 = tmp_board + .get_checker_field(&self.color, checker2 as u8) + .unwrap_or(0); + if from2 == 0 { + // empty move + dice2 = 0; + } + let mut to2 = from2; + if self.color == Color::White { + to2 += dice2 as usize; + if 24 < to2 { + // sortie + to2 = 0; + } + } else { + let fto2 = to2 as i16 - dice2 as i16; + to2 = if fto2 < 0 { 0 } else { fto2 as usize }; + } + + // Gestion prise de coin par puissance + let opp_rest_field = if self.color == Color::White { 13 } else { 12 }; + if to1 == opp_rest_field && to2 == opp_rest_field { + if self.color == Color::White { + to1 -= 1; + to2 -= 1; + } else { + to1 += 1; + to2 += 1; + } + } + + let checker_move1 = CheckerMove::new(from1, to1).unwrap_or_default(); + let checker_move2 = CheckerMove::new(from2, to2).unwrap_or_default(); + + let chosen_move = if self.color == Color::White { + (checker_move1, checker_move2) + } else { + // XXX : really ? + (checker_move1.mirror(), checker_move2.mirror()) + }; + + return chosen_move; + } + + // Fallback : utiliser la stratégie par défaut + let rules = MoveRules::new(&self.color, &self.game.board, self.game.dice); + let possible_moves = rules.get_possible_moves_sequences(true, vec![]); + + let chosen_move = *possible_moves + .first() + .unwrap_or(&(CheckerMove::default(), CheckerMove::default())); + + if self.color == Color::White { + chosen_move + } else { + (chosen_move.0.mirror(), chosen_move.1.mirror()) + } + } +} diff --git a/bot/src/strategy/erroneous_moves.rs b/bot/src/strategy/erroneous_moves.rs new file mode 100644 index 0000000..f57ec6c --- /dev/null +++ b/bot/src/strategy/erroneous_moves.rs @@ -0,0 +1,62 @@ +use crate::{BotStrategy, CheckerMove, Color, GameState, PlayerId, PointsRules}; + +#[derive(Debug)] +pub struct ErroneousStrategy { + pub game: GameState, + pub player_id: PlayerId, + pub color: Color, +} + +impl Default for ErroneousStrategy { + fn default() -> Self { + let game = GameState::default(); + Self { + game, + player_id: 2, + color: Color::Black, + } + } +} + +impl BotStrategy for ErroneousStrategy { + fn get_game(&self) -> &GameState { + &self.game + } + fn get_mut_game(&mut self) -> &mut GameState { + &mut self.game + } + + fn set_color(&mut self, color: Color) { + self.color = color; + } + + fn set_player_id(&mut self, player_id: PlayerId) { + self.player_id = player_id; + } + + fn calculate_points(&self) -> u8 { + let dice_roll_count = self + .get_game() + .players + .get(&self.player_id) + .unwrap() + .dice_roll_count; + let points_rules = PointsRules::new(&Color::White, &self.game.board, self.game.dice); + points_rules.get_points(dice_roll_count).0 + } + + fn calculate_adv_points(&self) -> u8 { + self.calculate_points() + } + + fn choose_go(&self) -> bool { + true + } + + fn choose_move(&self) -> (CheckerMove, CheckerMove) { + ( + CheckerMove::new(1, 10).unwrap(), + CheckerMove::new(2, 7).unwrap(), + ) + } +} diff --git a/bot/src/strategy/mod.rs b/bot/src/strategy/mod.rs new file mode 100644 index 0000000..b9fa3b2 --- /dev/null +++ b/bot/src/strategy/mod.rs @@ -0,0 +1,7 @@ +pub mod client; +pub mod default; +pub mod dqn; +pub mod dqnburn; +pub mod erroneous_moves; +pub mod random; +pub mod stable_baselines3; diff --git a/bot/src/strategy/random.rs b/bot/src/strategy/random.rs new file mode 100644 index 0000000..0bfd1c6 --- /dev/null +++ b/bot/src/strategy/random.rs @@ -0,0 +1,67 @@ +use crate::{BotStrategy, CheckerMove, Color, GameState, PlayerId}; +use store::MoveRules; + +#[derive(Debug)] +pub struct RandomStrategy { + pub game: GameState, + pub player_id: PlayerId, + pub color: Color, +} + +impl Default for RandomStrategy { + fn default() -> Self { + let game = GameState::default(); + Self { + game, + player_id: 1, + color: Color::White, + } + } +} + +impl BotStrategy for RandomStrategy { + fn get_game(&self) -> &GameState { + &self.game + } + fn get_mut_game(&mut self) -> &mut GameState { + &mut self.game + } + + fn set_color(&mut self, color: Color) { + self.color = color; + } + + fn set_player_id(&mut self, player_id: PlayerId) { + self.player_id = player_id; + } + + fn calculate_points(&self) -> u8 { + self.game.dice_points.0 + } + + fn calculate_adv_points(&self) -> u8 { + self.game.dice_points.1 + } + + fn choose_go(&self) -> bool { + true + } + + fn choose_move(&self) -> (CheckerMove, CheckerMove) { + let rules = MoveRules::new(&self.color, &self.game.board, self.game.dice); + let possible_moves = rules.get_possible_moves_sequences(true, vec![]); + + use rand::{seq::SliceRandom, thread_rng}; + let mut rng = thread_rng(); + let choosen_move = possible_moves + .choose(&mut rng) + .cloned() + .unwrap_or((CheckerMove::default(), CheckerMove::default())); + + if self.color == Color::White { + choosen_move + } else { + (choosen_move.0.mirror(), choosen_move.1.mirror()) + } + } +} diff --git a/bot/src/strategy/stable_baselines3.rs b/bot/src/strategy/stable_baselines3.rs new file mode 100644 index 0000000..541bfbb --- /dev/null +++ b/bot/src/strategy/stable_baselines3.rs @@ -0,0 +1,272 @@ +use crate::{BotStrategy, CheckerMove, Color, GameState, PlayerId, PointsRules}; +use serde::{Deserialize, Serialize}; +use std::fs::File; +use std::io::Read; +use std::io::Write; +use std::path::Path; +use std::process::Command; +use store::MoveRules; + +#[derive(Debug)] +pub struct StableBaselines3Strategy { + pub game: GameState, + pub player_id: PlayerId, + pub color: Color, + pub model_path: String, +} + +impl Default for StableBaselines3Strategy { + fn default() -> Self { + let game = GameState::default(); + Self { + game, + player_id: 2, + color: Color::Black, + model_path: "models/trictrac_ppo.zip".to_string(), + } + } +} + +#[derive(Serialize, Deserialize)] +struct GameStateJson { + board: Vec, + active_player: u8, + dice: [u8; 2], + white_points: u8, + white_holes: u8, + black_points: u8, + black_holes: u8, + turn_stage: u8, +} + +#[derive(Deserialize)] +struct ActionJson { + action_type: u8, + from1: usize, + to1: usize, + from2: usize, + to2: usize, +} + +impl StableBaselines3Strategy { + pub fn new(model_path: &str) -> Self { + let game = GameState::default(); + Self { + game, + player_id: 2, + color: Color::Black, + model_path: model_path.to_string(), + } + } + + fn get_state_as_json(&self) -> GameStateJson { + // Convertir l'état du jeu en un format compatible avec notre modèle Python + let mut board = vec![0; 24]; + + // Remplir les positions des pièces blanches (valeurs positives) + for (pos, count) in self.game.board.get_color_fields(Color::White) { + if pos < 24 { + board[pos] = count; + } + } + + // Remplir les positions des pièces noires (valeurs négatives) + for (pos, count) in self.game.board.get_color_fields(Color::Black) { + if pos < 24 { + board[pos] = -count; + } + } + + // Convertir l'étape du tour en entier + let turn_stage = match self.game.turn_stage { + store::TurnStage::RollDice => 0, + store::TurnStage::RollWaiting => 1, + store::TurnStage::MarkPoints => 2, + store::TurnStage::HoldOrGoChoice => 3, + store::TurnStage::Move => 4, + store::TurnStage::MarkAdvPoints => 5, + }; + + // Récupérer les points et trous des joueurs + let white_points = self.game.players.get(&1).map_or(0, |p| p.points); + let white_holes = self.game.players.get(&1).map_or(0, |p| p.holes); + let black_points = self.game.players.get(&2).map_or(0, |p| p.points); + let black_holes = self.game.players.get(&2).map_or(0, |p| p.holes); + + // Créer l'objet JSON + GameStateJson { + board, + active_player: self.game.active_player_id as u8, + dice: [self.game.dice.values.0, self.game.dice.values.1], + white_points, + white_holes, + black_points, + black_holes, + turn_stage, + } + } + + fn predict_action(&self) -> Option { + // Convertir l'état du jeu en JSON + let state_json = self.get_state_as_json(); + let state_str = serde_json::to_string(&state_json).unwrap(); + + // Écrire l'état dans un fichier temporaire + let temp_input_path = "temp_state.json"; + let mut file = File::create(temp_input_path).ok()?; + file.write_all(state_str.as_bytes()).ok()?; + + // Exécuter le script Python pour faire une prédiction + let output_path = "temp_action.json"; + let python_script = format!( + r#" +import sys +import json +import numpy as np +from stable_baselines3 import PPO +import torch + +# Charger le modèle +model = PPO.load("{}") + +# Lire l'état du jeu +with open("temp_state.json", "r") as f: + state_dict = json.load(f) + +# Convertir en format d'observation attendu par le modèle +observation = {{ + 'board': np.array(state_dict['board'], dtype=np.int8), + 'active_player': state_dict['active_player'], + 'dice': np.array(state_dict['dice'], dtype=np.int32), + 'white_points': state_dict['white_points'], + 'white_holes': state_dict['white_holes'], + 'black_points': state_dict['black_points'], + 'black_holes': state_dict['black_holes'], + 'turn_stage': state_dict['turn_stage'], +}} + +# Prédire l'action +action, _ = model.predict(observation) + +# Convertir l'action en format lisible +action_dict = {{ + 'action_type': int(action[0]), + 'from1': int(action[1]), + 'to1': int(action[2]), + 'from2': int(action[3]), + 'to2': int(action[4]), +}} + +# Écrire l'action dans un fichier +with open("{}", "w") as f: + json.dump(action_dict, f) +"#, + self.model_path, output_path + ); + + let temp_script_path = "temp_predict.py"; + let mut script_file = File::create(temp_script_path).ok()?; + script_file.write_all(python_script.as_bytes()).ok()?; + + // Exécuter le script Python + let status = Command::new("python").arg(temp_script_path).status().ok()?; + + if !status.success() { + return None; + } + + // Lire la prédiction + if Path::new(output_path).exists() { + let mut file = File::open(output_path).ok()?; + let mut contents = String::new(); + file.read_to_string(&mut contents).ok()?; + + // Nettoyer les fichiers temporaires + std::fs::remove_file(temp_input_path).ok(); + std::fs::remove_file(temp_script_path).ok(); + std::fs::remove_file(output_path).ok(); + + // Analyser la prédiction + let action: ActionJson = serde_json::from_str(&contents).ok()?; + Some(action) + } else { + None + } + } +} + +impl BotStrategy for StableBaselines3Strategy { + fn get_game(&self) -> &GameState { + &self.game + } + + fn get_mut_game(&mut self) -> &mut GameState { + &mut self.game + } + + fn set_color(&mut self, color: Color) { + self.color = color; + } + + fn set_player_id(&mut self, player_id: PlayerId) { + self.player_id = player_id; + } + + fn calculate_points(&self) -> u8 { + // Utiliser la prédiction du modèle uniquement si c'est une action de type "mark" (1) + if let Some(action) = self.predict_action() { + if action.action_type == 1 { + // Marquer les points calculés par le modèle (ici on utilise la somme des dés comme proxy) + return self.game.dice.values.0 + self.game.dice.values.1; + } + } + + // Fallback vers la méthode standard si la prédiction échoue + let dice_roll_count = self + .get_game() + .players + .get(&self.player_id) + .unwrap() + .dice_roll_count; + let points_rules = PointsRules::new(&Color::White, &self.game.board, self.game.dice); + points_rules.get_points(dice_roll_count).0 + } + + fn calculate_adv_points(&self) -> u8 { + self.calculate_points() + } + + fn choose_go(&self) -> bool { + // Utiliser la prédiction du modèle uniquement si c'est une action de type "go" (2) + if let Some(action) = self.predict_action() { + return action.action_type == 2; + } + + // Fallback vers la méthode standard si la prédiction échoue + true + } + + fn choose_move(&self) -> (CheckerMove, CheckerMove) { + // Utiliser la prédiction du modèle uniquement si c'est une action de type "move" (0) + if let Some(action) = self.predict_action() { + if action.action_type == 0 { + let move1 = CheckerMove::new(action.from1, action.to1).unwrap_or_default(); + let move2 = CheckerMove::new(action.from2, action.to2).unwrap_or_default(); + return (move1, move2); + } + } + + // Fallback vers la méthode standard si la prédiction échoue + let rules = MoveRules::new(&self.color, &self.game.board, self.game.dice); + let possible_moves = rules.get_possible_moves_sequences(true, vec![]); + let choosen_move = *possible_moves + .first() + .unwrap_or(&(CheckerMove::default(), CheckerMove::default())); + + if self.color == Color::White { + choosen_move + } else { + (choosen_move.0.mirror(), choosen_move.1.mirror()) + } + } +} diff --git a/bot/src/training_common.rs b/bot/src/training_common.rs new file mode 100644 index 0000000..0a581dd --- /dev/null +++ b/bot/src/training_common.rs @@ -0,0 +1,351 @@ +use std::cmp::{max, min}; +use std::fmt::{Debug, Display, Formatter}; + +use serde::{Deserialize, Serialize}; +use store::{CheckerMove, GameEvent, GameState}; + +/// Types d'actions possibles dans le jeu +#[derive(Debug, Copy, Clone, Eq, Serialize, Deserialize, PartialEq)] +pub enum TrictracAction { + /// Lancer les dés + Roll, + /// Continuer après avoir gagné un trou + Go, + /// Effectuer un mouvement de pions + Move { + dice_order: bool, // true = utiliser dice[0] en premier, false = dice[1] en premier + checker1: usize, // premier pion à déplacer en numérotant depuis la colonne de départ (0-15) 0 : aucun pion + checker2: usize, // deuxième pion (0-15) + }, + // Marquer les points : à activer si support des écoles + // Mark, +} + +impl Display for TrictracAction { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = format!("{self:?}"); + writeln!(f, "{}", s.chars().rev().collect::())?; + Ok(()) + } +} + +impl TrictracAction { + /// Encode une action en index pour le réseau de neurones + pub fn to_action_index(&self) -> usize { + match self { + TrictracAction::Roll => 0, + TrictracAction::Go => 1, + TrictracAction::Move { + dice_order, + checker1, + checker2, + } => { + // Encoder les mouvements dans l'espace d'actions + // Indices 2+ pour les mouvements + // de 2 à 513 (2 à 257 pour dé 1 en premier, 258 à 513 pour dé 2 en premier) + let mut start = 2; + if !dice_order { + // 16 * 16 = 256 + start += 256; + } + start + checker1 * 16 + checker2 + } // TrictracAction::Mark => 514, + } + } + + pub fn to_event(&self, state: &GameState) -> Option { + match self { + TrictracAction::Roll => { + // Lancer les dés + Some(GameEvent::Roll { + player_id: state.active_player_id, + }) + } + // TrictracAction::Mark => { + // // Marquer des points + // let points = self.game. + // Some(GameEvent::Mark { + // player_id: self.active_player_id, + // points, + // }) + // } + TrictracAction::Go => { + // Continuer après avoir gagné un trou + Some(GameEvent::Go { + player_id: state.active_player_id, + }) + } + TrictracAction::Move { + dice_order, + checker1, + checker2, + } => { + // Effectuer un mouvement + let (dice1, dice2) = if *dice_order { + (state.dice.values.0, state.dice.values.1) + } else { + (state.dice.values.1, state.dice.values.0) + }; + + let color = &store::Color::White; + let from1 = state + .board + .get_checker_field(color, *checker1 as u8) + .unwrap_or(0); + let mut to1 = from1 + dice1 as usize; + let checker_move1 = store::CheckerMove::new(from1, to1).unwrap_or_default(); + + let mut tmp_board = state.board.clone(); + let move_result = tmp_board.move_checker(color, checker_move1); + if move_result.is_err() { + None + // panic!("Error while moving checker {move_result:?}") + } else { + let from2 = tmp_board + .get_checker_field(color, *checker2 as u8) + .unwrap_or(0); + let mut to2 = from2 + dice2 as usize; + + // Gestion prise de coin par puissance + let opp_rest_field = 13; + if to1 == opp_rest_field && to2 == opp_rest_field { + to1 -= 1; + to2 -= 1; + } + + let checker_move1 = store::CheckerMove::new(from1, to1).unwrap_or_default(); + let checker_move2 = store::CheckerMove::new(from2, to2).unwrap_or_default(); + + Some(GameEvent::Move { + player_id: state.active_player_id, + moves: (checker_move1, checker_move2), + }) + } + } + } + } + + /// Décode un index d'action en TrictracAction + pub fn from_action_index(index: usize) -> Option { + match index { + 0 => Some(TrictracAction::Roll), + 1 => Some(TrictracAction::Go), + // 514 => Some(TrictracAction::Mark), + i if i >= 2 => { + let move_code = i - 2; + let (dice_order, checker1, checker2) = Self::decode_move(move_code); + Some(TrictracAction::Move { + dice_order, + checker1, + checker2, + }) + } + _ => None, + } + } + + /// Décode un entier en paire de mouvements + fn decode_move(code: usize) -> (bool, usize, usize) { + let mut encoded = code; + let dice_order = code < 256; + if !dice_order { + encoded -= 256 + } + let checker1 = encoded / 16; + let checker2 = encoded % 16; + (dice_order, checker1, checker2) + } + + /// Retourne la taille de l'espace d'actions total + pub fn action_space_size() -> usize { + // 1 (Roll) + 1 (Go) + mouvements possibles + // Pour les mouvements : 2*25*25 = 1250 (choix du dé + position 0-24 pour chaque from) + // Mais on peut optimiser en limitant aux positions valides (1-24) + 2 + (2 * 16 * 16) // = 514 + } + + // pub fn to_game_event(&self, player_id: PlayerId, dice: Dice) -> GameEvent { + // match action { + // TrictracAction::Roll => Some(GameEvent::Roll { player_id }), + // TrictracAction::Mark => Some(GameEvent::Mark { player_id, points }), + // TrictracAction::Go => Some(GameEvent::Go { player_id }), + // TrictracAction::Move { + // dice_order, + // from1, + // from2, + // } => { + // // Effectuer un mouvement + // let checker_move1 = store::CheckerMove::new(move1.0, move1.1).unwrap_or_default(); + // let checker_move2 = store::CheckerMove::new(move2.0, move2.1).unwrap_or_default(); + // + // Some(GameEvent::Move { + // player_id: self.agent_player_id, + // moves: (checker_move1, checker_move2), + // }) + // } + // }; + // } +} + +/// Obtient les actions valides pour l'état de jeu actuel +pub fn get_valid_actions(game_state: &crate::GameState) -> Vec { + use store::TurnStage; + + let mut valid_actions = Vec::new(); + + let active_player_id = game_state.active_player_id; + let player_color = game_state.player_color_by_id(&active_player_id); + + if let Some(color) = player_color { + match game_state.turn_stage { + TurnStage::RollDice => { + valid_actions.push(TrictracAction::Roll); + } + TurnStage::MarkPoints | TurnStage::MarkAdvPoints | TurnStage::RollWaiting => { + // valid_actions.push(TrictracAction::Mark); + panic!( + "get_valid_actions not implemented for turn stage {:?}", + game_state.turn_stage + ); + } + TurnStage::HoldOrGoChoice => { + valid_actions.push(TrictracAction::Go); + + // Ajoute aussi les mouvements possibles + let rules = store::MoveRules::new(&color, &game_state.board, game_state.dice); + let possible_moves = rules.get_possible_moves_sequences(true, vec![]); + + // Modififier checker_moves_to_trictrac_action si on doit gérer Black + assert_eq!(color, store::Color::White); + for (move1, move2) in possible_moves { + valid_actions.push(checker_moves_to_trictrac_action( + &move1, &move2, &color, game_state, + )); + } + } + TurnStage::Move => { + let rules = store::MoveRules::new(&color, &game_state.board, game_state.dice); + let possible_moves = rules.get_possible_moves_sequences(true, vec![]); + + // Modififier checker_moves_to_trictrac_action si on doit gérer Black + assert_eq!(color, store::Color::White); + for (move1, move2) in possible_moves { + valid_actions.push(checker_moves_to_trictrac_action( + &move1, &move2, &color, game_state, + )); + } + } + } + } + + if valid_actions.is_empty() { + panic!("empty valid_actions for state {game_state}"); + } + valid_actions +} + +// Valid only for White player +fn checker_moves_to_trictrac_action( + move1: &CheckerMove, + move2: &CheckerMove, + color: &store::Color, + state: &crate::GameState, +) -> TrictracAction { + let to1 = move1.get_to(); + let to2 = move2.get_to(); + let from1 = move1.get_from(); + let from2 = move2.get_from(); + let dice = state.dice; + + let mut diff_move1 = if to1 > 0 { + // Mouvement sans sortie + to1 - from1 + } else { + // sortie, on utilise la valeur du dé + if to2 > 0 { + // sortie pour le mouvement 1 uniquement + let dice2 = to2 - from2; + if dice2 == dice.values.0 as usize { + dice.values.1 as usize + } else { + dice.values.0 as usize + } + } else { + // double sortie + if from1 < from2 { + max(dice.values.0, dice.values.1) as usize + } else { + min(dice.values.0, dice.values.1) as usize + } + } + }; + + // modification de diff_move1 si on est dans le cas d'un mouvement par puissance + let rest_field = 12; + if to1 == rest_field + && to2 == rest_field + && max(dice.values.0 as usize, dice.values.1 as usize) + min(from1, from2) != rest_field + { + // prise par puissance + diff_move1 += 1; + } + let dice_order = diff_move1 == dice.values.0 as usize; + + let checker1 = state.board.get_field_checker(color, from1) as usize; + let mut tmp_board = state.board.clone(); + // should not raise an error for a valid action + let move_res = tmp_board.move_checker(color, *move1); + if move_res.is_err() { + panic!("error while moving checker {move_res:?}"); + } + let checker2 = tmp_board.get_field_checker(color, from2) as usize; + TrictracAction::Move { + dice_order, + checker1, + checker2, + } +} + +/// Retourne les indices des actions valides +pub fn get_valid_action_indices(game_state: &crate::GameState) -> Vec { + get_valid_actions(game_state) + .into_iter() + .map(|action| action.to_action_index()) + .collect() +} + +/// Sélectionne une action valide aléatoire +pub fn sample_valid_action(game_state: &crate::GameState) -> Option { + use rand::{seq::SliceRandom, thread_rng}; + + let valid_actions = get_valid_actions(game_state); + let mut rng = thread_rng(); + valid_actions.choose(&mut rng).cloned() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn to_action_index() { + let action = TrictracAction::Move { + dice_order: true, + checker1: 3, + checker2: 4, + }; + let index = action.to_action_index(); + assert_eq!(Some(action), TrictracAction::from_action_index(index)); + assert_eq!(54, index); + } + + #[test] + fn from_action_index() { + let action = TrictracAction::Move { + dice_order: true, + checker1: 3, + checker2: 4, + }; + assert_eq!(Some(action), TrictracAction::from_action_index(54)); + } +} diff --git a/bot/src/training_common_big.rs b/bot/src/training_common_big.rs new file mode 100644 index 0000000..db9ee2b --- /dev/null +++ b/bot/src/training_common_big.rs @@ -0,0 +1,262 @@ +use std::cmp::{max, min}; + +use serde::{Deserialize, Serialize}; +use store::{CheckerMove, Dice}; + +/// Types d'actions possibles dans le jeu +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum TrictracAction { + /// Lancer les dés + Roll, + /// Continuer après avoir gagné un trou + Go, + /// Effectuer un mouvement de pions + Move { + dice_order: bool, // true = utiliser dice[0] en premier, false = dice[1] en premier + from1: usize, // position de départ du premier pion (0-24) + from2: usize, // position de départ du deuxième pion (0-24) + }, + // Marquer les points : à activer si support des écoles + // Mark, +} + +impl TrictracAction { + /// Encode une action en index pour le réseau de neurones + pub fn to_action_index(&self) -> usize { + match self { + TrictracAction::Roll => 0, + TrictracAction::Go => 1, + TrictracAction::Move { + dice_order, + from1, + from2, + } => { + // Encoder les mouvements dans l'espace d'actions + // Indices 2+ pour les mouvements + // de 2 à 1251 (2 à 626 pour dé 1 en premier, 627 à 1251 pour dé 2 en premier) + let mut start = 2; + if !dice_order { + // 25 * 25 = 625 + start += 625; + } + start + from1 * 25 + from2 + } // TrictracAction::Mark => 1252, + } + } + + /// Décode un index d'action en TrictracAction + pub fn from_action_index(index: usize) -> Option { + match index { + 0 => Some(TrictracAction::Roll), + // 1252 => Some(TrictracAction::Mark), + 1 => Some(TrictracAction::Go), + i if i >= 3 => { + let move_code = i - 3; + let (dice_order, from1, from2) = Self::decode_move(move_code); + Some(TrictracAction::Move { + dice_order, + from1, + from2, + }) + } + _ => None, + } + } + + /// Décode un entier en paire de mouvements + fn decode_move(code: usize) -> (bool, usize, usize) { + let mut encoded = code; + let dice_order = code < 626; + if !dice_order { + encoded -= 625 + } + let from1 = encoded / 25; + let from2 = 1 + encoded % 25; + (dice_order, from1, from2) + } + + /// Retourne la taille de l'espace d'actions total + pub fn action_space_size() -> usize { + // 1 (Roll) + 1 (Go) + mouvements possibles + // Pour les mouvements : 2*25*25 = 1250 (choix du dé + position 0-24 pour chaque from) + // Mais on peut optimiser en limitant aux positions valides (1-24) + 2 + (2 * 25 * 25) // = 1252 + } + + // pub fn to_game_event(&self, player_id: PlayerId, dice: Dice) -> GameEvent { + // match action { + // TrictracAction::Roll => Some(GameEvent::Roll { player_id }), + // TrictracAction::Mark => Some(GameEvent::Mark { player_id, points }), + // TrictracAction::Go => Some(GameEvent::Go { player_id }), + // TrictracAction::Move { + // dice_order, + // from1, + // from2, + // } => { + // // Effectuer un mouvement + // let checker_move1 = store::CheckerMove::new(move1.0, move1.1).unwrap_or_default(); + // let checker_move2 = store::CheckerMove::new(move2.0, move2.1).unwrap_or_default(); + // + // Some(GameEvent::Move { + // player_id: self.agent_player_id, + // moves: (checker_move1, checker_move2), + // }) + // } + // }; + // } +} + +/// Obtient les actions valides pour l'état de jeu actuel +pub fn get_valid_actions(game_state: &crate::GameState) -> Vec { + use store::TurnStage; + + let mut valid_actions = Vec::new(); + + let active_player_id = game_state.active_player_id; + let player_color = game_state.player_color_by_id(&active_player_id); + + if let Some(color) = player_color { + match game_state.turn_stage { + TurnStage::RollDice => { + valid_actions.push(TrictracAction::Roll); + } + TurnStage::MarkPoints | TurnStage::MarkAdvPoints | TurnStage::RollWaiting => { + panic!( + "get_valid_actions not implemented for turn stage {:?}", + game_state.turn_stage + ); + // valid_actions.push(TrictracAction::Mark); + } + TurnStage::HoldOrGoChoice => { + valid_actions.push(TrictracAction::Go); + + // Ajoute aussi les mouvements possibles + let rules = store::MoveRules::new(&color, &game_state.board, game_state.dice); + let possible_moves = rules.get_possible_moves_sequences(true, vec![]); + + // Modififier checker_moves_to_trictrac_action si on doit gérer Black + assert_eq!(color, store::Color::White); + for (move1, move2) in possible_moves { + valid_actions.push(checker_moves_to_trictrac_action( + &move1, + &move2, + &game_state.dice, + )); + } + } + TurnStage::Move => { + let rules = store::MoveRules::new(&color, &game_state.board, game_state.dice); + let possible_moves = rules.get_possible_moves_sequences(true, vec![]); + + // Modififier checker_moves_to_trictrac_action si on doit gérer Black + assert_eq!(color, store::Color::White); + for (move1, move2) in possible_moves { + valid_actions.push(checker_moves_to_trictrac_action( + &move1, + &move2, + &game_state.dice, + )); + } + } + } + } + + if valid_actions.is_empty() { + panic!("empty valid_actions for state {game_state}"); + } + valid_actions +} + +// Valid only for White player +fn checker_moves_to_trictrac_action( + move1: &CheckerMove, + move2: &CheckerMove, + dice: &Dice, +) -> TrictracAction { + let to1 = move1.get_to(); + let to2 = move2.get_to(); + let from1 = move1.get_from(); + let from2 = move2.get_from(); + + let mut diff_move1 = if to1 > 0 { + // Mouvement sans sortie + to1 - from1 + } else { + // sortie, on utilise la valeur du dé + if to2 > 0 { + // sortie pour le mouvement 1 uniquement + let dice2 = to2 - from2; + if dice2 == dice.values.0 as usize { + dice.values.1 as usize + } else { + dice.values.0 as usize + } + } else { + // double sortie + if from1 < from2 { + max(dice.values.0, dice.values.1) as usize + } else { + min(dice.values.0, dice.values.1) as usize + } + } + }; + + // modification de diff_move1 si on est dans le cas d'un mouvement par puissance + let rest_field = 12; + if to1 == rest_field + && to2 == rest_field + && max(dice.values.0 as usize, dice.values.1 as usize) + min(from1, from2) != rest_field + { + // prise par puissance + diff_move1 += 1; + } + TrictracAction::Move { + dice_order: diff_move1 == dice.values.0 as usize, + from1: move1.get_from(), + from2: move2.get_from(), + } +} + +/// Retourne les indices des actions valides +pub fn get_valid_action_indices(game_state: &crate::GameState) -> Vec { + get_valid_actions(game_state) + .into_iter() + .map(|action| action.to_action_index()) + .collect() +} + +/// Sélectionne une action valide aléatoire +pub fn sample_valid_action(game_state: &crate::GameState) -> Option { + use rand::{seq::SliceRandom, thread_rng}; + + let valid_actions = get_valid_actions(game_state); + let mut rng = thread_rng(); + valid_actions.choose(&mut rng).cloned() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn to_action_index() { + let action = TrictracAction::Move { + dice_order: true, + from1: 3, + from2: 4, + }; + let index = action.to_action_index(); + assert_eq!(Some(action), TrictracAction::from_action_index(index)); + assert_eq!(81, index); + } + + #[test] + fn from_action_index() { + let action = TrictracAction::Move { + dice_order: true, + from1: 3, + from2: 4, + }; + assert_eq!(Some(action), TrictracAction::from_action_index(81)); + } +} diff --git a/bot/src/trictrac_board.rs b/bot/src/trictrac_board.rs new file mode 100644 index 0000000..01b2a82 --- /dev/null +++ b/bot/src/trictrac_board.rs @@ -0,0 +1,149 @@ +// https://docs.rs/board-game/ implementation +use crate::training_common::{get_valid_actions, TrictracAction}; +use board_game::board::{ + Board as BoardGameBoard, BoardDone, BoardMoves, Outcome, PlayError, Player as BoardGamePlayer, +}; +use board_game::impl_unit_symmetry_board; +use internal_iterator::InternalIterator; +use std::fmt; +use std::ops::ControlFlow; +use store::Color; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TrictracBoard(crate::GameState); + +impl Default for TrictracBoard { + fn default() -> Self { + TrictracBoard(crate::GameState::new_with_players("white", "black")) + } +} + +impl fmt::Display for TrictracBoard { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl_unit_symmetry_board!(TrictracBoard); + +impl BoardGameBoard for TrictracBoard { + // impl TrictracBoard { + type Move = TrictracAction; + + fn next_player(&self) -> BoardGamePlayer { + self.0 + .who_plays() + .map(|p| { + if p.color == Color::Black { + BoardGamePlayer::B + } else { + BoardGamePlayer::A + } + }) + .unwrap_or(BoardGamePlayer::A) + } + + fn is_available_move(&self, mv: Self::Move) -> Result { + self.check_done()?; + let is_valid = mv + .to_event(&self.0) + .map(|evt| self.0.validate(&evt)) + .unwrap_or(false); + Ok(is_valid) + } + + fn play(&mut self, mv: Self::Move) -> Result<(), PlayError> { + self.check_can_play(mv)?; + self.0.consume(&mv.to_event(&self.0).unwrap()); + Ok(()) + } + + fn outcome(&self) -> Option { + if self.0.stage == crate::Stage::Ended { + self.0.determine_winner().map(|player_id| { + Outcome::WonBy(if player_id == 1 { + BoardGamePlayer::A + } else { + BoardGamePlayer::B + }) + }) + } else { + None + } + } + + fn can_lose_after_move() -> bool { + true + } +} + +impl<'a> BoardMoves<'a, TrictracBoard> for TrictracBoard { + type AllMovesIterator = TrictracAllMovesIterator; + type AvailableMovesIterator = TrictracAvailableMovesIterator<'a>; + + fn all_possible_moves() -> Self::AllMovesIterator { + TrictracAllMovesIterator::default() + } + + fn available_moves(&'a self) -> Result { + TrictracAvailableMovesIterator::new(self) + } +} + +#[derive(Debug, Clone)] +pub struct TrictracAllMovesIterator; + +impl Default for TrictracAllMovesIterator { + fn default() -> Self { + TrictracAllMovesIterator + } +} + +impl InternalIterator for TrictracAllMovesIterator { + type Item = TrictracAction; + + fn try_for_each ControlFlow>(self, mut f: F) -> ControlFlow { + f(TrictracAction::Roll)?; + f(TrictracAction::Go)?; + for dice_order in [false, true] { + for checker1 in 0..16 { + for checker2 in 0..16 { + f(TrictracAction::Move { + dice_order, + checker1, + checker2, + })?; + } + } + } + + ControlFlow::Continue(()) + } +} + +#[derive(Debug, Clone)] +pub struct TrictracAvailableMovesIterator<'a> { + board: &'a TrictracBoard, +} + +impl<'a> TrictracAvailableMovesIterator<'a> { + pub fn new(board: &'a TrictracBoard) -> Result { + board.check_done()?; + Ok(TrictracAvailableMovesIterator { board }) + } + + pub fn board(&self) -> &'a TrictracBoard { + self.board + } +} + +impl InternalIterator for TrictracAvailableMovesIterator<'_> { + type Item = TrictracAction; + + fn try_for_each(self, f: F) -> ControlFlow + where + F: FnMut(Self::Item) -> ControlFlow, + { + get_valid_actions(&self.board.0).into_iter().try_for_each(f) + } +} diff --git a/client/.cargo/config.toml b/client_bevy/.cargo/config.toml similarity index 100% rename from client/.cargo/config.toml rename to client_bevy/.cargo/config.toml diff --git a/client/Cargo.toml b/client_bevy/Cargo.toml similarity index 100% rename from client/Cargo.toml rename to client_bevy/Cargo.toml diff --git a/client/assets/Inconsolata.ttf b/client_bevy/assets/Inconsolata.ttf similarity index 100% rename from client/assets/Inconsolata.ttf rename to client_bevy/assets/Inconsolata.ttf diff --git a/client/assets/board.png b/client_bevy/assets/board.png similarity index 100% rename from client/assets/board.png rename to client_bevy/assets/board.png diff --git a/client/assets/sound/click.wav b/client_bevy/assets/sound/click.wav similarity index 100% rename from client/assets/sound/click.wav rename to client_bevy/assets/sound/click.wav diff --git a/client/assets/sound/throw.wav b/client_bevy/assets/sound/throw.wav similarity index 100% rename from client/assets/sound/throw.wav rename to client_bevy/assets/sound/throw.wav diff --git a/client/assets/tac.png b/client_bevy/assets/tac.png similarity index 100% rename from client/assets/tac.png rename to client_bevy/assets/tac.png diff --git a/client/assets/tic.png b/client_bevy/assets/tic.png similarity index 100% rename from client/assets/tic.png rename to client_bevy/assets/tic.png diff --git a/client/src/main.rs b/client_bevy/src/main.rs similarity index 100% rename from client/src/main.rs rename to client_bevy/src/main.rs diff --git a/client_cli/Cargo.toml b/client_cli/Cargo.toml index 1b08baa..6c1d4e1 100644 --- a/client_cli/Cargo.toml +++ b/client_cli/Cargo.toml @@ -12,3 +12,7 @@ pico-args = "0.5.0" pretty_assertions = "1.4.0" renet = "0.0.13" store = { path = "../store" } +bot = { path = "../bot" } +itertools = "0.13.0" +env_logger = "0.11.6" +log = "0.4.20" diff --git a/client_cli/src/app.rs b/client_cli/src/app.rs index 4bfcdee..88a8b42 100644 --- a/client_cli/src/app.rs +++ b/client_cli/src/app.rs @@ -1,68 +1,169 @@ -use pretty_assertions::assert_eq; -use store::{CheckerMove, GameEvent, GameState, PlayerId}; +use bot::{ + BotStrategy, DefaultStrategy, DqnBurnStrategy, DqnStrategy, ErroneousStrategy, RandomStrategy, + StableBaselines3Strategy, +}; +use itertools::Itertools; + +use crate::game_runner::GameRunner; +use store::{CheckerMove, GameEvent, GameState, Stage, TurnStage}; + +#[derive(Debug, Default)] +pub struct AppArgs { + pub seed: Option, + pub bot: Option, +} // Application. #[derive(Debug, Default)] pub struct App { // should the application exit? pub should_quit: bool, - pub game: GameState, - first_move: Option, - player_id: Option, + pub schools_enabled: bool, + pub game: GameRunner, } impl App { // Constructs a new instance of [`App`]. - pub fn new() -> Self { - // Self::default() - - let mut state = GameState::default(); - // local : player - let player_id: Option = state.init_player("myself"); - state.init_player("adversary"); - println!("player_id ? {:?}", player_id); + pub fn new(args: AppArgs) -> Self { + let bot_strategies: Vec> = args + .bot + .as_deref() + .map(|str_bots| { + str_bots + .split(",") + .filter_map(|s| match s.trim() { + "dummy" => { + Some(Box::new(DefaultStrategy::default()) as Box) + } + "random" => { + Some(Box::new(RandomStrategy::default()) as Box) + } + "erroneous" => { + Some(Box::new(ErroneousStrategy::default()) as Box) + } + "ai" => Some(Box::new(StableBaselines3Strategy::default()) + as Box), + "dqn" => Some(Box::new(DqnStrategy::default()) as Box), + "dqnburn" => { + Some(Box::new(DqnBurnStrategy::default()) as Box) + } + s if s.starts_with("ai:") => { + let path = s.trim_start_matches("ai:"); + Some(Box::new(StableBaselines3Strategy::new(path)) + as Box) + } + s if s.starts_with("dqn:") => { + let path = s.trim_start_matches("dqn:"); + Some(Box::new(DqnStrategy::new_with_model(path)) + as Box) + } + s if s.starts_with("dqnburn:") => { + let path = s.trim_start_matches("dqnburn:"); + Some(Box::new(DqnBurnStrategy::new_with_model(&path.to_string())) + as Box) + } + _ => None, + }) + .collect() + }) + .unwrap_or_default(); + let schools_enabled = false; + let should_quit = bot_strategies.len() > 1; Self { - game: state, - should_quit: false, - first_move: None, - player_id, + game: GameRunner::new(schools_enabled, bot_strategies, args.seed.map(|s| s as u64)), + should_quit, + schools_enabled, } } - fn get_my_player(&mut self) {} - - // Constructs a new instance of [`App`]. pub fn start(&mut self) { - self.game = GameState::new(); + self.game.state = GameState::new(self.schools_enabled); } pub fn input(&mut self, input: &str) { - println!("'{}'", input); + // println!("'{}'", input); match input { + "state" => self.show_state(), + "history" => self.show_history(), "quit" => self.quit(), + // run bots game (when two bots) + "bots" => self.bots_all(), + "" => self.bots_next_step(), + // play (when one bot) "roll" => self.roll_dice(), + "go" => self.go(), _ => self.add_move(input), } println!("{}", self.display()); } + // --- 2 bots game actions + + fn bots_all(&mut self) {} + + fn bots_next_step(&mut self) {} + // Set running to false to quit the application. pub fn quit(&mut self) { self.should_quit = true; } + pub fn show_state(&self) { + println!("{:?}", self.game.state) + } + + pub fn show_history(&self) { + for hist in self.game.state.history.iter() { + println!("{hist:?}\n"); + } + } + fn roll_dice(&mut self) { - if self.player_id.is_none() { + if self.game.player_id.is_none() { println!("player_id not set "); return; } - self.game.consume(&GameEvent::Roll { - player_id: self.player_id.unwrap(), + if self.game.state.turn_stage != TurnStage::RollDice { + println!("Not in the dice roll stage"); + return; + } + let dice = self.game.dice_roller.roll(); + + // get correct points for these board and dice + // let points_rules = PointsRules::new( + // &self + // .game + // .state + // .player_color_by_id(&self.game.player_id.unwrap()) + // .unwrap(), + // &self.game.state.board, + // dice, + // ); + self.game.handle_event(&GameEvent::Roll { + player_id: self.game.player_id.unwrap(), + }); + self.game.handle_event(&GameEvent::RollResult { + player_id: self.game.player_id.unwrap(), + dice, + }); + } + + fn go(&mut self) { + if self.game.player_id.is_none() { + println!("player_id not set "); + return; + } + if self.game.state.turn_stage != TurnStage::HoldOrGoChoice { + println!("Not in position to go"); + return; + } + self.game.handle_event(&GameEvent::Go { + player_id: self.game.player_id.unwrap(), }); } fn add_move(&mut self, input: &str) { - if self.player_id.is_none() { + if self.game.player_id.is_none() { println!("player_id not set "); return; } @@ -71,34 +172,81 @@ impl App { .map(|str| str.parse().unwrap_or(0)) .collect(); if positions.len() == 2 && positions[0] != 0 && positions[1] != 0 { - let checker_move = CheckerMove::new(positions[0], positions[1]); - if checker_move.is_ok() { - if self.first_move.is_some() { + if let Ok(checker_move) = CheckerMove::new(positions[0], positions[1]) { + // if checker_move.is_ok() { + if self.game.first_move.is_some() { let move_event = GameEvent::Move { - player_id: self.player_id.unwrap(), - moves: (self.first_move.unwrap(), checker_move.unwrap()), + player_id: self.game.player_id.unwrap(), + moves: (self.game.first_move.unwrap(), checker_move), }; - if !self.game.validate(&move_event) { + if !self.game.state.validate(&move_event) { println!("Move invalid"); - self.first_move = None; + self.game.first_move = None; return; } - self.game.consume(&move_event); - self.first_move = None; + self.game.handle_event(&move_event); + self.game.first_move = None; } else { - self.first_move = Some(checker_move.unwrap()); + self.game.first_move = Some(checker_move); } return; } } - println!("invalid move : {}", input); + println!("invalid move : {input}"); } pub fn display(&mut self) -> String { + let winner = self + .game + .state + .determine_winner() + .and_then(|id| self.game.state.players.get(&id)); + let str_won: String = winner + .map(|p| { + let mut name = " winner: ".to_owned(); + name.push_str(&p.name); + name + }) + .unwrap_or("".to_owned()); let mut output = "-------------------------------".to_owned(); - output = output + "\nRolled dice : " + &self.game.dices.to_display_string(); - output = output + "\n-------------------------------"; - output = output + "\n" + &self.game.board.to_display_grid(9); + output += format!( + "\n{:?}{} > {} > {:?}", + self.game.state.stage, + str_won, + self.game + .state + .who_plays() + .map(|pl| &pl.name) + .unwrap_or(&"?".to_owned()), + self.game.state.turn_stage + ) + .as_str(); + + output = output + "\nRolled dice : " + &self.game.state.dice.to_display_string(); + + if self.game.state.stage != Stage::PreGame { + output = output + "\nRolled dice jans : " + &format!("{:?}", self.game.state.dice_jans); + output = output + + "\nLast move : " + + &self.game.state.dice_moves.0.to_display_string() + + ", " + + &self.game.state.dice_moves.1.to_display_string(); + + // display players points + output += format!("\n\n{:<11} :: {:<5} :: {}", "Player", "holes", "points").as_str(); + + for player_id in self.game.state.players.keys().sorted() { + let player = &self.game.state.players[player_id]; + output += format!( + "\n{}. {:<8} :: {:<5} :: {}", + &player_id, &player.name, &player.holes, &player.points, + ) + .as_str(); + } + } + + output += "\n-------------------------------\n"; + output += &self.game.state.board.to_display_grid(9); output } } @@ -106,34 +254,36 @@ impl App { #[cfg(test)] mod tests { use super::*; + use pretty_assertions::assert_eq; #[test] fn test_display() { let expected = "------------------------------- +PreGame > ? > RollDice Rolled dice : 0 & 0 ------------------------------- - 13 14 15 16 17 18 19 20 21 22 23 24 + 13 14 15 16 17 18 19 20 21 22 23 24 ---------------------------------------------------------------- - | | | X | - | | | X | - | | | X | - | | | X | - | | | X | - | | | X | - | | | X | - | | | X | - | | | 15 | - |----------------------------- | | ------------------------------| - | | | 15 | - | | | O | - | | | O | - | | | O | - | | | O | - | | | O | - | | | O | - | | | O | - | | | O | + | | | X | + | | | X | + | | | X | + | | | X | + | | | X | + | | | X | + | | | X | + | | | X | + | | | 15 | + |------------------------------ | | -----------------------------| + | | | 15 | + | | | O | + | | | O | + | | | O | + | | | O | + | | | O | + | | | O | + | | | O | + | | | O | ---------------------------------------------------------------- 12 11 10 9 8 7 6 5 4 3 2 1 "; @@ -144,36 +294,48 @@ Rolled dice : 0 & 0 #[test] fn test_move() { let expected = "------------------------------- -Rolled dice : 0 & 0 +InGame > myself > RollDice +Rolled dice : 4 & 6 +Rolled dice jans : {} +Last move : CheckerMove { from: 24, to: 18 } , CheckerMove { from: 24, to: 20 } + +Player :: holes :: points +1. myself :: 0 :: 0 +2. bot :: 0 :: 0 ------------------------------- - 13 14 15 16 17 18 19 20 21 22 23 24 + 13 14 15 16 17 18 19 20 21 22 23 24 ---------------------------------------------------------------- - | | | X | - | | | X | - | | | X | - | | | X | - | | | X | - | | | X | - | | | X | - | | | X | - | | | 15 | - |----------------------------- | | ------------------------------| - | | | 13 | - | | | O | - | | | O | - | | | O | - | | | O | - | | | O | - | | | O | - | | | O | - | | | O O O | + | X | | X X | + | | | X | + | | | X | + | | | X | + | | | X | + | | | X | + | | | X | + | | | X | + | | | 13 | + |------------------------------ | | -----------------------------| + | | | 13 | + | | | O | + | | | O | + | | | O | + | | | O | + | | | O | + | | | O | + | | | O | + | | | O O O | ---------------------------------------------------------------- 12 11 10 9 8 7 6 5 4 3 2 1 "; - let mut app = App::new(); + let mut app = App::new(AppArgs { + seed: Some(1327), + bot: Some("dummy".into()), + }); + println!("avant : {}", app.display()); + app.input("roll"); + app.input("1 3"); app.input("1 4"); - app.input("1 5"); self::assert_eq!(app.display(), expected); } } diff --git a/client_cli/src/game_runner.rs b/client_cli/src/game_runner.rs new file mode 100644 index 0000000..b595c91 --- /dev/null +++ b/client_cli/src/game_runner.rs @@ -0,0 +1,131 @@ +use bot::{Bot, BotStrategy}; +use log::{debug, error}; +use store::{CheckerMove, DiceRoller, GameEvent, GameState, PlayerId, TurnStage}; + +// Application Game +#[derive(Debug, Default)] +pub struct GameRunner { + pub state: GameState, + pub dice_roller: DiceRoller, + pub first_move: Option, + pub player_id: Option, + bots: Vec, +} + +impl GameRunner { + // Constructs a new instance of [`App`]. + pub fn new( + schools_enabled: bool, + bot_strategies: Vec>, + seed: Option, + ) -> Self { + let mut state = GameState::new(schools_enabled); + // local : player + let player_id: Option = if bot_strategies.len() > 1 { + None + } else { + state.init_player("myself") + }; + + // bots + let bots: Vec = bot_strategies + .into_iter() + .map(|strategy| { + let bot_id: PlayerId = state.init_player("bot").unwrap(); + let bot_color = state.player_color_by_id(&bot_id).unwrap(); + Bot::new(strategy, bot_color) + }) + .collect(); + // let bot_strategy = Box::new(DefaultStrategy::default()); + // let bot: Bot = Bot::new(bot_strategy, bot_color, schools_enabled); + // let bot: Bot = Bot::new(bot_strategy, bot_color); + + let first_player_id = if bots.len() > 1 { + bots[0].player_id + } else { + player_id.unwrap() + }; + let mut game = Self { + state, + dice_roller: DiceRoller::new(seed), + first_move: None, + player_id, + bots, + }; + game.handle_event(&GameEvent::BeginGame { + goes_first: first_player_id, + }); + game + } + + pub fn handle_event(&mut self, event: &GameEvent) -> Option { + if event == &GameEvent::PlayError { + return None; + } + let valid_event = if self.state.validate(event) { + debug!( + "--------------- new valid event {event:?} (stage {:?}) -----------", + self.state.turn_stage + ); + self.state.consume(event); + debug!( + " --> stage {:?} ; active player points {:?}", + self.state.turn_stage, + self.state.who_plays().map(|p| p.points) + ); + event + } else { + debug!("{}", self.state); + error!("event not valid : {event:?}"); + // panic!("crash and burn {} \nevt not valid {event:?}", self.state); + &GameEvent::PlayError + }; + + // chain all successive bot actions + if self.bots.is_empty() { + return None; + } + + // Collect bot actions to avoid borrow conflicts + let bot_events: Vec = self + .bots + .iter_mut() + .filter_map(|bot| bot.handle_event(valid_event)) + .collect(); + + // if bot_events.len() > 1 { + // println!( + // "There might be a problem : 2 bots events : {:?}", + // bot_events + // ); + // } + + let mut next_event = None; + for bot_event in bot_events { + let bot_result_event = self.handle_event(&bot_event); + if let Some(bot_id) = bot_event.player_id() { + next_event = if self.bot_needs_dice_roll(bot_id) { + let dice = self.dice_roller.roll(); + self.handle_event(&GameEvent::RollResult { + player_id: bot_id, + dice, + }) + } else { + bot_result_event + }; + } + } + + if let Some(winner) = self.state.determine_winner() { + next_event = Some(store::GameEvent::EndGame { + reason: store::EndGameReason::PlayerWon { winner }, + }); + } + + next_event + } + + fn bot_needs_dice_roll(&self, bot_id: PlayerId) -> bool { + self.state.active_player_id == bot_id && self.state.turn_stage == TurnStage::RollWaiting + } +} diff --git a/client_cli/src/main.rs b/client_cli/src/main.rs index 1ed2455..0107b43 100644 --- a/client_cli/src/main.rs +++ b/client_cli/src/main.rs @@ -1,13 +1,48 @@ // Application. pub mod app; +mod game_runner; use anyhow::Result; -use app::App; +use app::{App, AppArgs}; use std::io; +// see pico-args example at https://github.com/RazrFalcon/pico-args/blob/master/examples/app.rs +const HELP: &str = "\ +Trictrac CLI + +USAGE: + trictrac-cli [OPTIONS] + +FLAGS: + -h, --help Prints help information + +OPTIONS: + --seed SEED Sets the random generator seed + --bot STRATEGY_BOT Add a bot player with strategy STRATEGY, a second bot may be added to play against the first : --bot STRATEGY_BOT1,STRATEGY_BOT2 + Available strategies: + - dummy: Default strategy selecting the first valid move + - ai: AI strategy using the default model at models/trictrac_ppo.zip + - ai:/path/to/model.zip: AI strategy using a custom model + - dqn: DQN strategy using native Rust implementation with Burn + - dqn:/path/to/model: DQN strategy using a custom model + +ARGS: + +"; + fn main() -> Result<()> { + env_logger::init(); + let args = match parse_args() { + Ok(v) => v, + Err(e) => { + eprintln!("Error: {e}."); + std::process::exit(1); + } + }; + // println!("{:#?}", args); + // Create an application. - let mut app = App::new(); + let mut app = App::new(args); // Start the main loop. while !app.should_quit { @@ -17,5 +52,38 @@ fn main() -> Result<()> { app.input(input.trim()); } + // display app final state + println!("{}", app.display()); + Ok(()) } + +fn parse_args() -> Result { + let mut pargs = pico_args::Arguments::from_env(); + + // Help has a higher priority and should be handled separately. + if pargs.contains(["-h", "--help"]) { + print!("{HELP}"); + std::process::exit(0); + } + + let args = AppArgs { + // Parses an optional value that implements `FromStr`. + seed: pargs.opt_value_from_str("--seed")?, + bot: pargs.opt_value_from_str("--bot")?, + // Parses an optional value from `&str` using a specified function. + // width: pargs.opt_value_from_fn("--width", parse_width)?.unwrap_or(10), + }; + + // It's up to the caller what to do with the remaining arguments. + let remaining = pargs.finish(); + if !remaining.is_empty() { + eprintln!("Warning: unused arguments left: {remaining:?}."); + } + + Ok(args) +} + +// fn parse_width(s: &str) -> Result { +// s.parse().map_err(|_| "not a number") +// } diff --git a/client_tui/Cargo.toml b/client_tui/Cargo.toml index 04c4ad7..31b1550 100644 --- a/client_tui/Cargo.toml +++ b/client_tui/Cargo.toml @@ -6,9 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.75" +anyhow = "1.0.89" bincode = "1.3.3" -crossterm = "0.27.0" -ratatui = "0.24.0" -renet = "0.0.13" +crossterm = "0.28.1" +ratatui = "0.28.1" +# renet = "0.0.13" store = { path = "../store" } diff --git a/client_tui/src/ui.rs b/client_tui/src/ui.rs index 8995a46..3108353 100644 --- a/client_tui/src/ui.rs +++ b/client_tui/src/ui.rs @@ -25,6 +25,6 @@ pub fn render(app: &mut App, f: &mut Frame) { ) .style(Style::default().fg(Color::Yellow)) .alignment(Alignment::Center), - f.size(), + f.area(), ) } diff --git a/devenv.lock b/devenv.lock index ea67978..c3d5629 100644 --- a/devenv.lock +++ b/devenv.lock @@ -3,11 +3,10 @@ "devenv": { "locked": { "dir": "src/modules", - "lastModified": 1698243190, - "narHash": "sha256-n+SbyNQRhUcaZoU00d+7wi17HJpw/kAUrXOL4zRcqE8=", + "lastModified": 1753667201, "owner": "cachix", "repo": "devenv", - "rev": "86f476f7edb86159fd20764489ab4e4df6edb4b6", + "rev": "4d584d7686a50387f975879788043e55af9f0ad4", "type": "github" }, "original": { @@ -20,11 +19,10 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "lastModified": 1747046372, "owner": "edolstra", "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", "type": "github" }, "original": { @@ -33,37 +31,39 @@ "type": "github" } }, - "flake-utils": { + "git-hooks": { "inputs": { - "systems": "systems" + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { - "lastModified": 1685518550, - "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", + "lastModified": 1750779888, + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "cachix", + "repo": "git-hooks.nix", "type": "github" } }, "gitignore": { "inputs": { "nixpkgs": [ - "pre-commit-hooks", + "git-hooks", "nixpkgs" ] }, "locked": { - "lastModified": 1660459072, - "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=", + "lastModified": 1709087332, "owner": "hercules-ci", "repo": "gitignore.nix", - "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", "type": "github" }, "original": { @@ -74,11 +74,10 @@ }, "nixpkgs": { "locked": { - "lastModified": 1698553279, - "narHash": "sha256-T/9P8yBSLcqo/v+FTOBK+0rjzjPMctVymZydbvR/Fak=", + "lastModified": 1753432016, "owner": "NixOS", "repo": "nixpkgs", - "rev": "90e85bc7c1a6fc0760a94ace129d3a1c61c3d035", + "rev": "6027c30c8e9810896b92429f0092f624f7b1aace", "type": "github" }, "original": { @@ -88,66 +87,14 @@ "type": "github" } }, - "nixpkgs-stable": { - "locked": { - "lastModified": 1685801374, - "narHash": "sha256-otaSUoFEMM+LjBI1XL/xGB5ao6IwnZOXc47qhIgJe8U=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "c37ca420157f4abc31e26f436c1145f8951ff373", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-23.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "pre-commit-hooks": { - "inputs": { - "flake-compat": "flake-compat", - "flake-utils": "flake-utils", - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ], - "nixpkgs-stable": "nixpkgs-stable" - }, - "locked": { - "lastModified": 1698227354, - "narHash": "sha256-Fi5H9jbaQLmLw9qBi/mkR33CoFjNbobo5xWdX4tKz1Q=", - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "rev": "bd38df3d508dfcdff52cd243d297f218ed2257bf", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "type": "github" - } - }, "root": { "inputs": { "devenv": "devenv", + "git-hooks": "git-hooks", "nixpkgs": "nixpkgs", - "pre-commit-hooks": "pre-commit-hooks" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" + "pre-commit-hooks": [ + "git-hooks" + ] } } }, diff --git a/devenv.nix b/devenv.nix index 127c215..1b51c9d 100644 --- a/devenv.nix +++ b/devenv.nix @@ -1,16 +1,27 @@ { pkgs, ... }: { - # https://devenv.sh/basics/ - # env.GREET = "devenv"; - packages = [ + packages = [ + + # pour burn-rs + pkgs.SDL2_gfx + # (compilation sdl2-sys) + pkgs.cmake + pkgs.libffi + pkgs.wayland-scanner + + # dev tools + pkgs.samply # code profiler + pkgs.feedgnuplot # to visualize bots training results + # for bevy - pkgs.alsaLib + pkgs.alsa-lib pkgs.udev # bevy fast compile - pkgs.clang pkgs.lld + pkgs.clang + pkgs.lld # copié de https://github.com/mmai/Hyperspeedcube/blob/develop/devenv.nix # TODO : retirer ce qui est inutile @@ -27,16 +38,12 @@ pkgs.xorg.libXi pkgs.xorg.libX11 - pkgs.vulkan-headers pkgs.vulkan-loader + pkgs.vulkan-headers + pkgs.vulkan-loader # ------------ fin copie ]; - # enterShell = '' - # hello - # git --version - # ''; - # https://devenv.sh/languages/ languages.rust.enable = true; @@ -44,7 +51,7 @@ # scripts.hello.exec = "echo hello from $GREET"; # https://devenv.sh/pre-commit-hooks/ - pre-commit.hooks.shellcheck.enable = true; + # pre-commit.hooks.shellcheck.enable = true; # https://devenv.sh/processes/ # processes.ping.exec = "ping example.com"; diff --git a/doc/backlog.md b/doc/backlog.md index 4cdd180..cf23e3b 100644 --- a/doc/backlog.md +++ b/doc/backlog.md @@ -1,34 +1,59 @@ # Backlog -position dans tutoriel : - ## DONE -### Epic : jeu simple - -Store -- gestion des évènements - - déplacement de dames - - jet des dés -- déplacements physiques possibles - ## TODO +### stack overflow + +- +- +- + +Méthodes pour limiter la stack : réduire la taille de la pile avant de lancer ton binaire en ligne de commande : + +```sh +ulimit -s 6144 # Limite la pile à 6Mo +# just trainbot +RUST_BACKTRACE=1 LD_LIBRARY_PATH=./target/debug ./target/debug/train_dqn_burn +ulimit -s unlimited # Pour revenir à la normale +``` + +- bot burn + - train = `just trainbot` + - durée d'entrainement selon params ? + - save + - load and run against default bot + - many configs, save models selon config + - retrain against himself ? + +### Doc + +Cheatsheet : arbre des situations et priorité des règles + ### Epic : jeu simple - déplacements autorisés par les règles (pourront être validés physiquement si jeu avec écoles) - calcul des points automatique (pas d'écoles) Server -- + +- Client + - client tui (ratatui) - client desktop (bevy) - client web ### Epic : jeu avec écoles + - déplacement de fiches points : validation physique - évenements de déclaration d'école & contre école ### Epic : Bot + +- OpenAi gym + - doc gymnasium + - Rust implementation for OpenAi gym + - Backgammon (?) diff --git a/doc/diagrammes.md b/doc/diagrammes.md new file mode 100644 index 0000000..d3e02f7 --- /dev/null +++ b/doc/diagrammes.md @@ -0,0 +1,143 @@ +# Diagrammes d'architecture + + +## Diagramme de Classes / Structures + +Ce diagramme montre les relations statiques entre les composants principaux. + +@startuml + +!theme vibrant + +package "client_cli" { + class GameRunner { + - state: GameState + - bots: Vec + + new(Vec>) + + handle_event(&GameEvent) + } +} + +package "bot" { + class Bot { + - strategy: Box + + new(Box) + + handle_event(&GameEvent): Option + } + + interface BotStrategy { + + choose_move(): (CheckerMove, CheckerMove) + + get_game(): &GameState + ' ... autres méthodes + } + + class DefaultStrategy + class DqnStrategy + class ErroneousStrategy +} + +package "store" { + class GameState { + + stage: Stage + + turn_stage: TurnStage + + board: Board + + active_player_id: PlayerId + ' ... + + validate(&GameEvent): bool + + consume(&GameEvent) + } + + class GameEvent +} + +GameRunner "1" *-- "1..2" Bot : contient +Bot "1" *-- "1" BotStrategy : utilise +BotStrategy <|.. DefaultStrategy : implémente +BotStrategy <|.. DqnStrategy : implémente +BotStrategy <|.. ErroneousStrategy : implémente + +GameRunner ..> GameState : dépend de +GameRunner ..> GameEvent : gère +Bot ..> GameState : dépend de +Bot ..> GameEvent : traite et génère +BotStrategy ..> GameState : analyse +@enduml + + +## Diagramme de Séquence : Boucle de jeu d'un Bot + +Ce diagramme montre les interactions dynamiques lors d'un tour de jeu où c'est à un bot de jouer. + +@startuml +!theme vibrant +autonumber + +participant "main (client_cli)" as Main +participant "runner: GameRunner" as Runner +participant "bot: Bot" as Bot +participant "strategy: BotStrategy" as Strategy +participant "state: GameState" as GameState + +Main -> Runner: new(strategies, seed) +activate Runner +Runner -> GameState: new() +activate GameState +GameState --> Runner: state +deactivate GameState +Runner -> Main: runner +deactivate Runner + +... Boucle de jeu principale ... + +Main -> Runner: handle_event(event) +activate Runner + +Runner -> GameState: validate(event) +activate GameState +GameState --> Runner: bool +deactivate GameState + +Runner -> GameState: consume(event) +activate GameState +deactivate GameState + +Runner -> Bot: handle_event(event) +activate Bot + +note right of Bot: Le bot vérifie si c'est son tour de jouer + +Bot -> Strategy: get_mut_game() +activate Strategy +Strategy --> Bot: &mut GameState +deactivate Strategy + +' Supposons que c'est au bot de jouer un coup +Bot -> Strategy: choose_move() +activate Strategy +Strategy -> GameState: Accède à l'état (board, dice, etc.) +activate GameState +deactivate GameState +Strategy --> Bot: moves +deactivate Strategy + +Bot --> Runner: Some(GameEvent::Move) +deactivate Bot + +Runner -> Runner: handle_event(GameEvent::Move) +note right of Runner: Appel récursif pour traiter le coup du bot + +Runner -> GameState: validate(GameEvent::Move) +activate GameState +GameState --> Runner: true +deactivate GameState + +Runner -> GameState: consume(GameEvent::Move) +activate GameState +note right of GameState: L'état du jeu est mis à jour\n(pions déplacés, joueur actif changé) +deactivate GameState + +Runner --> Main: Option (ou None) +deactivate Runner + +@enduml + diff --git a/doc/refs/claudeAIquestion.md b/doc/refs/claudeAIquestion.md new file mode 100644 index 0000000..0f3f626 --- /dev/null +++ b/doc/refs/claudeAIquestion.md @@ -0,0 +1,775 @@ +# Description + +Je développe un jeu de TricTrac () dans le langage rust. +Pour le moment je me concentre sur l'application en ligne de commande simple, donc ne t'occupe pas des dossiers 'client_bevy', 'client_tui', et 'server' qui ne seront utilisés que pour de prochaines évolutions. + +Les règles du jeu et l'état d'une partie sont implémentées dans 'store', l'application ligne de commande est implémentée dans 'client_cli', elle permet déjà de jouer contre un bot, ou de faire jouer deux bots l'un contre l'autre. +Les stratégies de bots sont implémentées dans le dossier 'bot'. + +Plus précisément, l'état du jeu est défini par le struct GameState dans store/src/game.rs, la méthode to_string_id() permet de coder cet état de manière compacte dans une chaîne de caractères, mais il n'y a pas l'historique des coups joués. Il y a aussi fmt::Display d'implémenté pour une representation textuelle plus lisible. + +'client_cli/src/game_runner.rs' contient la logique permettant de faire jouer deux bots l'un contre l'autre. +'bot/src/default.rs' contient le code d'une stratégie de bot basique : il détermine la liste des mouvements valides (avec la méthode get_possible_moves_sequences de store::MoveRules) et joue simplement le premier de la liste. + +J'aimerais maintenant ajouter une stratégie de bot plus forte en entrainant un agent/bot avec OpenAI Gym. + +Pour bénéficier des bibliothèques disponibles en python, comme 'Stable-Baselines3', j'ai utilisé PyO3 pour appeler mon code rust depuis python. Le code rust exposé se trouve dans 'store/src/engine.rs', il ne contient pour le moment que du code d'exemple pour m'assurer que la passerelle entre python et rust fonctionne, mais tout est à implémenter : l'état du jeu, les mouvements disponibles, etc. + +La classe python 'store/python/trictracEnv.py' est elle aussi presque vide, elle ne contient pour le moment qu'un squelette avec des fonctions à implémenter. + +Peux-tu m'aider à modifier engine.rs et trictracenv.py pour permettre d'entrainer un agent (bot) ? + +J'aimerais utiliser ce que j'ai déjà implémenté en rust : l'état d'une partie et les règles, et la fonction renvoyant la liste des coups disponibles pour un joueur à partir d'un état du jeu (get_possible_moves_sequences), mais je ne sais pas trop comment m'y prendre. + +## Réponse de Claude + +Je vois que je n'ai pas l'autorisation d'utiliser l'outil Edit. Je vais vous montrer les modifications nécessaires pour les deux fichiers principaux. + +Voici le contenu à utiliser pour `store/src/engine.rs` : + +```rust +//! # Expose trictrac game state and rules in a python module +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyTuple}; + +use crate::board::CheckerMove; +use crate::dice::Dice; +use crate::game::{GameEvent, GameState, Stage, TurnStage}; +use crate::game_rules_moves::MoveRules; +use crate::game_rules_points::{PointsRules, PossibleJans}; +use crate::player::{Color, Player, PlayerId}; +use std::collections::HashMap; + +#[pyclass] +struct TricTrac { + game_state: GameState, + dice_roll_sequence: Vec<(u8, u8)>, + current_dice_index: usize, +} + +#[pymethods] +impl TricTrac { + #[new] + fn new() -> Self { + let mut game_state = GameState::new(false); // schools_enabled = false + + // Initialiser 2 joueurs + game_state.init_player("player1"); + game_state.init_player("bot"); + + // Commencer la partie avec le joueur 1 + game_state.consume(&GameEvent::BeginGame { goes_first: 1 }); + + TricTrac { + game_state, + dice_roll_sequence: Vec::new(), + current_dice_index: 0, + } + } + + /// Obtenir l'état du jeu sous forme de chaîne de caractères compacte + fn get_state_id(&self) -> String { + self.game_state.to_string_id() + } + + /// Obtenir l'état du jeu sous forme de dictionnaire pour faciliter l'entrainement + fn get_state_dict(&self) -> PyResult> { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let state_dict = PyDict::new(py); + + // Informations essentielles sur l'état du jeu + state_dict.set_item("active_player", self.game_state.active_player_id)?; + state_dict.set_item("stage", format!("{:?}", self.game_state.stage))?; + state_dict.set_item("turn_stage", format!("{:?}", self.game_state.turn_stage))?; + + // Dés + let (dice1, dice2) = self.game_state.dice.values; + state_dict.set_item("dice", (dice1, dice2))?; + + // Points des joueurs + if let Some(white_player) = self.game_state.get_white_player() { + state_dict.set_item("white_points", white_player.points)?; + state_dict.set_item("white_holes", white_player.holes)?; + } + + if let Some(black_player) = self.game_state.get_black_player() { + state_dict.set_item("black_points", black_player.points)?; + state_dict.set_item("black_holes", black_player.holes)?; + } + + // Positions des pièces + let white_positions = self.get_checker_positions(Color::White); + let black_positions = self.get_checker_positions(Color::Black); + + state_dict.set_item("white_positions", white_positions)?; + state_dict.set_item("black_positions", black_positions)?; + + // État compact pour la comparaison d'états + state_dict.set_item("state_id", self.game_state.to_string_id())?; + + Ok(state_dict.into()) + } + + /// Renvoie les positions des pièces pour un joueur spécifique + fn get_checker_positions(&self, color: Color) -> Vec<(usize, i8)> { + self.game_state.board.get_color_fields(color) + } + + /// Obtenir la liste des mouvements légaux sous forme de paires (from, to) + fn get_available_moves(&self) -> Vec<((usize, usize), (usize, usize))> { + // L'agent joue toujours le joueur actif + let color = self.game_state.player_color_by_id(&self.game_state.active_player_id).unwrap_or(Color::White); + + // Si ce n'est pas le moment de déplacer les pièces, retourner une liste vide + if self.game_state.turn_stage != TurnStage::Move && self.game_state.turn_stage != TurnStage::HoldOrGoChoice { + return vec![]; + } + + let rules = MoveRules::new(&color, &self.game_state.board, self.game_state.dice); + let possible_moves = rules.get_possible_moves_sequences(true, vec![]); + + // Convertir les mouvements CheckerMove en tuples (from, to) pour Python + possible_moves.into_iter() + .map(|(move1, move2)| ( + (move1.get_from(), move1.get_to()), + (move2.get_from(), move2.get_to()) + )).collect() + } + + /// Jouer un coup ((from1, to1), (from2, to2)) + fn play_move(&mut self, moves: ((usize, usize), (usize, usize))) -> bool { + let ((from1, to1), (from2, to2)) = moves; + + // Vérifier que c'est au tour du joueur de jouer + if self.game_state.turn_stage != TurnStage::Move && self.game_state.turn_stage != TurnStage::HoldOrGoChoice { + return false; + } + + let move1 = CheckerMove::new(from1, to1).unwrap_or_default(); + let move2 = CheckerMove::new(from2, to2).unwrap_or_default(); + + let event = GameEvent::Move { + player_id: self.game_state.active_player_id, + moves: (move1, move2), + }; + + // Vérifier si le mouvement est valide + if !self.game_state.validate(&event) { + return false; + } + + // Exécuter le mouvement + self.game_state.consume(&event); + + // Si l'autre joueur doit lancer les dés maintenant, simuler ce lancement + if self.game_state.turn_stage == TurnStage::RollDice { + self.roll_dice(); + } + + true + } + + /// Lancer les dés (soit aléatoirement, soit en utilisant une séquence prédéfinie) + fn roll_dice(&mut self) -> (u8, u8) { + // Vérifier que c'est au bon moment pour lancer les dés + if self.game_state.turn_stage != TurnStage::RollDice && self.game_state.turn_stage != TurnStage::RollWaiting { + return self.game_state.dice.values; + } + + // Simuler un lancer de dés + let dice_values = if !self.dice_roll_sequence.is_empty() && self.current_dice_index < self.dice_roll_sequence.len() { + // Utiliser la séquence prédéfinie + let dice = self.dice_roll_sequence[self.current_dice_index]; + self.current_dice_index += 1; + dice + } else { + // Générer aléatoirement + ( + (1 + (rand::random::() % 6)), + (1 + (rand::random::() % 6)) + ) + }; + + // Envoyer les événements appropriés + let roll_event = GameEvent::Roll { + player_id: self.game_state.active_player_id, + }; + + if self.game_state.validate(&roll_event) { + self.game_state.consume(&roll_event); + } + + let roll_result_event = GameEvent::RollResult { + player_id: self.game_state.active_player_id, + dice: Dice { values: dice_values }, + }; + + if self.game_state.validate(&roll_result_event) { + self.game_state.consume(&roll_result_event); + } + + dice_values + } + + /// Marquer des points + fn mark_points(&mut self, points: u8) -> bool { + // Vérifier que c'est au bon moment pour marquer des points + if self.game_state.turn_stage != TurnStage::MarkPoints && self.game_state.turn_stage != TurnStage::MarkAdvPoints { + return false; + } + + let event = GameEvent::Mark { + player_id: self.game_state.active_player_id, + points, + }; + + // Vérifier si l'événement est valide + if !self.game_state.validate(&event) { + return false; + } + + // Exécuter l'événement + self.game_state.consume(&event); + + // Si l'autre joueur doit lancer les dés maintenant, simuler ce lancement + if self.game_state.turn_stage == TurnStage::RollDice { + self.roll_dice(); + } + + true + } + + /// Choisir de "continuer" (Go) après avoir gagné un trou + fn choose_go(&mut self) -> bool { + // Vérifier que c'est au bon moment pour choisir de continuer + if self.game_state.turn_stage != TurnStage::HoldOrGoChoice { + return false; + } + + let event = GameEvent::Go { + player_id: self.game_state.active_player_id, + }; + + // Vérifier si l'événement est valide + if !self.game_state.validate(&event) { + return false; + } + + // Exécuter l'événement + self.game_state.consume(&event); + + // Simuler le lancer de dés pour le prochain tour + self.roll_dice(); + + true + } + + /// Calcule les points maximaux que le joueur actif peut obtenir avec les dés actuels + fn calculate_points(&self) -> u8 { + let active_player = self.game_state.players.get(&self.game_state.active_player_id); + + if let Some(player) = active_player { + let dice_roll_count = player.dice_roll_count; + let color = player.color; + + let points_rules = PointsRules::new(&color, &self.game_state.board, self.game_state.dice); + let (points, _) = points_rules.get_points(dice_roll_count); + + points + } else { + 0 + } + } + + /// Réinitialise la partie + fn reset(&mut self) { + self.game_state = GameState::new(false); + + // Initialiser 2 joueurs + self.game_state.init_player("player1"); + self.game_state.init_player("bot"); + + // Commencer la partie avec le joueur 1 + self.game_state.consume(&GameEvent::BeginGame { goes_first: 1 }); + + // Réinitialiser l'index de la séquence de dés + self.current_dice_index = 0; + } + + /// Vérifie si la partie est terminée + fn is_done(&self) -> bool { + self.game_state.stage == Stage::Ended || self.game_state.determine_winner().is_some() + } + + /// Obtenir le gagnant de la partie + fn get_winner(&self) -> Option { + self.game_state.determine_winner() + } + + /// Obtenir le score du joueur actif (nombre de trous) + fn get_score(&self, player_id: PlayerId) -> i32 { + if let Some(player) = self.game_state.players.get(&player_id) { + player.holes as i32 + } else { + -1 + } + } + + /// Obtenir l'ID du joueur actif + fn get_active_player_id(&self) -> PlayerId { + self.game_state.active_player_id + } + + /// Définir une séquence de dés à utiliser (pour la reproductibilité) + fn set_dice_sequence(&mut self, sequence: Vec<(u8, u8)>) { + self.dice_roll_sequence = sequence; + self.current_dice_index = 0; + } + + /// Afficher l'état du jeu (pour le débogage) + fn __str__(&self) -> String { + format!("{}", self.game_state) + } +} + +/// A Python module implemented in Rust. The name of this function must match +/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to +/// import the module. +#[pymodule] +fn trictrac(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + + Ok(()) +} +``` + +Et pour `store/python/trictracEnv.py` : + +```python +import gym +import numpy as np +from gym import spaces +import trictrac # module Rust exposé via PyO3 +from typing import Dict, List, Tuple, Optional, Any, Union + +class TricTracEnv(gym.Env): + """Environnement OpenAI Gym pour le jeu de Trictrac""" + + metadata = {"render.modes": ["human"]} + + def __init__(self, opponent_strategy="random"): + super(TricTracEnv, self).__init__() + + # Instancier le jeu + self.game = trictrac.TricTrac() + + # Stratégie de l'adversaire + self.opponent_strategy = opponent_strategy + + # Constantes + self.MAX_FIELD = 24 # Nombre de cases sur le plateau + self.MAX_CHECKERS = 15 # Nombre maximum de pièces par joueur + + # Définition de l'espace d'observation + # Format: + # - Position des pièces blanches (24) + # - Position des pièces noires (24) + # - Joueur actif (1: blanc, 2: noir) (1) + # - Valeurs des dés (2) + # - Points de chaque joueur (2) + # - Trous de chaque joueur (2) + # - Phase du jeu (1) + self.observation_space = spaces.Dict({ + 'board': spaces.Box(low=-self.MAX_CHECKERS, high=self.MAX_CHECKERS, shape=(self.MAX_FIELD,), dtype=np.int8), + 'active_player': spaces.Discrete(3), # 0: pas de joueur, 1: blanc, 2: noir + 'dice': spaces.MultiDiscrete([7, 7]), # Valeurs des dés (1-6) + 'white_points': spaces.Discrete(13), # Points du joueur blanc (0-12) + 'white_holes': spaces.Discrete(13), # Trous du joueur blanc (0-12) + 'black_points': spaces.Discrete(13), # Points du joueur noir (0-12) + 'black_holes': spaces.Discrete(13), # Trous du joueur noir (0-12) + 'turn_stage': spaces.Discrete(6), # Étape du tour + }) + + # Définition de l'espace d'action + # Format: + # - Action type: 0=move, 1=mark, 2=go + # - Move: (from1, to1, from2, to2) ou zeros + self.action_space = spaces.Dict({ + 'action_type': spaces.Discrete(3), + 'move': spaces.MultiDiscrete([self.MAX_FIELD + 1, self.MAX_FIELD + 1, + self.MAX_FIELD + 1, self.MAX_FIELD + 1]) + }) + + # État courant + self.state = self._get_observation() + + # Historique des états pour éviter les situations sans issue + self.state_history = [] + + # Pour le débogage et l'entraînement + self.steps_taken = 0 + self.max_steps = 1000 # Limite pour éviter les parties infinies + + def reset(self): + """Réinitialise l'environnement et renvoie l'état initial""" + self.game.reset() + self.state = self._get_observation() + self.state_history = [] + self.steps_taken = 0 + return self.state + + def step(self, action): + """ + Exécute une action et retourne (state, reward, done, info) + + Action format: + { + 'action_type': 0/1/2, # 0=move, 1=mark, 2=go + 'move': [from1, to1, from2, to2] # Utilisé seulement si action_type=0 + } + """ + action_type = action['action_type'] + reward = 0 + done = False + info = {} + + # Vérifie que l'action est valide pour le joueur humain (id=1) + player_id = self.game.get_active_player_id() + is_agent_turn = player_id == 1 # L'agent joue toujours le joueur 1 + + if is_agent_turn: + # Exécute l'action selon son type + if action_type == 0: # Move + from1, to1, from2, to2 = action['move'] + move_made = self.game.play_move(((from1, to1), (from2, to2))) + if not move_made: + # Pénaliser les mouvements invalides + reward -= 2.0 + info['invalid_move'] = True + else: + # Petit bonus pour un mouvement valide + reward += 0.1 + elif action_type == 1: # Mark + points = self.game.calculate_points() + marked = self.game.mark_points(points) + if not marked: + # Pénaliser les actions invalides + reward -= 2.0 + info['invalid_mark'] = True + else: + # Bonus pour avoir marqué des points + reward += 0.1 * points + elif action_type == 2: # Go + go_made = self.game.choose_go() + if not go_made: + # Pénaliser les actions invalides + reward -= 2.0 + info['invalid_go'] = True + else: + # Petit bonus pour l'action valide + reward += 0.1 + else: + # Tour de l'adversaire + self._play_opponent_turn() + + # Vérifier si la partie est terminée + if self.game.is_done(): + done = True + winner = self.game.get_winner() + if winner == 1: + # Bonus si l'agent gagne + reward += 10.0 + info['winner'] = 'agent' + else: + # Pénalité si l'adversaire gagne + reward -= 5.0 + info['winner'] = 'opponent' + + # Récompense basée sur la progression des trous + agent_holes = self.game.get_score(1) + opponent_holes = self.game.get_score(2) + reward += 0.5 * (agent_holes - opponent_holes) + + # Mettre à jour l'état + new_state = self._get_observation() + + # Vérifier les états répétés + if self._is_state_repeating(new_state): + reward -= 0.2 # Pénalité légère pour éviter les boucles + info['repeating_state'] = True + + # Ajouter l'état à l'historique + self.state_history.append(self._get_state_id()) + + # Limiter la durée des parties + self.steps_taken += 1 + if self.steps_taken >= self.max_steps: + done = True + info['timeout'] = True + + # Comparer les scores en cas de timeout + if agent_holes > opponent_holes: + reward += 5.0 + info['winner'] = 'agent' + elif opponent_holes > agent_holes: + reward -= 2.0 + info['winner'] = 'opponent' + + self.state = new_state + return self.state, reward, done, info + + def _play_opponent_turn(self): + """Simule le tour de l'adversaire avec la stratégie choisie""" + player_id = self.game.get_active_player_id() + + # Boucle tant qu'il est au tour de l'adversaire + while player_id == 2 and not self.game.is_done(): + # Action selon l'étape du tour + state_dict = self._get_state_dict() + turn_stage = state_dict.get('turn_stage') + + if turn_stage == 'RollDice' or turn_stage == 'RollWaiting': + self.game.roll_dice() + elif turn_stage == 'MarkPoints' or turn_stage == 'MarkAdvPoints': + points = self.game.calculate_points() + self.game.mark_points(points) + elif turn_stage == 'HoldOrGoChoice': + # Stratégie simple: toujours continuer (Go) + self.game.choose_go() + elif turn_stage == 'Move': + available_moves = self.game.get_available_moves() + if available_moves: + if self.opponent_strategy == "random": + # Choisir un mouvement au hasard + move = available_moves[np.random.randint(0, len(available_moves))] + else: + # Par défaut, prendre le premier mouvement valide + move = available_moves[0] + self.game.play_move(move) + + # Mise à jour de l'ID du joueur actif + player_id = self.game.get_active_player_id() + + def _get_observation(self): + """Convertit l'état du jeu en un format utilisable par l'apprentissage par renforcement""" + state_dict = self._get_state_dict() + + # Créer un tableau représentant le plateau + board = np.zeros(self.MAX_FIELD, dtype=np.int8) + + # Remplir les positions des pièces blanches (valeurs positives) + white_positions = state_dict.get('white_positions', []) + for pos, count in white_positions: + if 1 <= pos <= self.MAX_FIELD: + board[pos-1] = count + + # Remplir les positions des pièces noires (valeurs négatives) + black_positions = state_dict.get('black_positions', []) + for pos, count in black_positions: + if 1 <= pos <= self.MAX_FIELD: + board[pos-1] = -count + + # Créer l'observation complète + observation = { + 'board': board, + 'active_player': state_dict.get('active_player', 0), + 'dice': np.array([ + state_dict.get('dice', (1, 1))[0], + state_dict.get('dice', (1, 1))[1] + ]), + 'white_points': state_dict.get('white_points', 0), + 'white_holes': state_dict.get('white_holes', 0), + 'black_points': state_dict.get('black_points', 0), + 'black_holes': state_dict.get('black_holes', 0), + 'turn_stage': self._turn_stage_to_int(state_dict.get('turn_stage', 'RollDice')), + } + + return observation + + def _get_state_dict(self) -> Dict: + """Récupère l'état du jeu sous forme de dictionnaire depuis le module Rust""" + return self.game.get_state_dict() + + def _get_state_id(self) -> str: + """Récupère l'identifiant unique de l'état actuel""" + return self.game.get_state_id() + + def _is_state_repeating(self, new_state) -> bool: + """Vérifie si l'état se répète trop souvent""" + state_id = self.game.get_state_id() + # Compter les occurrences de l'état dans l'historique récent + count = sum(1 for s in self.state_history[-10:] if s == state_id) + return count >= 3 # Considéré comme répétitif si l'état apparaît 3 fois ou plus + + def _turn_stage_to_int(self, turn_stage: str) -> int: + """Convertit l'étape du tour en entier pour l'observation""" + stages = { + 'RollDice': 0, + 'RollWaiting': 1, + 'MarkPoints': 2, + 'HoldOrGoChoice': 3, + 'Move': 4, + 'MarkAdvPoints': 5 + } + return stages.get(turn_stage, 0) + + def render(self, mode="human"): + """Affiche l'état actuel du jeu""" + if mode == "human": + print(str(self.game)) + print(f"État actuel: {self._get_state_id()}") + + # Afficher les actions possibles + if self.game.get_active_player_id() == 1: + turn_stage = self._get_state_dict().get('turn_stage') + print(f"Étape: {turn_stage}") + + if turn_stage == 'Move' or turn_stage == 'HoldOrGoChoice': + print("Mouvements possibles:") + moves = self.game.get_available_moves() + for i, move in enumerate(moves): + print(f" {i}: {move}") + + if turn_stage == 'HoldOrGoChoice': + print("Option: Go (continuer)") + + def get_action_mask(self): + """Retourne un masque des actions valides dans l'état actuel""" + state_dict = self._get_state_dict() + turn_stage = state_dict.get('turn_stage') + + # Masque par défaut (toutes les actions sont invalides) + mask = { + 'action_type': np.zeros(3, dtype=bool), + 'move': np.zeros((self.MAX_FIELD + 1, self.MAX_FIELD + 1, + self.MAX_FIELD + 1, self.MAX_FIELD + 1), dtype=bool) + } + + if self.game.get_active_player_id() != 1: + return mask # Pas au tour de l'agent + + # Activer les types d'actions valides selon l'étape du tour + if turn_stage == 'Move' or turn_stage == 'HoldOrGoChoice': + mask['action_type'][0] = True # Activer l'action de mouvement + + # Activer les mouvements valides + valid_moves = self.game.get_available_moves() + for ((from1, to1), (from2, to2)) in valid_moves: + mask['move'][from1, to1, from2, to2] = True + + if turn_stage == 'MarkPoints' or turn_stage == 'MarkAdvPoints': + mask['action_type'][1] = True # Activer l'action de marquer des points + + if turn_stage == 'HoldOrGoChoice': + mask['action_type'][2] = True # Activer l'action de continuer (Go) + + return mask + + def sample_valid_action(self): + """Échantillonne une action valide selon le masque d'actions""" + mask = self.get_action_mask() + + # Trouver les types d'actions valides + valid_action_types = np.where(mask['action_type'])[0] + + if len(valid_action_types) == 0: + # Aucune action valide (pas le tour de l'agent) + return { + 'action_type': 0, + 'move': np.zeros(4, dtype=np.int32) + } + + # Choisir un type d'action + action_type = np.random.choice(valid_action_types) + + action = { + 'action_type': action_type, + 'move': np.zeros(4, dtype=np.int32) + } + + # Si c'est un mouvement, sélectionner un mouvement valide + if action_type == 0: + valid_moves = np.where(mask['move']) + if len(valid_moves[0]) > 0: + # Sélectionner un mouvement valide aléatoirement + idx = np.random.randint(0, len(valid_moves[0])) + from1 = valid_moves[0][idx] + to1 = valid_moves[1][idx] + from2 = valid_moves[2][idx] + to2 = valid_moves[3][idx] + action['move'] = np.array([from1, to1, from2, to2], dtype=np.int32) + + return action + + def close(self): + """Nettoie les ressources à la fermeture de l'environnement""" + pass + +# Exemple d'utilisation avec Stable-Baselines3 +def example_usage(): + from stable_baselines3 import PPO + from stable_baselines3.common.vec_env import DummyVecEnv + + # Fonction d'enveloppement pour créer l'environnement + def make_env(): + return TricTracEnv() + + # Créer un environnement vectorisé (peut être parallélisé) + env = DummyVecEnv([make_env]) + + # Créer le modèle + model = PPO("MultiInputPolicy", env, verbose=1) + + # Entraîner le modèle + model.learn(total_timesteps=10000) + + # Sauvegarder le modèle + model.save("trictrac_ppo") + + print("Entraînement terminé et modèle sauvegardé") + +if __name__ == "__main__": + # Tester l'environnement + env = TricTracEnv() + obs = env.reset() + + print("Environnement initialisé") + env.render() + + # Jouer quelques coups aléatoires + for _ in range(10): + action = env.sample_valid_action() + obs, reward, done, info = env.step(action) + + print(f"\nAction: {action}") + print(f"Reward: {reward}") + print(f"Info: {info}") + env.render() + + if done: + print("Game over!") + break + + env.close() +``` + +Ces implémentations vous permettront de créer un environnement Gym pour votre jeu Trictrac qui: + +1. Expose l'état du jeu dans un format approprié pour l'apprentissage par renforcement +2. Gère le déroulement des parties et le tour de l'adversaire +3. Définit un système de récompenses pour l'entraînement de l'agent +4. Fournit des masques d'actions pour garantir que seules les actions valides sont sélectionnées +5. Inclut des exemples pour l'intégration avec Stable-Baselines3 + +Ce code nécessitera probablement quelques ajustements en fonction de la façon dont votre code Rust est organisé, mais il devrait vous donner une base solide pour commencer. + +Pour utiliser cette implémentation : + +1. Mettez à jour `engine.rs` pour exposer l'API nécessaire à Python +2. Compilez la bibliothèque Rust avec maturin : `maturin build -m store/Cargo.toml --release` +3. Implémentez `trictracEnv.py` avec l'environnement Gym +4. Créez un script d'entraînement qui utilise Stable-Baselines3 ou une autre bibliothèque d'apprentissage par renforcement + +Voulez-vous que j'explique plus en détail certaines parties de l'implémentation ou que je vous aide à résoudre des problèmes spécifiques ? diff --git a/doc/refs/claudeAIquestionOnlyRust.md b/doc/refs/claudeAIquestionOnlyRust.md new file mode 100644 index 0000000..ac81f7a --- /dev/null +++ b/doc/refs/claudeAIquestionOnlyRust.md @@ -0,0 +1,268 @@ +# Description + +Je développe un jeu de TricTrac () dans le langage rust. +Pour le moment ne t'occupe pas des dossiers 'client_bevy', 'client_tui', et 'server' qui ne seront utilisés que pour de prochaines évolutions. + +Les règles du jeu et l'état d'une partie sont implémentées dans 'store', l'application ligne de commande est implémentée dans 'client_cli', elle permet de jouer contre un bot, ou de faire jouer deux bots l'un contre l'autre. +Les stratégies de bots sont implémentées dans le dossier 'bot'. + +L'état du jeu est défini par le struct GameState dans store/src/game.rs, la méthode to_string_id() permet de coder cet état de manière compacte dans une chaîne de caractères, mais il n'y a pas l'historique des coups joués. Il y a aussi fmt::Display d'implémenté pour une representation textuelle plus lisible. + +'client_cli/src/game_runner.rs' contient la logique permettant de faire jouer deux bots l'un contre l'autre. +'bot/src/default.rs' contient le code d'une stratégie de bot basique : il détermine la liste des mouvements valides (avec la méthode get_possible_moves_sequences de store::MoveRules) et joue simplement le premier de la liste. + +Dans une précédente session tu m'avais aidé à ajouter une stratégie de bot plus forte en entrainant un agent/bot avec OpenAI Gym en utilisant PyO3 et Stable-Baselines3. Tu m'avais fourni le code pour `store/src/engine.rs` et `store/python/trictracEnv.py`. + +Ça fonctionne, mais je voudrais maintenant passer à une solution entièrement rust. +Je pensais utiliser burn et l'algorithme DQN implémenté dans s3-burn ( ). Peux-tu m'aider à faire ça ? + +----- fin d'implémentation : + +Parfait ! Le jeu fonctionne maintenant avec la stratégie DQN ! Le bot DQN a même gagné la partie contre le bot +dummy. + +Résumé + +J'ai réussi à migrer l'implémentation d'une solution Python/PyO3/Stable-Baselines3 vers une solution entièrement +Rust avec DQN. Voici ce qui a été accompli : + +✅ Ce qui fonctionne maintenant : + +1. Suppression complète de PyO3 du module store pour éviter les conflits +2. Implémentation DQN native en Rust avec : + + - Réseau de neurones simplifié (3 couches, ReLU) + - Initialisation Xavier/Glorot des poids + - Buffer de replay pour l'expérience + - Stratégie epsilon-greedy + - Sauvegarde/chargement de modèles en JSON + +3. Intégration complète dans le CLI avec la commande : + + - --bot dqn : DQN avec configuration par défaut + - --bot dqn:/path/to/model : DQN avec modèle personnalisé + +4. Compilation réussie sans dépendances Burn problématiques + +🔄 Points à améliorer plus tard : + +1. Entraînement complet : Actuellement la méthode train() simule l'entraînement. Il faudrait implémenter la + backpropagation complète. +2. Espace d'actions plus sophistiqué : L'espace d'actions est simplifié (3 actions). On pourrait l'étendre pour + couvrir tous les mouvements possibles. +3. Système de récompenses : Le système de récompenses est basique et pourrait être enrichi. +4. Migration vers Burn : Quand les problèmes de compatibilité seront résolus, on pourra migrer vers Burn pour de + meilleures performances. + +La base DQN est maintenant fonctionnelle et entièrement en Rust ! 🎉 + +--- + +Ton implémentation démarre avec un bot non entraîné et il s'entraîne lorsqu'il , le modèle n'est pas entraîné de manière séparée ? On démarre avec un modèle non entraîné et il s'entraîne en même temps que le bot joue ? + +Je voudrais que le fichier dqn.rs actuel ne contienne que le code nécessaire pour de charger un modèle entraîné et jouer contre les adversaires (c'est à dire l'implémentation de BotStrategy). Et donc que tu extraies le code qui permet d'entraîner le modèle dans un autre fichier, et que tu ajoutes une commande pour lancer l'entraînement. + +--- + +● Parfait ! Testons maintenant un entraînement court : + +● Bash(cd /home/henri/travaux/programmes/trictrac && cargo run --bin=train_dqn -- --episodes 10 --save-every 5)… +⎿  Error: Command timed out after 2m 0.0s warning: unused import: `DqnConfig` +--> bot/src/strategy/dqn.rs:5:25 +| +5 | use super::dqn_common::{DqnConfig, SimpleNeuralNetwork, game_state_to_vector}; +| ^^^^^^^^^ +| += note: `#[warn(unused_imports)]` on by default + + warning: unused import: `store::MoveRules` + --> bot/src/strategy/erroneous_moves.rs:2:5 + … +175 lines (ctrl+r to see all) + +● Il y a un problème dans la logique d'entraînement - il semble y avoir une boucle infinie avec des erreurs "rules not followed". Il faut corriger la logique du TrictracEnv. Le problème est probablement que l'environnement ne gère pas +correctement les étapes du jeu. + +● Update Todos +⎿  ☒ Analyser le code existant avec PyO3/Stable-Baselines3 +☒ Examiner l'environnement Gym Python actuel +☒ Créer un environnement DQN natif en Rust +☒ Implémenter la stratégie bot DQN en Rust +☒ Ajouter les dépendances Burn et sb3-burn +☒ Restructurer le code DQN +☒ Créer un module d'entraînement séparé +☒ Ajouter une commande d'entraînement + +● Parfait ! J'ai réussi à restructurer complètement le code DQN selon tes demandes : + +✅ Restructuration terminée avec succès + +📁 Structure du code : + +1. dqn.rs - Contient uniquement la stratégie DqnStrategy pour charger et utiliser un modèle pré-entraîné +2. dqn_common.rs - Code partagé (réseau de neurones, configuration, utilitaires) +3. dqn_trainer.rs - Logique d'entraînement complète avec environnement et agent +4. train_dqn - Binaire dédié pour lancer l'entraînement + +🚀 Commandes disponibles : + +Pour jouer avec un modèle DQN : +cargo run --bin=client_cli -- --bot dqn,dummy # Sans modèle (fallback) +cargo run --bin=client_cli -- --bot dqn:/path/to/model.json,dummy # Avec modèle + +Pour entraîner un modèle DQN : +cargo run --bin=train_dqn # Entraînement par défaut (1000 épisodes) +cargo run --bin=train_dqn -- --help # Voir les options +cargo run --bin=train_dqn -- --episodes 5000 --save-every 500 + +⚠️ Note sur l'environnement d'entraînement : + +L'entraînement a révélé un problème dans la logique de l'environnement (boucle infinie avec "rules not followed"). C'est normal pour une première implémentation - l'environnement d'entraînement nécessite des ajustements pour bien +gérer les règles du jeu Trictrac. + +Le code est maintenant parfaitement séparé entre : + +- Utilisation (stratégie simple qui charge un modèle) +- Entraînement (environnement complet avec logique d'apprentissage) + +--- + +● ✅ Espace d'actions étendu implémenté avec succès ! + +J'ai complètement étendu l'espace d'actions du DQN pour couvrir tous les choix possibles dans le jeu Trictrac : + +🎯 Nouvel espace d'actions : + +TrictracAction enum comprend : + +- Roll - Lancer les dés (index 0) +- Mark { points } - Marquer des points (indices 1-13 pour 0-12 points) +- Go - Continuer après avoir gagné un trou (index 14) +- Move { move1, move2 } - Mouvement de pions (indices 15+) + - Encode (from, to) pour les deux pions + - Espace total : 331,791 actions possibles + +🧠 Améliorations intelligentes : + +1. Masquage d'actions valides - Le DQN ne considère que les actions légales dans l'état actuel +2. Encodage compact - Mouvements encodés en un seul entier pour efficacité +3. Décodage automatique - Conversion transparente entre indices et actions +4. Stratégie adaptative - La stratégie DQN utilise maintenant le bon type d'action selon le contexte + +🔧 Fonctionnalités ajoutées : + +- get_valid_actions() - Obtient les actions valides pour l'état actuel +- TrictracAction::to_action_index() / from_action_index() - Conversion bidirectionnelle +- sample_valid_action() - Sélection aléatoire d'une action valide +- Réseau de neurones élargi (512 neurones cachés) pour gérer l'espace étendu + +📊 Utilisation dans le jeu : + +Le bot DQN peut maintenant : + +- Choisir le nombre exact de points à marquer (pas seulement le maximum) +- Sélectionner des mouvements spécifiques parmi toutes les combinaisons possibles +- Décider intelligemment entre "Go" et les mouvements alternatifs + +L'espace d'actions est maintenant parfaitement aligné avec la complexité réelle du jeu Trictrac ! 🎲 + +--- + +Pour réduire la taille de l'espace des actions, est-ce qu'on peut tenir compte de l'état du jeu pour le choix des actions ? +Ça voudrait dire que pour le choix du mouvement par exemple, comme on connait la valeur des dés on a juste à choisir quel dé on joue en premier (2 choix), puis la position de départ du premier pion (25 choix), et la position de départ du deuxième pion (25 choix), la position de départ étant déduite de la valeur des dés. + +.... + +● Parfait ! J'ai implémenté l'espace d'actions compactes basé sur le contexte du jeu. Voici les amélirations majeures apportées : + +✅ Réduction drastique de l'espace d'actions : + +- Avant : 331,791 actions possibles à chaque étape +- Maintenant : 1-200 actions selon le contexte du jeu + +✅ Actions contextuelles : + +- Roll : Seulement quand c'est possible (1 action) +- Mark : Seulement les points valides 0-12 (≤13 actions) +- Go : Toujours 1 action +- MoveChoice : 2 ordres de dés × ~10 positions de départ × ~10 positions = ~200 actions au lieu de 331,791 + +✅ Architecture modulaire : + +- CompactAction : actions simplifiées basées sur le contexte +- to_trictrac_action() : conversion automatique vers les actions complètes +- get_valid_compact_actions() : génération contextuelle des actions valides +- context_action_space_size() : taille dynamique de l'espace d'actions + +L'approche utilise exactement votre suggestion : pour les mouvements, au lieu d'encoder toutes les combinaisons possibles, on choisit : + +1. L'ordre des dés (2 choix max si dés différents) +2. Les positions de départ (seulement celles avec des pions) +3. Les destinations sont automatiquement calculées selon les règles + +Cela réduit l'espace d'actions de 99.94% tout en gardant toute la richesse du jeu ! + +--- + +● Parfait ! J'ai créé le fichier train_burn_rl.rs qui utilise votre environnement Burn-RL corrigé. + +Pour lancer l'entraînement avec Burn-RL : + +cargo run --bin=train_burn_rl + +Ou avec des options personnalisées : + +cargo run --bin=train_burn_rl -- --episodes 2000 --save-every 200 --max-steps 1000 + +Caractéristiques de cet entraîneur : + +✅ Utilise l'environnement Burn-RL que vous avez corrigé +✅ Actions contextuelles via get_valid_actions() +✅ Politique epsilon-greedy simple pour commencer +✅ Statistiques détaillées avec moyennes mobiles +✅ Configuration flexible via arguments CLI +✅ Logging progressif pour suivre l'entraînement + +Options disponibles : + +- --episodes : nombre d'épisodes (défaut: 1000) +- --save-every : fréquence d'affichage des stats (défaut: 100) +- --max-steps : nombre max de steps par épisode (défaut: 500) +- --help : aide complète + +Cet entraîneur sert de base pour tester l'environnement Burn-RL. Une fois que tout fonctionne bien, on pourra y intégrer un vrai agent DQN avec réseaux de neurones ! + +--- + +Session où je n'ai cessé de recevoir ce message : + +⎿ API Error (429 {"type":"error","error":{"type":"rate_limit_error","message":"This request would exceed the rate limit for your organization (813e6b21-ec6f-44c3-a7f0-408244105e5c) of 20,000 input tokens per minute. For details, refer to: . You can see the response headers for current usage. Please reduce the prompt length or the maximum tokens requested, or try again later. You may also contact sales at to discuss your options for a rate limit increase."}}) · Retrying in 391 seconds… (attempt 1/10) + +✶ Coaching… (403s · ↑ 382 tokens · esc to interrupt) + +Pour à la fin de la session avoir ce résumé : + +Total cost: $0.95 +Total duration (API): 1h 24m 22.8s +Total duration (wall): 1h 43m 3.5s +Total code changes: 746 lines added, 0 lines removed +Token usage by model: +claude-3-5-haiku: 18.8k input, 443 output, 0 cache read, 0 cache write +claude-sonnet: 10 input, 666 output, 0 cache read, 245.6k cache write + +Mais pourtant 2 millions indiqués dans la page usage : , et 7.88 dollars de consommés sur . + +I just had a claude code session in which I kept having this error, even if the agent didn't seem to read a lot of files : API Error (429 {"type":"error","error":{"type":"rate_limit_error","message":"This request would exceed the rate limit for your organization (813e6b21-ec6f-44c3-a7f0-408244105e5c) of 20,000 input tokens per minute. + +at the end of the session the token usage and cost indicated was this : + +Total cost: $0.95 +Total duration (API): 1h 24m 22.8s +Total duration (wall): 1h 43m 3.5s +Total code changes: 746 lines added, 0 lines removed +Token usage by model: +claude-3-5-haiku: 18.8k input, 443 output, 0 cache read, 0 cache write +claude-sonnet: 10 input, 666 output, 0 cache read, 245.6k cache write + +but the usage on the /usage page was 2,073,698 token in, and the cost on the /cost page was $7.90. + +When looking at the costs csv file, it seems that it is the "input cache write 5m" that consumed nearly all the tokens ( $7,71 ). Is it a bug ? diff --git a/doc/refs/dqn-burn.md b/doc/refs/dqn-burn.md new file mode 100644 index 0000000..22c6479 --- /dev/null +++ b/doc/refs/dqn-burn.md @@ -0,0 +1,106 @@ +# DQN avec burn-rl + +## Paramètre d'entraînement dans dqn/burnrl/dqn_model.rs + +Ces constantes sont des hyperparamètres, c'est-à-dire des réglages que l'on fixe avant l'entraînement et qui conditionnent la manière dont le modèle va apprendre. + +MEMORY_SIZE + +- Ce que c'est : La taille de la "mémoire de rejeu" (Replay Memory/Buffer). +- À quoi ça sert : L'agent interagit avec l'environnement (le jeu de TricTrac) et stocke ses expériences (un état, l'action prise, la récompense obtenue, et l'état suivant) dans cette mémoire. Pour s'entraîner, au + lieu d'utiliser uniquement la dernière expérience, il pioche un lot (batch) d'expériences aléatoires dans cette mémoire. +- Pourquoi c'est important : + 1. Décorrélation : Ça casse la corrélation entre les expériences successives, ce qui rend l'entraînement plus stable et efficace. + 2. Réutilisation : Une même expérience peut être utilisée plusieurs fois pour l'entraînement, ce qui améliore l'efficacité des données. +- Dans votre code : const MEMORY_SIZE: usize = 4096; signifie que l'agent gardera en mémoire les 4096 dernières transitions. + +DENSE_SIZE + +- Ce que c'est : La taille des couches cachées du réseau de neurones. "Dense" signifie que chaque neurone d'une couche est connecté à tous les neurones de la couche suivante. +- À quoi ça sert : C'est la "capacité de réflexion" de votre agent. Le réseau de neurones (ici, Net) prend l'état du jeu en entrée, le fait passer à travers des couches de calcul (de taille DENSE_SIZE), et sort une + estimation de la qualité de chaque action possible. +- Pourquoi c'est important : + - Une valeur trop petite : le modèle ne sera pas assez "intelligent" pour apprendre les stratégies complexes du TricTrac. + - Une valeur trop grande : l'entraînement sera plus lent et le modèle pourrait "sur-apprendre" (overfitting), c'est-à-dire devenir très bon sur les situations vues en entraînement mais incapable de généraliser + sur de nouvelles situations. +- Dans votre code : const DENSE_SIZE: usize = 128; définit que les couches cachées du réseau auront 128 neurones. + +EPS_START, EPS_END et EPS_DECAY + +Ces trois constantes gèrent la stratégie d'exploration de l'agent, appelée "epsilon-greedy". Le but est de trouver un équilibre entre : + +- L'Exploitation : Jouer le coup que le modèle pense être le meilleur. +- L'Exploration : Jouer un coup au hasard pour découvrir de nouvelles stratégies, potentiellement meilleures. + +epsilon (ε) est la probabilité de faire un choix aléatoire (explorer). + +- `EPS_START` (Epsilon de départ) : + + - Ce que c'est : La valeur d'epsilon au tout début de l'entraînement. + - Rôle : Au début, le modèle ne sait rien. Il est donc crucial qu'il explore beaucoup pour accumuler des expériences variées. Une valeur élevée (proche de 1.0) est typique. + - Dans votre code : const EPS_START: f64 = 0.9; signifie qu'au début, l'agent a 90% de chances de jouer un coup au hasard. + +- `EPS_END` (Epsilon final) : + + - Ce que c'est : La valeur minimale d'epsilon, atteinte après un certain nombre d'étapes. + - Rôle : Même après un long entraînement, on veut conserver une petite part d'exploration pour éviter que l'agent ne se fige dans une stratégie sous-optimale. + - Dans votre code : const EPS_END: f64 = 0.05; signifie qu'à la fin, l'agent explorera encore avec 5% de probabilité. + +- `EPS_DECAY` (Décroissance d'epsilon) : + - Ce que c'est : Contrôle la vitesse à laquelle epsilon passe de EPS_START à EPS_END. + - Rôle : C'est un facteur de "lissage" dans la formule de décroissance exponentielle. Plus cette valeur est élevée, plus la décroissance est lente, et donc plus l'agent passera de temps à explorer. + - Dans votre code : const EPS_DECAY: f64 = 1000.0; est utilisé dans la formule EPS_END + (EPS_START - EPS_END) \* f64::exp(-(step as f64) / EPS_DECAY); pour faire diminuer progressivement la valeur d'epsilon à + chaque étape (step) de l'entraînement. + +En résumé, ces constantes définissent l'architecture du "cerveau" de votre bot (DENSE*SIZE), sa mémoire à court terme (MEMORY_SIZE), et comment il apprend à équilibrer entre suivre sa stratégie et en découvrir de +nouvelles (EPS*\*). + +## Paramètres DQNTrainingConfig + +1. `gamma` (Facteur d'actualisation / _Discount Factor_) + + - À quoi ça sert ? Ça détermine l'importance des récompenses futures. Une valeur proche de 1 (ex: 0.99) + indique à l'agent qu'une récompense obtenue dans le futur est presque aussi importante qu'une + récompense immédiate. Il sera donc "patient" et capable de faire des sacrifices à court terme pour un + gain plus grand plus tard. + - Intuition : Un gamma de 0 rendrait l'agent "myope", ne se souciant que du prochain coup. Un gamma de + 0.99 l'encourage à élaborer des stratégies à long terme. + +2. `tau` (Taux de mise à jour douce / _Soft Update Rate_) + + - À quoi ça sert ? Pour stabiliser l'apprentissage, les algorithmes DQN utilisent souvent deux réseaux + : un réseau principal qui apprend vite et un "réseau cible" (copie du premier) qui évolue lentement. + tau contrôle la vitesse à laquelle les connaissances du réseau principal sont transférées vers le + réseau cible. + - Intuition : Une petite valeur (ex: 0.005) signifie que le réseau cible, qui sert de référence stable, + ne se met à jour que très progressivement. C'est comme un "mentor" qui n'adopte pas immédiatement + toutes les nouvelles idées de son "élève", ce qui évite de déstabiliser tout l'apprentissage sur un + coup de chance (ou de malchance). + +3. `learning_rate` (Taux d'apprentissage) + + - À quoi ça sert ? C'est peut-être le plus classique des hyperparamètres. Il définit la "taille du + pas" lors de la correction des erreurs. Après chaque prédiction, l'agent compare le résultat à ce + qui s'est passé et ajuste ses poids. Le learning_rate détermine l'ampleur de cet ajustement. + - Intuition : Trop élevé, et l'agent risque de sur-corriger et de ne jamais converger (comme chercher + le fond d'une vallée en faisant des pas de géant). Trop bas, et l'apprentissage sera extrêmement + lent. + +4. `batch_size` (Taille du lot) + + - À quoi ça sert ? L'agent apprend de ses expériences passées, qu'il stocke dans une "mémoire". Pour + chaque session d'entraînement, au lieu d'apprendre d'une seule expérience, il en pioche un lot + (batch) au hasard (ex: 32 expériences). Il calcule l'erreur moyenne sur ce lot pour mettre à jour + ses poids. + - Intuition : Apprendre sur un lot plutôt que sur une seule expérience rend l'apprentissage plus + stable et plus général. L'agent se base sur une "moyenne" de situations plutôt que sur un cas + particulier qui pourrait être une anomalie. + +5. `clip_grad` (Plafonnement du gradient / _Gradient Clipping_) + - À quoi ça sert ? C'est une sécurité pour éviter le problème des "gradients qui explosent". Parfois, + une expérience très inattendue peut produire une erreur de prédiction énorme, ce qui entraîne une + correction (un "gradient") démesurément grande. Une telle correction peut anéantir tout ce que le + réseau a appris. + - Intuition : clip_grad impose une limite. Si la correction à apporter dépasse un certain seuil, elle + est ramenée à cette valeur maximale. C'est un garde-fou qui dit : "OK, on a fait une grosse erreur, + mais on va corriger calmement, sans tout casser". diff --git a/doc/refs/geminiQuestions.md b/doc/refs/geminiQuestions.md new file mode 100644 index 0000000..2801fe2 --- /dev/null +++ b/doc/refs/geminiQuestions.md @@ -0,0 +1,19 @@ +# Description du projet + +Je développe un jeu de TricTrac () dans le langage rust. +Pour le moment je me concentre sur l'application en ligne de commande simple, donc ne t'occupe pas des dossiers 'client_bevy', 'client_tui', et 'server' qui ne seront utilisés que pour de prochaines évolutions. + +Les règles du jeu et l'état d'une partie sont implémentées dans 'store', l'application ligne de commande est implémentée dans 'client_cli', elle permet déjà de jouer contre un bot, ou de faire jouer deux bots l'un contre l'autre. +Les stratégies de bots sont implémentées dans le dossier 'bot'. + +Plus précisément, l'état du jeu est défini par le struct GameState dans store/src/game.rs, la méthode to_string_id() permet de coder cet état de manière compacte dans une chaîne de caractères, mais il n'y a pas l'historique des coups joués. Il y a aussi fmt::Display d'implémenté pour une representation textuelle plus lisible. + +'client_cli/src/game_runner.rs' contient la logique permettant de faire jouer deux bots l'un contre l'autre. +'bot/src/strategy/default.rs' contient le code d'une stratégie de bot basique : il détermine la liste des mouvements valides (avec la méthode get_possible_moves_sequences de store::MoveRules) et joue simplement le premier de la liste. + +Je cherche maintenant à ajouter des stratégies de bot plus fortes en entrainant un agent/bot par reinforcement learning. +J'utilise la bibliothèque burn (). + +Une version utilisant l'algorithme DQN peut être lancée avec `cargo run --bin=burn_train -- dqn`). Elle effectue un entraînement, sauvegarde les données du modèle obtenu puis recharge le modèle depuis le disque pour tester l'agent. L'entraînement est fait dans la fonction 'run' du fichier bot/src/burnrl/dqn_model.rs, la sauvegarde du modèle dans la fonction 'save_model' et le chargement dans la fonction 'load_model'. + +J'essaie de faire l'équivalent avec les algorithmes PPO (fichier bot/src/burnrl/ppo_model.rs) et SAC (fichier bot/src/burnrl/sac_model.rs) : les fonctions 'run' sont implémentées mais pas les fonctions 'save_model' et 'load_model'. Peux-tu les implémenter ? diff --git a/doc/refs/inspirations.md b/doc/refs/inspirations.md index 436c039..2217ed7 100644 --- a/doc/refs/inspirations.md +++ b/doc/refs/inspirations.md @@ -1,46 +1,52 @@ # Inspirations tools - - config clippy ? - - bacon : tests runner (ou loom ?) + +- config clippy ? +- bacon : tests runner (ou loom ?) ## Rust libs -cf. https://blessed.rs/crates +cf. -nombres aléatoires avec seed : https://richard.dallaway.com/posts/2021-01-04-repeat-resume/ +nombres aléatoires avec seed : -- cli : https://lib.rs/crates/pico-args ( ou clap ) +- cli : ( ou clap ) - reseau async : tokio - web serveur : axum (uses tokio) - - https://fasterthanli.me/series/updating-fasterthanli-me-for-2022/part-2#the-opinions-of-axum-also-nice-error-handling + - - db : sqlx - - eyre, color-eyre (Results) - tracing (logging) - rayon ( sync <-> parallel ) -- front : yew + tauri +- front : yew + tauri - egui -- https://docs.rs/board-game/latest/board_game/ +- + +## network games + +- +- (wasm, rooms) +- ## Others -- plugins avec https://github.com/extism/extism + +- plugins avec ## Backgammon existing projects -* go : https://bgammon.org/blog/20240101-hello-world/ - - protocole de communication : https://code.rocket9labs.com/tslocum/bgammon/src/branch/main/PROTOCOL.md -* ocaml : https://github.com/jacobhilton/backgammon?tab=readme-ov-file - cli example : https://www.jacobh.co.uk/backgammon/ -* lib rust backgammon - - https://github.com/carlostrub/backgammon - - https://github.com/marktani/backgammon -* network webtarot -* front ? - +- go : + - protocole de communication : +- ocaml : + cli example : +- lib rust backgammon + - + - +- network webtarot +- front ? ## cli examples @@ -48,7 +54,7 @@ nombres aléatoires avec seed : https://richard.dallaway.com/posts/2021-01-04-re (No game) new game gnubg rolls 3, anthon rolls 1. - + GNU Backgammon Positions ID: 4HPwATDgc/ABMA Match ID : MIEFAAAAAAAA +12-11-10--9--8--7-------6--5--4--3--2--1-+ O: gnubg @@ -64,7 +70,7 @@ nombres aléatoires avec seed : https://richard.dallaway.com/posts/2021-01-04-re | O X | | X O | | O X | | X O | 0 points +13-14-15-16-17-18------19-20-21-22-23-24-+ X: anthon - + gnubg moves 8/5 6/5. ### jacobh @@ -72,33 +78,37 @@ nombres aléatoires avec seed : https://richard.dallaway.com/posts/2021-01-04-re Move 11: player O rolls a 6-2. Player O estimates that they have a 90.6111% chance of winning. -Os borne off: none - 24 23 22 21 20 19 18 17 16 15 14 13 -------------------------------------------------------------------- -| v v v v v v | | v v v v v v | -| | | | -| X O O O | | O O O | -| X O O O | | O O | -| O | | | -| | X | | -| | | | -| | | | -| | | | -| | | | -|------------------------------| |------------------------------| -| | | | -| | | | -| | | | -| | | | -| X | | | -| X X | | X | -| X X X | | X O | -| X X X | | X O O | -| | | | -| ^ ^ ^ ^ ^ ^ | | ^ ^ ^ ^ ^ ^ | -------------------------------------------------------------------- - 1 2 3 4 5 6 7 8 9 10 11 12 -Xs borne off: none +Os borne off: none + 24 23 22 21 20 19 18 17 16 15 14 13 + +--- + +| v v v v v v | | v v v v v v | +| | | | +| X O O O | | O O O | +| X O O O | | O O | +| O | | | +| | X | | +| | | | +| | | | +| | | | +| | | | +|------------------------------| |------------------------------| +| | | | +| | | | +| | | | +| | | | +| X | | | +| X X | | X | +| X X X | | X O | +| X X X | | X O O | +| | | | +| ^ ^ ^ ^ ^ ^ | | ^ ^ ^ ^ ^ ^ | + +--- + +1 2 3 4 5 6 7 8 9 10 11 12 +Xs borne off: none Move 12: player X rolls a 6-3. Your move (? for help): bar/22 @@ -107,13 +117,12 @@ Your move (? for help): ? Enter the start and end positions, separated by a forward slash (or any non-numeric character), of each counter you want to move. Each position should be number from 1 to 24, "bar" or "off". Unlike in standard notation, you should enter each counter movement individually. For example: - 24/18 18/13 - bar/3 13/10 13/10 8/5 - 2/off 1/off +24/18 18/13 +bar/3 13/10 13/10 8/5 +2/off 1/off You can also enter these commands: - p - show the previous move - n - show the next move - - toggle between showing the current and last moves - help - show this help text - quit - abandon game - +p - show the previous move +n - show the next move + - toggle between showing the current and last moves +help - show this help text +quit - abandon game diff --git a/doc/refs/journal.md b/doc/refs/journal.md index 1435822..dd6d99c 100644 --- a/doc/refs/journal.md +++ b/doc/refs/journal.md @@ -6,16 +6,56 @@ cargo init cargo add pico-args ``` -Organisation store / server / client selon https://herluf-ba.github.io/making-a-turn-based-multiplayer-game-in-rust-01-whats-a-turn-based-game-anyway +Organisation store / server / client selon _store_ est la bibliothèque contenant le _reducer_ qui transforme l'état du jeu en fonction des évènements. Elle est utilisée par le _server_ et le _client_. Seuls les évènements sont transmis entre clients et serveur. +## Config neovim debugger launchers + +Cela se passe dans la config neovim (lua/plugins/overrides.lua) + ## Organisation du store lib - - game::GameState - - error - - dice - - board - - user + +- game::GameState + - error + - dice + - board - user + - user + +## Algorithme de détermination des coups + +- strategy::choose_move + + - GameRules.get_possible_moves_sequences(with_excedents: bool) + - get_possible_moves_sequences_by_dices(dice_max, dice_min, with_excedents, false); + - get_possible_moves_sequences_by_dices(dice_min, dice_max, with_excedents, true); + - has_checkers_outside_last_quarter() ok + - board.get_possible_moves ok + - check_corner_rules(&(first_move, second_move)) ok + +- handle_event + - state.validate (ok) + - rules.moves_follow_rules (ok) + - moves_possible ok + - moves_follows_dices ok + - moves_allowed (ok) + - check_corner_rules ok + - can_take_corner_by_effect ok + - get_possible_moves_sequences -> cf. l.15 + - check_exit_rules + - get_possible_moves_sequences(without exedents) -> cf l.15 + - get_quarter_filling_moves_sequences + - get_possible_moves_sequences -> cf l.15 + - state.consume (RollResult) (ok) + - get_rollresult_jans -> points_rules.get_result_jans (ok) + - get_jans (ok) + - get_jans_by_ordered_dice (ok) + - get_jans_by_ordered_dice ( dices.poped ) + - move_rules.get_scoring_quarter_filling_moves_sequences (ok) + - get_quarter_filling_moves_sequences cf l.8 (ok) + - board.get_quarter_filling_candidate -> is_quarter_fillable ok + - move_rules.get_possible_moves_sequence -> cf l.15 + - get_jans_points -> jan.get_points ok diff --git a/doc/refs/outputs.md b/doc/refs/outputs.md new file mode 100644 index 0000000..895062b --- /dev/null +++ b/doc/refs/outputs.md @@ -0,0 +1,417 @@ +# Outputs + +## 50 episodes - 1000 steps max - desktop + +{"episode": 0, "reward": -1798.7162, "steps count": 1000, "duration": 11} +{"episode": 1, "reward": -1794.8162, "steps count": 1000, "duration": 32} +{"episode": 2, "reward": -1387.7109, "steps count": 1000, "duration": 58} +{"episode": 3, "reward": -42.5005, "steps count": 1000, "duration": 82} +{"episode": 4, "reward": -48.2005, "steps count": 1000, "duration": 109} +{"episode": 5, "reward": 1.2000, "steps count": 1000, "duration": 141} +{"episode": 6, "reward": 8.8000, "steps count": 1000, "duration": 184} +{"episode": 7, "reward": 6.9002, "steps count": 1000, "duration": 219} +{"episode": 8, "reward": 16.5001, "steps count": 1000, "duration": 248} +{"episode": 9, "reward": -2.6000, "steps count": 1000, "duration": 281} +{"episode": 10, "reward": 3.0999, "steps count": 1000, "duration": 324} +{"episode": 11, "reward": -34.7004, "steps count": 1000, "duration": 497} +{"episode": 12, "reward": -15.7998, "steps count": 1000, "duration": 466} +{"episode": 13, "reward": 6.9000, "steps count": 1000, "duration": 496} +{"episode": 14, "reward": 6.3000, "steps count": 1000, "duration": 540} +{"episode": 15, "reward": -2.6000, "steps count": 1000, "duration": 581} +{"episode": 16, "reward": -33.0003, "steps count": 1000, "duration": 641} +{"episode": 17, "reward": -36.8000, "steps count": 1000, "duration": 665} +{"episode": 18, "reward": -10.1997, "steps count": 1000, "duration": 753} +{"episode": 19, "reward": -88.1014, "steps count": 1000, "duration": 837} +{"episode": 20, "reward": -57.5002, "steps count": 1000, "duration": 881} +{"episode": 21, "reward": -17.7997, "steps count": 1000, "duration": 1159} +{"episode": 22, "reward": -25.4000, "steps count": 1000, "duration": 1235} +{"episode": 23, "reward": -104.4013, "steps count": 995, "duration": 1290} +{"episode": 24, "reward": -268.6004, "steps count": 1000, "duration": 1322} +{"episode": 25, "reward": -743.6052, "steps count": 1000, "duration": 1398} +{"episode": 26, "reward": -821.5029, "steps count": 1000, "duration": 1427} +{"episode": 27, "reward": -211.5993, "steps count": 1000, "duration": 1409} +{"episode": 28, "reward": -276.1974, "steps count": 1000, "duration": 1463} +{"episode": 29, "reward": -222.9980, "steps count": 1000, "duration": 1509} +{"episode": 30, "reward": -298.9973, "steps count": 1000, "duration": 1560} +{"episode": 31, "reward": -164.0011, "steps count": 1000, "duration": 1752} +{"episode": 32, "reward": -221.0990, "steps count": 1000, "duration": 1807} +{"episode": 33, "reward": -260.9996, "steps count": 1000, "duration": 1730} +{"episode": 34, "reward": -420.5959, "steps count": 1000, "duration": 1767} +{"episode": 35, "reward": -407.2964, "steps count": 1000, "duration": 1815} +{"episode": 36, "reward": -291.2966, "steps count": 1000, "duration": 1870} + +thread 'main' has overflowed its stack +fatal runtime error: stack overflow, aborting +error: Recipe `trainbot` was terminated on line 24 by signal 6 + +## 50 episodes - 700 steps max - desktop + +const MEMORY_SIZE: usize = 4096; +const DENSE_SIZE: usize = 128; +const EPS_DECAY: f64 = 1000.0; +const EPS_START: f64 = 0.9; +const EPS_END: f64 = 0.05; + +> Entraînement +> {"episode": 0, "reward": -862.8993, "steps count": 700, "duration": 6} +> {"episode": 1, "reward": -418.8971, "steps count": 700, "duration": 13} +> {"episode": 2, "reward": -64.9999, "steps count": 453, "duration": 14} +> {"episode": 3, "reward": -142.8002, "steps count": 700, "duration": 31} +> {"episode": 4, "reward": -74.4004, "steps count": 700, "duration": 45} +> {"episode": 5, "reward": -40.2002, "steps count": 700, "duration": 58} +> {"episode": 6, "reward": -21.1998, "steps count": 700, "duration": 70} +> {"episode": 7, "reward": 99.7000, "steps count": 642, "duration": 79} +> {"episode": 8, "reward": -5.9999, "steps count": 700, "duration": 99} +> {"episode": 9, "reward": -7.8999, "steps count": 700, "duration": 118} +> {"episode": 10, "reward": 92.5000, "steps count": 624, "duration": 117} +> {"episode": 11, "reward": -17.1998, "steps count": 700, "duration": 144} +> {"episode": 12, "reward": 1.7000, "steps count": 700, "duration": 157} +> {"episode": 13, "reward": -7.9000, "steps count": 700, "duration": 172} +> {"episode": 14, "reward": -7.9000, "steps count": 700, "duration": 196} +> {"episode": 15, "reward": -2.8000, "steps count": 700, "duration": 214} +> {"episode": 16, "reward": 16.8002, "steps count": 700, "duration": 250} +> {"episode": 17, "reward": -47.7001, "steps count": 700, "duration": 272} +> k{"episode": 18, "reward": -13.6000, "steps count": 700, "duration": 288} +> {"episode": 19, "reward": -79.9002, "steps count": 700, "duration": 304} +> {"episode": 20, "reward": -355.5985, "steps count": 700, "duration": 317} +> {"episode": 21, "reward": -205.5001, "steps count": 700, "duration": 333} +> {"episode": 22, "reward": -207.3974, "steps count": 700, "duration": 348} +> {"episode": 23, "reward": -161.7999, "steps count": 700, "duration": 367} + +--- + +const MEMORY_SIZE: usize = 8192; +const DENSE_SIZE: usize = 128; +const EPS_DECAY: f64 = 10000.0; +const EPS_START: f64 = 0.9; +const EPS_END: f64 = 0.05; + +> Entraînement +> {"episode": 0, "reward": -1119.9921, "steps count": 700, "duration": 6} +> {"episode": 1, "reward": -928.6963, "steps count": 700, "duration": 13} +> {"episode": 2, "reward": -364.5009, "steps count": 380, "duration": 11} +> {"episode": 3, "reward": -797.5981, "steps count": 700, "duration": 28} +> {"episode": 4, "reward": -577.5994, "steps count": 599, "duration": 34} +> {"episode": 5, "reward": -725.2992, "steps count": 700, "duration": 49} +> {"episode": 6, "reward": -638.8995, "steps count": 700, "duration": 59} +> {"episode": 7, "reward": -1039.1932, "steps count": 700, "duration": 73} +> field invalid : White, 3, Board { positions: [13, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -2, 0, -11] } + +thread 'main' panicked at store/src/game.rs:556:65: +called `Result::unwrap()` on an `Err` value: FieldInvalid +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +error: Recipe `trainbot` failed on line 27 with exit code 101 + +--- + +# [allow(unused)] + +const MEMORY_SIZE: usize = 8192; +const DENSE_SIZE: usize = 256; +const EPS_DECAY: f64 = 10000.0; +const EPS_START: f64 = 0.9; +const EPS_END: f64 = 0.05; + +> Entraînement +> {"episode": 0, "reward": -1102.6925, "steps count": 700, "duration": 9} +> field invalid : White, 6, Board { positions: [14, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, 0, 0, -13] } + +thread 'main' panicked at store/src/game.rs:556:65: +called `Result::unwrap()` on an `Err` value: FieldInvalid +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +error: Recipe `trainbot` failed on line 27 with exit code 101 + +--- + +const MEMORY_SIZE: usize = 8192; +const DENSE_SIZE: usize = 256; +const EPS_DECAY: f64 = 1000.0; +const EPS_START: f64 = 0.9; +const EPS_END: f64 = 0.05; + +> Entraînement +> {"episode": 0, "reward": -1116.2921, "steps count": 700, "duration": 9} +> {"episode": 1, "reward": -1116.2922, "steps count": 700, "duration": 18} +> {"episode": 2, "reward": -1119.9921, "steps count": 700, "duration": 29} +> {"episode": 3, "reward": -1089.1927, "steps count": 700, "duration": 41} +> {"episode": 4, "reward": -1116.2921, "steps count": 700, "duration": 53} +> {"episode": 5, "reward": -684.8043, "steps count": 700, "duration": 66} +> {"episode": 6, "reward": 0.3000, "steps count": 700, "duration": 80} +> {"episode": 7, "reward": 2.0000, "steps count": 700, "duration": 96} +> {"episode": 8, "reward": 30.9001, "steps count": 700, "duration": 112} +> {"episode": 9, "reward": 0.3000, "steps count": 700, "duration": 128} +> {"episode": 10, "reward": 0.3000, "steps count": 700, "duration": 141} +> {"episode": 11, "reward": 8.8000, "steps count": 700, "duration": 155} +> {"episode": 12, "reward": 7.1000, "steps count": 700, "duration": 169} +> {"episode": 13, "reward": 17.3001, "steps count": 700, "duration": 190} +> {"episode": 14, "reward": -107.9005, "steps count": 700, "duration": 210} +> {"episode": 15, "reward": 7.1001, "steps count": 700, "duration": 236} +> {"episode": 16, "reward": 17.3001, "steps count": 700, "duration": 268} +> {"episode": 17, "reward": 7.1000, "steps count": 700, "duration": 283} +> {"episode": 18, "reward": -5.9000, "steps count": 700, "duration": 300} +> {"episode": 19, "reward": -36.8009, "steps count": 700, "duration": 316} +> {"episode": 20, "reward": 19.0001, "steps count": 700, "duration": 332} +> {"episode": 21, "reward": 113.3000, "steps count": 461, "duration": 227} +> field invalid : White, 1, Board { positions: [0, 2, 2, 0, 2, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -7, -2, -1, 0, -1, -1] } + +thread 'main' panicked at store/src/game.rs:556:65: +called `Result::unwrap()` on an `Err` value: FieldInvalid +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +error: Recipe `trainbot` failed on line 27 with exit code 101 + +--- + +num_episodes: 50, +// memory_size: 8192, // must be set in dqn_model.rs with the MEMORY_SIZE constant +// max_steps: 700, // must be set in environment.rs with the MAX_STEPS constant +dense_size: 256, // neural network complexity +eps_start: 0.9, // epsilon initial value (0.9 => more exploration) +eps_end: 0.05, +eps_decay: 1000.0, + +> Entraînement +> {"episode": 0, "reward": -1118.8921, "steps count": 700, "duration": 9} +> {"episode": 1, "reward": -1119.9921, "steps count": 700, "duration": 17} +> {"episode": 2, "reward": -1118.8921, "steps count": 700, "duration": 28} +> {"episode": 3, "reward": -283.5977, "steps count": 700, "duration": 41} +> {"episode": 4, "reward": -23.4998, "steps count": 700, "duration": 54} +> {"episode": 5, "reward": -31.9999, "steps count": 700, "duration": 68} +> {"episode": 6, "reward": 2.0000, "steps count": 700, "duration": 82} +> {"episode": 7, "reward": 109.3000, "steps count": 192, "duration": 26} +> {"episode": 8, "reward": -4.8000, "steps count": 700, "duration": 102} +> {"episode": 9, "reward": 15.6001, "steps count": 700, "duration": 124} +> {"episode": 10, "reward": 15.6002, "steps count": 700, "duration": 144} +> {"episode": 11, "reward": -65.7008, "steps count": 700, "duration": 162} +> {"episode": 12, "reward": 19.0002, "steps count": 700, "duration": 182} +> {"episode": 13, "reward": 20.7001, "steps count": 700, "duration": 197} +> {"episode": 14, "reward": 12.2002, "steps count": 700, "duration": 229} +> {"episode": 15, "reward": -32.0007, "steps count": 700, "duration": 242} +> {"episode": 16, "reward": 10.5000, "steps count": 700, "duration": 287} +> {"episode": 17, "reward": 24.1001, "steps count": 700, "duration": 318} +> {"episode": 18, "reward": 25.8002, "steps count": 700, "duration": 335} +> {"episode": 19, "reward": 29.2001, "steps count": 700, "duration": 367} +> {"episode": 20, "reward": 9.1000, "steps count": 700, "duration": 366} +> {"episode": 21, "reward": 3.7001, "steps count": 700, "duration": 398} +> {"episode": 22, "reward": 10.5000, "steps count": 700, "duration": 417} +> {"episode": 23, "reward": 10.5000, "steps count": 700, "duration": 438} +> {"episode": 24, "reward": 13.9000, "steps count": 700, "duration": 444} +> {"episode": 25, "reward": 7.1000, "steps count": 700, "duration": 486} +> {"episode": 26, "reward": 12.2001, "steps count": 700, "duration": 499} +> {"episode": 27, "reward": 8.8001, "steps count": 700, "duration": 554} +> {"episode": 28, "reward": -6.5000, "steps count": 700, "duration": 608} +> {"episode": 29, "reward": -3.1000, "steps count": 700, "duration": 633} +> {"episode": 30, "reward": -32.0001, "steps count": 700, "duration": 696} +> {"episode": 31, "reward": 22.4002, "steps count": 700, "duration": 843} +> {"episode": 32, "reward": -77.9004, "steps count": 700, "duration": 817} +> {"episode": 33, "reward": -368.5993, "steps count": 700, "duration": 827} +> {"episode": 34, "reward": -254.6986, "steps count": 700, "duration": 852} +> {"episode": 35, "reward": -433.1992, "steps count": 700, "duration": 884} +> {"episode": 36, "reward": -521.6010, "steps count": 700, "duration": 905} +> {"episode": 37, "reward": -71.1004, "steps count": 700, "duration": 930} +> {"episode": 38, "reward": -251.0004, "steps count": 700, "duration": 956} +> {"episode": 39, "reward": -594.7045, "steps count": 700, "duration": 982} +> {"episode": 40, "reward": -154.4001, "steps count": 700, "duration": 1008} +> {"episode": 41, "reward": -171.3994, "steps count": 700, "duration": 1033} +> {"episode": 42, "reward": -118.7004, "steps count": 700, "duration": 1059} +> {"episode": 43, "reward": -137.4003, "steps count": 700, "duration": 1087} + +thread 'main' has overflowed its stack +fatal runtime error: stack overflow, aborting +error: Recipe `trainbot` was terminated on line 27 by signal 6 + +--- + +num_episodes: 40, +// memory_size: 8192, // must be set in dqn_model.rs with the MEMORY_SIZE constant +// max_steps: 1500, // must be set in environment.rs with the MAX_STEPS constant +dense_size: 256, // neural network complexity +eps_start: 0.9, // epsilon initial value (0.9 => more exploration) +eps_end: 0.05, +eps_decay: 1000.0, + +> Entraînement +> {"episode": 0, "reward": -2399.9993, "steps count": 1500, "duration": 31} +> {"episode": 1, "reward": -2061.6736, "steps count": 1500, "duration": 81} +> {"episode": 2, "reward": -48.9010, "steps count": 1500, "duration": 145} +> {"episode": 3, "reward": 3.8000, "steps count": 1500, "duration": 215} +> {"episode": 4, "reward": -6.3999, "steps count": 1500, "duration": 302} +> {"episode": 5, "reward": 20.8004, "steps count": 1500, "duration": 374} +> {"episode": 6, "reward": 49.6992, "steps count": 1500, "duration": 469} +> {"episode": 7, "reward": 29.3002, "steps count": 1500, "duration": 597} +> {"episode": 8, "reward": 34.3999, "steps count": 1500, "duration": 710} +> {"episode": 9, "reward": 115.3003, "steps count": 966, "duration": 515} +> {"episode": 10, "reward": 25.9004, "steps count": 1500, "duration": 852} +> {"episode": 11, "reward": -122.0007, "steps count": 1500, "duration": 1017} +> {"episode": 12, "reward": -274.9966, "steps count": 1500, "duration": 1073} +> {"episode": 13, "reward": 54.8994, "steps count": 651, "duration": 518} +> {"episode": 14, "reward": -439.8978, "steps count": 1500, "duration": 1244} +> {"episode": 15, "reward": -506.1997, "steps count": 1500, "duration": 1676} +> {"episode": 16, "reward": -829.5031, "steps count": 1500, "duration": 1855} +> {"episode": 17, "reward": -545.2961, "steps count": 1500, "duration": 1892} +> {"episode": 18, "reward": -795.2026, "steps count": 1500, "duration": 2008} +> {"episode": 19, "reward": -637.1031, "steps count": 1500, "duration": 2124} +> {"episode": 20, "reward": -989.6997, "steps count": 1500, "duration": 2241} + +thread 'main' has overflowed its stack +fatal runtime error: stack overflow, aborting +error: Recipe `trainbot` was terminated on line 27 by signal 6 + +--- + +num_episodes: 40, +// memory_size: 8192, // must be set in dqn_model.rs with the MEMORY_SIZE constant +// max_steps: 1000, // must be set in environment.rs with the MAX_STEPS constant +dense_size: 256, // neural network complexity +eps_start: 0.9, // epsilon initial value (0.9 => more exploration) +eps_end: 0.05, +eps_decay: 10000.0, + +> Entraînement +> {"episode": 0, "reward": -1598.8848, "steps count": 1000, "duration": 16} +> {"episode": 1, "reward": -1531.9866, "steps count": 1000, "duration": 34} +> {"episode": 2, "reward": -515.6000, "steps count": 530, "duration": 25} +> {"episode": 3, "reward": -396.1008, "steps count": 441, "duration": 27} +> {"episode": 4, "reward": -540.6996, "steps count": 605, "duration": 43} +> {"episode": 5, "reward": -976.0975, "steps count": 1000, "duration": 89} +> {"episode": 6, "reward": -1014.2944, "steps count": 1000, "duration": 117} +> {"episode": 7, "reward": -806.7012, "steps count": 1000, "duration": 140} +> {"episode": 8, "reward": -1276.6891, "steps count": 1000, "duration": 166} +> {"episode": 9, "reward": -1554.3855, "steps count": 1000, "duration": 197} +> {"episode": 10, "reward": -1178.3925, "steps count": 1000, "duration": 219} +> {"episode": 11, "reward": -1457.4869, "steps count": 1000, "duration": 258} +> {"episode": 12, "reward": -1475.8882, "steps count": 1000, "duration": 291} + +--- + +num_episodes: 40, +// memory_size: 8192, // must be set in dqn_model.rs with the MEMORY_SIZE constant +// max_steps: 1000, // must be set in environment.rs with the MAX_STEPS constant +dense_size: 256, // neural network complexity +eps_start: 0.9, // epsilon initial value (0.9 => more exploration) +eps_end: 0.05, +eps_decay: 3000.0, + +> Entraînement +> {"episode": 0, "reward": -1598.8848, "steps count": 1000, "duration": 15} +> {"episode": 1, "reward": -1599.9847, "steps count": 1000, "duration": 33} +> {"episode": 2, "reward": -751.7018, "steps count": 1000, "duration": 57} +> {"episode": 3, "reward": -402.8979, "steps count": 1000, "duration": 81} +> {"episode": 4, "reward": -289.2985, "steps count": 1000, "duration": 108} +> {"episode": 5, "reward": -231.4988, "steps count": 1000, "duration": 140} +> {"episode": 6, "reward": -138.0006, "steps count": 1000, "duration": 165} +> {"episode": 7, "reward": -145.0998, "steps count": 1000, "duration": 200} +> {"episode": 8, "reward": -60.4005, "steps count": 1000, "duration": 236} +> {"episode": 9, "reward": -35.7999, "steps count": 1000, "duration": 276} +> {"episode": 10, "reward": -42.2002, "steps count": 1000, "duration": 313} +> {"episode": 11, "reward": 69.0002, "steps count": 874, "duration": 300} +> {"episode": 12, "reward": 93.2000, "steps count": 421, "duration": 153} +> {"episode": 13, "reward": -324.9010, "steps count": 866, "duration": 364} +> {"episode": 14, "reward": -1331.3883, "steps count": 1000, "duration": 478} +> {"episode": 15, "reward": -1544.5859, "steps count": 1000, "duration": 514} +> {"episode": 16, "reward": -1599.9847, "steps count": 1000, "duration": 552} + +--- + +Nouveaux points... + +num_episodes: 40, +// memory_size: 8192, // must be set in dqn_model.rs with the MEMORY_SIZE constant +// max_steps: 1000, // must be set in environment.rs with the MAX_STEPS constant +dense_size: 256, // neural network complexity +eps_start: 0.9, // epsilon initial value (0.9 => more exploration) +eps_end: 0.05, +eps_decay: 3000.0, + +> Entraînement +> {"episode": 0, "reward": -1798.1161, "steps count": 1000, "duration": 15} +> {"episode": 1, "reward": -1800.0162, "steps count": 1000, "duration": 34} +> {"episode": 2, "reward": -1718.6151, "steps count": 1000, "duration": 57} +> {"episode": 3, "reward": -1369.5055, "steps count": 1000, "duration": 82} +> {"episode": 4, "reward": -321.5974, "steps count": 1000, "duration": 115} +> {"episode": 5, "reward": -213.2988, "steps count": 1000, "duration": 148} +> {"episode": 6, "reward": -175.4995, "steps count": 1000, "duration": 172} +> {"episode": 7, "reward": -126.1011, "steps count": 1000, "duration": 203} +> {"episode": 8, "reward": -105.1011, "steps count": 1000, "duration": 242} +> {"episode": 9, "reward": -46.3007, "steps count": 1000, "duration": 281} +> {"episode": 10, "reward": -57.7006, "steps count": 1000, "duration": 323} +> {"episode": 11, "reward": -15.7997, "steps count": 1000, "duration": 354} +> {"episode": 12, "reward": -38.6999, "steps count": 1000, "duration": 414} +> {"episode": 13, "reward": 10.7002, "steps count": 1000, "duration": 513} +> {"episode": 14, "reward": -10.1999, "steps count": 1000, "duration": 585} +> {"episode": 15, "reward": -8.3000, "steps count": 1000, "duration": 644} +> {"episode": 16, "reward": -463.4984, "steps count": 973, "duration": 588} +> {"episode": 17, "reward": -148.8951, "steps count": 1000, "duration": 646} +> {"episode": 18, "reward": 3.0999, "steps count": 1000, "duration": 676} +> {"episode": 19, "reward": -12.0999, "steps count": 1000, "duration": 753} +> {"episode": 20, "reward": 6.9000, "steps count": 1000, "duration": 801} +> {"episode": 21, "reward": 14.5001, "steps count": 1000, "duration": 850} +> {"episode": 22, "reward": -19.6999, "steps count": 1000, "duration": 937} +> {"episode": 23, "reward": 83.0000, "steps count": 456, "duration": 532} +> {"episode": 24, "reward": -13.9998, "steps count": 1000, "duration": 1236} +> {"episode": 25, "reward": 25.9003, "steps count": 1000, "duration": 1264} +> {"episode": 26, "reward": 1.2002, "steps count": 1000, "duration": 1349} +> {"episode": 27, "reward": 3.1000, "steps count": 1000, "duration": 1364} +> {"episode": 28, "reward": -6.4000, "steps count": 1000, "duration": 1392} +> {"episode": 29, "reward": -4.4998, "steps count": 1000, "duration": 1444} +> {"episode": 30, "reward": 3.1000, "steps count": 1000, "duration": 1611} + +thread 'main' has overflowed its stack +fatal runtime error: stack overflow, aborting + +--- + +num_episodes: 40, +// memory_size: 8192, // must be set in dqn_model.rs with the MEMORY_SIZE constant +// max_steps: 700, // must be set in environment.rs with the MAX_STEPS constant +dense_size: 256, // neural network complexity +eps_start: 0.9, // epsilon initial value (0.9 => more exploration) +eps_end: 0.05, +eps_decay: 3000.0, + +{"episode": 0, "reward": -1256.1014, "steps count": 700, "duration": 9} +{"episode": 1, "reward": -1256.1013, "steps count": 700, "duration": 20} +{"episode": 2, "reward": -1256.1014, "steps count": 700, "duration": 31} +{"episode": 3, "reward": -1258.7015, "steps count": 700, "duration": 44} +{"episode": 4, "reward": -1206.8009, "steps count": 700, "duration": 56} +{"episode": 5, "reward": -473.2974, "steps count": 700, "duration": 68} +{"episode": 6, "reward": -285.2984, "steps count": 700, "duration": 82} +{"episode": 7, "reward": -332.6987, "steps count": 700, "duration": 103} +{"episode": 8, "reward": -359.2984, "steps count": 700, "duration": 114} +{"episode": 9, "reward": -118.7008, "steps count": 700, "duration": 125} +{"episode": 10, "reward": -83.9004, "steps count": 700, "duration": 144} +{"episode": 11, "reward": -68.7006, "steps count": 700, "duration": 165} +{"episode": 12, "reward": -49.7002, "steps count": 700, "duration": 180} +{"episode": 13, "reward": -68.7002, "steps count": 700, "duration": 204} +{"episode": 14, "reward": -38.3001, "steps count": 700, "duration": 223} +{"episode": 15, "reward": -19.2999, "steps count": 700, "duration": 240} +{"episode": 16, "reward": -19.1998, "steps count": 700, "duration": 254} +{"episode": 17, "reward": -21.1999, "steps count": 700, "duration": 250} +{"episode": 18, "reward": -26.8998, "steps count": 700, "duration": 280} +{"episode": 19, "reward": -11.6999, "steps count": 700, "duration": 301} +{"episode": 20, "reward": -13.5998, "steps count": 700, "duration": 317} +{"episode": 21, "reward": 5.4000, "steps count": 700, "duration": 334} +{"episode": 22, "reward": 3.5000, "steps count": 700, "duration": 353} +{"episode": 23, "reward": 13.0000, "steps count": 700, "duration": 374} +{"episode": 24, "reward": 7.3001, "steps count": 700, "duration": 391} +{"episode": 25, "reward": -4.1000, "steps count": 700, "duration": 408} +{"episode": 26, "reward": -17.3998, "steps count": 700, "duration": 437} +{"episode": 27, "reward": 11.1001, "steps count": 700, "duration": 480} +{"episode": 28, "reward": -4.1000, "steps count": 700, "duration": 505} +{"episode": 29, "reward": -13.5999, "steps count": 700, "duration": 522} +{"episode": 30, "reward": -0.3000, "steps count": 700, "duration": 540} +{"episode": 31, "reward": -15.4998, "steps count": 700, "duration": 572} +{"episode": 32, "reward": 14.9001, "steps count": 700, "duration": 630} +{"episode": 33, "reward": -4.1000, "steps count": 700, "duration": 729} +{"episode": 34, "reward": 5.4000, "steps count": 700, "duration": 777} +{"episode": 35, "reward": 7.3000, "steps count": 700, "duration": 748} +{"episode": 36, "reward": 9.2001, "steps count": 700, "duration": 767} +{"episode": 37, "reward": 13.0001, "steps count": 700, "duration": 791} +{"episode": 38, "reward": -13.5999, "steps count": 700, "duration": 813} +{"episode": 39, "reward": 26.3002, "steps count": 700, "duration": 838} + +> Sauvegarde du modèle de validation +> Modèle de validation sauvegardé : models/burn_dqn_50_model.mpk +> Chargement du modèle pour test +> Chargement du modèle depuis : models/burn_dqn_50_model.mpk +> Test avec le modèle chargé +> Episode terminé. Récompense totale: 70.00, Étapes: 700 diff --git a/doc/refs/tutorial_academieDesJeux.md b/doc/refs/tutorial_academieDesJeux.md index d34fadb..e00fe90 100644 --- a/doc/refs/tutorial_academieDesJeux.md +++ b/doc/refs/tutorial_academieDesJeux.md @@ -37,7 +37,7 @@ Si on peut remplir le coin de l'adversaire avec deux dames (ce qui est interdit) Si on a le choix entre remplir son coin directement ou par puissance, on doit obligatoirement le faire directement. -Tant que l'adversaire à la possibilité de remplir un cadran, on ne peut pas jouer dans ce cadran. +Tant que l'adversaire à la possibilité de remplir un des deux cadrans de son côté (son jeu "ordinaire", par opposition au jeu "de retour"), on ne peut pas jouer dans ce cadran. Quand on joue dans ses propres deux cadrans, on dit qu'on joue le jeu ordinaire Quand on joue dans les deux cadrans de l'adversaire, on dit qu'on joue le jeu de retour @@ -64,6 +64,7 @@ Si on doit passer par une case occupée par deux dames adverses ou plus pour att Remarques - on peut "passer" sur une dame adverse (donc battue) pour battre une seconde dame adverse (avec la somme des deux dés). +- comme pour les déplacements, il est possible de passer par le coin de repos vide de l'adversaire pour battre à vrai une dame en "tout d'une" (c'est s'arrêter sur le coin de repos qui est interdit) - même s'il ne reste que deux dames dans son coin de repos (et qu'en théorie elle ne peuvent en sortir qu'en même temps), elles peuvent tout de même battre une dame adverse (à vrai et à faux). En revanche elles ne peuvent pas participer au battage du coin adverse (cf. prochain paragraphe). Autre jan de récompense : diff --git a/doc/store.puml b/doc/store.puml new file mode 100644 index 0000000..dd90df5 --- /dev/null +++ b/doc/store.puml @@ -0,0 +1,172 @@ +@startuml + +class "CheckerMove" { + - from: Field + - to: Field + + to_display_string() + + new(from: Field, to: Field) + + mirror() + + chain(cmove: Self) + + get_from() + + get_to() + + is_exit() + + doable_with_dice(dice: usize) +} + +class "Board" { + - positions: [i8;24] + + new() + + mirror() + + set_positions(positions: [ i8 ; 24 ]) + + count_checkers(color: Color, from: Field, to: Field) + + to_vec() + + to_gnupg_pos_id() + + to_display_grid(col_size: usize) + + set(color: & Color, field: Field, amount: i8) + + blocked(color: & Color, field: Field) + + passage_blocked(color: & Color, field: Field) + + get_field_checkers(field: Field) + + get_checkers_color(field: Field) + + is_field_in_small_jan(field: Field) + + get_color_fields(color: Color) + + get_color_corner(color: & Color) + + get_possible_moves(color: Color, dice: u8, with_excedants: bool, check_rest_corner_exit: bool, forbid_exits: bool) + + passage_possible(color: & Color, cmove: & CheckerMove) + + move_possible(color: & Color, cmove: & CheckerMove) + + any_quarter_filled(color: Color) + + is_quarter_filled(color: Color, field: Field) + + get_quarter_filling_candidate(color: Color) + + is_quarter_fillable(color: Color, field: Field) + - get_quarter_fields(field: Field) + + move_checker(color: & Color, cmove: CheckerMove) + + remove_checker(color: & Color, field: Field) + + add_checker(color: & Color, field: Field) +} + +class "MoveRules" { + + board: Board + + dice: Dice + + new(color: & Color, board: & Board, dice: Dice) + + set_board(color: & Color, board: & Board) + - get_board_from_color(color: & Color, board: & Board) + + moves_follow_rules(moves: & ( CheckerMove , CheckerMove )) + - moves_possible(moves: & ( CheckerMove , CheckerMove )) + - moves_follows_dices(moves: & ( CheckerMove , CheckerMove )) + - get_move_compatible_dices(cmove: & CheckerMove) + + moves_allowed(moves: & ( CheckerMove , CheckerMove )) + - check_opponent_can_fill_quarter_rule(moves: & ( CheckerMove , CheckerMove )) + - check_must_fill_quarter_rule(moves: & ( CheckerMove , CheckerMove )) + - check_corner_rules(moves: & ( CheckerMove , CheckerMove )) + - has_checkers_outside_last_quarter() + - check_exit_rules(moves: & ( CheckerMove , CheckerMove )) + + get_possible_moves_sequences(with_excedents: bool, ignored_rules: Vec < TricTracRule >) + + get_scoring_quarter_filling_moves_sequences() + - get_sequence_origin_from_destination(sequence: ( CheckerMove , CheckerMove ), destination: Field) + + get_quarter_filling_moves_sequences() + - get_possible_moves_sequences_by_dices(dice1: u8, dice2: u8, with_excedents: bool, ignore_empty: bool, ignored_rules: Vec < TricTracRule >) + - _get_direct_exit_moves(state: & GameState) + - is_move_by_puissance(moves: & ( CheckerMove , CheckerMove )) + - can_take_corner_by_effect() +} + +class "DiceRoller" { + - rng: StdRng + + new(opt_seed: Option < u64 >) + + roll() +} + +class "Dice" { + + values: (u8,u8) + + to_bits_string() + + to_display_string() + + is_double() +} + +class "GameState" { + + stage: Stage + + turn_stage: TurnStage + + board: Board + + active_player_id: PlayerId + + players: HashMap + + history: Vec + + dice: Dice + + dice_points: (u8,u8) + + dice_moves: (CheckerMove,CheckerMove) + + dice_jans: PossibleJans + - roll_first: bool + + schools_enabled: bool + + new(schools_enabled: bool) + - set_schools_enabled(schools_enabled: bool) + - get_active_player() + - get_opponent_id() + + to_vec_float() + + to_vec() + + to_string_id() + + who_plays() + + get_white_player() + + get_black_player() + + player_id_by_color(color: Color) + + player_id(player: & Player) + + player_color_by_id(player_id: & PlayerId) + + validate(event: & GameEvent) + + init_player(player_name: & str) + - add_player(player_id: PlayerId, player: Player) + + switch_active_player() + + consume(valid_event: & GameEvent) + - new_pick_up() + - get_rollresult_jans(dice: & Dice) + + determine_winner() + - inc_roll_count(player_id: PlayerId) + - mark_points(player_id: PlayerId, points: u8) +} + +class "Player" { + + name: String + + color: Color + + points: u8 + + holes: u8 + + can_bredouille: bool + + can_big_bredouille: bool + + dice_roll_count: u8 + + new(name: String, color: Color) + + to_bits_string() + + to_vec() +} + +class "PointsRules" { + + board: Board + + dice: Dice + + move_rules: MoveRules + + new(color: & Color, board: & Board, dice: Dice) + + set_dice(dice: Dice) + + update_positions(positions: [ i8 ; 24 ]) + - get_jans(board_ini: & Board, dice_rolls_count: u8) + + get_jans_points(jans: HashMap < Jan , Vec < ( CheckerMove , CheckerMove ) > >) + + get_points(dice_rolls_count: u8) + + get_result_jans(dice_rolls_count: u8) +} + + + + +"MoveRules" <-- "Board" +"MoveRules" <-- "Dice" + + + + + + +"GameState" <-- "Board" +"HashMap" <-- "Player" +"GameState" <-- "HashMap" +"GameState" <-- "Dice" + + + + +"PointsRules" <-- "Board" +"PointsRules" <-- "Dice" +"PointsRules" <-- "MoveRules" + +@enduml diff --git a/doc/traité.md b/doc/traité.md index 19fc711..e2241ad 100644 --- a/doc/traité.md +++ b/doc/traité.md @@ -1,3 +1,5 @@ +# traité + En 12 chapitres (trous) de 12 sous-chapitres (points / niveaux de compréhension) ? Célébration -> s'inspirer du _petit traité invitant à la découverte de l'art subtil du go_ @@ -30,3 +32,9 @@ L'encyclopédie - le dictionnaire du trictrac - fabriquer un boîtier/plateau de jeu - jouer en ligne + +## rêveries + +Trictrac : un domaine grand et complexe, un univers dans lequel on peut s'absorber. Un jeu geek parfait. Qui a la noblesse d'avoir été populaire, qui a la noblesse de règles nécessitant apprentissage et presque companionage. +Pourquoi s'investir dans ce genre d'activité ? Toucher un absolu. Sauver de la mort une pépite du passé. Entrer dans le monde des morts comme Orphée ou Ulysse ? +Et maîtriser un vocabulaire, des gestes, des règles affinées au fil des siècles. diff --git a/doc/vocabulary.md b/doc/vocabulary.md index d14bcfe..a738230 100644 --- a/doc/vocabulary.md +++ b/doc/vocabulary.md @@ -2,3 +2,4 @@ Dames : checkers / men cases : points +cadrant : quarter diff --git a/doc/workflow.md b/doc/workflow.md new file mode 100644 index 0000000..2139332 --- /dev/null +++ b/doc/workflow.md @@ -0,0 +1,25 @@ +# Workflow + +@startuml + +state c <> +state haswon <> +state MarkPoints #lightblue +state MarkAdvPoints #lightblue +note right of MarkPoints : automatic 'Mark' transition\nwhen no school +note right of MarkAdvPoints : automatic 'Mark' transition\nwhen no school + +[*] -> RollDice : BeginGame +RollDice --> RollWaiting : Roll (current player) +RollWaiting --> MarkPoints : RollResult (engine) +MarkPoints --> c : Mark (current player) +c --> HoldHorGoChoice : [new hole] +c --> [*] : [has won] +c --> Move : [not new hole] +HoldHorGoChoice --> RollDice : Go +HoldHorGoChoice --> MarkAdvPoints : Move +Move --> MarkAdvPoints : Move +MarkAdvPoints --> haswon : Mark (adversary) +haswon --> RollDice : [has not won] +haswon --> [*] : [has won] +@enduml diff --git a/justfile b/justfile new file mode 100644 index 0000000..f89bc7c --- /dev/null +++ b/justfile @@ -0,0 +1,41 @@ +#!/usr/bin/env -S just --justfile +# ^ A shebang isn't required, but allows a justfile to be executed +# like a script, with `./justfile test`, for example. + +doc: + cargo doc --no-deps +shell: + devenv shell +runcli: + RUST_LOG=info cargo run --bin=client_cli +runclibots: + cargo run --bin=client_cli -- --bot random,dqnburn:./bot/models/burnrl_dqn_40.mpk + #cargo run --bin=client_cli -- --bot dqn:./bot/models/dqn_model_final.json,dummy + # RUST_LOG=info cargo run --bin=client_cli -- --bot dummy,dqn +match: + cargo build --release --bin=client_cli + LD_LIBRARY_PATH=./target/release ./target/release/client_cli -- --bot dummy,dqn +profile: + echo '1' | sudo tee /proc/sys/kernel/perf_event_paranoid + cargo build --profile profiling + samply record ./target/profiling/client_cli --bot dummy,dummy +pythonlib: + maturin build -m store/Cargo.toml --release + pip install --no-deps --force-reinstall --prefix .devenv/state/venv target/wheels/*.whl +trainsimple: + cargo build --release --bin=train_dqn_simple + LD_LIBRARY_PATH=./target/release ./target/release/train_dqn_simple | tee /tmp/train.out +trainbot algo: + #python ./store/python/trainModel.py + # cargo run --bin=train_dqn # ok + # ./bot/scripts/trainValid.sh + ./bot/scripts/train.sh {{algo}} +plottrainbot algo: + ./bot/scripts/train.sh plot {{algo}} +debugtrainbot: + cargo build --bin=train_dqn_burn + RUST_BACKTRACE=1 LD_LIBRARY_PATH=./target/debug ./target/debug/train_dqn_burn +profiletrainbot: + echo '1' | sudo tee /proc/sys/kernel/perf_event_paranoid + cargo build --profile profiling --bin=train_dqn_burn + LD_LIBRARY_PATH=./target/profiling samply record ./target/profiling/train_dqn_burn diff --git a/server/src/main.rs b/server/src/main.rs index c2b88c4..5bc3234 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,4 +1,3 @@ -use bincode; use log::{info, trace, warn}; use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; use std::thread; @@ -44,7 +43,7 @@ fn main() { .unwrap(); let mut transport = NetcodeServerTransport::new(current_time, server_config, socket).unwrap(); - trace!("❂ TricTrac server listening on {}", SERVER_ADDR); + trace!("❂ TricTrac server listening on {SERVER_ADDR}"); let mut game_state = store::GameState::default(); let mut last_updated = Instant::now(); @@ -81,7 +80,7 @@ fn main() { // Tell all players that a new player has joined server.broadcast_message(0, bincode::serialize(&event).unwrap()); - info!("🎉 Client {} connected.", client_id); + info!("🎉 Client {client_id} connected."); // In TicTacTussle the game can begin once two players has joined if game_state.players.len() == 2 { let event = store::GameEvent::BeginGame { @@ -92,14 +91,17 @@ fn main() { trace!("The game gas begun"); } } - ServerEvent::ClientDisconnected { client_id, reason: _ } => { + ServerEvent::ClientDisconnected { + client_id, + reason: _, + } => { // First consume a disconnect event let event = store::GameEvent::PlayerDisconnected { player_id: client_id, }; game_state.consume(&event); server.broadcast_message(0, bincode::serialize(&event).unwrap()); - info!("Client {} disconnected", client_id); + info!("Client {client_id} disconnected"); // Then end the game, since tic tac toe can't go on with a single player let event = store::GameEvent::EndGame { @@ -122,7 +124,7 @@ fn main() { if let Ok(event) = bincode::deserialize::(&message) { if game_state.validate(&event) { game_state.consume(&event); - trace!("Player {} sent:\n\t{:#?}", client_id, event); + trace!("Player {client_id} sent:\n\t{event:#?}"); server.broadcast_message(0, bincode::serialize(&event).unwrap()); // Determine if a player has won the game @@ -133,7 +135,7 @@ fn main() { server.broadcast_message(0, bincode::serialize(&event).unwrap()); } } else { - warn!("Player {} sent invalid event:\n\t{:#?}", client_id, event); + warn!("Player {client_id} sent invalid event:\n\t{event:#?}"); } } } diff --git a/store/Cargo.toml b/store/Cargo.toml index 4fef1f8..a071dd1 100644 --- a/store/Cargo.toml +++ b/store/Cargo.toml @@ -5,9 +5,16 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "store" +# Only "rlib" is needed for other Rust crates to use this library +crate-type = ["rlib"] + [dependencies] base64 = "0.21.7" +# provides macros for creating log messages to be used by a logger (for example env_logger) log = "0.4.20" +merge = "0.1.0" rand = "0.8.5" -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1.0", features = ["derive"] } transpose = "0.2.2" diff --git a/store/src/board.rs b/store/src/board.rs index aa959f9..d0f3615 100644 --- a/store/src/board.rs +++ b/store/src/board.rs @@ -1,4 +1,4 @@ -use crate::player::{Color, Player}; +use crate::player::Color; use crate::Error; use serde::{Deserialize, Serialize}; use std::cmp; @@ -6,13 +6,16 @@ use std::fmt; /// field (aka 'point') position on the board (from 0 to 24, 0 being 'outside') pub type Field = usize; +pub type FieldWithCount = (Field, i8); -#[derive(Debug, Copy, Clone, Serialize, PartialEq, Deserialize)] +#[derive(Debug, Copy, Clone, Serialize, PartialEq, Eq, Deserialize)] pub struct CheckerMove { from: Field, to: Field, } +pub const EMPTY_MOVE: CheckerMove = CheckerMove { from: 0, to: 0 }; + fn transpose(matrix: Vec>) -> Vec> { let num_cols = matrix.first().unwrap().len(); let mut row_iters: Vec<_> = matrix.into_iter().map(Vec::into_iter).collect(); @@ -26,20 +29,41 @@ fn transpose(matrix: Vec>) -> Vec> { out } +impl Default for CheckerMove { + fn default() -> Self { + EMPTY_MOVE + } +} + impl CheckerMove { + pub fn to_display_string(self) -> String { + format!("{self:?} ") + } + pub fn new(from: Field, to: Field) -> Result { + // println!("from {} to {}", from, to); // check if the field is on the board // we allow 0 for 'to', which represents the exit of a checker - if from < 1 || 24 < from || 24 < to { + // and (0, 0) which represent the absence of a move (when there is only one checker left on the + // board) + if ((from, to) != (0, 0)) && (!(1..25).contains(&from) || 24 < to) { return Err(Error::FieldInvalid); } // check that the destination is after the origin field - if to < from && to != 0 { - return Err(Error::MoveInvalid); - } + // --> not applicable for black moves + // if to < from && to != 0 { + // return Err(Error::MoveInvalid); + // } Ok(Self { from, to }) } + /// Get the mirrord CheckerMove (ie change colors) + pub fn mirror(&self) -> Self { + let from = if self.from == 0 { 0 } else { 25 - self.from }; + let to = if self.to == 0 { 0 } else { 25 - self.to }; + Self { from, to } + } + // Construct the move resulting of two successive moves pub fn chain(self, cmove: Self) -> Result { if self.to != cmove.from { @@ -58,10 +82,19 @@ impl CheckerMove { pub fn get_to(&self) -> Field { self.to } + + pub fn is_exit(&self) -> bool { + self.to == 0 && self != &EMPTY_MOVE + } + + pub fn doable_with_dice(&self, dice: usize) -> bool { + (self.to == 0 && 25 - self.from <= dice) + || (self.from < self.to && self.to - self.from == dice) + } } /// Represents the Tric Trac board -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Board { positions: [i8; 24], } @@ -81,7 +114,7 @@ impl fmt::Display for Board { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut s = String::new(); s.push_str(&format!("{:?}", self.positions)); - write!(f, "{}", s) + write!(f, "{s}") } } @@ -91,12 +124,86 @@ impl Board { Board::default() } + /// Get the mirrord board (ie change colors) + pub fn mirror(&self) -> Self { + let mut positions = self.positions.map(|c| 0 - c); + positions.reverse(); + Board { positions } + } + + /// Globally set pieces on board ( for tests ) + pub fn set_positions(&mut self, color: &Color, positions: [i8; 24]) { + let mut new_positions = positions; + if color == &Color::Black { + new_positions = new_positions.map(|c| 0 - c); + new_positions.reverse(); + } + self.positions = new_positions; + } + + pub fn count_checkers(&self, color: Color, from: Field, to: Field) -> u8 { + if to == 0 || from == 0 { + return 0; + } + self.positions[(from - 1)..to] + .iter() + .filter(|count| { + if color == Color::White { + **count > 0 + } else { + **count < 0 + } + }) + .sum::() + .unsigned_abs() + } + + // get the number of the last checker in a field + pub fn get_field_checker(&self, color: &Color, field: Field) -> u8 { + assert_eq!(color, &Color::White); // sinon ajouter la gestion des noirs avec mirror + let mut total_count: u8 = 0; + for (i, checker_count) in self.positions.iter().enumerate() { + // count white checkers (checker_count > 0) + if *checker_count > 0 { + total_count += *checker_count as u8; + if field == i + 1 { + return total_count; + } + } + } + 0 + } + + // get the field of the nth checker + pub fn get_checker_field(&self, color: &Color, checker_pos: u8) -> Option { + assert_eq!(color, &Color::White); // sinon ajouter la gestion des noirs avec mirror + if checker_pos == 0 { + return None; + } + let mut total_count: u8 = 0; + for (i, checker_count) in self.positions.iter().enumerate() { + // count white checkers (checker_count > 0) + if *checker_count > 0 { + total_count += *checker_count as u8; + } + // return the current field if it contains the checker + if checker_pos <= total_count { + return Some(i + 1); + } + } + None + } + + pub fn to_vec(&self) -> Vec { + self.positions.to_vec() + } + // maybe todo : operate on bits (cf. https://github.com/bungogood/bkgm/blob/a2fb3f395243bcb0bc9f146df73413f73f5ea1e0/src/position.rs#L217) pub fn to_gnupg_pos_id(&self) -> String { // Pieces placement -> 77bits (24 + 23 + 30 max) // inspired by https://www.gnu.org/software/gnubg/manual/html_node/A-technical-description-of-the-Position-ID.html // - white positions - let white_board = self.positions.clone(); + let white_board = self.positions; let mut pos_bits = white_board.iter().fold(vec![], |acc, nb| { let mut new_acc = acc.clone(); if *nb > 0 { @@ -108,7 +215,7 @@ impl Board { }); // - black positions - let mut black_board = self.positions.clone(); + let mut black_board = self.positions; black_board.reverse(); let mut pos_black_bits = black_board.iter().fold(vec![], |acc, nb| { let mut new_acc = acc.clone(); @@ -164,7 +271,7 @@ impl Board { .map(|cells| { cells .into_iter() - .map(|cell| format!("{:>5}", cell)) + .map(|cell| format!("{cell:>5}")) .collect::>() .join("") }) @@ -175,29 +282,28 @@ impl Board { .map(|cells| { cells .into_iter() - .map(|cell| format!("{:>5}", cell)) + .map(|cell| format!("{cell:>5}")) .collect::>() .join("") }) .collect(); let mut output = " - 13 14 15 16 17 18 19 20 21 22 23 24 + 13 14 15 16 17 18 19 20 21 22 23 24 ----------------------------------------------------------------\n" .to_owned(); for mut line in upper { // add middle bar - line.replace_range(30..30, "| |"); + line.replace_range(31..31, "| |"); output = output + " |" + &line + " |\n"; } - output = output + " |----------------------------- | | ------------------------------|\n"; + output += " |------------------------------ | | -----------------------------|\n"; for mut line in lower { // add middle bar - line.replace_range(30..30, "| |"); + line.replace_range(31..31, "| |"); output = output + " |" + &line + " |\n"; } - output = output - + " ---------------------------------------------------------------- + output += " ---------------------------------------------------------------- 12 11 10 9 8 7 6 5 4 3 2 1 \n"; output } @@ -249,6 +355,13 @@ impl Board { /// Check if a field is blocked for a player pub fn blocked(&self, color: &Color, field: Field) -> Result { + // the square is blocked on the opponent rest corner + // let opp_corner_field = if color == &Color::White { 13 } else { 12 }; + self.passage_blocked(color, field) + // .map(|blocked| blocked || opp_corner_field == field) + } + + pub fn passage_blocked(&self, color: &Color, field: Field) -> Result { if 24 < field { return Err(Error::FieldInvalid); } @@ -258,42 +371,55 @@ impl Board { return Ok(false); } - // the square is blocked on the opponent rest corner or if there are opponent's men on the square - match color { - Color::White => { - if field == 13 || self.positions[field - 1] < 0 { - Ok(true) - } else { - Ok(false) - } - } - Color::Black => { - if field == 12 || self.positions[23 - field] > 1 { - Ok(true) - } else { - Ok(false) - } - } - } + // the square is blocked if there are opponent's men on the square + let blocked = if color == &Color::White { + self.positions[field - 1] < 0 + } else { + self.positions[field - 1] > 0 + }; + Ok(blocked) } pub fn get_field_checkers(&self, field: Field) -> Result<(u8, Option<&Color>), Error> { - if field < 1 || field > 24 { + if !(1..25).contains(&field) { return Err(Error::FieldInvalid); } let checkers_count = self.positions[field - 1]; - let color = if checkers_count < 0 { - Some(&Color::Black) - } else if checkers_count > 0 { - Some(&Color::White) - } else { - None + let color = match checkers_count.cmp(&0) { + cmp::Ordering::Less => Some(&Color::Black), + cmp::Ordering::Greater => Some(&Color::White), + cmp::Ordering::Equal => None, }; - Ok((checkers_count.abs() as u8, color)) + Ok((checkers_count.unsigned_abs(), color)) } pub fn get_checkers_color(&self, field: Field) -> Result, Error> { - self.get_field_checkers(field).map(|(count, color)| color) + self.get_field_checkers(field).map(|(_ount, color)| color) + } + + pub fn is_field_in_small_jan(field: Field) -> bool { + !(7..=18).contains(&field) + } + + /// returns the list of Fields containing Checkers of the Color + pub fn get_color_fields(&self, color: Color) -> Vec<(usize, i8)> { + match color { + Color::White => self + .positions + .iter() + .enumerate() + .filter(|&(_, count)| *count > 0) + .map(|(i, count)| (i + 1, *count)) + .collect(), + Color::Black => self + .positions + .iter() + .enumerate() + .filter(|&(_, count)| *count < 0) + .rev() + .map(|(i, count)| (i + 1, (0 - count))) + .collect(), + } } // Get the corner field for the color @@ -305,11 +431,166 @@ impl Board { } } + pub fn get_possible_moves( + &self, + color: Color, + dice: u8, + with_excedants: bool, + check_rest_corner_exit: bool, + forbid_exits: bool, + ) -> Vec { + let mut moves = Vec::new(); + + let get_dest = |from| { + if color == Color::White { + if from + dice as i32 == 25 { + 0 + } else { + from + dice as i32 + } + } else { + from - dice as i32 + } + }; + + for (field, count) in self.get_color_fields(color) { + // check rest corner exit + if field == self.get_color_corner(&color) && count == 2 && check_rest_corner_exit { + continue; + } + let mut dest = get_dest(field as i32); + if dest == 0 && forbid_exits { + continue; + } + if !(0..25).contains(&dest) { + if with_excedants && !forbid_exits { + dest = 0; + } else { + continue; + } + } + if let Ok(cmove) = CheckerMove::new(field, dest.unsigned_abs() as usize) { + if let Ok(false) = self.blocked(&color, dest.unsigned_abs() as usize) { + moves.push(cmove); + } + } + } + moves + } + + pub fn passage_possible(&self, color: &Color, cmove: &CheckerMove) -> bool { + !self.passage_blocked(color, cmove.to).unwrap_or(true) + } + pub fn move_possible(&self, color: &Color, cmove: &CheckerMove) -> bool { let blocked = self.blocked(color, cmove.to).unwrap_or(true); // Check if there is a player's checker on the 'from' square let has_checker = self.get_checkers_color(cmove.from).unwrap_or(None) == Some(color); - has_checker && !blocked + (has_checker && !blocked) || cmove == &EMPTY_MOVE + } + + /// Return if there is a quarter filled by the color + pub fn any_quarter_filled(&self, color: Color) -> bool { + [1, 7, 13, 19] + .iter() + .any(|field| self.is_quarter_filled(color, *field)) + } + + /// Return if the quarter containing `field` is filled by the `color` + pub fn is_quarter_filled(&self, color: Color, field: Field) -> bool { + let fields = self.get_quarter_fields(field); + !fields.iter().any(|field| { + if color == Color::White { + self.positions[field - 1] < 2 + } else { + self.positions[field - 1] > -2 + } + }) + } + + pub fn get_quarter_filling_candidate(&self, color: Color) -> Vec { + let mut missing = vec![]; + // first quarter + for quarter in [1..7, 7..13, 13..19, 19..25] { + missing = vec![]; + for field in quarter { + let field_count = if color == Color::Black { + 0 - self.positions[field - 1] + } else { + self.positions[field - 1] + }; + if field_count < 0 { + // opponent checker found : this quarter cannot be filled + missing = vec![]; + continue; + } + if field_count == 0 { + missing.push(field); + missing.push(field); + } else if field_count == 1 { + missing.push(field); + } + } + if missing.len() < 3 { + // fillable quarter found (no more than two missing checkers) + if let Some(field) = missing.first() { + // We check that there are sufficient checkers left to fill the quarter + if !self.is_quarter_fillable(color, *field) { + missing = vec![]; + } + } + // there will be no other fillable quarter + break; + } + } + missing + } + + /// Returns whether the `color` player can still fill the quarter containing the `field` + /// * `color` - color of the player + /// * `field` - field belonging to the quarter + pub fn is_quarter_fillable(&self, color: Color, field: Field) -> bool { + let fields = self.get_quarter_fields(field); + + // opponent rest corner + if color == Color::White && fields.contains(&13) + || color == Color::Black && fields.contains(&12) + { + return false; + } + + // is there a sufficient number of checkers on or before each fields ? + for field in fields { + // Number of checkers needed before this field (included) : + // 2 checkers by field, from the begining of the quarter + let mut field_pos = field % 6; + if field_pos == 0 { + field_pos = 6; + } + if color == Color::Black { + field_pos = 7 - field_pos; + } + let needed = 2 * field_pos; + + let (from, to) = if color == Color::White { + (1, field) + } else { + (field, 24) + }; + if self.count_checkers(color, from, to) < needed as u8 { + return false; + } + } + true + } + + /// Returns the 6 fields of the quarter containing the `field` + fn get_quarter_fields(&self, field: Field) -> [Field; 6] { + if field == 0 { + return [0; 6]; + } + let min = 1 + ((field - 1) / 6) * 6; + core::array::from_fn(|i| i + min) } pub fn move_checker(&mut self, color: &Color, cmove: CheckerMove) -> Result<(), Error> { @@ -319,38 +600,47 @@ impl Board { } pub fn remove_checker(&mut self, color: &Color, field: Field) -> Result<(), Error> { + if field == 0 { + return Ok(()); + } let checker_color = self.get_checkers_color(field)?; if Some(color) != checker_color { + println!("field invalid : {color:?}, {field:?}, {self:?}"); return Err(Error::FieldInvalid); } - self.positions[field - 1] -= 1; + let unit = match color { + Color::White => 1, + Color::Black => -1, + }; + self.positions[field - 1] -= unit; Ok(()) } pub fn add_checker(&mut self, color: &Color, field: Field) -> Result<(), Error> { - let checker_color = self.get_checkers_color(field)?; - // error if the case contains the other color - if None != checker_color && Some(color) != checker_color { - return Err(Error::FieldInvalid); + // Sortie + if field == 0 { + return Ok(()); } - self.positions[field - 1] += 1; + + // let checker_color = self.get_checkers_color(field)?; + let (count, checker_color) = self.get_field_checkers(field)?; + // error if the case contains the other color + if checker_color.is_some() && Some(color) != checker_color { + return if count > 1 { + Err(Error::FieldBlocked) + } else { + Err(Error::FieldBlockedByOne) + }; + } + let unit = match color { + Color::White => 1, + Color::Black => -1, + }; + self.positions[field - 1] += unit; Ok(()) } } -/// Trait to move checkers -pub trait Move { - /// Move a checker - fn move_checker(&mut self, player: &Player, dice: u8, from: Field) -> Result<&mut Self, Error> - where - Self: Sized; - - /// Move permitted - fn move_permitted(&mut self, player: &Player, dice: u8) -> Result<&mut Self, Error> - where - Self: Sized; -} - // Unit Tests #[cfg(test)] mod tests { @@ -364,7 +654,7 @@ mod tests { #[test] fn blocked_outofrange() -> Result<(), Error> { let board = Board::new(); - assert!(!board.blocked(&Color::White, 0).is_err()); + assert!(board.blocked(&Color::White, 0).is_ok()); assert!(board.blocked(&Color::White, 28).is_err()); Ok(()) } @@ -404,7 +694,95 @@ mod tests { #[test] fn set_wrong_amount1() { let mut board = Board::new(); - let player = Player::new("".into(), Color::White); assert!(board.set(&Color::White, 23, -3).is_err()); } + + #[test] + fn move_possible() { + let board = Board::new(); + assert!(board.move_possible(&Color::White, &EMPTY_MOVE)); + } + + #[test] + fn get_color_fields() { + let board = Board::new(); + assert_eq!(board.get_color_fields(Color::White), vec![(1, 15)]); + assert_eq!(board.get_color_fields(Color::Black), vec![(24, 15)]); + } + + #[test] + fn is_quarter_fillable() { + let mut board = Board::new(); + board.set_positions( + &Color::White, + [ + 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, + ], + ); + assert!(board.is_quarter_fillable(Color::Black, 1)); + assert!(!board.is_quarter_fillable(Color::Black, 12)); + assert!(board.is_quarter_fillable(Color::Black, 13)); + assert!(board.is_quarter_fillable(Color::Black, 24)); + assert!(board.is_quarter_fillable(Color::White, 1)); + assert!(board.is_quarter_fillable(Color::White, 12)); + assert!(!board.is_quarter_fillable(Color::White, 13)); + assert!(board.is_quarter_fillable(Color::White, 24)); + board.set_positions( + &Color::White, + [ + 5, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, -5, + ], + ); + assert!(board.is_quarter_fillable(Color::Black, 13)); + assert!(!board.is_quarter_fillable(Color::Black, 24)); + assert!(!board.is_quarter_fillable(Color::White, 1)); + assert!(board.is_quarter_fillable(Color::White, 12)); + board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 1, 0, + ], + ); + assert!(board.is_quarter_fillable(Color::Black, 16)); + } + + #[test] + fn get_quarter_filling_candidate() { + let mut board = Board::new(); + board.set_positions( + &Color::White, + [ + 3, 1, 2, 2, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + assert_eq!(vec![2], board.get_quarter_filling_candidate(Color::White)); + } + + #[test] + fn get_checker_field() { + let mut board = Board::new(); + board.set_positions( + &Color::White, + [ + 3, 1, 2, 2, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + assert_eq!(None, board.get_checker_field(&Color::White, 0)); + assert_eq!(Some(3), board.get_checker_field(&Color::White, 5)); + assert_eq!(Some(3), board.get_checker_field(&Color::White, 6)); + assert_eq!(None, board.get_checker_field(&Color::White, 14)); + } + + #[test] + fn get_field_checker() { + let mut board = Board::new(); + board.set_positions( + &Color::White, + [ + 3, 1, 2, 2, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + assert_eq!(4, board.get_field_checker(&Color::White, 2)); + assert_eq!(6, board.get_field_checker(&Color::White, 3)); + } } diff --git a/store/src/dice.rs b/store/src/dice.rs index e258d1f..348410d 100644 --- a/store/src/dice.rs +++ b/store/src/dice.rs @@ -1,34 +1,56 @@ -use crate::Error; use rand::distributions::{Distribution, Uniform}; +use rand::{rngs::StdRng, SeedableRng}; use serde::{Deserialize, Serialize}; -/// Represents the two dices +#[derive(Debug)] +pub struct DiceRoller { + rng: StdRng, +} + +impl Default for DiceRoller { + fn default() -> Self { + Self::new(None) + } +} + +impl DiceRoller { + pub fn new(opt_seed: Option) -> Self { + Self { + rng: match opt_seed { + None => StdRng::from_rng(rand::thread_rng()).unwrap(), + Some(seed) => SeedableRng::seed_from_u64(seed), + }, + } + } + + /// Roll the dices which generates two random numbers between 1 and 6, replicating a perfect + /// dice. We use the operating system's random number generator. + pub fn roll(&mut self) -> Dice { + let between = Uniform::new_inclusive(1, 6); + + let v = (between.sample(&mut self.rng), between.sample(&mut self.rng)); + + Dice { values: (v.0, v.1) } + } + + // Heads or tails + // pub fn coin(self) -> bool { + // let between = Uniform::new_inclusive(1, 2); + // let mut rng = rand::thread_rng(); + // between.sample(&mut rng) == 1 + // } +} + +/// Represents the two dice /// -/// Trictrac is always played with two dices. -#[derive(Debug, Clone, Copy, Serialize, PartialEq, Deserialize, Default)] -pub struct Dices { +/// Trictrac is always played with two dice. +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Deserialize, Default)] +pub struct Dice { /// The two dice values pub values: (u8, u8), } -impl Dices { - /// Roll the dices which generates two random numbers between 1 and 6, replicating a perfect - /// dice. We use the operating system's random number generator. - pub fn roll(self) -> Self { - let between = Uniform::new_inclusive(1, 6); - let mut rng = rand::thread_rng(); - - let v = (between.sample(&mut rng), between.sample(&mut rng)); - - Dices { values: (v.0, v.1) } - } - - /// Heads or tails - pub fn coin(self) -> bool { - let between = Uniform::new_inclusive(1, 2); - let mut rng = rand::thread_rng(); - between.sample(&mut rng) == 1 - } +impl Dice { pub fn to_bits_string(self) -> String { format!("{:0>3b}{:0>3b}", self.values.0, self.values.1) } @@ -37,6 +59,10 @@ impl Dices { format!("{} & {}", self.values.0, self.values.1) } + pub fn is_double(&self) -> bool { + self.values.0 == self.values.1 + } + // pub fn to_bits(self) -> [bool;6] { // self.to_bits_string().into_bytes().iter().map(|strbit| *strbit == '1' as u8).collect() // } @@ -49,26 +75,32 @@ impl Dices { // } } -/// Trait to roll the dices -pub trait Roll { - /// Roll the dices - fn roll(&mut self) -> &mut Self; -} - #[cfg(test)] mod tests { use super::*; #[test] fn test_roll() { - let dices = Dices::default().roll(); - assert!(dices.values.0 >= 1 && dices.values.0 <= 6); - assert!(dices.values.1 >= 1 && dices.values.1 <= 6); + let dice = DiceRoller::default().roll(); + assert!(dice.values.0 >= 1 && dice.values.0 <= 6); + assert!(dice.values.1 >= 1 && dice.values.1 <= 6); + } + + #[test] + fn test_seed() { + let seed = Some(123); + let dice1 = DiceRoller::new(seed).roll(); + let dice2 = DiceRoller::new(seed).roll(); + let dice3 = DiceRoller::new(seed).roll(); + assert!(dice1.values.0 == dice2.values.0); + assert!(dice1.values.0 == dice3.values.0); + assert!(dice1.values.1 == dice2.values.1); + assert!(dice1.values.1 == dice3.values.1); } #[test] fn test_to_bits_string() { - let dices = Dices { values: (4, 2) }; - assert!(dices.to_bits_string() == "100010"); + let dice = Dice { values: (4, 2) }; + assert!(dice.to_bits_string() == "100010"); } } diff --git a/store/src/error.rs b/store/src/error.rs index b6839f5..270c3f7 100644 --- a/store/src/error.rs +++ b/store/src/error.rs @@ -14,6 +14,8 @@ pub enum Error { PlayerInvalid, /// Field blocked FieldBlocked, + /// Field blocked + FieldBlockedByOne, /// Invalid field FieldInvalid, /// Not your turn @@ -40,6 +42,7 @@ impl fmt::Display for Error { Error::PlayerInvalid => write!(f, "Invalid player"), Error::DoublingNotPermitted => write!(f, "Doubling not permitted"), Error::FieldBlocked => write!(f, "Field blocked"), + Error::FieldBlockedByOne => write!(f, "Field blocked by one opponent"), Error::FieldInvalid => write!(f, "Invalid field"), Error::NotYourTurn => write!(f, "Not your turn"), Error::MoveInvalid => write!(f, "Invalid move"), diff --git a/store/src/game.rs b/store/src/game.rs index 977648a..b63ffcd 100644 --- a/store/src/game.rs +++ b/store/src/game.rs @@ -1,10 +1,10 @@ //! # Play a TricTrac Game -use crate::board::{Board, CheckerMove, Field, Move}; -use crate::dice::{Dices, Roll}; +use crate::board::{Board, CheckerMove}; +use crate::dice::Dice; +use crate::game_rules_moves::MoveRules; +use crate::game_rules_points::{PointsRules, PossibleJans}; use crate::player::{Color, Player, PlayerId}; -use crate::Error; -use log::{error, info}; -use std::cmp; +use log::{debug, error}; // use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -25,12 +25,42 @@ pub enum Stage { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum TurnStage { RollDice, + RollWaiting, MarkPoints, + HoldOrGoChoice, Move, + MarkAdvPoints, +} + +impl From for TurnStage { + fn from(item: u8) -> Self { + match item { + 0 => TurnStage::RollWaiting, + 1 => TurnStage::RollDice, + 2 => TurnStage::MarkPoints, + 3 => TurnStage::HoldOrGoChoice, + 4 => TurnStage::Move, + 5 => TurnStage::MarkAdvPoints, + _ => TurnStage::RollWaiting, + } + } +} + +impl From for u8 { + fn from(stage: TurnStage) -> u8 { + match stage { + TurnStage::RollWaiting => 0, + TurnStage::RollDice => 1, + TurnStage::MarkPoints => 2, + TurnStage::HoldOrGoChoice => 3, + TurnStage::Move => 4, + TurnStage::MarkAdvPoints => 5, + } + } } /// Represents a TricTrac game -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct GameState { pub stage: Stage, pub turn_stage: TurnStage, @@ -39,19 +69,30 @@ pub struct GameState { pub players: HashMap, pub history: Vec, /// last dice pair rolled - pub dices: Dices, + pub dice: Dice, + /// players points computed for the last dice pair rolled + pub dice_points: (u8, u8), + pub dice_moves: (CheckerMove, CheckerMove), + pub dice_jans: PossibleJans, /// true if player needs to roll first roll_first: bool, + // NOTE: add to a Setting struct if other fields needed + pub schools_enabled: bool, } // implement Display trait impl fmt::Display for GameState { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let mut s = String::new(); - s.push_str(&format!("Dices: {:?}\n", self.dices)); + s.push_str(&format!( + "Stage: {:?} / {:?}\n", + self.stage, self.turn_stage + )); + s.push_str(&format!("Dice: {:?}\n", self.dice)); // s.push_str(&format!("Who plays: {}\n", self.who_plays().map(|player| &player.name ).unwrap_or(""))); s.push_str(&format!("Board: {:?}\n", self.board)); - write!(f, "{}", s) + // s.push_str(&format!("History: {:?}\n", self.history)); + write!(f, "{s}") } } @@ -64,22 +105,113 @@ impl Default for GameState { active_player_id: 0, players: HashMap::new(), history: Vec::new(), - dices: Dices::default(), + dice: Dice::default(), + dice_points: (0, 0), + dice_moves: (CheckerMove::default(), CheckerMove::default()), + dice_jans: PossibleJans::default(), roll_first: true, + schools_enabled: false, } } } impl GameState { /// Create a new default game - pub fn new() -> Self { - GameState::default() + pub fn new(schools_enabled: bool) -> Self { + let mut gs = GameState::default(); + gs.set_schools_enabled(schools_enabled); + gs + } + + pub fn new_with_players(p1_name: &str, p2_name: &str) -> Self { + let mut game = Self::default(); + if let Some(p1) = game.init_player(p1_name) { + game.init_player(p2_name); + game.consume(&GameEvent::BeginGame { goes_first: p1 }); + } + game + } + + fn set_schools_enabled(&mut self, schools_enabled: bool) { + self.schools_enabled = schools_enabled; + } + + fn get_active_player(&self) -> Option<&Player> { + self.players.get(&self.active_player_id) + } + + fn get_opponent_id(&self) -> Option { + self.players + .keys() + .copied() + .filter(|k| k != &self.active_player_id) + .collect::>() + .first() + .copied() } // ------------------------------------------------------------------------- // accessors // ------------------------------------------------------------------------- + pub fn to_vec_float(&self) -> Vec { + self.to_vec().iter().map(|&x| x as f32).collect() + } + + /// Get state as a vector (to be used for bot training input) : + /// length = 36 + /// i8 for board positions with negative values for blacks + pub fn to_vec(&self) -> Vec { + let state_len = 36; + let mut state = Vec::with_capacity(state_len); + + // length = 24 + state.extend(self.board.to_vec()); + + // active player -> length = 1 + // white : 0 (false) + // black : 1 (true) + state.push( + self.who_plays() + .map(|player| if player.color == Color::Black { 1 } else { 0 }) + .unwrap_or(0), // White by default + ); + + // step -> length = 1 + let turn_stage: u8 = self.turn_stage.into(); + state.push(turn_stage as i8); + + // dice roll -> length = 2 + state.push(self.dice.values.0 as i8); + state.push(self.dice.values.1 as i8); + + // points, trous, bredouille, grande bredouille length=4 x2 joueurs = 8 + let white_player: Vec = self + .get_white_player() + .unwrap() + .to_vec() + .iter() + .map(|&x| x as i8) + .collect(); + state.extend(white_player); + let black_player: Vec = self + .get_black_player() + .unwrap() + .to_vec() + .iter() + .map(|&x| x as i8) + .collect(); + // .iter().map(|&x| x as i8) .collect() + state.extend(black_player); + + // ensure state has length state_len + state.truncate(state_len); + while state.len() < state_len { + state.push(0); + } + state + } + /// Calculate game state id : pub fn to_string_id(&self) -> String { // Pieces placement -> 77 bits (24 + 23 + 30 max) @@ -100,16 +232,19 @@ impl GameState { .unwrap_or('0'), // White by default ); - // step -> 2 bits + // step -> 3 bits let step_bits = match self.turn_stage { - TurnStage::RollDice => "01", - TurnStage::MarkPoints => "01", - TurnStage::Move => "10", + TurnStage::RollWaiting => "000", + TurnStage::RollDice => "001", + TurnStage::MarkPoints => "010", + TurnStage::HoldOrGoChoice => "011", + TurnStage::Move => "100", + TurnStage::MarkAdvPoints => "101", }; pos_bits.push_str(step_bits); // dice roll -> 6 bits - let dice_bits = self.dices.to_bits_string(); + let dice_bits = self.dice.to_bits_string(); pos_bits.push_str(&dice_bits); // points 10bits x2 joueurs = 20bits @@ -118,7 +253,7 @@ impl GameState { pos_bits.push_str(&white_bits); pos_bits.push_str(&black_bits); - pos_bits = format!("{:0>108}", pos_bits); + pos_bits = format!("{pos_bits:0>108}"); // println!("{}", pos_bits); let pos_u8 = pos_bits .as_bytes() @@ -130,7 +265,7 @@ impl GameState { } pub fn who_plays(&self) -> Option<&Player> { - self.players.get(&self.active_player_id) + self.get_active_player() } pub fn get_white_player(&self) -> Option<&Player> { @@ -165,6 +300,14 @@ impl GameState { .next() } + pub fn player_color_by_id(&self, player_id: &PlayerId) -> Option { + self.players + .iter() + .filter(|(id, _)| *id == player_id) + .map(|(_, player)| player.color) + .next() + } + // ---------------------------------------------------------------------------------- // Rules checks // ---------------------------------------------------------------------------------- @@ -184,15 +327,14 @@ impl GameState { return false; } } - EndGame { reason } => match reason { - EndGameReason::PlayerWon { winner: _ } => { + EndGame { reason } => { + if let EndGameReason::PlayerWon { winner: _ } = reason { // Check that the game has started before someone wins it if self.stage != Stage::InGame { return false; } } - _ => {} - }, + } PlayerJoined { player_id, name: _ } => { // Check that there isn't another player with the same id if self.players.contains_key(player_id) { @@ -214,8 +356,13 @@ impl GameState { if self.active_player_id != *player_id { return false; } + // Check the turn stage + if self.turn_stage != TurnStage::RollDice { + error!("bad stage {:?}", self.turn_stage); + return false; + } } - Mark { player_id, points } => { + RollResult { player_id, dice: _ } => { // Check player exists if !self.players.contains_key(player_id) { return false; @@ -224,11 +371,39 @@ impl GameState { if self.active_player_id != *player_id { return false; } + // Check the turn stage + if self.turn_stage != TurnStage::RollWaiting { + error!("bad stage {:?}", self.turn_stage); + return false; + } } - Move { player_id, moves } => { + Mark { + player_id, + points: _, + } => { // Check player exists if !self.players.contains_key(player_id) { - error!("Player {} unknown", player_id); + return false; + } + // Check player is currently the one making their move + if self.active_player_id != *player_id { + return false; + } + + // Check points are correct + // let (board, moves) = if *color == Color::Black { + // (board.mirror(), (moves.0.mirror(), moves.1.mirror())) + // } else { + // (board.clone(), *moves) + // }; + // let rules_points: u8 = self.get_points().iter().map(|r| r.0).sum(); + // if rules_points != *points { + // return false; + // } + } + Go { player_id } => { + if !self.players.contains_key(player_id) { + error!("Player {player_id} unknown"); return false; } // Check player is currently the one making their move @@ -236,100 +411,58 @@ impl GameState { error!("Player not active : {}", self.active_player_id); return false; } + // Check the player can leave (ie the game is in the KeepOrLeaveChoice stage) + if self.turn_stage != TurnStage::HoldOrGoChoice { + error!("bad stage {:?}", self.turn_stage); + error!( + "black player points : {:?}", + self.get_black_player() + .map(|player| (player.points, player.holes)) + ); + // error!("history {:?}", self.history); + return false; + } + } + Move { player_id, moves } => { + // Check player exists + if !self.players.contains_key(player_id) { + error!("Player {player_id} unknown"); + return false; + } + // Check player is currently the one making their move + if self.active_player_id != *player_id { + error!("Player not active : {}", self.active_player_id); + return false; + } + // Check the turn stage + if self.turn_stage != TurnStage::Move + && self.turn_stage != TurnStage::HoldOrGoChoice + { + error!("bad stage {:?}", self.turn_stage); + return false; + } let color = &self.players[player_id].color; - // Check moves possibles on the board - if !self.moves_possible(color, moves) { - return false; - } - - // Check moves conforms to the dices - if !self.moves_follows_dices(color, moves) { - return false; - } - - // Check move is allowed by the rules (to desactivate when playing with schools) - if !self.moves_allowed(color, moves) { + let rules = MoveRules::new(color, &self.board, self.dice); + let moves = if *color == Color::Black { + (moves.0.mirror(), moves.1.mirror()) + } else { + *moves + }; + if !rules.moves_follow_rules(&moves) { + error!("rules not followed "); return false; } } + PlayError => { + return true; + } } // We couldn't find anything wrong with the event so it must be good true } - fn moves_possible(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { - // Check move is physically possible - if !self.board.move_possible(color, &moves.0) { - return false; - } - - // Chained_move : "Tout d'une" - let chained_move = moves.0.chain(moves.1); - if chained_move.is_ok() { - if !self.board.move_possible(color, &chained_move.unwrap()) { - return false; - } - } else if !self.board.move_possible(color, &moves.1) { - return false; - } - true - } - - fn moves_follows_dices(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { - let (dice1, dice2) = self.dices.values; - let (move1, move2): &(CheckerMove, CheckerMove) = moves.into(); - let dist1 = (move1.get_to() - move1.get_from()) as u8; - let dist2 = (move2.get_to() - move2.get_from()) as u8; - print!("{}, {}, {}, {}", dist1, dist2, dice1, dice2); - // basic : same number - if cmp::min(dist1, dist2) != cmp::min(dice1, dice2) - || cmp::max(dist1, dist2) != cmp::max(dice1, dice2) - { - return false; - } - // prise de coin par puissance - // sorties - // no rule was broken - true - } - - fn moves_allowed(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { - // ------- corner rules ---------- - let corner_field: Field = self.board.get_color_corner(color); - let (corner_count, _color) = self.board.get_field_checkers(corner_field).unwrap(); - let (from0, to0, from1, to1) = ( - moves.0.get_from(), - moves.0.get_to(), - moves.1.get_from(), - moves.1.get_to(), - ); - // 2 checkers must go at the same time on an empty corner - if (to0 == corner_field || to1 == corner_field) && (to0 != to1) && corner_count == 0 { - return false; - } - - // the lat 2 checkers of a corner must leave at the same time - if (from0 == corner_field || from1 == corner_field) && (from0 != from1) && corner_count == 2 - { - return false; - } - - // ------- exit rules ---------- - // -- toutes les dames doivent être dans le jan de retour - // -- si on peut sortir, on doit sortir - // -- priorité : - // - dame se trouvant sur la flêche correspondant au dé - // - dame se trouvant plus loin de la sortie que la flêche (point défaillant) - // - dame se trouvant plus près que la flêche (point exédant) - - // --- cadran rempli si possible ---- - // --- interdit de jouer dans cadran que l'adversaire peut encore remplir ---- - // no rule was broken - true - } - // ---------------------------------------------------------------------------------- // State updates // ---------------------------------------------------------------------------------- @@ -341,7 +474,6 @@ impl GameState { } let player_id = self.players.len() + 1; - println!("player_id {}", player_id); let color = if player_id == 1 { Color::White } else { @@ -352,6 +484,7 @@ impl GameState { Some(player_id as PlayerId) } + #[cfg(test)] fn add_player(&mut self, player_id: PlayerId, player: Player) { self.players.insert(player_id, player); } @@ -372,12 +505,24 @@ impl GameState { match valid_event { BeginGame { goes_first } => { self.active_player_id = *goes_first; + // if self.who_plays().is_none() { + // let active_color = match self.dice.coin() { + // false => Color::Black, + // true => Color::White, + // }; + // let color_player_id = self.player_id_by_color(active_color); + // if color_player_id.is_some() { + // self.active_player_id = *color_player_id.unwrap(); + // } + // } self.stage = Stage::InGame; self.turn_stage = TurnStage::RollDice; } - EndGame { reason: _ } => self.stage = Stage::Ended, + EndGame { reason: _ } => { + self.stage = Stage::Ended; + } PlayerJoined { player_id, name } => { - let color = if self.players.len() > 0 { + let color = if !self.players.is_empty() { Color::White } else { Color::Black @@ -391,6 +536,7 @@ impl GameState { points: 0, can_bredouille: true, can_big_bredouille: true, + dice_roll_count: 0, }, ); } @@ -398,52 +544,186 @@ impl GameState { self.players.remove(player_id); } Roll { player_id: _ } => { - self.roll(); - self.turn_stage = TurnStage::MarkPoints; + self.turn_stage = TurnStage::RollWaiting; } - Mark { player_id, points } => { - self.mark_points(*player_id, *points); - if self.stage != Stage::Ended { - self.turn_stage = TurnStage::Move; + RollResult { player_id: _, dice } => { + self.dice = *dice; + self.inc_roll_count(self.active_player_id); + self.turn_stage = TurnStage::MarkPoints; + (self.dice_jans, self.dice_points) = self.get_rollresult_jans(dice); + debug!("points from result : {:?}", self.dice_points); + if !self.schools_enabled { + // Schools are not enabled. We mark points automatically + // the points earned by the opponent will be marked on its turn + let new_hole = self.mark_points(self.active_player_id, self.dice_points.0); + if new_hole { + let holes_count = self.get_active_player().unwrap().holes; + debug!("new hole -> {holes_count:?}"); + if holes_count > 12 { + self.stage = Stage::Ended; + } else { + self.turn_stage = TurnStage::HoldOrGoChoice; + } + } else { + self.turn_stage = TurnStage::Move; + } } } + Mark { player_id, points } => { + if self.schools_enabled { + let new_hole = self.mark_points(*player_id, *points); + if new_hole { + if self.get_active_player().unwrap().holes > 12 { + self.stage = Stage::Ended; + } else { + self.turn_stage = if self.turn_stage == TurnStage::MarkAdvPoints { + TurnStage::RollDice + } else { + TurnStage::HoldOrGoChoice + }; + } + } else { + self.turn_stage = if self.turn_stage == TurnStage::MarkAdvPoints { + TurnStage::RollDice + } else { + TurnStage::Move + }; + } + } + } + Go { player_id: _ } => self.new_pick_up(), Move { player_id, moves } => { let player = self.players.get(player_id).unwrap(); self.board.move_checker(&player.color, moves.0).unwrap(); self.board.move_checker(&player.color, moves.1).unwrap(); - self.active_player_id = self - .players - .keys() - .find(|id| *id != player_id) - .unwrap() - .clone(); + self.dice_moves = *moves; + self.active_player_id = *self.players.keys().find(|id| *id != player_id).unwrap(); + self.turn_stage = if self.schools_enabled { + TurnStage::MarkAdvPoints + } else { + // The player has moved, we can mark its opponent's points (which is now the current player) + let new_hole = self.mark_points(self.active_player_id, self.dice_points.1); + if new_hole && self.get_active_player().unwrap().holes > 12 { + self.stage = Stage::Ended; + } + TurnStage::RollDice + }; } + PlayError => {} } - self.history.push(valid_event.clone()); } + /// Set a new pick up ('relevé') after a player won a hole and choose to 'go', + /// or after a player has bore off (took of his men off the board) + fn new_pick_up(&mut self) { + self.players.iter_mut().for_each(|(_id, p)| { + // reset points + p.points = 0; + // reset dice_roll_count + p.dice_roll_count = 0; + // reset bredouille + p.can_bredouille = true; + // XXX : switch colors + // désactivé pour le moment car la vérification des mouvements échoue, cf. https://code.rhumbs.fr/henri/trictrac/issues/31 + // p.color = p.color.opponent_color(); + }); + // joueur actif = joueur ayant sorti ses dames ou est parti (donc deux jeux successifs) + self.turn_stage = TurnStage::RollDice; + // reset board + self.board = Board::new(); + } + + fn get_rollresult_jans(&self, dice: &Dice) -> (PossibleJans, (u8, u8)) { + let player = &self.players.get(&self.active_player_id).unwrap(); + debug!( + "get rollresult for {:?} {:?} {:?} (roll count {:?})", + player.color, self.board, dice, player.dice_roll_count + ); + let points_rules = PointsRules::new(&player.color, &self.board, *dice); + points_rules.get_result_jans(player.dice_roll_count) + } + /// Determines if someone has won the game pub fn determine_winner(&self) -> Option { - None + // A player has won if he has got 12 holes + self.players + .iter() + .filter(|(_, p)| p.holes > 11) + .map(|(id, _)| *id) + .next() } - fn mark_points(&mut self, player_id: PlayerId, points: u8) { - todo!() + fn inc_roll_count(&mut self, player_id: PlayerId) { + self.players.get_mut(&player_id).map(|p| { + p.dice_roll_count = p.dice_roll_count.saturating_add(1); + p + }); + } + + fn mark_points(&mut self, player_id: PlayerId, points: u8) -> bool { + // Update player points and holes + let mut new_hole = false; + self.players.get_mut(&player_id).map(|p| { + let sum_points = p.points + points; + let jeux = sum_points / 12; + let holes = match (jeux, p.can_bredouille) { + (0, _) => 0, + (_, false) => 2 * jeux - 1, + (_, true) => 2 * jeux, + }; + + new_hole = holes > 0; + if new_hole { + p.can_bredouille = true; + } + p.points = sum_points % 12; + p.holes += holes; + + // if points > 0 && p.holes > 15 { + if points > 0 { + debug!( + "player {player_id:?} holes : {:?} (+{holes:?}) points : {:?} (+{points:?} - {jeux:?})", + p.holes, p.points + ) + } + p + }); + + // Opponent updates + let maybe_op = if player_id == self.active_player_id { + self.get_opponent_id() + } else { + Some(player_id) + }; + if let Some(opp_id) = maybe_op { + if points > 0 { + self.players.get_mut(&opp_id).map(|opponent| { + // Cancel opponent bredouille + opponent.can_bredouille = false; + // Reset opponent points if the player finished a hole + if new_hole { + opponent.points = 0; + opponent.can_bredouille = true; + } + opponent + }); + } + } + + new_hole } } /// The reasons why a game could end -#[derive(Debug, Clone, Copy, Serialize, PartialEq, Deserialize)] +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Deserialize)] pub enum EndGameReason { - // In tic tac toe it doesn't make sense to keep playing when one of the players disconnect. - // Note that it might make sense to keep playing in some other game (like Team Fight Tactics for instance). PlayerLeft { player_id: PlayerId }, PlayerWon { winner: PlayerId }, } /// An event that progresses the GameState forward -#[derive(Debug, Clone, Serialize, PartialEq, Deserialize)] +#[derive(Debug, Clone, Serialize, PartialEq, Eq, Deserialize)] pub enum GameEvent { BeginGame { goes_first: PlayerId, @@ -461,80 +741,94 @@ pub enum GameEvent { Roll { player_id: PlayerId, }, + RollResult { + player_id: PlayerId, + dice: Dice, + }, Mark { player_id: PlayerId, points: u8, }, + Go { + player_id: PlayerId, + }, Move { player_id: PlayerId, moves: (CheckerMove, CheckerMove), }, + PlayError, } -impl Roll for GameState { - fn roll(&mut self) -> &mut Self { - self.dices = self.dices.roll(); - if self.who_plays().is_none() { - let active_color = match self.dices.coin() { - false => Color::Black, - true => Color::White, - }; - let color_player_id = self.player_id_by_color(active_color); - if color_player_id.is_some() { - self.active_player_id = *color_player_id.unwrap(); +impl GameEvent { + pub fn player_id(&self) -> Option { + match self { + Self::PlayerJoined { player_id, name: _ } => Some(*player_id), + Self::PlayerDisconnected { player_id } => Some(*player_id), + Self::Roll { player_id } => Some(*player_id), + Self::RollResult { player_id, dice: _ } => Some(*player_id), + Self::Mark { + player_id, + points: _, + } => Some(*player_id), + Self::Go { player_id } => Some(*player_id), + Self::Move { + player_id, + moves: _, + } => Some(*player_id), + _ => None, + } + } + + pub fn get_mirror(&self) -> Self { + // let mut mirror = self.clone(); + let mirror_player_id = if let Some(player_id) = self.player_id() { + if player_id == 1 { + 2 + } else { + 1 } - } - self - } -} - -impl Move for GameState { - fn move_checker(&mut self, player: &Player, dice: u8, from: usize) -> Result<&mut Self, Error> { - // check if move is permitted - let _ = self.move_permitted(player, dice)?; - - // remove checker from old position - self.board.set(&player.color, from, -1)?; - - // move checker to new position, in case it is reaching the off position, set it off - let new_position = from as i8 - dice as i8; - if new_position < 0 { - // self.board.set_off(player, 1)?; } else { - // self.board.set(player, new_position as usize, 1)?; + 0 + }; + + match self { + Self::PlayerJoined { player_id: _, name } => Self::PlayerJoined { + player_id: mirror_player_id, + name: name.clone(), + }, + Self::PlayerDisconnected { player_id: _ } => GameEvent::PlayerDisconnected { + player_id: mirror_player_id, + }, + Self::Roll { player_id: _ } => GameEvent::Roll { + player_id: mirror_player_id, + }, + Self::RollResult { player_id: _, dice } => GameEvent::RollResult { + player_id: mirror_player_id, + dice: *dice, + }, + Self::Mark { + player_id: _, + points, + } => GameEvent::Mark { + player_id: mirror_player_id, + points: *points, + }, + Self::Go { player_id: _ } => GameEvent::Go { + player_id: mirror_player_id, + }, + Self::Move { + player_id: _, + moves: (move1, move2), + } => Self::Move { + player_id: mirror_player_id, + moves: (move1.mirror(), move2.mirror()), + }, + Self::BeginGame { goes_first } => GameEvent::BeginGame { + goes_first: (if *goes_first == 1 { 2 } else { 1 }), + }, + Self::EndGame { reason } => GameEvent::EndGame { reason: *reason }, + Self::PlayError => GameEvent::PlayError, } - - // switch to other player if all dices have been consumed - self.switch_active_player(); - self.roll_first = true; - - Ok(self) - } - - /// Implements checks to validate if the player is allowed to move - fn move_permitted(&mut self, player: &Player, dice: u8) -> Result<&mut Self, Error> { - let maybe_player_id = self.player_id(&player); - // check if player is allowed to move - if maybe_player_id != Some(&self.active_player_id) { - return Err(Error::NotYourTurn); - } - - // if player is nobody, you can not play and have to roll first - if maybe_player_id.is_none() { - return Err(Error::RollFirst); - } - - // check if player has to roll first - if self.roll_first { - return Err(Error::RollFirst); - } - - // check if dice value has actually been rolled - if dice != self.dices.values.0 && dice != self.dices.values.1 { - return Err(Error::DiceInvalid); - } - - Ok(self) } } @@ -542,64 +836,73 @@ impl Move for GameState { mod tests { use super::*; - #[test] - fn test_to_string_id() { + fn init_test_gamestate(turn: TurnStage) -> GameState { let mut state = GameState::default(); state.add_player(1, Player::new("player1".into(), Color::White)); state.add_player(2, Player::new("player2".into(), Color::Black)); + state.active_player_id = 1; + state.turn_stage = turn; + state + } + + #[test] + fn to_string_id() { + let state = init_test_gamestate(TurnStage::RollDice); let string_id = state.to_string_id(); // println!("string_id : {}", string_id); - assert!(string_id == "Dz8+AAAAAT8/MAAAAAQAADAD"); + assert_eq!(string_id, "Hz88AAAAAz8/IAAAAAQAADAD"); } #[test] - fn test_moves_possible() { - let mut state = GameState::default(); - let player1 = Player::new("player1".into(), Color::White); - let player_id = 1; - state.add_player(player_id, player1); - state.add_player(2, Player::new("player2".into(), Color::Black)); - state.consume(&GameEvent::BeginGame { - goes_first: player_id, - }); + fn hold_or_go() { + let mut game_state = init_test_gamestate(TurnStage::MarkPoints); + game_state.schools_enabled = true; + let pid = game_state.active_player_id; + game_state.consume( + &(GameEvent::Mark { + player_id: pid, + points: 13, + }), + ); + let player = game_state.get_active_player().unwrap(); + assert_eq!(player.points, 1); + assert_eq!(player.holes, 2); // because can bredouille + assert_eq!(game_state.turn_stage, TurnStage::HoldOrGoChoice); - // Chained moves + // Go + game_state.consume( + &(GameEvent::Go { + player_id: game_state.active_player_id, + }), + ); + assert_eq!(game_state.active_player_id, pid); + let player = game_state.get_active_player().unwrap(); + assert_eq!(player.points, 0); + assert_eq!(game_state.turn_stage, TurnStage::RollDice); + + // Hold + let mut game_state = init_test_gamestate(TurnStage::MarkPoints); + game_state.schools_enabled = true; + let pid = game_state.active_player_id; + game_state.consume( + &(GameEvent::Mark { + player_id: pid, + points: 13, + }), + ); let moves = ( - CheckerMove::new(1, 5).unwrap(), - CheckerMove::new(5, 9).unwrap(), + CheckerMove::new(1, 3).unwrap(), + CheckerMove::new(1, 3).unwrap(), ); - assert!(state.moves_possible(&Color::White, &moves)); - - // not chained moves - let moves = ( - CheckerMove::new(1, 5).unwrap(), - CheckerMove::new(6, 9).unwrap(), + game_state.consume( + &(GameEvent::Move { + player_id: game_state.active_player_id, + moves, + }), ); - assert!(!state.moves_possible(&Color::White, &moves)); - } - - #[test] - fn test_moves_follow_dices() { - let mut state = GameState::default(); - let player1 = Player::new("player1".into(), Color::White); - let player_id = 1; - state.add_player(player_id, player1); - state.add_player(2, Player::new("player2".into(), Color::Black)); - state.consume(&GameEvent::BeginGame { - goes_first: player_id, - }); - state.consume(&GameEvent::Roll { player_id }); - let dices = state.dices.values; - let moves = ( - CheckerMove::new(1, (1 + dices.0).into()).unwrap(), - CheckerMove::new((1 + dices.0).into(), (1 + dices.0 + dices.1).into()).unwrap(), - ); - assert!(state.moves_follows_dices(&Color::White, &moves)); - - let badmoves = ( - CheckerMove::new(1, (2 + dices.0).into()).unwrap(), - CheckerMove::new((1 + dices.0).into(), (1 + dices.0 + dices.1).into()).unwrap(), - ); - assert!(!state.moves_follows_dices(&Color::White, &badmoves)); + assert_ne!(game_state.active_player_id, pid); + assert_eq!(game_state.players.get(&pid).unwrap().points, 1); + assert_eq!(game_state.get_active_player().unwrap().points, 0); + assert_eq!(game_state.turn_stage, TurnStage::MarkAdvPoints); } } diff --git a/store/src/game_rules_moves.rs b/store/src/game_rules_moves.rs new file mode 100644 index 0000000..31c43fa --- /dev/null +++ b/store/src/game_rules_moves.rs @@ -0,0 +1,1297 @@ +//! # Play a TricTrac Game +use crate::board::{Board, CheckerMove, Field, EMPTY_MOVE}; +use crate::dice::Dice; +use crate::game::GameState; +use crate::player::Color; +use log::info; +use std::cmp; + +#[derive(std::cmp::PartialEq, Debug)] +pub enum MoveError { + // Opponent corner is forbidden + OpponentCorner, + // 2 checkers must go at the same time on an empty corner + // & the last 2 checkers of a corner must leave at the same time + CornerNeedsTwoCheckers, + // Prise de coin de repos par puissance alors qu'il est possible + // de le prendre directement (par "effet") + CornerByEffectPossible, + // toutes les dames doivent être dans le jan de retour + ExitNeedsAllCheckersOnLastQuarter, + // mouvement avec nombre en exédant alors qu'une séquence de mouvements + // sans nombre en excédant est possible + ExitByEffectPossible, + // Sortie avec nombre en excédant d'une dame qui n'est pas la plus éloignée + ExitNotFasthest, + // Jeu dans un cadran que l'adversaire peut encore remplir + OpponentCanFillQuarter, + // remplir cadran si possible & conserver cadran rempli si possible ---- + MustFillQuarter, + // On n'a pas le droit de jouer d'une manière qui empêche de jouer les deux dés si on a la possibilité de les jouer. + MustPlayAllDice, + // Si on ne peut jouer qu'un seul dé, on doit jouer le plus fort si possible. + MustPlayStrongerDie, +} + +#[derive(std::cmp::PartialEq, Debug, Clone)] +pub enum TricTracRule { + Exit, + MustFillQuarter, + Corner, +} + +/// MoveRules always consider that the current player is White +/// You must use 'mirror' functions on board & CheckerMoves if player is Black +#[derive(Default)] +pub struct MoveRules { + pub board: Board, + pub dice: Dice, +} + +impl MoveRules { + /// Revert board if color is black + pub fn new(color: &Color, board: &Board, dice: Dice) -> Self { + Self { + board: Self::get_board_from_color(color, board), + dice, + } + } + + pub fn set_board(&mut self, color: &Color, board: &Board) { + self.board = Self::get_board_from_color(color, board); + } + + fn get_board_from_color(color: &Color, board: &Board) -> Board { + if *color == Color::Black { + board.mirror() + } else { + board.clone() + } + } + + pub fn moves_follow_rules( + &self, + moves: &(CheckerMove, CheckerMove), + // ignored_rules: Vec, + ) -> bool { + // Check moves possibles on the board + // Check moves conforms to the dice + // Check move is allowed by the rules (to desactivate when playing with schools) + self.moves_possible(moves) && self.moves_follows_dices(moves) && { + let is_allowed = self.moves_allowed(moves); + // let is_allowed = self.moves_allowed(moves, ignored_rules); + if is_allowed.is_err() { + info!("Move not allowed : {:?}", is_allowed.unwrap_err()); + false + } else { + true + } + } + // && self.moves_allowed(moves).is_ok() + } + + /// ---- moves_possibles : First of three checks for moves + fn moves_possible(&self, moves: &(CheckerMove, CheckerMove)) -> bool { + let color = &Color::White; + + let move0_from = moves.0.get_from(); + if 0 < move0_from && move0_from == moves.1.get_from() { + if let Ok((field_count, Some(field_color))) = self.board.get_field_checkers(move0_from) + { + if color != field_color || field_count < 2 { + info!("Move not physically possible"); + return false; + } + } + } + + if let Ok(chained_move) = moves.0.chain(moves.1) { + // Check intermediary move and chained_move : "Tout d'une" + if !self.board.passage_possible(color, &moves.0) + || !self.board.move_possible(color, &chained_move) + { + info!("Tout d'une : Move not physically possible"); + return false; + } + } else if !self.board.move_possible(color, &moves.0) + || !self.board.move_possible(color, &moves.1) + { + // Move is not physically possible + info!("Move not physically possible"); + return false; + } + true + } + + /// ----- moves_follows_dices : Second of three checks for moves + fn moves_follows_dices(&self, moves: &(CheckerMove, CheckerMove)) -> bool { + // Prise de coin par puissance + if self.is_move_by_puissance(moves) { + return true; + } + + let (dice1, dice2) = self.dice.values; + let (move1, move2): &(CheckerMove, CheckerMove) = moves; + + let move1_dices = self.get_move_compatible_dices(move1); + if move1_dices.is_empty() { + info!("Move does not follow dice"); + return false; + } + let move2_dices = self.get_move_compatible_dices(move2); + if move2_dices.is_empty() { + info!("Move does not follow dice"); + return false; + } + if move1_dices.len() == 1 + && move2_dices.len() == 1 + && move1_dices[0] == move2_dices[0] + && dice1 != dice2 + { + info!("Move does not follow dice"); + return false; + } + + // no rule was broken + true + } + + fn get_move_compatible_dices(&self, cmove: &CheckerMove) -> Vec { + let (dice1, dice2) = self.dice.values; + + let mut move_dices = Vec::new(); + if cmove.get_to() == 0 { + // handle empty move (0, 0) only one checker left, exiting with the first die. + if cmove.get_from() == 0 { + move_dices.push(dice1); + move_dices.push(dice2); + return move_dices; + } + + // Exits + let min_dist = 25 - cmove.get_from(); + if dice1 as usize >= min_dist { + move_dices.push(dice1); + } + if dice2 as usize >= min_dist { + move_dices.push(dice2); + } + } else { + let dist = (cmove.get_to() as i8 - cmove.get_from() as i8).unsigned_abs(); + if dice1 == dist { + move_dices.push(dice1); + } + if dice2 == dist { + move_dices.push(dice2); + } + } + move_dices + } + + /// ---- moves_allowed : Third of three checks for moves + pub fn moves_allowed( + &self, + moves: &(CheckerMove, CheckerMove), + // ignored_rules: Vec, + ) -> Result<(), MoveError> { + self.check_corner_rules(moves)?; + + if self.is_move_by_puissance(moves) { + if self.can_take_corner_by_effect() { + return Err(MoveError::CornerByEffectPossible); + } else { + // subsequent rules cannot be broken whith a move by puissance + return Ok(()); + } + } + + // Si possible, les deux dés doivent être joués + if moves.0.get_from() == 0 || moves.1.get_from() == 0 { + let mut possible_moves_sequences = self.get_possible_moves_sequences(true, vec![]); + possible_moves_sequences.retain(|moves| self.check_exit_rules(moves).is_ok()); + // possible_moves_sequences.retain(|moves| self.check_corner_rules(moves).is_ok()); + if !possible_moves_sequences.contains(moves) && !possible_moves_sequences.is_empty() { + if *moves == (EMPTY_MOVE, EMPTY_MOVE) { + return Err(MoveError::MustPlayAllDice); + } + let empty_removed = possible_moves_sequences + .iter() + .filter(|(c1, c2)| *c1 != EMPTY_MOVE && *c2 != EMPTY_MOVE); + if empty_removed.count() > 0 { + return Err(MoveError::MustPlayAllDice); + } + return Err(MoveError::MustPlayStrongerDie); + } + } + + // check exit rules + // if !ignored_rules.contains(&TricTracRule::Exit) { + self.check_exit_rules(moves)?; + // } + + // --- interdit de jouer dans un cadran que l'adversaire peut encore remplir ---- + self.check_opponent_can_fill_quarter_rule(moves)?; + + // --- remplir cadran si possible & conserver cadran rempli si possible ---- + // if !ignored_rules.contains(&TricTracRule::MustFillQuarter) { + self.check_must_fill_quarter_rule(moves)?; + // } + // no rule was broken + Ok(()) + } + + // --- interdit de jouer dans un cadran que l'adversaire peut encore remplir ---- + fn check_opponent_can_fill_quarter_rule( + &self, + moves: &(CheckerMove, CheckerMove), + ) -> Result<(), MoveError> { + let farthest = cmp::max(moves.0.get_to(), moves.1.get_to()); + let in_opponent_side = farthest > 12; + if in_opponent_side && self.board.is_quarter_fillable(Color::Black, farthest) { + return Err(MoveError::OpponentCanFillQuarter); + } + Ok(()) + } + + fn check_must_fill_quarter_rule( + &self, + moves: &(CheckerMove, CheckerMove), + ) -> Result<(), MoveError> { + let filling_moves_sequences = self.get_quarter_filling_moves_sequences(); + if !filling_moves_sequences.contains(moves) && !filling_moves_sequences.is_empty() { + return Err(MoveError::MustFillQuarter); + } + Ok(()) + } + + fn check_corner_rules(&self, moves: &(CheckerMove, CheckerMove)) -> Result<(), MoveError> { + let (from0, to0, from1, to1) = ( + moves.0.get_from(), + moves.0.get_to(), + moves.1.get_from(), + moves.1.get_to(), + ); + + // Player corner + let corner_field: Field = self.board.get_color_corner(&Color::White); + let (corner_count, _color) = self.board.get_field_checkers(corner_field).unwrap(); + + // 2 checkers must go at the same time on an empty corner + if (to0 == corner_field || to1 == corner_field) && (to0 != to1) && corner_count == 0 { + return Err(MoveError::CornerNeedsTwoCheckers); + } + + // the last 2 checkers of a corner must leave at the same time + if (from0 == corner_field || from1 == corner_field) && (from0 != from1) && corner_count == 2 + { + return Err(MoveError::CornerNeedsTwoCheckers); + } + + // Oponnent corner + let corner_field: Field = self.board.get_color_corner(&Color::Black); + if to1 == corner_field || (to0 == corner_field && to0 != from1) { + return Err(MoveError::OpponentCorner); + } + + Ok(()) + } + + fn has_checkers_outside_last_quarter(&self) -> bool { + !self + .board + .get_color_fields(Color::White) + .iter() + .filter(|(field, _count)| *field < 19) + .collect::>() + .is_empty() + } + + fn check_exit_rules(&self, moves: &(CheckerMove, CheckerMove)) -> Result<(), MoveError> { + if !moves.0.is_exit() && !moves.1.is_exit() { + return Ok(()); + } + // toutes les dames doivent être dans le jan de retour + if self.has_checkers_outside_last_quarter() { + return Err(MoveError::ExitNeedsAllCheckersOnLastQuarter); + } + + // toutes les sorties directes sont autorisées, ainsi que les nombres défaillants + let ignored_rules = vec![TricTracRule::Exit]; + let possible_moves_sequences_without_excedent = + self.get_possible_moves_sequences(false, ignored_rules); + if possible_moves_sequences_without_excedent.contains(moves) { + return Ok(()); + } + // À ce stade au moins un des déplacements concerne un nombre en excédant + // - si d'autres séquences de mouvements sans nombre en excédant sont possibles, on + // refuse cette séquence + if !possible_moves_sequences_without_excedent.is_empty() { + return Err(MoveError::ExitByEffectPossible); + } + + // - la dame choisie doit être la plus éloignée de la sortie + let mut checkers = self.board.get_color_fields(Color::White); + checkers.sort_by(|a, b| b.0.cmp(&a.0)); + let mut farthest = 24; + let mut next_farthest = 24; + let mut has_two_checkers = false; + if let Some((field, count)) = checkers.first() { + farthest = *field; + if *count > 1 { + next_farthest = *field; + has_two_checkers = true; + } else if let Some((field, _count)) = checkers.get(1) { + next_farthest = *field; + has_two_checkers = true; + } + } + + // s'il reste au moins deux dames, on vérifie que les plus éloignées soint choisies + if has_two_checkers { + if moves.0.get_to() == 0 && moves.1.get_to() == 0 { + // Deux coups sortants en excédant + if cmp::max(moves.0.get_from(), moves.1.get_from()) > next_farthest { + return Err(MoveError::ExitNotFasthest); + } + } else { + // Un seul coup sortant en excédant le coup sortant doit concerner la plus éloignée du bord + let exit_move_field = if moves.0.get_to() == 0 { + moves.0.get_from() + } else { + moves.1.get_from() + }; + if exit_move_field != farthest { + return Err(MoveError::ExitNotFasthest); + } + } + } + Ok(()) + } + + pub fn get_possible_moves_sequences( + &self, + with_excedents: bool, + ignored_rules: Vec, + ) -> Vec<(CheckerMove, CheckerMove)> { + let (dice1, dice2) = self.dice.values; + let (dice_max, dice_min) = if dice1 > dice2 { + (dice1, dice2) + } else { + (dice2, dice1) + }; + let mut moves_seqs = self.get_possible_moves_sequences_by_dices( + dice_max, + dice_min, + with_excedents, + false, + ignored_rules.clone(), + ); + // if we got valid sequences with the highest die, we don't accept sequences using only the + // lowest die + let ignore_empty = !moves_seqs.is_empty(); + let mut moves_seqs_order2 = self.get_possible_moves_sequences_by_dices( + dice_min, + dice_max, + with_excedents, + ignore_empty, + ignored_rules, + ); + moves_seqs.append(&mut moves_seqs_order2); + let empty_removed = moves_seqs + .iter() + .filter(|(c1, c2)| *c1 != EMPTY_MOVE && *c2 != EMPTY_MOVE); + if empty_removed.count() > 0 { + moves_seqs.retain(|(c1, c2)| *c1 != EMPTY_MOVE && *c2 != EMPTY_MOVE); + } + moves_seqs + } + + pub fn get_scoring_quarter_filling_moves_sequences(&self) -> Vec<(CheckerMove, CheckerMove)> { + let all_seqs = self.get_quarter_filling_moves_sequences(); + if all_seqs.is_empty() { + return vec![]; + } + let missing_fields = self.board.get_quarter_filling_candidate(Color::White); + match missing_fields.len() { + // preserve an already filled quarter : return one sequence + 0 => vec![*all_seqs.last().unwrap()], + // two fields, two dices : all_seqs should already contain only one possibility + 2 => all_seqs, + 1 => { + let dest_field = missing_fields.first().unwrap(); + let mut filling_moves_origins = vec![]; + all_seqs.iter().fold(vec![], |mut acc, seq| { + let origins = self.get_sequence_origin_from_destination(*seq, *dest_field); + for origin in origins { + if !filling_moves_origins.contains(&origin) { + filling_moves_origins.push(origin); + acc.push(*seq); + } + } + acc + }) + } + _ => vec![], // cannot be + } + } + + fn get_sequence_origin_from_destination( + &self, + sequence: (CheckerMove, CheckerMove), + destination: Field, + ) -> Vec { + let mut origin = vec![]; + if sequence.0.get_to() == destination { + origin.push(sequence.0.get_from()); + } + if sequence.1.get_to() == destination { + if sequence.0.get_to() == sequence.1.get_from() { + // tout d'une + origin.push(sequence.0.get_from()); + } else { + origin.push(sequence.1.get_from()); + } + } + origin + } + + // Get all moves filling a quarter or preserving a filled quarter + pub fn get_quarter_filling_moves_sequences(&self) -> Vec<(CheckerMove, CheckerMove)> { + let mut moves_seqs = Vec::new(); + let color = &Color::White; + let ignored_rules = vec![TricTracRule::Exit, TricTracRule::MustFillQuarter]; + for moves in self.get_possible_moves_sequences(true, ignored_rules) { + let mut board = self.board.clone(); + board.move_checker(color, moves.0).unwrap(); + board.move_checker(color, moves.1).unwrap(); + // println!("get_quarter_filling_moves_sequences board : {:?}", board); + if board.any_quarter_filled(*color) && !moves_seqs.contains(&moves) { + moves_seqs.push(moves); + } + } + moves_seqs + } + + fn get_possible_moves_sequences_by_dices( + &self, + dice1: u8, + dice2: u8, + with_excedents: bool, + ignore_empty: bool, + ignored_rules: Vec, + ) -> Vec<(CheckerMove, CheckerMove)> { + let mut moves_seqs = Vec::new(); + let color = &Color::White; + let forbid_exits = self.has_checkers_outside_last_quarter(); + for first_move in + self.board + .get_possible_moves(*color, dice1, with_excedents, false, forbid_exits) + { + let mut board2 = self.board.clone(); + if board2.move_checker(color, first_move).is_err() { + println!("err move"); + continue; + } + + // XXX : the goal here is to replicate moves_allowed() checks without using get_possible_moves_sequences to + // avoid an infinite loop... + let mut has_second_dice_move = false; + for second_move in + board2.get_possible_moves(*color, dice2, with_excedents, true, forbid_exits) + { + if self.check_corner_rules(&(first_move, second_move)).is_ok() + && self + .check_opponent_can_fill_quarter_rule(&(first_move, second_move)) + .is_ok() + && !(self.is_move_by_puissance(&(first_move, second_move)) + && self.can_take_corner_by_effect()) + && (ignored_rules.contains(&TricTracRule::Exit) + || self.check_exit_rules(&(first_move, second_move)).is_ok()) + && (ignored_rules.contains(&TricTracRule::MustFillQuarter) + || self + .check_must_fill_quarter_rule(&(first_move, second_move)) + .is_ok()) + { + moves_seqs.push((first_move, second_move)); + has_second_dice_move = true; + } + } + if !has_second_dice_move + && with_excedents + && !ignore_empty + && self.check_corner_rules(&(first_move, EMPTY_MOVE)).is_ok() + && self + .check_opponent_can_fill_quarter_rule(&(first_move, EMPTY_MOVE)) + .is_ok() + && !(self.is_move_by_puissance(&(first_move, EMPTY_MOVE)) + && self.can_take_corner_by_effect()) + && (ignored_rules.contains(&TricTracRule::Exit) + || self.check_exit_rules(&(first_move, EMPTY_MOVE)).is_ok()) + && (ignored_rules.contains(&TricTracRule::MustFillQuarter) + || self + .check_must_fill_quarter_rule(&(first_move, EMPTY_MOVE)) + .is_ok()) + { + // empty move + moves_seqs.push((first_move, EMPTY_MOVE)); + } + //if board2.get_color_fields(*color).is_empty() { + } + moves_seqs + } + + fn _get_direct_exit_moves(&self, state: &GameState) -> Vec { + let mut moves = Vec::new(); + let (dice1, dice2) = state.dice.values; + + // sorties directes simples + let (field1_candidate, field2_candidate) = (25 - dice1 as usize, 25 - dice2 as usize); + let (count1, col1) = state.board.get_field_checkers(field1_candidate).unwrap(); + let (count2, col2) = state.board.get_field_checkers(field2_candidate).unwrap(); + if count1 > 0 { + moves.push(CheckerMove::new(field1_candidate, 0).unwrap()); + } + if dice2 != dice1 { + if count2 > 0 { + moves.push(CheckerMove::new(field2_candidate, 0).unwrap()); + } + } else if count1 > 1 { + // doublet et deux dames disponibles + moves.push(CheckerMove::new(field1_candidate, 0).unwrap()); + } + + // sortie directe tout d'une + let fieldall_candidate = (25 - dice1 - dice2) as usize; + let (countall, _col) = state.board.get_field_checkers(fieldall_candidate).unwrap(); + let color = &Color::White; + if countall > 0 { + if col1.is_none() || col1 == Some(color) { + moves.push(CheckerMove::new(fieldall_candidate, field1_candidate).unwrap()); + moves.push(CheckerMove::new(field1_candidate, 0).unwrap()); + } + if col2.is_none() || col2 == Some(color) { + moves.push(CheckerMove::new(fieldall_candidate, field2_candidate).unwrap()); + moves.push(CheckerMove::new(field2_candidate, 0).unwrap()); + } + } + moves + } + + fn is_move_by_puissance(&self, moves: &(CheckerMove, CheckerMove)) -> bool { + let (dice1, dice2) = self.dice.values; + let dist1 = (moves.0.get_to() as i8 - moves.0.get_from() as i8).unsigned_abs(); + let dist2 = (moves.1.get_to() as i8 - moves.1.get_from() as i8).unsigned_abs(); + + // Both corners must be empty + let (count1, _color) = self.board.get_field_checkers(12).unwrap(); + let (count2, _color2) = self.board.get_field_checkers(13).unwrap(); + if count1 > 0 || count2 > 0 { + return false; + } + + let color = &Color::White; + moves.0.get_to() == moves.1.get_to() + && moves.0.get_to() == self.board.get_color_corner(color) + && (cmp::min(dist1, dist2) == cmp::min(dice1, dice2) - 1 + && cmp::max(dist1, dist2) == cmp::max(dice1, dice2) - 1) + } + + fn can_take_corner_by_effect(&self) -> bool { + // return false if corner already taken + let color = &Color::White; + let corner_field: Field = self.board.get_color_corner(color); + let (count, _col) = self.board.get_field_checkers(corner_field).unwrap(); + if count > 0 { + return false; + } + + let (dice1, dice2) = self.dice.values; + let (field1, field2) = (corner_field - dice1 as usize, corner_field - dice2 as usize); + let res1 = self.board.get_field_checkers(field1); + let res2 = self.board.get_field_checkers(field2); + if res1.is_err() || res2.is_err() { + return false; + } + let (count1, opt_color1) = res1.unwrap(); + let (count2, opt_color2) = res2.unwrap(); + count1 > 0 && count2 > 0 && opt_color1 == Some(color) && opt_color2 == Some(color) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_take_corner_by_effect() { + let mut rules = MoveRules::default(); + rules.board.set_positions( + &Color::White, + [ + 10, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, + ], + ); + rules.dice.values = (4, 4); + assert!(rules.can_take_corner_by_effect()); + + rules.dice.values = (5, 5); + assert!(!rules.can_take_corner_by_effect()); + + rules.board.set_positions( + &Color::White, + [ + 10, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, + ], + ); + rules.dice.values = (4, 4); + assert!(!rules.can_take_corner_by_effect()); + } + + #[test] + fn prise_en_puissance() { + let mut state = MoveRules::default(); + // prise par puissance ok + state.board.set_positions( + &Color::White, + [ + 10, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, + ], + ); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(8, 12).unwrap(), + CheckerMove::new(8, 12).unwrap(), + ); + assert!(state.is_move_by_puissance(&moves)); + assert!(state.moves_follows_dices(&moves)); + assert!(state.moves_allowed(&moves).is_ok()); + + // opponent corner must be empty + state.board.set_positions( + &Color::White, + [ + 10, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, + ], + ); + assert!(!state.is_move_by_puissance(&moves)); + assert!(!state.moves_follows_dices(&moves)); + + // Si on a la possibilité de prendre son coin à la fois par effet, c'est à dire naturellement, et aussi par puissance, on doit le prendre par effet + state.board.set_positions( + &Color::White, + [ + 5, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, + ], + ); + assert_eq!( + Err(MoveError::CornerByEffectPossible), + state.moves_allowed(&moves) + ); + + // on a déjà pris son coin : on ne peux plus y deplacer des dames par puissance + state.board.set_positions( + &Color::White, + [ + 8, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, + ], + ); + assert!(!state.is_move_by_puissance(&moves)); + assert!(!state.moves_follows_dices(&moves)); + } + + #[test] + fn exit() { + let mut state = MoveRules::default(); + // exit ok + state.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, + ], + ); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(20, 0).unwrap(), + CheckerMove::new(20, 0).unwrap(), + ); + assert!(state.moves_follows_dices(&moves)); + assert!(state.moves_allowed(&moves).is_ok()); + + // toutes les dames doivent être dans le jan de retour + state.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, + ], + ); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(20, 0).unwrap(), + CheckerMove::new(20, 0).unwrap(), + ); + assert_eq!( + Err(MoveError::ExitNeedsAllCheckersOnLastQuarter), + state.moves_allowed(&moves) + ); + + // on ne peut pas sortir une dame avec un nombre excédant si on peut en jouer une avec un nombre défaillant + state.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 0, 2, 0, + ], + ); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(20, 0).unwrap(), + CheckerMove::new(23, 0).unwrap(), + ); + assert_eq!( + Err(MoveError::ExitByEffectPossible), + state.moves_allowed(&moves) + ); + + // on doit jouer le nombre excédant le plus éloigné + state.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, + ], + ); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(20, 0).unwrap(), + CheckerMove::new(23, 0).unwrap(), + ); + assert_eq!(Err(MoveError::ExitNotFasthest), state.moves_allowed(&moves)); + let moves = ( + CheckerMove::new(20, 0).unwrap(), + CheckerMove::new(21, 0).unwrap(), + ); + assert!(state.moves_allowed(&moves).is_ok()); + + // Cas de la dernière dame + state.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, + ], + ); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(23, 0).unwrap(), + CheckerMove::new(0, 0).unwrap(), + ); + assert!(state.moves_follows_dices(&moves)); + assert!(state.moves_allowed(&moves).is_ok()); + } + + #[test] + fn move_check_opponent_fillable_quarter() { + let mut state = MoveRules::default(); + state.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, + ], + ); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(11, 16).unwrap(), + CheckerMove::new(11, 16).unwrap(), + ); + assert!(state.moves_allowed(&moves).is_ok()); + + state.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 1, 0, + ], + ); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(11, 16).unwrap(), + CheckerMove::new(11, 16).unwrap(), + ); + assert_eq!( + Err(MoveError::OpponentCanFillQuarter), + state.moves_allowed(&moves) + ); + + state.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 1, 0, + ], + ); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(11, 16).unwrap(), + CheckerMove::new(16, 21).unwrap(), + ); + assert!(state.moves_allowed(&moves).is_ok()); + + state.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, -12, + ], + ); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(11, 16).unwrap(), + CheckerMove::new(16, 21).unwrap(), + ); + assert_eq!( + Err(MoveError::OpponentCanFillQuarter), + state.moves_allowed(&moves) + ); + } + + #[test] + fn move_check_fillable_quarter() { + let mut state = MoveRules::default(); + state.board.set_positions( + &Color::White, + [ + 3, 3, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, + ], + ); + state.dice.values = (5, 4); + let moves = ( + CheckerMove::new(1, 6).unwrap(), + CheckerMove::new(2, 6).unwrap(), + ); + assert!(state.moves_allowed(&moves).is_ok()); + let moves = ( + CheckerMove::new(1, 5).unwrap(), + CheckerMove::new(2, 7).unwrap(), + ); + assert_eq!(Err(MoveError::MustFillQuarter), state.moves_allowed(&moves)); + + state.board.set_positions( + &Color::White, + [ + 2, 3, 2, 2, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + state.dice.values = (2, 3); + let moves = ( + CheckerMove::new(6, 8).unwrap(), + CheckerMove::new(6, 9).unwrap(), + ); + assert_eq!(Err(MoveError::MustFillQuarter), state.moves_allowed(&moves)); + let moves = ( + CheckerMove::new(2, 4).unwrap(), + CheckerMove::new(5, 8).unwrap(), + ); + assert!(state.moves_allowed(&moves).is_ok()); + } + + #[test] + fn move_play_all_dice() { + let mut state = MoveRules::default(); + state.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, + ], + ); + state.dice.values = (1, 3); + let moves = ( + CheckerMove::new(22, 0).unwrap(), + CheckerMove::new(0, 0).unwrap(), + ); + + assert_eq!(Err(MoveError::MustPlayAllDice), state.moves_allowed(&moves)); + let moves = ( + CheckerMove::new(22, 23).unwrap(), + CheckerMove::new(23, 0).unwrap(), + ); + assert!(state.moves_allowed(&moves).is_ok()); + } + + #[test] + fn move_opponent_rest_corner_rules() { + // fill with 2 checkers : forbidden + let mut state = MoveRules::default(); + state.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + state.dice.values = (1, 1); + let moves = ( + CheckerMove::new(12, 13).unwrap(), + CheckerMove::new(12, 13).unwrap(), + ); + assert!(state.moves_follows_dices(&moves)); + assert!(state.moves_allowed(&moves).is_err()); + + // repos lors d'un déplacement tout d'une + let moves = ( + CheckerMove::new(12, 13).unwrap(), + CheckerMove::new(13, 14).unwrap(), + ); + assert!(state.moves_allowed(&moves).is_ok()); + + // one checker : forbidden + let moves = ( + CheckerMove::new(12, 13).unwrap(), + CheckerMove::new(11, 12).unwrap(), + ); + assert!(state.moves_allowed(&moves).is_err()); + } + + #[test] + fn move_rest_corner_enter() { + // direct + let mut state = MoveRules::default(); + state.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + state.dice.values = (2, 1); + let moves = ( + CheckerMove::new(10, 12).unwrap(), + CheckerMove::new(11, 12).unwrap(), + ); + assert!(state.moves_follows_dices(&moves)); + assert!(state.moves_allowed(&moves).is_ok()); + + // par puissance + state.dice.values = (3, 2); + let moves = ( + CheckerMove::new(10, 12).unwrap(), + CheckerMove::new(11, 12).unwrap(), + ); + assert!(state.moves_follows_dices(&moves)); + assert!(state.moves_allowed(&moves).is_ok()); + } + + #[test] + fn move_rest_corner_blocked() { + let mut state = MoveRules::default(); + state.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + state.dice.values = (2, 1); + let moves = ( + CheckerMove::new(0, 0).unwrap(), + CheckerMove::new(0, 0).unwrap(), + ); + assert!(state.moves_follows_dices(&moves)); + assert!(state.moves_allowed(&moves).is_ok()); + + state.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, + ], + ); + state.dice.values = (2, 1); + let moves = ( + CheckerMove::new(23, 24).unwrap(), + CheckerMove::new(0, 0).unwrap(), + ); + assert!(state.moves_follows_dices(&moves)); + // let res = state.moves_allowed(&moves); + // println!("{:?}", res); + assert!(state.moves_allowed(&moves).is_ok()); + + let moves = ( + CheckerMove::new(0, 0).unwrap(), + CheckerMove::new(0, 0).unwrap(), + ); + assert_eq!(Err(MoveError::MustPlayAllDice), state.moves_allowed(&moves)); + } + + #[test] + fn move_rest_corner_exit() { + let mut state = MoveRules::default(); + state.board.set_positions( + &Color::White, + [ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, 0, 0, + ], + ); + state.dice.values = (2, 3); + let moves = ( + CheckerMove::new(12, 14).unwrap(), + CheckerMove::new(1, 4).unwrap(), + ); + assert_eq!( + Err(MoveError::CornerNeedsTwoCheckers), + state.moves_allowed(&moves) + ); + } + + #[test] + fn move_rest_corner_toutdune() { + let mut state = MoveRules::default(); + // We can't go to the occupied rest corner as an intermediary step + state.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + state.dice.values = (2, 1); + let moves = ( + CheckerMove::new(11, 13).unwrap(), + CheckerMove::new(13, 14).unwrap(), + ); + assert!(!state.moves_possible(&moves)); + + // We can use the empty rest corner as an intermediary step + state.board.set_positions( + &Color::White, + [ + 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, -2, 0, 0, 0, -2, 0, -2, -2, -2, -2, -3, + ], + ); + state.dice.values = (6, 5); + let moves = ( + CheckerMove::new(8, 13).unwrap(), + CheckerMove::new(13, 19).unwrap(), + ); + + assert!(state.moves_possible(&moves)); + assert!(state.moves_allowed(&moves).is_ok()); + } + + #[test] + fn move_play_stronger_dice() { + let mut state = MoveRules::default(); + state.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, -1, -1, -1, 0, 0, 0, 0, 0, 0, + ], + ); + state.dice.values = (2, 3); + let moves = ( + CheckerMove::new(12, 14).unwrap(), + CheckerMove::new(0, 0).unwrap(), + ); + // let poss = state.get_possible_moves_sequences(&Color::White, true); + // println!("{:?}", poss); + assert_eq!( + Err(MoveError::MustPlayStrongerDie), + state.moves_allowed(&moves) + ); + let moves = ( + CheckerMove::new(12, 15).unwrap(), + CheckerMove::new(0, 0).unwrap(), + ); + assert!(state.moves_allowed(&moves).is_ok()); + } + + #[test] + fn moves_possible() { + let mut state = MoveRules::default(); + + // Chained moves + let moves = ( + CheckerMove::new(1, 5).unwrap(), + CheckerMove::new(5, 9).unwrap(), + ); + assert!(state.moves_possible(&moves)); + + // not chained moves + let moves = ( + CheckerMove::new(1, 5).unwrap(), + CheckerMove::new(6, 9).unwrap(), + ); + assert!(!state.moves_possible(&moves)); + + // Can't move the same checker twice + state.board.set_positions( + &Color::White, + [ + 3, 3, 1, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + state.dice.values = (2, 1); + let moves = ( + CheckerMove::new(3, 5).unwrap(), + CheckerMove::new(3, 4).unwrap(), + ); + assert!(!state.moves_possible(&moves)); + + // black moves + let state = MoveRules::new(&Color::Black, &Board::default(), Dice::default()); + let moves = ( + CheckerMove::new(24, 20).unwrap().mirror(), + CheckerMove::new(20, 19).unwrap().mirror(), + ); + assert!(state.moves_possible(&moves)); + } + + #[test] + fn filling_moves_sequences() { + let mut state = MoveRules::default(); + state.board.set_positions( + &Color::White, + [ + 3, 3, 1, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + state.dice.values = (2, 1); + let filling_moves_sequences = state.get_quarter_filling_moves_sequences(); + // println!( + // "test filling_moves_sequences : {:?}", + // filling_moves_sequences + // ); + assert_eq!(2, filling_moves_sequences.len()); + + state.board.set_positions( + &Color::White, + [ + 3, 2, 3, 2, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + state.dice.values = (2, 2); + let filling_moves_sequences = state.get_quarter_filling_moves_sequences(); + // println!("{:?}", filling_moves_sequences); + assert_eq!(2, filling_moves_sequences.len()); + + state.board.set_positions( + &Color::White, + [ + 3, 1, 2, 2, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + state.dice.values = (2, 1); + let filling_moves_sequences = state.get_quarter_filling_moves_sequences(); + // println!( + // "test filling_moves_sequences 2 : {:?}", + // filling_moves_sequences + // ); + assert_eq!(2, filling_moves_sequences.len()); + + // positions + state.board.set_positions( + &Color::White, + [ + 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, -2, 0, 0, 0, -2, 0, -2, -2, -2, -2, -3, + ], + ); + state.dice.values = (6, 5); + let filling_moves_sequences = state.get_quarter_filling_moves_sequences(); + assert_eq!(1, filling_moves_sequences.len()); + } + + #[test] + fn scoring_filling_moves_sequences() { + let mut state = MoveRules::default(); + + state.board.set_positions( + &Color::White, + [ + 3, 1, 2, 2, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + state.dice.values = (2, 1); + assert_eq!(1, state.get_scoring_quarter_filling_moves_sequences().len()); + + state.board.set_positions( + &Color::White, + [ + 2, 3, 3, 3, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + state.dice.values = (2, 1); + let filling_moves_sequences = state.get_scoring_quarter_filling_moves_sequences(); + // println!("{:?}", filling_moves_sequences); + assert_eq!(3, filling_moves_sequences.len()); + + // preserve filling + state.board.set_positions( + &Color::White, + [ + 2, 2, 2, 2, 2, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -1, -2, -3, -5, 0, -1, + ], + ); + state.dice.values = (3, 1); + assert_eq!(1, state.get_scoring_quarter_filling_moves_sequences().len()); + + // preserve filling (black) + let mut state = MoveRules::new(&Color::Black, &Board::default(), Dice::default()); + state.board.set_positions( + &Color::Black, + [ + 1, 0, 5, 3, 2, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -4, -2, -2, -2, -2, -2, + ], + ); + state.dice.values = (3, 1); + assert_eq!(1, state.get_scoring_quarter_filling_moves_sequences().len()); + } + + // prise de coin par puissance et conservation de jan #18 + // https://www.youtube.com/watch?v=5Bkxvd7MSps + #[test] + fn corner_by_effect_and_filled_corner() { + let mut state = MoveRules::default(); + + state.board.set_positions( + &Color::White, + [ + 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, -2, 0, 0, 0, -2, 0, -2, -2, -2, -2, -3, + ], + ); + state.dice.values = (6, 5); + + let moves = ( + CheckerMove::new(7, 12).unwrap(), + CheckerMove::new(8, 12).unwrap(), + ); + assert_eq!( + Err(MoveError::CornerByEffectPossible), + state.moves_allowed(&moves) + ); + + // on ne peut pas rompre le plein car il y a un autre mouvement possible + let moves = ( + CheckerMove::new(6, 12).unwrap(), + CheckerMove::new(7, 12).unwrap(), + ); + assert_eq!(Err(MoveError::MustFillQuarter), state.moves_allowed(&moves)); + + // seul mouvement possible + let moves = ( + CheckerMove::new(8, 13).unwrap(), + CheckerMove::new(13, 19).unwrap(), + ); + assert!(state.moves_allowed(&moves).is_ok()); + + // s'il n'y a pas d'autre solution, on peut rompre + } + + #[test] + fn get_possible_moves_sequences() { + let mut state = MoveRules::default(); + + state.board.set_positions( + &Color::White, + [ + 2, 0, -2, -2, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + state.dice.values = (2, 3); + let moves = ( + CheckerMove::new(9, 11).unwrap(), + CheckerMove::new(11, 14).unwrap(), + ); + assert_eq!( + vec![moves], + state.get_possible_moves_sequences(true, vec![]) + ); + } +} diff --git a/store/src/game_rules_points.rs b/store/src/game_rules_points.rs new file mode 100644 index 0000000..4e94d08 --- /dev/null +++ b/store/src/game_rules_points.rs @@ -0,0 +1,885 @@ +use crate::board::{Board, FieldWithCount, EMPTY_MOVE}; +use crate::dice::Dice; +use crate::game_rules_moves::MoveRules; +use crate::player::Color; +use crate::CheckerMove; +use crate::Error; + +use log::debug; +use serde::{Deserialize, Serialize}; +use std::cmp; +use std::collections::HashMap; + +#[allow(clippy::enum_variant_names)] +#[derive(PartialEq, Eq, Hash, Clone, Debug, Serialize, Deserialize)] +pub enum Jan { + FilledQuarter, + TrueHitSmallJan, + TrueHitBigJan, + TrueHitOpponentCorner, + FirstPlayerToExit, + SixTables, + TwoTables, + Mezeas, + + FalseHitSmallJan, + FalseHitBigJan, + ContreTwoTables, + ContreMezeas, + HelplessMan, +} + +impl Jan { + pub fn get_points(&self, is_double: bool) -> i8 { + match self { + Self::FalseHitSmallJan | Self::ContreTwoTables | Self::ContreMezeas => { + if is_double { + -6 + } else { + -4 + } + } + Self::FalseHitBigJan | Self::HelplessMan => { + if is_double { + -4 + } else { + -2 + } + } + Self::TrueHitBigJan => { + if is_double { + 4 + } else { + 2 + } + } + _ => { + if is_double { + 6 + } else { + 4 + } + } + } + } +} + +pub type PossibleJans = HashMap>; + +pub trait PossibleJansMethods { + fn push(&mut self, jan: Jan, cmoves: (CheckerMove, CheckerMove)); + fn merge(&mut self, other: Self); + // fn get_points(&self) -> u8; +} + +impl PossibleJansMethods for PossibleJans { + fn push(&mut self, jan: Jan, cmoves: (CheckerMove, CheckerMove)) { + if let Some(ways) = self.get_mut(&jan) { + if !ways.contains(&cmoves) { + ways.push(cmoves); + } + } else { + self.insert(jan, [cmoves].into()); + } + } + + fn merge(&mut self, other: Self) { + for (jan, cmoves_list) in other { + for cmoves in cmoves_list { + // pour un même mouvement, le battage à vrai est prioritaire sur le battage à faux. + match jan { + Jan::FalseHitBigJan => { + let mut has_true_hit = false; + if let Some(true_moves) = self.get(&Jan::TrueHitBigJan) { + has_true_hit = true_moves.contains(&cmoves); + } + if !has_true_hit { + self.push(jan.clone(), cmoves); + } + } + Jan::FalseHitSmallJan => { + let mut has_true_hit = false; + if let Some(true_moves) = self.get(&Jan::TrueHitSmallJan) { + has_true_hit = true_moves.contains(&cmoves); + } + if !has_true_hit { + self.push(jan.clone(), cmoves); + } + } + Jan::TrueHitBigJan => { + if let Some(false_moves) = self.get_mut(&Jan::FalseHitBigJan) { + false_moves.retain(|fmoves| *fmoves != cmoves); + } + self.push(jan.clone(), cmoves); + } + Jan::TrueHitSmallJan => { + if let Some(false_moves) = self.get_mut(&Jan::FalseHitSmallJan) { + false_moves.retain(|fmoves| *fmoves != cmoves); + } + self.push(jan.clone(), cmoves); + } + _ => { + self.push(jan.clone(), cmoves); + } + } + } + } + } +} + +/// PointsRules always consider that the current player is White +/// You must use 'mirror' function on board if player is Black +#[derive(Default)] +pub struct PointsRules { + pub board: Board, + pub dice: Dice, + pub move_rules: MoveRules, +} + +impl PointsRules { + /// Revert board if color is black + pub fn new(color: &Color, board: &Board, dice: Dice) -> Self { + let board = if *color == Color::Black { + board.mirror() + } else { + board.clone() + }; + // the board is already reverted for black, so we pretend color is white + let move_rules = MoveRules::new(&Color::White, &board, dice); + // let move_rules = MoveRules::new(color, &board, dice); + + // let move_rules = MoveRules::new(color, &self.board, dice, moves); + Self { + board, + dice, + move_rules, + } + } + + pub fn set_dice(&mut self, dice: Dice) { + self.dice = dice; + self.move_rules.dice = dice; + } + + pub fn update_positions(&mut self, color: &Color, positions: [i8; 24]) { + self.board.set_positions(color, positions); + self.move_rules.board.set_positions(color, positions); + } + + fn get_jans(&self, board_ini: &Board, dice_rolls_count: u8) -> PossibleJans { + let dices = &[self.dice.values.0, self.dice.values.1]; + let dices_reversed = &[self.dice.values.1, self.dice.values.0]; + let dice1 = self.dice.values.0 as usize; + let dice2 = self.dice.values.1 as usize; + + // « JAN DE RÉCOMPENSE » + // Battre à vrai une dame située dans la table des grands jans + // Battre à vrai une dame située dans la table des petits jans + let mut jans = get_jans_by_ordered_dice(board_ini, dices, None, false); + let jans_revert_dices = get_jans_by_ordered_dice(board_ini, dices_reversed, None, false); + jans.merge(jans_revert_dices); + + // Battre à vrai le coin de repos de l'adversaire + let corner_field = board_ini.get_color_corner(&Color::White); + let adv_corner_field = board_ini.get_color_corner(&Color::Black); + let (adv_corner_count, _color) = board_ini.get_field_checkers(adv_corner_field).unwrap(); + if adv_corner_count == 0 { + let from0 = adv_corner_field - self.dice.values.0 as usize; + let from1 = adv_corner_field - self.dice.values.1 as usize; + + let (from0_count, _from0_color) = board_ini.get_field_checkers(from0).unwrap(); + let (from1_count, _from1_color) = board_ini.get_field_checkers(from1).unwrap(); + let hit_moves = vec![( + CheckerMove::new(from0, adv_corner_field).unwrap(), + CheckerMove::new(from1, adv_corner_field).unwrap(), + )]; + + if from0 == from1 { + // doublet + if from0_count > if from0 == corner_field { 3 } else { 1 } { + jans.insert(Jan::TrueHitOpponentCorner, hit_moves); + } + } else { + // simple + if from0_count > if from0 == corner_field { 2 } else { 0 } + && from1_count > if from1 == corner_field { 2 } else { 0 } + { + jans.insert(Jan::TrueHitOpponentCorner, hit_moves); + } + } + } + + // « JAN DE REMPLISSAGE » + // Faire un petit jan, un grand jan ou un jan de retour + let filling_moves_sequences = self + .move_rules + .get_scoring_quarter_filling_moves_sequences(); + if !filling_moves_sequences.is_empty() { + jans.insert(Jan::FilledQuarter, filling_moves_sequences); + } + + // « AUTRE » + // sortir le premier toutes ses dames + let mut checkers = board_ini.get_color_fields(Color::White); + checkers.sort_by(|a, b| b.0.cmp(&a.0)); + let checkers_count = checkers.iter().fold(0, |acc, (_f, count)| acc + count); + if checkers_count < 3 { + let mut farthest = 24; + let mut next_farthest = 24; + if let Some((field, count)) = checkers.first() { + farthest = *field; + if *count > 1 { + next_farthest = *field; + } else if let Some((field, _count)) = checkers.get(1) { + next_farthest = *field; + } + } + + if farthest + cmp::max(self.dice.values.0, self.dice.values.1) as usize > 23 + && next_farthest + cmp::min(self.dice.values.0, self.dice.values.1) as usize > 23 + { + let exit_moves = vec![( + CheckerMove::new(farthest, 0).unwrap(), + if checkers_count > 1 { + CheckerMove::new(next_farthest, 0).unwrap() + } else { + CheckerMove::new(0, 0).unwrap() + }, + )]; + + jans.insert(Jan::FirstPlayerToExit, exit_moves); + } + } + + // « JANS RARES » + // Jan de 3 coups ou de 6 tables + if dice_rolls_count == 3 { + // on devrait avoir 4 cases occupées par une dame chacune + let fields_with_single: Vec<&(usize, i8)> = + checkers.iter().filter(|(_, c)| c == &1).collect(); + if fields_with_single.len() == 4 { + let checkers_fields: Vec = checkers.iter().map(|(f, _)| *f).collect(); + let mut missing_for_6tables: Vec = Vec::from([2, 3, 4, 5, 6, 7]) + .into_iter() + .filter(|f| !checkers_fields.contains(f)) + .collect(); + if missing_for_6tables.len() == 2 { + // Les dés doivent permettre le mouvement de deux dames du talon vers les 2 cases + // vides + let mut dice_to: Vec = vec![ + 1 + self.dice.values.0 as usize, + 1 + self.dice.values.1 as usize, + ]; + missing_for_6tables.sort(); + dice_to.sort(); + if dice_to == missing_for_6tables { + let moves = vec![( + CheckerMove::new(1, missing_for_6tables[0]).unwrap(), + CheckerMove::new(1, missing_for_6tables[1]).unwrap(), + )]; + jans.insert(Jan::SixTables, moves); + } + } + } + } + + // Jans nécessitant que deux dames uniquement soient sorties du talon + let (talon, candidates): (Vec, Vec) = + checkers.iter().partition(|(field, _count)| field == &1); + let candidates_fields = candidates.iter().fold(vec![], |mut acc, (f, c)| { + acc.extend_from_slice(&vec![*f; *c as usize]); + acc + }); + if !talon.is_empty() && talon[0].1 == 13 && candidates_fields.len() == 2 { + let field1 = candidates_fields[0]; + let field2 = candidates_fields[1]; + + // Jan de 2 tables et contre jan de 2 tables + let jan = if adv_corner_count == 0 { + Jan::TwoTables + } else { + Jan::ContreTwoTables + }; + if (field1 + dice1 == 12 && field2 + dice2 == 13) + || (field1 + dice2 == 12 && field2 + dice1 == 13) + { + let moves = vec![( + CheckerMove::new(field1, 12).unwrap(), + CheckerMove::new(field2, 13).unwrap(), + )]; + jans.insert(jan, moves); + } else if (field1 + dice1 == 13 && field2 + dice2 == 12) + || (field1 + dice2 == 13 && field2 + dice1 == 12) + { + let moves = vec![( + CheckerMove::new(field1, 13).unwrap(), + CheckerMove::new(field2, 12).unwrap(), + )]; + jans.insert(jan, moves); + } + + // Jan de Mezeas et contre jan de Mezeas + let jan = if adv_corner_count == 0 { + Jan::Mezeas + } else { + Jan::ContreMezeas + }; + if field1 == 12 && field2 == 12 && (dice1 == 1 || dice2 == 1) { + let moves = vec![( + CheckerMove::new(field1, field1 + dice1).unwrap(), + CheckerMove::new(field2, field2 + dice2).unwrap(), + )]; + jans.insert(jan, moves); + } + } + + // Jan qui ne peut : dés non jouables + let poss = self.move_rules.get_possible_moves_sequences(true, vec![]); + let moves = poss.iter().fold(vec![], |mut acc, (m1, m2)| { + acc.push(*m1); + acc.push(*m2); + acc + }); + let moves_dice1: Vec<&CheckerMove> = + moves.iter().filter(|m| m.doable_with_dice(dice1)).collect(); + let moves_dice2: Vec<&CheckerMove> = + moves.iter().filter(|m| m.doable_with_dice(dice2)).collect(); + if poss.is_empty() { + jans.insert( + Jan::HelplessMan, + vec![(CheckerMove::default(), CheckerMove::default())], + ); + } else if moves_dice1.is_empty() || moves_dice2.is_empty() { + jans.insert(Jan::HelplessMan, vec![poss[0]]); + } + + jans + } + + pub fn get_jans_points(&self, jans: HashMap>) -> (u8, u8) { + let (points, adv_points) = jans + .into_iter() + .fold((0, 0), |acc: (i8, i8), (jan, moves)| { + // println!("get_points : {:?}", jan); + let is_double = if jan == Jan::HelplessMan { + moves[0] == (CheckerMove::default(), CheckerMove::default()) + } else { + self.dice.is_double() + }; + let points = jan.get_points(is_double) * (moves.len() as i8); + if points < 0 { + (acc.0, acc.1 - points) + } else { + (acc.0 + points, acc.1) + } + }); + + (points as u8, adv_points as u8) + } + + pub fn get_points(&self, dice_rolls_count: u8) -> (u8, u8) { + let jans = self.get_jans(&self.board, dice_rolls_count); + self.get_jans_points(jans) + } + + pub fn get_result_jans(&self, dice_rolls_count: u8) -> (PossibleJans, (u8, u8)) { + let jans = self.get_jans(&self.board, dice_rolls_count); + debug!("jans : {jans:?}"); + let points_jans = jans.clone(); + (jans, self.get_jans_points(points_jans)) + } +} + +fn get_jans_by_ordered_dice( + board_ini: &Board, + // dices: &Vec, + dices: &[u8], + only_from: Option, + only_false_hit: bool, +) -> PossibleJans { + let mut jans = PossibleJans::default(); + let mut dices: Vec = dices.to_vec(); + if let Some(dice) = dices.pop() { + let color = Color::White; + let mut board = board_ini.clone(); + let corner_field = board.get_color_corner(&color); + let adv_corner_field = board.get_color_corner(&Color::Black); + let froms = if let Some(from) = only_from { + vec![from] + } else { + board + .get_color_fields(color) + .iter() + .map(|cf| cf.0) + .collect() + }; + for from in froms { + // for (from, _) in board.get_color_fields(color) { + let to = if from + dice as usize > 24 { + 0 + } else { + from + dice as usize + }; + if let Ok(cmove) = CheckerMove::new(from, to) { + // print!( + // " ", + // dice, from, to + // ); + // On vérifie que le mouvement n'est pas interdit par les règles des coins de + // repos : + // - on ne va pas sur le coin de l'adversaire + // - ni sur son propre coin de repos avec une seule dame + // - règle non prise en compte pour le battage des dames : on ne sort pas de son coin de repos s'il n'y reste que deux dames + let (corner_count, _color) = board.get_field_checkers(corner_field).unwrap(); + if to != adv_corner_field && (to != corner_field || corner_count > 1) { + // si only_false_hit est vrai, on est déja dans une tentative tout d'une + let mut can_try_toutdune = !only_false_hit; + let mut only_falsehit = false; + match board.move_checker(&color, cmove) { + Err(Error::FieldBlockedByOne) => { + let jan = match (Board::is_field_in_small_jan(to), only_false_hit) { + (true, false) => Jan::TrueHitSmallJan, + (true, true) => Jan::FalseHitSmallJan, + (false, false) => Jan::TrueHitBigJan, + (false, true) => Jan::FalseHitBigJan, + }; + jans.push(jan, (cmove, EMPTY_MOVE)); + } + Err(Error::FieldBlocked) => { + only_falsehit = true; + } + Err(_) => { + can_try_toutdune = false; + // let next_dice_jan = self.get_jans(&board, &dices); + // jans possibles en tout d'une après un battage à vrai : + // truehit + } + Ok(()) => {} + } + if can_try_toutdune { + // Try tout d'une : + // - use original board before first die move + // - use a virtual dice by adding current dice to remaining dice + // - limit the checker to the current one + let next_dice_jan = get_jans_by_ordered_dice( + board_ini, + &dices.iter().map(|d| d + dice).collect::>(), + Some(from), + only_falsehit, + ); + jans.merge(next_dice_jan); + } + } + // Second die + let next_dice_jan = get_jans_by_ordered_dice(board_ini, &dices, None, false); + jans.merge(next_dice_jan); + } + } + } + + jans +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn get_jans_by_dice_order() { + let mut rules = PointsRules::default(); + rules.board.set_positions( + &Color::White, + [ + 2, 0, -1, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + + let jans = get_jans_by_ordered_dice(&rules.board, &[2, 3], None, false); + assert_eq!(1, jans.len()); + assert_eq!(3, jans.get(&Jan::TrueHitSmallJan).unwrap().len()); + + let jans = get_jans_by_ordered_dice(&rules.board, &[2, 2], None, false); + assert_eq!(1, jans.len()); + assert_eq!(1, jans.get(&Jan::TrueHitSmallJan).unwrap().len()); + + // On peut passer par une dame battue pour battre une autre dame + // mais pas par une case remplie par l'adversaire + rules.board.set_positions( + &Color::White, + [ + 2, 0, -1, -2, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + + let mut jans = get_jans_by_ordered_dice(&rules.board, &[2, 3], None, false); + let jans_revert_dices = get_jans_by_ordered_dice(&rules.board, &[3, 2], None, false); + assert_eq!(2, jans.len()); + assert_eq!(1, jans_revert_dices.len()); + jans.merge(jans_revert_dices); + assert_eq!(2, jans.get(&Jan::TrueHitSmallJan).unwrap().len()); + + rules.board.set_positions( + &Color::White, + [ + 2, 0, -1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + + let jans = get_jans_by_ordered_dice(&rules.board, &[2, 3], None, false); + assert_eq!(1, jans.len()); + assert_eq!(2, jans.get(&Jan::TrueHitSmallJan).unwrap().len()); + + rules.board.set_positions( + &Color::White, + [ + 2, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + + let jans = get_jans_by_ordered_dice(&rules.board, &[2, 3], None, false); + assert_eq!(1, jans.len()); + assert_eq!(1, jans.get(&Jan::TrueHitSmallJan).unwrap().len()); + + rules.board.set_positions( + &Color::White, + [ + 2, 0, 1, 1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + + let jans = get_jans_by_ordered_dice(&rules.board, &[2, 3], None, false); + assert_eq!(1, jans.len()); + assert_eq!(3, jans.get(&Jan::TrueHitSmallJan).unwrap().len()); + + // corners handling + + // deux dés bloqués (coin de repos et coin de l'adversaire) + rules.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + // le premier dé traité est le dernier du vecteur : 1 + let jans = get_jans_by_ordered_dice(&rules.board, &[2, 1], None, false); + // println!("jans (dés bloqués) : {:?}", jans.get(&Jan::TrueHit)); + assert_eq!(0, jans.len()); + + // dé dans son coin de repos : peut tout de même battre à vrai + rules.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + let jans = get_jans_by_ordered_dice(&rules.board, &[3, 3], None, false); + assert_eq!(1, jans.len()); + + // premier dé bloqué, mais tout d'une possible en commençant par le second + rules.board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + let mut jans = get_jans_by_ordered_dice(&rules.board, &[3, 1], None, false); + let jans_revert_dices = get_jans_by_ordered_dice(&rules.board, &[1, 3], None, false); + assert_eq!(1, jans_revert_dices.len()); + + jans.merge(jans_revert_dices); + assert_eq!(1, jans.len()); + // print!("jans (2) : {:?}", jans.get(&Jan::TrueHit)); + + // battage à faux : ne pas prendre en compte si en inversant l'ordre des dés il y a battage + // à vrai + } + + #[test] + fn get_result_jans() { + let mut board = Board::new(); + board.set_positions( + &Color::White, + [ + 0, 0, 5, 2, 4, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -2, -2, -2, -2, -2, -2, + ], + ); + let points_rules = PointsRules::new(&Color::Black, &board, Dice { values: (2, 4) }); + let jans = points_rules.get_result_jans(8); + assert!(!jans.0.is_empty()); + } + + #[test] + fn get_points() { + // ----- Jan de récompense + // Battre à vrai une dame située dans la table des petits jans : 4 + 4 + 4 = 12 + let mut rules = PointsRules::default(); + rules.update_positions( + &Color::White, + [ + 2, 0, -1, -1, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + rules.set_dice(Dice { values: (2, 3) }); + assert_eq!(12, rules.get_points(5).0); + + // Calcul des points pour noir + let mut board = Board::new(); + board.set_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, -2, + ], + ); + let rules = PointsRules::new(&Color::Black, &board, Dice { values: (2, 3) }); + assert_eq!(12, rules.get_points(5).0); + + // Battre à vrai une dame située dans la table des grands jans : 2 + 2 = 4 + let mut rules = PointsRules::default(); + rules.update_positions( + &Color::White, + [ + 2, 0, 0, -1, 2, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + rules.set_dice(Dice { values: (2, 4) }); + assert_eq!(4, rules.get_points(5).0); + // Battre à vrai une dame située dans la table des grands jans : 2 + let mut rules = PointsRules::default(); + rules.update_positions( + &Color::White, + [ + 2, 0, -2, -1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + rules.set_dice(Dice { values: (2, 4) }); + assert_eq!((2, 2), rules.get_points(5)); + + // Battre à vrai le coin adverse par doublet : 6 + rules.update_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + rules.set_dice(Dice { values: (2, 2) }); + assert_eq!(6, rules.get_points(5).0); + + // Cas de battage du coin de repos adverse impossible + rules.update_positions( + &Color::White, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + rules.set_dice(Dice { values: (1, 1) }); + assert_eq!(0, rules.get_points(5).0); + + // ---- Jan de remplissage + // Faire un petit jan : 4 + rules.update_positions( + &Color::White, + [ + 3, 1, 2, 2, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + rules.set_dice(Dice { values: (2, 1) }); + assert_eq!(1, rules.get_jans(&rules.board, 5).len()); + assert_eq!(4, rules.get_points(5).0); + + // Faire un petit jan avec un doublet : 6 + rules.update_positions( + &Color::White, + [ + 2, 3, 1, 2, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + rules.set_dice(Dice { values: (1, 1) }); + assert_eq!(6, rules.get_points(5).0); + + // Faire un petit jan avec 2 moyens : 6 + 6 = 12 + rules.update_positions( + &Color::White, + [ + 3, 3, 1, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + rules.set_dice(Dice { values: (1, 1) }); + assert_eq!(12, rules.get_points(5).0); + + // Conserver un jan avec un doublet : 6 + rules.update_positions( + &Color::White, + [ + 3, 3, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + rules.set_dice(Dice { values: (1, 1) }); + assert_eq!(6, rules.get_points(5).0); + + // Conserver un jan + rules.update_positions( + &Color::White, + [ + 2, 2, 2, 2, 2, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, -1, -2, -3, -5, 0, -1, + ], + ); + rules.set_dice(Dice { values: (3, 1) }); + assert_eq!((4, 0), rules.get_points(8)); + + // Conserver un jan (black) + let mut board = Board::new(); + board.set_positions( + &Color::White, + [ + 1, 0, 5, 3, 2, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -4, -2, -2, -2, -2, -2, + ], + ); + let rules = PointsRules::new(&Color::Black, &board, Dice { values: (3, 1) }); + assert_eq!((4, 0), rules.get_points(8)); + + // ---- Sorties + // Sortir toutes ses dames avant l'adversaire (simple) + let mut rules = PointsRules::default(); + rules.update_positions( + &Color::White, + [ + 0, 0, -2, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, + ], + ); + rules.set_dice(Dice { values: (3, 1) }); + assert_eq!(4, rules.get_points(5).0); + + // Sortir toutes ses dames avant l'adversaire (doublet) + rules.update_positions( + &Color::White, + [ + 0, 0, -2, 0, 0, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, + ], + ); + rules.set_dice(Dice { values: (2, 2) }); + assert_eq!(6, rules.get_points(5).0); + + // ---- JANS RARES + // Jan de six tables + rules.update_positions( + &Color::White, + [ + 10, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, + ], + ); + rules.set_dice(Dice { values: (2, 3) }); + assert_eq!(0, rules.get_points(5).0); + rules.update_positions( + &Color::White, + [ + 10, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, + ], + ); + rules.set_dice(Dice { values: (2, 3) }); + assert_eq!(4, rules.get_points(3).0); + rules.update_positions( + &Color::White, + [ + 10, 1, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, + ], + ); + rules.set_dice(Dice { values: (2, 3) }); + assert_eq!(0, rules.get_points(3).0); + rules.update_positions( + &Color::White, + [ + 10, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, + ], + ); + rules.set_dice(Dice { values: (2, 3) }); + assert_eq!(0, rules.get_points(3).0); + + // Jan de deux tables + rules.update_positions( + &Color::White, + [ + 13, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, + ], + ); + rules.set_dice(Dice { values: (2, 2) }); + assert_eq!(6, rules.get_points(5).0); + rules.update_positions( + &Color::White, + [ + 12, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, + ], + ); + rules.set_dice(Dice { values: (2, 2) }); + assert_eq!(0, rules.get_points(5).0); + + // Contre jan de deux tables + rules.update_positions( + &Color::White, + [ + 13, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, + ], + ); + rules.set_dice(Dice { values: (2, 2) }); + assert_eq!((0, 6), rules.get_points(5)); + + // Jan de mézéas + rules.update_positions( + &Color::White, + [ + 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, + ], + ); + rules.set_dice(Dice { values: (1, 1) }); + assert_eq!(6, rules.get_points(5).0); + rules.update_positions( + &Color::White, + [ + 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, + ], + ); + rules.set_dice(Dice { values: (1, 2) }); + assert_eq!(4, rules.get_points(5).0); + + // Contre jan de mézéas + rules.update_positions( + &Color::White, + [ + 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, + ], + ); + rules.set_dice(Dice { values: (1, 1) }); + assert_eq!((0, 6), rules.get_points(5)); + + // ---- JANS QUI NE PEUT + // Battre à faux une dame située dans la table des petits jans + let mut rules = PointsRules::default(); + rules.update_positions( + &Color::White, + [ + 2, 0, -2, -2, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + rules.set_dice(Dice { values: (2, 3) }); + assert_eq!((0, 4), rules.get_points(5)); + + // Battre à faux une dame située dans la table des grands jans + let mut rules = PointsRules::default(); + rules.update_positions( + &Color::White, + [ + 2, 0, -2, -1, -2, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + rules.set_dice(Dice { values: (2, 4) }); + assert_eq!((0, 2), rules.get_points(5)); + + // Pour chaque dé non jouable (dame impuissante) + let mut rules = PointsRules::default(); + rules.update_positions( + &Color::White, + [ + 2, 0, -2, -2, -2, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ); + rules.set_dice(Dice { values: (2, 4) }); + assert_eq!((0, 4), rules.get_points(5)); + } +} diff --git a/store/src/lib.rs b/store/src/lib.rs index 12f17b4..58a5727 100644 --- a/store/src/lib.rs +++ b/store/src/lib.rs @@ -1,5 +1,9 @@ mod game; -pub use game::{EndGameReason, GameEvent, GameState, Stage}; +mod game_rules_moves; +pub use game_rules_moves::MoveRules; +mod game_rules_points; +pub use game::{EndGameReason, GameEvent, GameState, Stage, TurnStage}; +pub use game_rules_points::PointsRules; mod player; pub use player::{Color, Player, PlayerId}; @@ -11,3 +15,4 @@ mod board; pub use board::CheckerMove; mod dice; +pub use dice::{Dice, DiceRoller}; diff --git a/store/src/player.rs b/store/src/player.rs index d728c66..d42120b 100644 --- a/store/src/player.rs +++ b/store/src/player.rs @@ -4,14 +4,23 @@ use std::fmt; // This just makes it easier to dissern between a player id and any ol' u64 pub type PlayerId = u64; -#[derive(Copy, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Copy, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Color { White, Black, } +impl Color { + pub fn opponent_color(&self) -> Self { + match self { + Self::White => Self::Black, + Self::Black => Self::White, + } + } +} + /// Struct for storing player related data. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Player { pub name: String, pub color: Color, @@ -19,6 +28,9 @@ pub struct Player { pub holes: u8, pub can_bredouille: bool, pub can_big_bredouille: bool, + /// Number of dice rolls since beginning of the current setting (all 15 dames in the talon ) + /// (used to check jan de 3 coups) + pub dice_roll_count: u8, } impl Player { @@ -30,6 +42,7 @@ impl Player { holes: 0, can_bredouille: true, can_big_bredouille: true, + dice_roll_count: 0, } } @@ -39,6 +52,15 @@ impl Player { self.points, self.holes, self.can_bredouille as u8, self.can_big_bredouille as u8 ) } + + pub fn to_vec(&self) -> Vec { + vec![ + self.points, + self.holes, + self.can_bredouille as u8, + self.can_big_bredouille as u8, + ] + } } /// Represents a player in the game. @@ -63,6 +85,7 @@ pub enum CurrentPlayer { impl CurrentPlayer { /// Returns the other player, i.e. the player who is not the current player. + #[cfg(test)] pub fn other(&self) -> Self { match *self { CurrentPlayer::Nobody => CurrentPlayer::Nobody, @@ -104,6 +127,7 @@ mod tests { holes: 3, can_bredouille: true, can_big_bredouille: false, + dice_roll_count: 0, }; println!("{}", player.to_bits_string()); assert!(player.to_bits_string() == "1011001110");