From 172948fc1774e99d5e0b3a3c315dad48c4875431 Mon Sep 17 00:00:00 2001 From: Viliam Vadocz Date: Wed, 16 Jul 2025 23:58:48 +0200 Subject: [PATCH 1/6] Add MVP of visualizer --- Cargo.lock | 691 +++++++++++++++++++++++++++++++++++++--------- Cargo.toml | 5 + src/cli.rs | 6 + src/game.rs | 27 ++ src/main.rs | 2 + src/tournament.rs | 73 ++++- src/visualizer.rs | 13 + visualizer.html | 55 ++++ 8 files changed, 729 insertions(+), 143 deletions(-) create mode 100644 src/visualizer.rs create mode 100644 visualizer.html diff --git a/Cargo.lock b/Cargo.lock index f9e3c53..b1bb498 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "android-tzdata" @@ -19,69 +19,80 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", + "once_cell_polyfill", "windows-sys", ] [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "autocfg" -version = "1.2.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "block-buffer" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] [[package]] name = "board-game-traits" @@ -96,61 +107,70 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "552c3faebbf05f0aebf152b9c90d76732ffac8bfead18786aa9ce3c5aae706a7" dependencies = [ "num-traits", - "thiserror", + "thiserror 1.0.69", ] [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.0.96" +version = "1.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" +checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[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 = "chrono" -version = "0.4.38" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-targets", + "windows-link", ] [[package]] name = "clap" -version = "4.5.4" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" dependencies = [ "anstream", "anstyle", @@ -160,24 +180,24 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "color-print" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee543c60ff3888934877a5671f45494dd27ed4ba25c6670b9a7576b7ed7a8c0" +checksum = "3aa954171903797d5623e047d9ab69d91b493657917bdfb8c2c80ecaf9cdb6f4" dependencies = [ "color-print-proc-macro", ] [[package]] name = "color-print-proc-macro" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ff1a80c5f3cb1ca7c06ffdd71b6a6dd6d8f896c42141fbd43f50ed28dcdb93" +checksum = "692186b5ebe54007e45a59aea47ece9eb4108e141326c304cdc91699a7118a22" dependencies = [ "nom", "proc-macro2", @@ -187,32 +207,67 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] [[package]] name = "ctrlc" -version = "3.4.4" +version = "3.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345" +checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73" dependencies = [ "nix", "windows-sys", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "fern" version = "0.6.2" @@ -222,40 +277,86 @@ dependencies = [ "log", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" -version = "0.2.14" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", - "wasi", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] name = "half" -version = "2.4.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", "num-traits", ] +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -267,38 +368,76 @@ dependencies = [ "cc", ] +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + [[package]] name = "libc" -version = "0.2.154" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libm" -version = "0.2.8" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "log" -version = "0.4.21" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "minimal-lexical" @@ -308,9 +447,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "nix" -version = "0.28.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags", "cfg-if", @@ -339,9 +478,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -349,9 +488,32 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "open" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pgn-traits" @@ -364,28 +526,37 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "racetrack" version = "0.2.1" @@ -397,11 +568,14 @@ dependencies = [ "color-print", "ctrlc", "fern", + "json", "log", + "open", "pgn-traits", - "rand", + "rand 0.8.5", "rand_distr", "tiltak", + "tungstenite", ] [[package]] @@ -411,8 +585,18 @@ 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", +] + +[[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]] @@ -422,7 +606,17 @@ 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]] @@ -431,7 +625,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", +] + +[[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]] @@ -441,9 +644,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ "num-traits", - "rand", + "rand 0.8.5", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[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 = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "strsim" version = "0.11.1" @@ -452,9 +678,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.60" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -463,32 +689,51 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.30.11" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87341a165d73787554941cd5ef55ad728011566fe714e987d1b976c15dbc3a83" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" dependencies = [ - "cfg-if", "core-foundation-sys", "libc", + "memchr", "ntapi", - "once_cell", "windows", ] [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "thiserror-impl", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -498,7 +743,7 @@ dependencies = [ [[package]] name = "tiltak" version = "0.1.0" -source = "git+https://github.com/MortenLohne/tiltak#c193cc880561fa01e972760f2257bf968e6e1716" +source = "git+https://github.com/MortenLohne/tiltak#1a970d76bc3b6cbfe19319dd97534c973a9a520b" dependencies = [ "arrayvec", "board-game-traits", @@ -507,48 +752,93 @@ dependencies = [ "log", "num-traits", "pgn-traits", - "rand", + "rand 0.8.5", "rand_distr", "sysinfo", ] +[[package]] +name = "tungstenite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.1", + "sha1", + "thiserror 2.0.12", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -557,9 +847,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -567,9 +857,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -580,9 +870,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "winapi" @@ -608,37 +901,130 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.52.0" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ - "windows-core", + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", "windows-targets", ] [[package]] name = "windows-core" -version = "0.52.0" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.4", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ "windows-targets", ] +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -652,48 +1038,77 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[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.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 59f42c8..9ab9b0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,8 @@ bpci = "0.1.0-beta.7" rand_distr = "0.4.3" color-print = "0.3.5" ctrlc = "3.4.2" +# TODO: Consider putting these dependencies behind a feature flag +# since they are only used by the visualizer. +open = "5.3.2" +tungstenite = "0.27.0" +json = "0.12.4" diff --git a/src/cli.rs b/src/cli.rs index ba58ba6..a8c7ee0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -23,6 +23,7 @@ pub struct CliOptions { pub komi: Komi, pub tournament_type: TournamentType, pub sprt: Option, + pub visualize: bool, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -155,6 +156,10 @@ pub fn parse_cli_arguments_from( .value_name("options") .num_args(0..) .action(ArgAction::Append)) + .arg(Arg::new("visualize") + .long("visualize") + .help("Visualize the match using the PTN-Ninja API") + .action(ArgAction::SetTrue)) .try_get_matches_from(itr)?; let engines: Vec = matches @@ -432,5 +437,6 @@ pub fn parse_cli_arguments_from( komi: *matches.get_one::("komi").unwrap(), tournament_type, sprt, + visualize: matches.get_flag("visualize"), }) } diff --git a/src/game.rs b/src/game.rs index fd42f7c..5abd2dd 100644 --- a/src/game.rs +++ b/src/game.rs @@ -3,11 +3,13 @@ use crate::openings::Opening; use crate::tournament::{EngineId, Worker}; use crate::uci::parser::parse_info_string; use crate::uci::UciInfo; +use crate::visualizer; use board_game_traits::Color; use chrono::{Datelike, Local}; use log::{error, warn}; use pgn_traits::PgnPosition; use std::fmt::Write; +use std::sync::mpsc; use std::time::Instant; use std::{io, thread}; use tiltak::position::Komi; @@ -34,12 +36,31 @@ impl ScheduledGame { self, worker: &mut Worker, position_settings: &B::Settings, + tx: Option>>>>, // FIXME: Long type ) -> io::Result> { + let visualize = tx.is_some(); + let (mini_tx, mini_rx) = mpsc::channel(); + if let Some(tx) = tx.as_ref() { + open::that("visualizer.html").unwrap(); // HACK: Relies on the file being there + tx.send(mini_rx).unwrap(); + } else { + drop(mini_rx); + } + let mut position = B::from_fen_with_settings(&self.opening.root_position.to_fen(), position_settings) .unwrap(); let white = self.white_engine_id.0; let black = self.black_engine_id.0; + if visualize { + mini_tx + .send(visualizer::Message::Start { + white: worker.engines[white].name().to_string(), + black: worker.engines[black].name().to_string(), + root_position: position.clone(), + }) + .unwrap(); + } let mut moves: Vec> = self .opening @@ -54,6 +75,9 @@ impl ScheduledGame { for PtnMove { mv, .. } in moves.iter() { position.do_move(mv.clone()); + if visualize { + mini_tx.send(visualizer::Message::Ply(mv.clone())).unwrap(); + } } worker.engines[white].uci_write_line(&format!("teinewgame {}", self.size))?; @@ -164,6 +188,9 @@ impl ScheduledGame { ); } position.do_move(mv.clone()); + if visualize { + mini_tx.send(visualizer::Message::Ply(mv.clone())).unwrap(); + } let score_string = match last_uci_info { Some(uci_info) => format!( diff --git a/src/main.rs b/src/main.rs index 63b4f06..cf584a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,7 @@ mod sprt; mod tests; mod tournament; pub mod uci; +mod visualizer; fn main() -> Result<()> { let cli_args = cli::parse_cli_arguments(); @@ -146,6 +147,7 @@ fn run_match( pgn_writer: Mutex::new(pgnout), tournament_type: cli_args.tournament_type, sprt: cli_args.sprt, + visualize: cli_args.visualize, }; let tournament = Tournament::new(settings); diff --git a/src/tournament.rs b/src/tournament.rs index f4139a3..7063db7 100644 --- a/src/tournament.rs +++ b/src/tournament.rs @@ -4,7 +4,7 @@ use crate::openings::Opening; use crate::pgn_writer::PgnWriter; use crate::simulation::MatchScore; use crate::sprt::{PentanomialResult, SprtParameters}; -use crate::{exit_with_error, simulation}; +use crate::{exit_with_error, simulation, visualizer}; use board_game_traits::GameResult::*; use pgn_traits::PgnPosition; use std::num::NonZeroUsize; @@ -53,6 +53,7 @@ pub struct TournamentSettings { pub pgn_writer: Mutex>, pub tournament_type: TournamentType, pub sprt: Option, + pub visualize: bool, } impl fmt::Debug for TournamentSettings { @@ -146,12 +147,13 @@ pub struct Tournament { pgn_writer: Mutex>, tournament_type: TournamentType, sprt: Option, + visualize: bool, } impl Tournament where B: PgnPosition + Clone + Send + 'static, - B::Move: Send, + B::Move: Send + std::fmt::Display, B::Settings: Send + Sync, { pub fn new(settings: TournamentSettings) -> Self { @@ -167,6 +169,7 @@ where pgn_writer: settings.pgn_writer, tournament_type: settings.tournament_type, sprt: settings.sprt, + visualize: settings.visualize, } } @@ -222,6 +225,59 @@ where }) .collect(); + // We create a channel to send per-game `Receiver`s to the WebSocket server thread. + let (tx, rx) = std::sync::mpsc::channel(); + if self.visualize { + // TODO: Maybe move this to `visualizer.rs` for easier to read code? + Builder::new() + .name("WebSocket Server".to_string()) + .spawn(move || { + let server = std::net::TcpListener::bind(format!("127.0.0.1:{}", visualizer::PORT)).unwrap(); + // Every time the visualizer window is opened, we get a new connection. + for (i, stream) in server.incoming().enumerate() { + // Get the move receiver for that game. + let move_rx: std::sync::mpsc::Receiver> = + rx.recv().unwrap(); + Builder::new() + .name(format!("WebSocket Move Relay #{i}")) + .spawn(move || { + let mut websocket = tungstenite::accept(stream.unwrap()).unwrap(); + let Ok(visualizer::Message::Start { + white, + black, + root_position, + }) = move_rx.recv() + else { + // TODO: Maybe do this with types? Have a one-shot that sends a Start, and with that a new channel that only sends moves? + panic!("We should have received a Start message."); + }; + let tps = root_position.to_fen(); + websocket + .send(tungstenite::Message::Text( + json::object! { + action: "SET_CURRENT_PTN", + value: format!("[Player1 \"{white}\"]\n[Player2 \"{black}\"]\n[TPS \"{tps}\"]"), + }.to_string().into() + )) + .unwrap(); + while let Ok(visualizer::Message::Ply(mv)) = move_rx.recv() { + // TODO: Evals + websocket.send(tungstenite::Message::Text( + json::object! { + action: "INSERT_PLY", + value: mv.to_string(), + }.to_string().into() + )).unwrap(); + } + }) + .unwrap(); + } + }) + .unwrap(); + } else { + drop(rx); + } + let visualize_tx = Arc::new(tx); let tournament_arc = Arc::new(self); println!( @@ -239,6 +295,7 @@ where .into_iter() .map(|mut worker| { let thread_tournament = tournament_arc.clone(); + let thread_visualize_tx = visualize_tx.clone(); let engine_names = engine_names.clone(); Builder::new() .name(format!("#{}", worker.id)) // Note: The threads' names are used for logging @@ -248,9 +305,15 @@ where break; } let round_number = scheduled_game.round_number; - let game = match scheduled_game - .play_game(&mut worker, &thread_tournament.position_settings) - { + let game = match scheduled_game.play_game( + &mut worker, + &thread_tournament.position_settings, + if thread_tournament.visualize { + Some(thread_visualize_tx.clone()) + } else { + None + }, + ) { Ok(game) => game, // If an error occurs that wasn't handled in play_game(), soft-abort the match // and write a dummy game to the pgn output, so that later games won't be held up diff --git a/src/visualizer.rs b/src/visualizer.rs new file mode 100644 index 0000000..1cfe581 --- /dev/null +++ b/src/visualizer.rs @@ -0,0 +1,13 @@ +use pgn_traits::PgnPosition; + +// The value must be the same as in `visualizer.html`. +pub const PORT: u16 = 30564; + +pub enum Message { + Start { + white: String, + black: String, + root_position: P, + }, + Ply(P::Move), +} diff --git a/visualizer.html b/visualizer.html new file mode 100644 index 0000000..fb0e82e --- /dev/null +++ b/visualizer.html @@ -0,0 +1,55 @@ + + + + + + + Racetrack Visualizer + + + + + + + + + + \ No newline at end of file From 57c1b4fee15ae699b50b5554d7b76ed01ff1f42f Mon Sep 17 00:00:00 2001 From: Viliam Vadocz Date: Thu, 17 Jul 2025 00:23:49 +0200 Subject: [PATCH 2/6] Move the WebSocket server out of tournament.rs --- src/tournament.rs | 55 ++++------------------------------------------ src/visualizer.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 51 deletions(-) diff --git a/src/tournament.rs b/src/tournament.rs index 7063db7..97d148d 100644 --- a/src/tournament.rs +++ b/src/tournament.rs @@ -228,52 +228,7 @@ where // We create a channel to send per-game `Receiver`s to the WebSocket server thread. let (tx, rx) = std::sync::mpsc::channel(); if self.visualize { - // TODO: Maybe move this to `visualizer.rs` for easier to read code? - Builder::new() - .name("WebSocket Server".to_string()) - .spawn(move || { - let server = std::net::TcpListener::bind(format!("127.0.0.1:{}", visualizer::PORT)).unwrap(); - // Every time the visualizer window is opened, we get a new connection. - for (i, stream) in server.incoming().enumerate() { - // Get the move receiver for that game. - let move_rx: std::sync::mpsc::Receiver> = - rx.recv().unwrap(); - Builder::new() - .name(format!("WebSocket Move Relay #{i}")) - .spawn(move || { - let mut websocket = tungstenite::accept(stream.unwrap()).unwrap(); - let Ok(visualizer::Message::Start { - white, - black, - root_position, - }) = move_rx.recv() - else { - // TODO: Maybe do this with types? Have a one-shot that sends a Start, and with that a new channel that only sends moves? - panic!("We should have received a Start message."); - }; - let tps = root_position.to_fen(); - websocket - .send(tungstenite::Message::Text( - json::object! { - action: "SET_CURRENT_PTN", - value: format!("[Player1 \"{white}\"]\n[Player2 \"{black}\"]\n[TPS \"{tps}\"]"), - }.to_string().into() - )) - .unwrap(); - while let Ok(visualizer::Message::Ply(mv)) = move_rx.recv() { - // TODO: Evals - websocket.send(tungstenite::Message::Text( - json::object! { - action: "INSERT_PLY", - value: mv.to_string(), - }.to_string().into() - )).unwrap(); - } - }) - .unwrap(); - } - }) - .unwrap(); + visualizer::run_websocket_server(rx); } else { drop(rx); } @@ -308,11 +263,9 @@ where let game = match scheduled_game.play_game( &mut worker, &thread_tournament.position_settings, - if thread_tournament.visualize { - Some(thread_visualize_tx.clone()) - } else { - None - }, + thread_tournament + .visualize + .then_some(thread_visualize_tx.clone()), ) { Ok(game) => game, // If an error occurs that wasn't handled in play_game(), soft-abort the match diff --git a/src/visualizer.rs b/src/visualizer.rs index 1cfe581..d3afe9c 100644 --- a/src/visualizer.rs +++ b/src/visualizer.rs @@ -1,3 +1,6 @@ +use std::sync::mpsc::Receiver; +use std::thread::Builder; + use pgn_traits::PgnPosition; // The value must be the same as in `visualizer.html`. @@ -11,3 +14,56 @@ pub enum Message { }, Ply(P::Move), } + +pub fn run_websocket_server

(rx: Receiver>>) +where + P: PgnPosition + Send + 'static, + P::Move: std::fmt::Display + Send, +{ + // TODO: Maybe move this to `visualizer.rs` for easier to read code? + Builder::new() + .name("WebSocket Server".to_string()) + .spawn(move || { + let server = std::net::TcpListener::bind(format!("127.0.0.1:{PORT}")).unwrap(); + // Every time the visualizer window is opened, we get a new connection. + for (i, stream) in server.incoming().enumerate() { + // Get the move receiver for that game. + let move_rx: std::sync::mpsc::Receiver> = + rx.recv().unwrap(); + Builder::new() + .name(format!("WebSocket Move Relay #{i}")) + .spawn(move || { + let mut websocket = tungstenite::accept(stream.unwrap()).unwrap(); + let Ok(Message::Start { + white, + black, + root_position, + }) = move_rx.recv() + else { + // TODO: Maybe do this with types? Have a one-shot that sends a Start, and with that a new channel that only sends moves? + panic!("We should have received a Start message."); + }; + let tps = root_position.to_fen(); + websocket + .send(tungstenite::Message::Text( + json::object! { + action: "SET_CURRENT_PTN", + value: format!("[Player1 \"{white}\"]\n[Player2 \"{black}\"]\n[TPS \"{tps}\"]"), + }.to_string().into() + )) + .unwrap(); + while let Ok(Message::Ply(mv)) = move_rx.recv() { + // TODO: Evals + websocket.send(tungstenite::Message::Text( + json::object! { + action: "INSERT_PLY", + value: mv.to_string(), + }.to_string().into() + )).unwrap(); + } + }) + .unwrap(); + } + }) + .unwrap(); +} From 44605e703e74795e4b67cdf3ec2148ed44615889 Mon Sep 17 00:00:00 2001 From: Viliam Vadocz Date: Thu, 17 Jul 2025 00:25:01 +0200 Subject: [PATCH 3/6] Rename visualizer module to visualize --- src/game.rs | 10 +++++----- src/main.rs | 2 +- src/tournament.rs | 4 ++-- src/{visualizer.rs => visualize.rs} | 0 4 files changed, 8 insertions(+), 8 deletions(-) rename src/{visualizer.rs => visualize.rs} (100%) diff --git a/src/game.rs b/src/game.rs index 5abd2dd..039cdf0 100644 --- a/src/game.rs +++ b/src/game.rs @@ -3,7 +3,7 @@ use crate::openings::Opening; use crate::tournament::{EngineId, Worker}; use crate::uci::parser::parse_info_string; use crate::uci::UciInfo; -use crate::visualizer; +use crate::visualize; use board_game_traits::Color; use chrono::{Datelike, Local}; use log::{error, warn}; @@ -36,7 +36,7 @@ impl ScheduledGame { self, worker: &mut Worker, position_settings: &B::Settings, - tx: Option>>>>, // FIXME: Long type + tx: Option>>>>, // FIXME: Long type ) -> io::Result> { let visualize = tx.is_some(); let (mini_tx, mini_rx) = mpsc::channel(); @@ -54,7 +54,7 @@ impl ScheduledGame { let black = self.black_engine_id.0; if visualize { mini_tx - .send(visualizer::Message::Start { + .send(visualize::Message::Start { white: worker.engines[white].name().to_string(), black: worker.engines[black].name().to_string(), root_position: position.clone(), @@ -76,7 +76,7 @@ impl ScheduledGame { for PtnMove { mv, .. } in moves.iter() { position.do_move(mv.clone()); if visualize { - mini_tx.send(visualizer::Message::Ply(mv.clone())).unwrap(); + mini_tx.send(visualize::Message::Ply(mv.clone())).unwrap(); } } @@ -189,7 +189,7 @@ impl ScheduledGame { } position.do_move(mv.clone()); if visualize { - mini_tx.send(visualizer::Message::Ply(mv.clone())).unwrap(); + mini_tx.send(visualize::Message::Ply(mv.clone())).unwrap(); } let score_string = match last_uci_info { diff --git a/src/main.rs b/src/main.rs index cf584a3..2d11292 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,7 @@ mod sprt; mod tests; mod tournament; pub mod uci; -mod visualizer; +mod visualize; fn main() -> Result<()> { let cli_args = cli::parse_cli_arguments(); diff --git a/src/tournament.rs b/src/tournament.rs index 97d148d..951ccd8 100644 --- a/src/tournament.rs +++ b/src/tournament.rs @@ -4,7 +4,7 @@ use crate::openings::Opening; use crate::pgn_writer::PgnWriter; use crate::simulation::MatchScore; use crate::sprt::{PentanomialResult, SprtParameters}; -use crate::{exit_with_error, simulation, visualizer}; +use crate::{exit_with_error, simulation, visualize}; use board_game_traits::GameResult::*; use pgn_traits::PgnPosition; use std::num::NonZeroUsize; @@ -228,7 +228,7 @@ where // We create a channel to send per-game `Receiver`s to the WebSocket server thread. let (tx, rx) = std::sync::mpsc::channel(); if self.visualize { - visualizer::run_websocket_server(rx); + visualize::run_websocket_server(rx); } else { drop(rx); } diff --git a/src/visualizer.rs b/src/visualize.rs similarity index 100% rename from src/visualizer.rs rename to src/visualize.rs From eba6d85b9b74a40d941c8f15ebad84775f0f81a9 Mon Sep 17 00:00:00 2001 From: Viliam Vadocz Date: Thu, 17 Jul 2025 01:16:08 +0200 Subject: [PATCH 4/6] Marginally improve error messages --- src/game.rs | 30 +++++++++++++++++++-------- src/tournament.rs | 4 +--- src/visualize.rs | 53 ++++++++++++++++++++++++++++++----------------- 3 files changed, 56 insertions(+), 31 deletions(-) diff --git a/src/game.rs b/src/game.rs index 039cdf0..fdc3a01 100644 --- a/src/game.rs +++ b/src/game.rs @@ -39,12 +39,14 @@ impl ScheduledGame { tx: Option>>>>, // FIXME: Long type ) -> io::Result> { let visualize = tx.is_some(); - let (mini_tx, mini_rx) = mpsc::channel(); + let (move_tx, move_rx) = mpsc::channel(); if let Some(tx) = tx.as_ref() { - open::that("visualizer.html").unwrap(); // HACK: Relies on the file being there - tx.send(mini_rx).unwrap(); - } else { - drop(mini_rx); + // HACK: Relies on the file being there + open::that("visualizer.html") + .expect("`visualizer.html` should be in the working directory."); + tx.send(move_rx).expect( + "The WebSocket server thread should still be alive to receive the move receiver.", + ); } let mut position = @@ -53,13 +55,13 @@ impl ScheduledGame { let white = self.white_engine_id.0; let black = self.black_engine_id.0; if visualize { - mini_tx + move_tx .send(visualize::Message::Start { white: worker.engines[white].name().to_string(), black: worker.engines[black].name().to_string(), root_position: position.clone(), }) - .unwrap(); + .expect("Sub-thread should be alive."); } let mut moves: Vec> = self @@ -76,7 +78,12 @@ impl ScheduledGame { for PtnMove { mv, .. } in moves.iter() { position.do_move(mv.clone()); if visualize { - mini_tx.send(visualize::Message::Ply(mv.clone())).unwrap(); + move_tx + .send(visualize::Message::Ply { + mv: mv.clone(), + eval: None, + }) + .expect("Sub-thread should be alive."); } } @@ -189,7 +196,12 @@ impl ScheduledGame { } position.do_move(mv.clone()); if visualize { - mini_tx.send(visualize::Message::Ply(mv.clone())).unwrap(); + move_tx + .send(visualize::Message::Ply { + mv: mv.clone(), + eval: None, + }) + .expect("Sub-thread should be alive."); } let score_string = match last_uci_info { diff --git a/src/tournament.rs b/src/tournament.rs index 951ccd8..221b827 100644 --- a/src/tournament.rs +++ b/src/tournament.rs @@ -225,12 +225,10 @@ where }) .collect(); - // We create a channel to send per-game `Receiver`s to the WebSocket server thread. + // Channel for sending per-game move `Receiver`s to the WebSocket server thread. let (tx, rx) = std::sync::mpsc::channel(); if self.visualize { visualize::run_websocket_server(rx); - } else { - drop(rx); } let visualize_tx = Arc::new(tx); let tournament_arc = Arc::new(self); diff --git a/src/visualize.rs b/src/visualize.rs index d3afe9c..3615037 100644 --- a/src/visualize.rs +++ b/src/visualize.rs @@ -4,7 +4,7 @@ use std::thread::Builder; use pgn_traits::PgnPosition; // The value must be the same as in `visualizer.html`. -pub const PORT: u16 = 30564; +const PORT: u16 = 30564; pub enum Message { Start { @@ -12,7 +12,10 @@ pub enum Message { black: String, root_position: P, }, - Ply(P::Move), + Ply { + mv: P::Move, + eval: Option, // -100 to 100 + }, } pub fn run_websocket_server

(rx: Receiver>>) @@ -20,47 +23,59 @@ where P: PgnPosition + Send + 'static, P::Move: std::fmt::Display + Send, { - // TODO: Maybe move this to `visualizer.rs` for easier to read code? Builder::new() .name("WebSocket Server".to_string()) .spawn(move || { - let server = std::net::TcpListener::bind(format!("127.0.0.1:{PORT}")).unwrap(); + let server = std::net::TcpListener::bind(format!("127.0.0.1:{PORT}")) + .expect("The port should be available for creating a TCP listener."); // Every time the visualizer window is opened, we get a new connection. for (i, stream) in server.incoming().enumerate() { // Get the move receiver for that game. - let move_rx: std::sync::mpsc::Receiver> = - rx.recv().unwrap(); + let move_rx = rx.recv().expect("The game thread should send the move receiver soon after opening the window."); Builder::new() .name(format!("WebSocket Move Relay #{i}")) - .spawn(move || { - let mut websocket = tungstenite::accept(stream.unwrap()).unwrap(); - let Ok(Message::Start { + .spawn(move || -> Result<(), tungstenite::Error> { + let mut websocket = tungstenite::accept(stream.unwrap()) + .expect("The incoming connection should be using `ws` instead of `wss`."); + + // Initialize the game from the first message. + let (white, black, tps) = match move_rx.recv() { + Ok(Message::Start { white, black, root_position, - }) = move_rx.recv() - else { - // TODO: Maybe do this with types? Have a one-shot that sends a Start, and with that a new channel that only sends moves? - panic!("We should have received a Start message."); + }) => (white, black, root_position.to_fen()), + Ok(_) => panic!("The first message sent should be a Start message."), + Err(_) => panic!("The game thread should still be alive.") }; - let tps = root_position.to_fen(); + // TODO: Get Komi somehow websocket .send(tungstenite::Message::Text( json::object! { action: "SET_CURRENT_PTN", value: format!("[Player1 \"{white}\"]\n[Player2 \"{black}\"]\n[TPS \"{tps}\"]"), }.to_string().into() - )) - .unwrap(); - while let Ok(Message::Ply(mv)) = move_rx.recv() { - // TODO: Evals + ))?; + + // Forward moves from the game to the browser. + while let Ok(Message::Ply{ mv, eval }) = move_rx.recv() { websocket.send(tungstenite::Message::Text( json::object! { action: "INSERT_PLY", value: mv.to_string(), }.to_string().into() - )).unwrap(); + ))?; + if let Some(cp) = eval { + websocket.send(tungstenite::Message::Text( + json::object! { + action: "SET_EVAL", + value: cp, + }.to_string().into() + ))? + } } + + Ok(()) }) .unwrap(); } From 784d73816377bb3453180a913f55d2f7054a53b7 Mon Sep 17 00:00:00 2001 From: Viliam Vadocz Date: Thu, 17 Jul 2025 01:29:13 +0200 Subject: [PATCH 5/6] Access komi by implementing a Visualize trait --- src/tournament.rs | 6 +++--- src/visualize.rs | 22 +++++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/tournament.rs b/src/tournament.rs index 221b827..5cbe590 100644 --- a/src/tournament.rs +++ b/src/tournament.rs @@ -152,8 +152,8 @@ pub struct Tournament { impl Tournament where - B: PgnPosition + Clone + Send + 'static, - B::Move: Send + std::fmt::Display, + B: PgnPosition + Clone + Send + 'static + visualize::Visualize, + B::Move: Send, B::Settings: Send + Sync, { pub fn new(settings: TournamentSettings) -> Self { @@ -228,7 +228,7 @@ where // Channel for sending per-game move `Receiver`s to the WebSocket server thread. let (tx, rx) = std::sync::mpsc::channel(); if self.visualize { - visualize::run_websocket_server(rx); + B::run_websocket_server(rx); } let visualize_tx = Arc::new(tx); let tournament_arc = Arc::new(self); diff --git a/src/visualize.rs b/src/visualize.rs index 3615037..c627217 100644 --- a/src/visualize.rs +++ b/src/visualize.rs @@ -2,6 +2,7 @@ use std::sync::mpsc::Receiver; use std::thread::Builder; use pgn_traits::PgnPosition; +use tiltak::position::Position; // The value must be the same as in `visualizer.html`. const PORT: u16 = 30564; @@ -18,12 +19,14 @@ pub enum Message { }, } -pub fn run_websocket_server

(rx: Receiver>>) -where - P: PgnPosition + Send + 'static, - P::Move: std::fmt::Display + Send, -{ - Builder::new() +// HACK: Workaround to get access to komi. +pub trait Visualize: PgnPosition { + fn run_websocket_server(rx: Receiver>>); +} + +impl Visualize for Position { + fn run_websocket_server(rx: Receiver>>>) { + Builder::new() .name("WebSocket Server".to_string()) .spawn(move || { let server = std::net::TcpListener::bind(format!("127.0.0.1:{PORT}")) @@ -39,12 +42,12 @@ where .expect("The incoming connection should be using `ws` instead of `wss`."); // Initialize the game from the first message. - let (white, black, tps) = match move_rx.recv() { + let (white, black, tps, komi) = match move_rx.recv() { Ok(Message::Start { white, black, root_position, - }) => (white, black, root_position.to_fen()), + }) => (white, black, root_position.to_fen(), root_position.komi()), Ok(_) => panic!("The first message sent should be a Start message."), Err(_) => panic!("The game thread should still be alive.") }; @@ -53,7 +56,7 @@ where .send(tungstenite::Message::Text( json::object! { action: "SET_CURRENT_PTN", - value: format!("[Player1 \"{white}\"]\n[Player2 \"{black}\"]\n[TPS \"{tps}\"]"), + value: format!("[Player1 \"{white}\"][Player2 \"{black}\"][TPS \"{tps}\"][Komi \"{komi}\"]"), }.to_string().into() ))?; @@ -81,4 +84,5 @@ where } }) .unwrap(); + } } From 06c863b43a4dbbed31926a358c337f56cc7a4812 Mon Sep 17 00:00:00 2001 From: Viliam Vadocz Date: Thu, 17 Jul 2025 01:36:20 +0200 Subject: [PATCH 6/6] Show the network eval in the visualizer --- src/game.rs | 30 ++++++++++++++++-------------- src/visualize.rs | 4 ++-- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/game.rs b/src/game.rs index fdc3a01..db94e91 100644 --- a/src/game.rs +++ b/src/game.rs @@ -195,28 +195,30 @@ impl ScheduledGame { ); } position.do_move(mv.clone()); - if visualize { - move_tx - .send(visualize::Message::Ply { - mv: mv.clone(), - eval: None, - }) - .expect("Sub-thread should be alive."); - } - let score_string = match last_uci_info { + let side_to_move_flipper = match position.side_to_move() { + Color::White => -1, + Color::Black => 1, + }; + let score_string = match &last_uci_info { Some(uci_info) => format!( "{:+.2}/{} {:.2}s", - match position.side_to_move() { - // Flip sign if last move was black's - Color::White => uci_info.cp_score as f64 / -100.0, - Color::Black => uci_info.cp_score as f64 / 100.0, - }, + (side_to_move_flipper * uci_info.cp_score) as f64 / 100.0, uci_info.depth, time_taken.as_secs_f32(), ), None => String::new(), }; + if visualize { + move_tx + .send(visualize::Message::Ply { + mv: mv.clone(), + eval: last_uci_info + .map(|uci_info| uci_info.cp_score * side_to_move_flipper), + }) + .expect("Sub-thread should be alive."); + } + moves.push(PtnMove { mv, annotations: vec![], diff --git a/src/visualize.rs b/src/visualize.rs index c627217..2f28d38 100644 --- a/src/visualize.rs +++ b/src/visualize.rs @@ -15,7 +15,7 @@ pub enum Message { }, Ply { mv: P::Move, - eval: Option, // -100 to 100 + eval: Option, }, } @@ -72,7 +72,7 @@ impl Visualize for Position { websocket.send(tungstenite::Message::Text( json::object! { action: "SET_EVAL", - value: cp, + value: cp.clamp(-100, 100), }.to_string().into() ))? }