66 Commits

18 changed files with 3299 additions and 1140 deletions

9
.gitignore vendored
View File

@@ -24,7 +24,14 @@ target
# Added by cargo
/target
/__pycache__
.env
node_modules/
/node_modules
bracket_pairings.txt
.cursor/
.claude/
.codex/

View File

@@ -1,2 +1,3 @@
combine_control_expr = false
chain_width = 100
tab_spaces = 2

554
Cargo.lock generated
View File

@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "anyhow"
version = "1.0.100"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "async-trait"
@@ -35,10 +35,16 @@ dependencies = [
]
[[package]]
name = "bytes"
version = "1.10.1"
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cfg-if"
@@ -46,6 +52,17 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chacha20"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.0",
]
[[package]]
name = "connect4-moderator-server"
version = "0.1.0"
@@ -53,7 +70,8 @@ dependencies = [
"anyhow",
"async-trait",
"futures-util",
"rand",
"local-ip-address",
"rand 0.10.1",
"tokio",
"tokio-tungstenite",
"tracing",
@@ -69,6 +87,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
@@ -79,12 +106,78 @@ dependencies = [
"typenum",
]
[[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",
]
[[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",
]
[[package]]
name = "data-encoding"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -95,6 +188,18 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "fnv"
version = "1.0.7"
@@ -102,16 +207,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "futures-core"
version = "0.3.31"
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-macro"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
@@ -120,28 +231,27 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
[[package]]
name = "futures-task"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.31"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-macro",
"futures-sink",
"futures-task",
"pin-project-lite",
"pin-utils",
"slab",
]
@@ -163,10 +273,57 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"r-efi 5.3.0",
"wasip2",
]
[[package]]
name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.0",
"wasip2",
"wasip3",
]
[[package]]
name = "getset"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912"
dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "http"
version = "1.3.1"
@@ -184,6 +341,30 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.16.1",
"serde",
"serde_core",
]
[[package]]
name = "itoa"
version = "1.0.15"
@@ -196,12 +377,29 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "local-ip-address"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79ef8c257c92ade496781a32a581d43e3d512cf8ce714ecf04ea80f93ed0ff4a"
dependencies = [
"libc",
"neli",
"windows-sys 0.61.2",
]
[[package]]
name = "lock_api"
version = "0.4.14"
@@ -217,6 +415,12 @@ version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "mio"
version = "1.1.0"
@@ -228,6 +432,35 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "neli"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22f9786d56d972959e1408b6a93be6af13b9c1392036c5c1fafa08a1b0c6ee87"
dependencies = [
"bitflags",
"byteorder",
"derive_builder",
"getset",
"libc",
"log",
"neli-proc-macros",
"parking_lot",
]
[[package]]
name = "neli-proc-macros"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05d8d08c6e98f20a62417478ebf7be8e1425ec9acecc6f63e22da633f6b71609"
dependencies = [
"either",
"proc-macro2",
"quote",
"serde",
"syn",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
@@ -272,12 +505,6 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -287,6 +514,38 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.103"
@@ -311,6 +570,12 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.9.2"
@@ -318,7 +583,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core",
"rand_core 0.9.3",
]
[[package]]
name = "rand"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20",
"getrandom 0.4.2",
"rand_core 0.10.0",
]
[[package]]
@@ -328,7 +604,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.9.3",
]
[[package]]
@@ -337,9 +613,15 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom",
"getrandom 0.3.4",
]
[[package]]
name = "rand_core"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -355,6 +637,55 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "sha1"
version = "0.10.6"
@@ -362,7 +693,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -406,6 +737,12 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.110"
@@ -448,9 +785,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.48.0"
version = "1.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
dependencies = [
"bytes",
"libc",
@@ -488,9 +825,9 @@ dependencies = [
[[package]]
name = "tracing"
version = "0.1.43"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-attributes",
@@ -510,9 +847,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.35"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
@@ -554,7 +891,7 @@ dependencies = [
"http",
"httparse",
"log",
"rand",
"rand 0.9.2",
"sha1",
"thiserror",
"utf-8",
@@ -572,6 +909,12 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "utf-8"
version = "0.7.6"
@@ -602,7 +945,50 @@ version = "1.0.1+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
dependencies = [
"wit-bindgen",
"wit-bindgen 0.46.0",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen 0.51.0",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]]
@@ -700,6 +1086,94 @@ version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "zerocopy"
version = "0.8.27"
@@ -719,3 +1193,9 @@ dependencies = [
"quote",
"syn",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

View File

@@ -4,11 +4,12 @@ version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.48", features = ["full"] }
tokio = { version = "1.50", features = ["full"] }
tokio-tungstenite = "0.28"
futures-util = "0.3.31"
tracing = "0.1.43"
tracing-subscriber = "0.3.22"
anyhow = "1.0.100"
rand = "0.9.2"
async-trait = "0.1.89"
futures-util = "0.3"
tracing = "0.1"
tracing-subscriber = "0.3"
anyhow = "1.0"
rand = "0.10"
async-trait = "0.1"
local-ip-address = "0.6"

View File

@@ -1,4 +1,4 @@
FROM rust:1.91.1 AS build
FROM rust:1.92 AS build
RUN rustup target add x86_64-unknown-linux-musl && \
apt update && \

View File

@@ -9,8 +9,7 @@ Download the [`gameloop.py`](https://github.com/joshuafhiggins/connect4-moderato
In order to run your AI, you'll need:
- Python 3
- `pip install websockets` (Windows) or `pip3 install websockets` (Linux/macOS)
- `pip install pip-system-certs` (Windows) or `pip3 install pip-system-certs` (Linux/macOS)
- `pip install websockets pip-system-certs wakepy` (Windows) or `pip3 install websockets pip-system-certs wakepy` (Linux/macOS)
To run the example, run `python gameloop.py` (Windows) or `python3 gameloop.py` (Linux/macOS).

View File

@@ -1,7 +1,7 @@
const WebSocket = require("ws");
const readline = require("readline");
const DEFAULT_URL = "wss://connect4.abunchofknowitalls.com";
const DEFAULT_URL = "ws://localhost:8080";
let ws;
let pingInterval;

9
docker-compose.yml Normal file
View File

@@ -0,0 +1,9 @@
services:
connect4-server:
env_file:
- ./.env
container_name: connect4-server
restart: unless-stopped
ports:
- 5102:8080
image: joshuafhiggins/connect4-server

View File

@@ -1 +1 @@
docker build . -t joshuafhiggins/connect4-moderator-server
docker build . -t joshuafhiggins/connect4-server

View File

@@ -1,6 +0,0 @@
docker run -d \
--env-file ./.env \
--name=connect4-moderator-server \
--restart unless-stopped \
-p 5102:8080 \
joshuafhiggins/connect4-moderator-server

View File

@@ -1,19 +1,28 @@
import asyncio
import websockets
DEFAULT_SERVER_URL = "wss://connect4.abunchofknowitalls.com"
from websockets.exceptions import ConnectionClosed
from wakepy import keep
from agent import Agent
DEFAULT_SERVER_URL = "wss://connect4.abunchofknowitalls.com/ws"
RECONNECT_INTERVAL_SECONDS = 5
RECONNECT_TIMEOUT_SECONDS = 60
MAX_RECONNECT_ATTEMPTS = (
(RECONNECT_TIMEOUT_SECONDS + RECONNECT_INTERVAL_SECONDS - 1)
// RECONNECT_INTERVAL_SECONDS
)
async def gameloop(socket):
player = Agent()
while True: # While game is active, continually anticipate messages
while True: # Receive messages until the connection closes
message = (await socket.recv()).split(":") # Receive message from server
match message[0]:
case "CONNECT":
await socket.send("READY")
case "RECONNECT":
await socket.send("READY")
case "GAME":
if message[1] == "START":
@@ -37,16 +46,51 @@ async def gameloop(socket):
case "ERROR":
print(f"{message[0]}: {':'.join(message[1:])}")
await socket.close()
async def join_server(username, server_url):
reconnecting = False
reconnect_deadline = None
reconnect_attempt = 0
while True:
try:
async with websockets.connect(server_url, ping_interval=30, ping_timeout=30) as socket:
if reconnecting:
await socket.send(f"RECONNECT:{username}")
print("Reconnected to server.")
else:
await socket.send(f"CONNECT:{username}")
reconnect_deadline = None
reconnect_attempt = 0
await gameloop(socket)
except (ConnectionClosed, OSError) as error:
print(f"Connection lost ({error}).")
else:
print("Connection closed.")
now = asyncio.get_running_loop().time()
if reconnect_deadline is None:
reconnect_deadline = now + RECONNECT_TIMEOUT_SECONDS
print(f"Attempting to reconnect every {RECONNECT_INTERVAL_SECONDS} seconds for up to {RECONNECT_TIMEOUT_SECONDS} seconds...")
remaining = reconnect_deadline - now
if remaining <= 0:
print("Failed to reconnect within 60 seconds. Exiting.")
return
reconnecting = True
reconnect_attempt += 1
wait_time = min(RECONNECT_INTERVAL_SECONDS, remaining)
print(
f"Reconnect attempt {reconnect_attempt}/{MAX_RECONNECT_ATTEMPTS}: "
f"retrying in {wait_time:.0f}s "
f"({remaining:.0f}s remaining)."
)
await asyncio.sleep(wait_time)
if __name__ == "__main__":
with keep.presenting():
server_url = (
input(f"Enter server address [{DEFAULT_SERVER_URL}]: ").strip()
or DEFAULT_SERVER_URL

57
src/lib.rs Normal file
View File

@@ -0,0 +1,57 @@
use std::{collections::HashMap, net::SocketAddr, sync::Arc};
use rand::RngExt;
use tokio::sync::{
mpsc::{error::SendError, UnboundedSender},
RwLock,
};
use tokio_tungstenite::tungstenite::Message;
use tracing::error;
use crate::{
tournaments::Tournament,
types::{Client, Color, Match},
};
pub mod server;
pub mod tournaments;
pub mod types;
pub type Clients = Arc<RwLock<HashMap<String, Arc<RwLock<Client>>>>>;
pub type Usernames = Arc<RwLock<HashMap<SocketAddr, String>>>;
pub type Observers = Arc<RwLock<HashMap<SocketAddr, UnboundedSender<Message>>>>;
pub type Matches = Arc<RwLock<HashMap<u32, Arc<RwLock<Match>>>>>;
pub type Reservations = Arc<RwLock<Vec<(String, String)>>>;
pub type WrappedTournament = Arc<RwLock<Option<Arc<RwLock<dyn Tournament + Send + Sync>>>>>;
pub const SERVER_PLAYER_USERNAME: &str = "The Server";
// pub const SERVER_PLAYER_ADDR: &str = "127.0.0.1:6666";
pub async fn broadcast_message(observers: &Observers, msg: &str) {
let observer_guard = observers.read().await;
for tx in observer_guard.values() {
let _ = send(tx, msg);
}
}
pub async fn gen_match_id(matches: &Matches) -> u32 {
let matches_guard = matches.read().await;
let mut result = rand::rng().random_range(100000..=999999);
while matches_guard.get(&result).is_some() {
result = rand::rng().random_range(100000..=999999);
}
result
}
pub fn random_move(board: &[Vec<Color>]) -> usize {
let mut random = rand::rng().random_range(0..7);
while board[random][5] != Color::None {
random = rand::rng().random_range(0..7);
}
random
}
pub fn send(tx: &UnboundedSender<Message>, text: &str) -> Result<(), SendError<Message>> {
tx.send(Message::text(text))
}

File diff suppressed because it is too large Load Diff

1447
src/server.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,431 @@
use async_trait::async_trait;
use crate::{
server::*,
tournaments::{RoundRobin, Tournament},
*,
};
type Score = u32;
#[derive(Clone)]
pub struct KnockoutBracket {
pub blitz_round_robin: RoundRobin,
pub players: Vec<(String, Score, bool)>,
pub pairings: Vec<String>,
pub current_matches: Vec<u32>,
pub previous_wait: u64,
pub completed: bool,
pub started: bool,
pub skip_round_robin: bool,
pub clients: Clients,
pub matches: Matches,
pub observers: Observers,
pub usernames: Vec<String>,
pub data: Vec<Vec<String>>,
}
impl KnockoutBracket {
async fn create_matches(&mut self) {
let clients_guard = self.clients.read().await;
let mut i = 0;
while i < self.pairings.len() {
let player1_username = self.pairings[i].clone();
let player2_username = self.pairings.get(i + 1);
if player2_username.is_none() {
break;
}
let player2_username = player2_username.unwrap().clone();
let match_id: u32 = gen_match_id(&self.matches).await;
self.current_matches.push(match_id);
let new_match = Arc::new(RwLock::new(Match::new(
match_id,
player1_username.clone(),
player2_username.clone(),
false,
)));
let match_guard = new_match.read().await;
let mut player1 = clients_guard.get(&player1_username).unwrap().write().await;
player1.current_match = Some(match_id);
player1.ready = false;
broadcast_message(
&self.observers,
&format!("READY:{}:{}", player1.username.clone(), false),
)
.await;
if match_guard.player1 == player1_username {
player1.color = Color::Red;
let _ = send(&player1.connection, "GAME:START:1");
} else {
player1.color = Color::Yellow;
let _ = send(&player1.connection, "GAME:START:0");
}
drop(player1);
let mut player2 = clients_guard.get(&player2_username).unwrap().write().await;
player2.current_match = Some(match_id);
player2.ready = false;
broadcast_message(
&self.observers,
&format!("READY:{}:{}", player2.username.clone(), false),
)
.await;
if match_guard.player1 == player2_username {
player2.color = Color::Red;
let _ = send(&player2.connection, "GAME:START:1");
} else {
player2.color = Color::Yellow;
let _ = send(&player2.connection, "GAME:START:0");
}
drop(player2);
self.matches.write().await.insert(match_id, new_match.clone());
broadcast_message(
&self.observers,
&format!(
"GAME:START:{},{},{}",
match_id, match_guard.player1, match_guard.player2
),
)
.await;
i += 2
}
}
}
#[async_trait]
impl Tournament for KnockoutBracket {
async fn new(ready_players: &[String], server: &Server) -> KnockoutBracket {
let previous_wait = server.waiting_timeout.read().await.clone();
let bracket_file = std::fs::read_to_string("bracket_pairings.txt").unwrap_or_default();
let bracket_players = bracket_file.split('\n').collect::<Vec<_>>();
let mut skip_round_robin =
!bracket_players.is_empty() && bracket_players.len() == ready_players.len();
if skip_round_robin {
for player in bracket_players {
let mut player_match = false;
for ready_player in ready_players {
if player == ready_player {
player_match = true;
break;
}
}
if !player_match {
skip_round_robin = false;
break;
}
}
}
KnockoutBracket {
blitz_round_robin: RoundRobin::new(ready_players, server).await,
players: Vec::new(),
pairings: Vec::new(),
current_matches: Vec::new(),
previous_wait,
completed: false,
started: false,
skip_round_robin,
clients: server.clients.clone(),
matches: server.matches.clone(),
observers: server.observers.clone(),
usernames: ready_players.to_vec(),
data: Vec::new(),
}
}
async fn next(&mut self, server: &Server) {
if self.completed {
return;
}
if !self.started {
self.blitz_round_robin.next(server).await;
}
if self.blitz_round_robin.completed && !self.started {
self.started = true;
*server.waiting_timeout.write().await = self.previous_wait;
let mut players = Vec::new();
for player in self.blitz_round_robin.players.values() {
players.push((player.0.clone(), player.1, false));
}
players.sort_by(|a, b| a.1.cmp(&b.1));
self.players = players;
for player in &self.players {
self.pairings.push(player.0.clone());
}
self.data.push(self.pairings.clone());
self.create_matches().await;
broadcast_message(
&self.observers,
&format!("GET:TOURNAMENT_DATA:{}", self.get_data().unwrap()),
)
.await;
return;
}
if self.started {
self.pairings.retain(|p| !p.is_empty());
if self.pairings.len() == 1 {
self.completed = true;
} else {
self.data.push(self.pairings.clone());
self.create_matches().await;
broadcast_message(
&self.observers,
&format!("GET:TOURNAMENT_DATA:{}", self.get_data().unwrap()),
)
.await;
}
}
}
async fn start(&mut self, server: &Server) {
if self.skip_round_robin {
let bracket_file = std::fs::read_to_string("bracket_pairings.txt").unwrap_or_default();
self.blitz_round_robin.completed = true;
self.started = true;
let mut i = 0;
bracket_file.split('\n').into_iter().for_each(|line| {
self.players.push((line.to_string(), i, false));
self.pairings.push(line.to_string());
i += 1;
});
self.data.push(self.pairings.clone());
self.create_matches().await;
broadcast_message(
&self.observers,
&format!("GET:TOURNAMENT_DATA:{}", self.get_data().unwrap()),
)
.await;
} else {
*server.waiting_timeout.write().await = 5;
self.blitz_round_robin.start(server).await;
}
}
async fn cancel(&mut self, server: &Server) {
if !self.started {
self.blitz_round_robin.cancel(server).await;
return;
}
for match_id in &self.current_matches {
server.terminate_match(*match_id).await;
}
let clients_guard = server.clients.read().await;
for username in &self.players {
let client = clients_guard.get(&username.0).cloned();
if client.is_none() {
continue;
}
let client = client.unwrap();
let client = client.read().await;
let _ = send(&client.connection, "TOURNAMENT:END");
}
}
async fn inform_winner(
&mut self,
winner: String,
match_id: u32,
player1: String,
player2: String,
) {
if !self.started {
self.blitz_round_robin.inform_winner(winner, match_id, player1, player2).await;
return;
}
let mut winner = winner;
// there's a tie
if winner.is_empty() {
let mut player1_track = (String::new(), 0, false);
let mut player2_track = (String::new(), 0, false);
for player in self.players.iter_mut() {
if player.0 == player1 {
player1_track = player.clone();
} else if player.0 == player2 {
player2_track = player.clone();
}
if !player1_track.0.is_empty() && !player2_track.0.is_empty() {
break;
}
}
if player1_track.2 || player2_track.2 {
if player1_track.1 < player2_track.1 {
winner = player2_track.0.clone();
} else {
winner = player1_track.0.clone();
}
} else {
for player in self.players.iter_mut() {
if player.0 == player1 || player.0 == player2 {
player.2 = true;
}
}
let new_match_id: u32 = gen_match_id(&self.matches).await;
self.current_matches.push(new_match_id);
let new_match = Arc::new(RwLock::new(Match::new_with_order(
new_match_id,
player2.clone(),
player1.clone(),
false,
)));
let match_guard = new_match.read().await;
let clients_guard = self.clients.read().await;
let mut player1 = clients_guard.get(&player1).unwrap().write().await;
player1.current_match = Some(new_match_id);
player1.ready = false;
let player1_name = player1.username.clone();
if match_guard.player1 == player1.username {
player1.color = Color::Red;
let _ = send(&player1.connection, "GAME:START:1");
} else {
player1.color = Color::Yellow;
let _ = send(&player1.connection, "GAME:START:0");
}
drop(player1);
let mut player2 = clients_guard.get(&player2).unwrap().write().await;
player2.current_match = Some(new_match_id);
player2.ready = false;
let player2_name = player2.username.clone();
if match_guard.player1 == player2.username {
player2.color = Color::Red;
let _ = send(&player2.connection, "GAME:START:1");
} else {
player2.color = Color::Yellow;
let _ = send(&player2.connection, "GAME:START:0");
}
drop(player2);
broadcast_message(
&self.observers,
&format!("READY:{}:{}", player1_name, false),
)
.await;
broadcast_message(
&self.observers,
&format!("READY:{}:{}", player2_name, false),
)
.await;
self.matches.write().await.insert(new_match_id, new_match.clone());
broadcast_message(
&self.observers,
&format!(
"GAME:START:{},{},{}",
new_match_id, match_guard.player1, match_guard.player2
),
)
.await;
self.current_matches.retain(|v| *v != match_id);
return;
}
}
let mut loser = String::new();
for i in 0..self.pairings.len() {
if self.pairings[i] == winner {
if i % 2 == 0 {
loser = self.pairings[i + 1].clone();
self.pairings[i + 1].clear();
} else {
loser = self.pairings[i - 1].clone();
self.pairings[i - 1].clear();
}
break;
}
}
// Reset tie tracking
for player in self.players.iter_mut() {
if player.0 == winner || player.0 == loser {
player.2 = false;
}
}
self.current_matches.retain(|v| *v != match_id);
}
fn contains_player(&self, username: String) -> bool {
self.usernames.contains(&username)
}
fn is_completed(&self) -> bool {
self.completed
}
fn get_players(&self) -> Vec<String> {
self.usernames.clone()
}
fn get_winner(&self) -> Option<String> {
if self.completed {
return Some(self.pairings[0].clone());
}
None
}
fn get_data(&self) -> Option<String> {
if !self.started {
return None;
}
let mut message = String::new();
for round in self.data.iter() {
for player in round.iter() {
message += player;
message += ",";
}
message.pop();
message.push('|');
}
if self.data.len() > 0 {
message.pop();
}
Some(message)
}
fn get_type(&self) -> String {
"KnockoutBracket".to_string()
}
}

31
src/tournaments/mod.rs Normal file
View File

@@ -0,0 +1,31 @@
use async_trait::async_trait;
use crate::server::Server;
pub mod round_robin;
pub use round_robin::RoundRobin;
pub mod knockout_bracket;
pub use knockout_bracket::KnockoutBracket;
#[async_trait]
pub trait Tournament {
async fn new(ready_players: &[String], server: &Server) -> Self
where
Self: Sized;
async fn next(&mut self, server: &Server);
async fn start(&mut self, server: &Server);
async fn cancel(&mut self, server: &Server);
async fn inform_winner(
&mut self,
winner: String,
match_id: u32,
player1: String,
player2: String,
);
fn contains_player(&self, username: String) -> bool;
fn is_completed(&self) -> bool;
fn get_players(&self) -> Vec<String>;
fn get_winner(&self) -> Option<String>;
fn get_data(&self) -> Option<String>;
fn get_type(&self) -> String;
}

View File

@@ -0,0 +1,249 @@
use std::collections::HashMap;
use async_trait::async_trait;
use crate::{server::Server, *};
type Score = u32;
type ID = u32;
#[derive(Clone)]
pub struct RoundRobin {
pub players: HashMap<ID, (String, Score)>,
pub top_half: Vec<ID>,
pub bottom_half: Vec<ID>,
pub completed: bool,
pub total_rounds: usize,
pub rounds_played: usize,
pub current_matches: Vec<ID>,
pub usernames: Vec<String>,
pub observers: Observers,
}
impl RoundRobin {
async fn create_matches(&mut self, clients: &Clients, matches: &Matches) {
let clients_guard = clients.read().await;
for (i, id) in self.top_half.iter().enumerate() {
let Some(player1_username) = self.players.get(id) else {
continue;
};
let Some(player2_id) = self.bottom_half.get(i) else {
continue;
};
let Some(player2_username) = self.players.get(player2_id) else {
continue;
};
let match_id: u32 = gen_match_id(matches).await;
let new_match = Arc::new(RwLock::new(Match::new(
match_id,
player1_username.0.clone(),
player2_username.0.clone(),
false,
)));
self.current_matches.push(match_id.clone());
let match_guard = new_match.read().await;
let mut player1 = clients_guard.get(&player1_username.0).unwrap().write().await;
player1.current_match = Some(match_id);
player1.ready = false;
broadcast_message(
&self.observers,
&format!("READY:{}:{}", player1.username.clone(), false),
)
.await;
if match_guard.player1 == player1_username.0 {
player1.color = Color::Red;
let _ = send(&player1.connection, "GAME:START:1");
} else {
player1.color = Color::Yellow;
let _ = send(&player1.connection, "GAME:START:0");
}
drop(player1);
let mut player2 = clients_guard.get(&player2_username.0).unwrap().write().await;
player2.current_match = Some(match_id);
player2.ready = false;
broadcast_message(
&self.observers,
&format!("READY:{}:{}", player2.username.clone(), false),
)
.await;
if match_guard.player1 == player2_username.0 {
player2.color = Color::Red;
let _ = send(&player2.connection, "GAME:START:1");
} else {
player2.color = Color::Yellow;
let _ = send(&player2.connection, "GAME:START:0");
}
drop(player2);
matches.write().await.insert(match_id, new_match.clone());
broadcast_message(
&self.observers,
&format!(
"GAME:START:{},{},{}",
match_id, match_guard.player1, match_guard.player2
),
)
.await;
}
}
}
#[async_trait]
impl Tournament for RoundRobin {
async fn new(ready_players: &[String], server: &Server) -> RoundRobin {
let mut result = RoundRobin {
players: HashMap::new(),
top_half: Vec::new(),
bottom_half: Vec::new(),
completed: false,
total_rounds: 0,
rounds_played: 0,
current_matches: Vec::new(),
usernames: ready_players.to_vec(),
observers: server.observers.clone(),
};
let size = ready_players.len();
let total_slots = if size % 2 == 0 { size } else { size + 1 };
result.total_rounds = if size < 2 { 0 } else { total_slots - 1 };
result.completed = result.total_rounds == 0;
for (id, player) in ready_players.iter().enumerate() {
result.players.insert(id as u32, (player.clone(), 0));
}
for i in 0..total_slots / 2 {
result.top_half.push(i as u32);
}
for i in total_slots / 2..total_slots {
result.bottom_half.push(i as u32);
}
result
}
async fn inform_winner(&mut self, winner: String, match_id: u32, _: String, _: String) {
if winner.is_empty() {
return;
}
for (_, username) in self.players.iter_mut() {
if username.0 == winner {
username.1 += 1;
break;
}
}
self.current_matches.retain(|id| !(*id == match_id));
}
async fn next(&mut self, server: &Server) {
if self.completed {
return;
}
let clients_guard = server.clients.read().await;
let mut player_scores: Vec<(String, u32)> = Vec::new();
for (_, username) in self.players.iter() {
let player = clients_guard.get(&username.0).unwrap().read().await;
player_scores.push((player.username.clone(), username.1));
}
drop(clients_guard);
player_scores.sort_by(|a, b| b.1.cmp(&a.1));
// Send scores
let mut message = "TOURNAMENT:SCORES:".to_string();
for (player, score) in player_scores.iter() {
message.push_str(&format!("{},{}|", player, score))
}
message.pop();
server.broadcast(&message).await;
if self.rounds_played >= self.total_rounds {
self.completed = true;
return;
}
let last_from_top = self.top_half.pop().unwrap();
let first_from_bottom = self.bottom_half.remove(0);
self.top_half.insert(1, first_from_bottom);
self.bottom_half.push(last_from_top);
self.rounds_played += 1;
self.create_matches(&server.clients, &server.matches).await;
}
async fn start(&mut self, server: &Server) {
if self.completed {
return;
}
self.rounds_played = 1;
self.create_matches(&server.clients, &server.matches).await;
}
async fn cancel(&mut self, server: &Server) {
for match_id in &self.current_matches {
server.terminate_match(*match_id).await;
}
let clients_guard = server.clients.read().await;
for (_, (username, _)) in self.players.iter() {
let client = clients_guard.get(username);
if client.is_none() {
continue;
}
let client = client.unwrap().read().await;
let _ = send(&client.connection, "TOURNAMENT:END");
}
}
fn contains_player(&self, username: String) -> bool {
self.usernames.contains(&username)
}
fn is_completed(&self) -> bool {
self.completed
}
fn get_players(&self) -> Vec<String> {
self.usernames.clone()
}
fn get_winner(&self) -> Option<String> {
if !self.is_completed() {
return None;
}
let mut best_score = 0;
let mut winner = None;
for (_, (username, score)) in self.players.iter() {
if *score > best_score {
best_score = *score;
winner = Some(username.clone());
}
}
winner
}
fn get_data(&self) -> Option<String> {
None
}
fn get_type(&self) -> String {
"RoundRobin".to_string()
}
}

View File

@@ -1,40 +1,39 @@
use rand::Rng;
use std::collections::HashMap;
use rand::RngExt;
use std::net::SocketAddr;
use std::sync::Arc;
use std::vec;
use async_trait::async_trait;
use std::time::Instant;
use std::{ops, vec};
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::RwLock;
use tokio_tungstenite::tungstenite::Message;
use crate::{broadcast_message_all_observers, gen_match_id, send, terminate_match};
pub type Clients = Arc<RwLock<HashMap<SocketAddr, Arc<RwLock<Client>>>>>;
pub type Usernames = Arc<RwLock<HashMap<String, SocketAddr>>>;
pub type Observers = Arc<RwLock<HashMap<SocketAddr, UnboundedSender<Message>>>>;
pub type Matches = Arc<RwLock<HashMap<u32, Arc<RwLock<Match>>>>>;
pub type WrappedTournament = Arc<RwLock<Option<Arc<RwLock<dyn Tournament + Send + Sync>>>>>;
pub struct Server {
pub clients: Clients,
pub usernames: Usernames,
pub observers: Observers,
pub matches: Matches,
pub admin: Arc<RwLock<Option<SocketAddr>>>,
pub admin_password: Arc<String>,
pub tournament: WrappedTournament,
pub waiting_timeout: Arc<RwLock<u64>>,
pub demo_mode: bool,
pub tournament_type: String,
}
#[derive(PartialEq, Clone)]
#[derive(PartialEq, Clone, Copy)]
pub enum Color {
Red,
Yellow,
None,
}
impl ops::Not for Color {
type Output = Color;
fn not(self) -> Color {
match self {
Color::Red => Color::Yellow,
Color::Yellow => Color::Red,
Color::None => Color::None,
}
}
}
impl From<Color> for bool {
fn from(color: Color) -> bool {
match color {
Color::Red => true,
Color::Yellow => false,
Color::None => panic!("Cannot convert Color::None to bool"),
}
}
}
#[derive(Clone)]
pub struct Client {
pub username: String,
@@ -42,8 +41,6 @@ pub struct Client {
pub ready: bool,
pub color: Color,
pub current_match: Option<u32>,
pub round_robin_id: u32,
pub score: u32,
pub addr: SocketAddr,
}
@@ -55,219 +52,52 @@ impl Client {
ready: false,
color: Color::None,
current_match: None,
round_robin_id: 0,
score: 0,
addr,
}
}
}
#[async_trait]
pub trait Tournament {
fn new(ready_players: &[SocketAddr]) -> Self where Self: Sized;
async fn next(&mut self, clients: &Clients, matches: &Matches, observers: &Observers);
async fn start(&mut self, clients: &Clients, matches: &Matches);
async fn cancel(&mut self, clients: &Clients, matches: &Matches, observers: &Observers);
fn is_completed(&self) -> bool;
}
#[derive(Clone)]
pub struct RoundRobin {
pub players: HashMap<u32, SocketAddr>,
pub top_half: Vec<u32>,
pub bottom_half: Vec<u32>,
pub is_completed: bool,
}
impl RoundRobin {
async fn create_matches(&self, clients: &Clients, matches: &Matches) {
let clients_guard = clients.read().await;
for (i, id) in self.top_half.iter().enumerate() {
let player1_addr = self.players.get(id).unwrap();
let player2_addr = self.players.get(self.bottom_half.get(i).unwrap());
if player2_addr.is_none() { continue; }
let player2_addr = player2_addr.unwrap();
let match_id: u32 = gen_match_id(matches).await;
let new_match = Arc::new(RwLock::new(Match::new(
match_id,
*player1_addr,
*player2_addr,
)));
let match_guard = new_match.read().await;
let mut player1 = clients_guard.get(player1_addr).unwrap().write().await;
player1.current_match = Some(match_id);
player1.ready = false;
if match_guard.player1 == *player1_addr {
player1.color = Color::Red;
let _ = send(&player1.connection, "GAME:START:1");
} else {
player1.color = Color::Yellow;
let _ = send(&player1.connection, "GAME:START:0");
}
drop(player1);
let mut player2 = clients_guard.get(player2_addr).unwrap().write().await;
player2.current_match = Some(match_id);
player2.ready = false;
if match_guard.player1 == *player2_addr {
player2.color = Color::Red;
let _ = send(&player2.connection, "GAME:START:1");
} else {
player2.color = Color::Yellow;
let _ = send(&player2.connection, "GAME:START:0");
}
drop(player2);
matches.write().await.insert(match_id, new_match.clone());
}
}
}
#[async_trait]
impl Tournament for RoundRobin {
fn new(ready_players: &[SocketAddr]) -> RoundRobin {
let mut result = RoundRobin {
players: HashMap::new(),
top_half: Vec::new(),
bottom_half: Vec::new(),
is_completed: false,
};
let size = ready_players.len();
for (id, player) in ready_players.iter().enumerate() {
result.players.insert(id as u32, *player);
}
for i in 0..size / 2 {
result.top_half.push(i as u32);
}
for i in size / 2..size {
result.bottom_half.push(i as u32);
}
result
}
async fn next(&mut self, clients: &Clients, matches: &Matches, observers: &Observers) {
if self.is_completed {
return;
}
if self.top_half.len() <= 1 || self.bottom_half.is_empty() {
self.is_completed = true;
return;
}
let last_from_top = self.top_half.pop().unwrap();
let first_from_bottom = self.bottom_half.remove(0);
self.top_half.insert(1, first_from_bottom);
self.bottom_half.push(last_from_top);
let expected_bottom_start = self.top_half.len() as u32;
if self.top_half[1] == 1 && self.bottom_half[0] == expected_bottom_start {
self.is_completed = true;
}
if self.is_completed() {
// Send scores
let clients_guard = clients.read().await;
let mut player_scores: Vec<(String, u32)> = Vec::new();
for (_, player_addr) in self.players.iter() {
let mut player = clients_guard.get(player_addr).unwrap().write().await;
let _ = send(&player.connection.clone(), "TOURNAMENT:END");
player_scores.push((player.username.clone(), player.score));
player.score = 0;
player.round_robin_id = 0;
}
player_scores.sort_by(|a, b| b.1.cmp(&a.1));
let mut message = "TOURNAMENT:END:".to_string();
for (player, score) in player_scores.iter() {
message.push_str(&format!("{},{}|", player, score))
}
message.pop();
broadcast_message_all_observers(observers, &message).await;
}
else {
// Create next matches
self.create_matches(clients, matches).await;
}
}
async fn start(&mut self, clients: &Clients, matches: &Matches) {
self.create_matches(clients, matches).await;
}
async fn cancel(&mut self, clients: &Clients, matches: &Matches, observers: &Observers) {
for (_, addr) in self.players.iter() {
let clients_guard = clients.read().await;
let client = clients_guard.get(addr);
if client.is_none() { continue; }
let client = client.unwrap().read().await;
let client_connection = client.connection.clone();
let client_ready = client.ready;
let match_id = client.current_match;
if match_id.is_none() { continue; }
let match_id = match_id.unwrap();
drop(client);
drop(clients_guard);
terminate_match(match_id, matches, clients, observers, false).await;
if !client_ready {
let _ = send(&client_connection, "TOURNAMENT:END");
}
}
}
fn is_completed(&self) -> bool { self.is_completed }
}
pub struct Match {
pub id: u32,
pub demo_mode: bool,
pub board: Vec<Vec<Color>>,
pub viewers: Vec<SocketAddr>,
pub ledger: Vec<(Color, usize)>,
pub move_to_dispatch: (Color, usize),
pub ledger: Vec<(Color, usize, Instant)>,
pub wait_thread: Option<tokio::task::JoinHandle<()>>,
pub player1: SocketAddr,
pub player2: SocketAddr,
pub timeout_thread: Option<tokio::task::JoinHandle<()>>,
pub player1: String,
pub player2: String,
}
impl Match {
pub fn new(id: u32, player1: SocketAddr, player2: SocketAddr) -> Match {
let first = if rand::rng().random_range(0..=1) == 0 {
player1.to_string().parse().unwrap()
pub fn new(id: u32, player1: String, player2: String, demo_mode: bool) -> Match {
let (first_player, second_player) = if rand::rng().random_range(0..=1) == 0 {
(player1, player2)
} else {
player2.to_string().parse().unwrap()
(player2, player1)
};
Match {
id,
demo_mode,
board: vec![vec![Color::None; 6]; 7],
viewers: Vec::new(),
ledger: Vec::new(),
move_to_dispatch: (Color::None, 0),
wait_thread: None,
player1: if player1 == first {player1} else {player2},
player2: if player1 == first {player2} else {player1},
timeout_thread: None,
player1: first_player,
player2: second_player,
}
}
pub fn new_with_order(id: u32, player1: String, player2: String, demo_mode: bool) -> Match {
Match {
id,
demo_mode,
board: vec![vec![Color::None; 6]; 7],
ledger: Vec::new(),
wait_thread: None,
timeout_thread: None,
player1,
player2,
}
}
@@ -279,4 +109,58 @@ impl Match {
}
}
}
pub fn end_game_check(&self) -> (Color, bool) {
let mut result = (Color::None, false);
let mut any_empty = true;
for x in 0..7 {
for y in 0..6 {
let color = self.board[x][y].clone();
let mut horizontal_end = true;
let mut vertical_end = true;
let mut diagonal_end_up = true;
let mut diagonal_end_down = true;
if any_empty && color == Color::None {
any_empty = false;
}
for i in 0..4 {
if x + i >= 7 || self.board[x + i][y] != color && horizontal_end {
horizontal_end = false;
}
if y + i >= 6 || self.board[x][y + i] != color && vertical_end {
vertical_end = false;
}
if x + i >= 7 || y + i >= 6 || self.board[x + i][y + i] != color && diagonal_end_up {
diagonal_end_up = false;
}
if x + i >= 7
|| (y as i32 - i as i32) < 0
|| self.board[x + i][y - i] != color && diagonal_end_down
{
diagonal_end_down = false;
}
}
if horizontal_end || vertical_end || diagonal_end_up || diagonal_end_down {
result = (color.clone(), false);
break;
}
}
if result.0 != Color::None {
break;
}
}
if any_empty && result.0 == Color::None {
result.1 = true;
}
result
}
}