Compare commits
76 Commits
v1.0.0
...
fbe2b7a35e
28
.gitea/workflows/cargo-check.yml
Normal file
28
.gitea/workflows/cargo-check.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Cargo Check
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "src/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- ".rustfmt.toml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "src/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- ".rustfmt.toml"
|
||||
|
||||
jobs:
|
||||
cargo-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Run cargo check
|
||||
run: cargo check --all-targets
|
||||
30
.gitea/workflows/cargo-fmt.yml
Normal file
30
.gitea/workflows/cargo-fmt.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Cargo Format Check
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "src/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- ".rustfmt.toml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "src/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- ".rustfmt.toml"
|
||||
|
||||
jobs:
|
||||
cargo-fmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain with rustfmt
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
|
||||
- name: Run cargo fmt check
|
||||
run: cargo fmt --all -- --check
|
||||
39
.gitea/workflows/docker-build.yml
Normal file
39
.gitea/workflows/docker-build.yml
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITEA_DOMAIN }}
|
||||
username: ${{ env.GITEA_REGISTRY_USER }}
|
||||
password: ${{ secrets.CONTAINER_REGISTRY_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -24,7 +24,14 @@ target
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
/__pycache__
|
||||
|
||||
.env
|
||||
|
||||
node_modules/
|
||||
/node_modules
|
||||
|
||||
bracket_pairings.txt
|
||||
|
||||
.cursor/
|
||||
.claude/
|
||||
.codex/
|
||||
@@ -1,2 +1,3 @@
|
||||
combine_control_expr = false
|
||||
chain_width = 100
|
||||
chain_width = 100
|
||||
tab_spaces = 2
|
||||
554
Cargo.lock
generated
554
Cargo.lock
generated
@@ -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"
|
||||
|
||||
15
Cargo.toml
15
Cargo.toml
@@ -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"
|
||||
|
||||
21
Dockerfile
21
Dockerfile
@@ -1,14 +1,19 @@
|
||||
FROM rust:1.91.1 AS build
|
||||
FROM rust:1.92 AS builder
|
||||
|
||||
RUN rustup target add x86_64-unknown-linux-musl && \
|
||||
apt update && \
|
||||
apt install -y musl-tools musl-dev && \
|
||||
update-ca-certificates
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends musl-tools musl-dev ca-certificates && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY ./src ./src
|
||||
COPY ./Cargo.lock .
|
||||
COPY ./Cargo.toml .
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY src ./src
|
||||
|
||||
RUN cargo build --target x86_64-unknown-linux-musl --release
|
||||
|
||||
ENTRYPOINT ["./target/x86_64-unknown-linux-musl/release/connect4-moderator-server"]
|
||||
FROM scratch AS runtime
|
||||
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/connect4-moderator-server /connect4-moderator-server
|
||||
|
||||
ENTRYPOINT ["/connect4-moderator-server"]
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -34,4 +33,4 @@ A JavaScript debug client that takes raw text as input and prints responses is p
|
||||
- `npm i`
|
||||
- `node debug_client.js`
|
||||
|
||||
I also apologize in advance for the code you'll go on to read.
|
||||
I also apologize in advance for the code you'll go on to read.
|
||||
|
||||
@@ -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
9
docker-compose.yml
Normal 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
|
||||
@@ -1 +1 @@
|
||||
docker build . -t joshuafhiggins/connect4-moderator-server
|
||||
docker build . -t joshuafhiggins/connect4-server
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
docker run -d \
|
||||
--env-file ./.env \
|
||||
--name=connect4-moderator-server \
|
||||
--restart unless-stopped \
|
||||
-p 5102:8080 \
|
||||
joshuafhiggins/connect4-moderator-server
|
||||
76
gameloop.py
76
gameloop.py
@@ -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,19 +46,54 @@ async def gameloop(socket):
|
||||
case "ERROR":
|
||||
print(f"{message[0]}: {':'.join(message[1:])}")
|
||||
|
||||
await socket.close()
|
||||
|
||||
|
||||
async def join_server(username, server_url):
|
||||
async with websockets.connect(server_url, ping_interval=30, ping_timeout=30) as socket:
|
||||
await socket.send(f"CONNECT:{username}")
|
||||
await gameloop(socket)
|
||||
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__":
|
||||
server_url = (
|
||||
input(f"Enter server address [{DEFAULT_SERVER_URL}]: ").strip()
|
||||
or DEFAULT_SERVER_URL
|
||||
)
|
||||
username = input("Enter username: ")
|
||||
asyncio.run(join_server(username, server_url))
|
||||
with keep.presenting():
|
||||
server_url = (
|
||||
input(f"Enter server address [{DEFAULT_SERVER_URL}]: ").strip()
|
||||
or DEFAULT_SERVER_URL
|
||||
)
|
||||
username = input("Enter username: ")
|
||||
asyncio.run(join_server(username, server_url))
|
||||
|
||||
57
src/lib.rs
Normal file
57
src/lib.rs
Normal 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))
|
||||
}
|
||||
1145
src/main.rs
1145
src/main.rs
File diff suppressed because it is too large
Load Diff
1444
src/server.rs
Normal file
1444
src/server.rs
Normal file
File diff suppressed because it is too large
Load Diff
431
src/tournaments/knockout_bracket.rs
Normal file
431
src/tournaments/knockout_bracket.rs
Normal 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
31
src/tournaments/mod.rs
Normal 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;
|
||||
}
|
||||
249
src/tournaments/round_robin.rs
Normal file
249
src/tournaments/round_robin.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
396
src/types.rs
396
src/types.rs
@@ -1,282 +1,166 @@
|
||||
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, Copy)]
|
||||
pub enum Color {
|
||||
Red,
|
||||
Yellow,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
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,
|
||||
pub connection: UnboundedSender<Message>,
|
||||
pub ready: bool,
|
||||
pub color: Color,
|
||||
pub current_match: Option<u32>,
|
||||
pub round_robin_id: u32,
|
||||
pub score: u32,
|
||||
pub addr: SocketAddr,
|
||||
pub username: String,
|
||||
pub connection: UnboundedSender<Message>,
|
||||
pub ready: bool,
|
||||
pub color: Color,
|
||||
pub current_match: Option<u32>,
|
||||
pub addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(username: String, connection: UnboundedSender<Message>, addr: SocketAddr) -> Client {
|
||||
Client {
|
||||
username,
|
||||
connection,
|
||||
ready: false,
|
||||
color: Color::None,
|
||||
current_match: None,
|
||||
round_robin_id: 0,
|
||||
score: 0,
|
||||
addr,
|
||||
}
|
||||
pub fn new(username: String, connection: UnboundedSender<Message>, addr: SocketAddr) -> Client {
|
||||
Client {
|
||||
username,
|
||||
connection,
|
||||
ready: false,
|
||||
color: Color::None,
|
||||
current_match: None,
|
||||
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 board: Vec<Vec<Color>>,
|
||||
pub viewers: Vec<SocketAddr>,
|
||||
pub ledger: Vec<(Color, usize)>,
|
||||
pub move_to_dispatch: (Color, usize),
|
||||
pub wait_thread: Option<tokio::task::JoinHandle<()>>,
|
||||
pub player1: SocketAddr,
|
||||
pub player2: SocketAddr,
|
||||
pub id: u32,
|
||||
pub demo_mode: bool,
|
||||
pub board: Vec<Vec<Color>>,
|
||||
pub ledger: Vec<(Color, usize, Instant)>,
|
||||
pub wait_thread: Option<tokio::task::JoinHandle<()>>,
|
||||
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()
|
||||
} else {
|
||||
player2.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, player1)
|
||||
};
|
||||
|
||||
Match {
|
||||
id,
|
||||
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},
|
||||
Match {
|
||||
id,
|
||||
demo_mode,
|
||||
board: vec![vec![Color::None; 6]; 7],
|
||||
ledger: Vec::new(),
|
||||
wait_thread: None,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn place_token(&mut self, color: Color, column: usize) {
|
||||
for i in 0..6 {
|
||||
if self.board[column][i] == Color::None {
|
||||
self.board[column][i] = color;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn place_token(&mut self, color: Color, column: usize) {
|
||||
for i in 0..6 {
|
||||
if self.board[column][i] == Color::None {
|
||||
self.board[column][i] = color;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if any_empty && result.0 == Color::None {
|
||||
result.1 = true;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user