From 0fc676fc04d7a0e0084a42bf287ffdc99a11431f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:37:47 +0000 Subject: [PATCH 1/5] build(deps): bump bytes from 1.10.1 to 1.11.1 Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.10.1 to 1.11.1. - [Release notes](https://github.com/tokio-rs/bytes/releases) - [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md) - [Commits](https://github.com/tokio-rs/bytes/compare/v1.10.1...v1.11.1) --- updated-dependencies: - dependency-name: bytes dependency-version: 1.11.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18c1285..e2bece3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -42,9 +42,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cfg-if" -- 2.49.1 From aef15857643631f2a60b4d003f436fdb703c6add Mon Sep 17 00:00:00 2001 From: Joshua Higgins Date: Thu, 19 Feb 2026 11:44:42 -0500 Subject: [PATCH 2/5] misc: remove todos --- src/main.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 87a253f..055cdba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,12 +8,6 @@ use tokio::net::{TcpListener, TcpStream}; use tokio_tungstenite::{accept_async, tungstenite::Message}; use tracing::{error, info}; -// TODO: Support reconnecting behaviors -// TODO: Other tournament types -// TODO: Max move wait time -// TODO: Tiebreakers, guarantee some amount of going first -// TODO: Send moves instantly, sleep only till waiting time - #[tokio::main] async fn main() -> Result<(), anyhow::Error> { // Initialize logging -- 2.49.1 From c754b2ec72b001d4d4e7831674d7ea92c0185326 Mon Sep 17 00:00:00 2001 From: Joshua Higgins Date: Thu, 19 Feb 2026 15:55:56 -0500 Subject: [PATCH 3/5] feat: tolerate disconnects/allow for soft reconnects --- README.md | 1 + gameloop.py | 69 ++++++++-- src/main.rs | 28 +++- src/server.rs | 234 ++++++++++++++++++++++++++------- src/tournaments/mod.rs | 2 + src/tournaments/round_robin.rs | 18 +++ src/types.rs | 10 ++ 7 files changed, 299 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index fd02f8e..b16a276 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ 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 wakepy` (Windows) or `pip3 install wakepy` (Linux/macOS) To run the example, run `python gameloop.py` (Windows) or `python3 gameloop.py` (Linux/macOS). diff --git a/gameloop.py b/gameloop.py index 0a60ab0..2dcb642 100644 --- a/gameloop.py +++ b/gameloop.py @@ -1,14 +1,22 @@ import asyncio import websockets +from websockets.exceptions import ConnectionClosed +from wakepy import keep DEFAULT_SERVER_URL = "wss://connect4.abunchofknowitalls.com" +RECONNECT_INTERVAL_SECONDS = 5 +RECONNECT_TIMEOUT_SECONDS = 60 +MAX_RECONNECT_ATTEMPTS = ( + (RECONNECT_TIMEOUT_SECONDS + RECONNECT_INTERVAL_SECONDS - 1) + // RECONNECT_INTERVAL_SECONDS +) from agent import Agent async def gameloop(socket): player = Agent() - while True: # While game is active, continually anticipate messages + while not socket.closed: # While game is active, continually anticipate messages message = (await socket.recv()).split(":") # Receive message from server match message[0]: @@ -37,19 +45,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)) diff --git a/src/main.rs b/src/main.rs index 055cdba..027b74e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,6 +84,19 @@ async fn handle_connection( let _ = send(&tx, "ERROR:INVALID:ID:"); } } + "RECONNECT" => { + if parts.len() > 1 { + let requested_username = parts[1].to_string(); + if let Err(e) = + sd.handle_reconnect_cmd(addr, tx.clone(), requested_username).await + { + error!("handle_reconnect: {}", e); + let _ = send(&tx, e.to_string().as_str()); + } + } else { + let _ = send(&tx, "ERROR:INVALID:RECONNECT:"); + } + } "DISCONNECT" => { if let Err(e) = sd.handle_disconnect_cmd(addr, tx.clone()).await { error!("handle_disconnect: {}", e); @@ -288,14 +301,17 @@ async fn handle_connection( if clients_guard.get(&addr).is_some() { let client = clients_guard.get(&addr).unwrap().read().await; let username = client.username.clone(); - if let Some(match_id) = client.current_match { - drop(client); - // TOOD: do a Technical Timeout instead - sd.terminate_match(match_id).await; - } else { - drop(client); + let tournament_guard = sd.tournament.read().await; + if client.current_match.is_some() { + sd.disconnected_clients.write().await.push(username.clone()); + } else if tournament_guard.is_some() { + let tourney = tournament_guard.clone().unwrap(); + if tourney.read().await.contains_player(addr) { + sd.disconnected_clients.write().await.push(username.clone()); + } } + drop(client); drop(clients_guard); sd.clients.write().await.remove(&addr); diff --git a/src/server.rs b/src/server.rs index b4cdb48..b8cc9c9 100644 --- a/src/server.rs +++ b/src/server.rs @@ -4,6 +4,7 @@ use crate::{tournaments::*, types::*, *}; pub struct Server { pub clients: Clients, + pub disconnected_clients: Arc>>, pub usernames: Usernames, pub observers: Observers, pub matches: Matches, @@ -20,6 +21,7 @@ impl Server { pub fn new(admin_password: String, demo_mode: bool) -> Server { Server { clients: Arc::new(RwLock::new(HashMap::new())), + disconnected_clients: Arc::new(RwLock::new(Vec::new())), usernames: Arc::new(RwLock::new(HashMap::new())), observers: Arc::new(RwLock::new(HashMap::new())), matches: Arc::new(RwLock::new(HashMap::new())), @@ -53,9 +55,21 @@ impl Server { ))); } + let mut reconnecting = false; + let disconnected_guard = self.disconnected_clients.read().await; + if disconnected_guard.contains(&requested_username) { + reconnecting = true; + } + let clients_guard = self.clients.read().await; + let mut reconnecting_client = None; for client in clients_guard.values() { if requested_username == client.read().await.username { + if reconnecting { + reconnecting_client = Some(client.clone()); + break; + } + return Err(anyhow::anyhow!(format!( "ERROR:INVALID:ID:{}", requested_username @@ -66,20 +80,141 @@ impl Server { drop(clients_guard); self.remove_observer_from_all_matches(addr).await; - - // not taken self.observers.write().await.remove(&addr); self.usernames.write().await.insert(requested_username.clone(), addr); - self.clients.write().await.insert( - addr.to_string().parse()?, - Arc::new(RwLock::new(Client::new( - requested_username, - tx.clone(), - addr.to_string().parse()?, - ))), - ); - let _ = send(&tx, "CONNECT:ACK"); + + if !reconnecting { + self.clients.write().await.insert( + addr.to_string().parse()?, + Arc::new(RwLock::new(Client::new( + requested_username, + tx.clone(), + addr.to_string().parse()?, + ))), + ); + + return Ok(()); + } + + // reconnecting + self.disconnected_clients.write().await.retain(|name| name != &requested_username); + let client_guard = reconnecting_client.unwrap(); + let mut client = client_guard.write().await; + let old_addr = client.addr; + client.addr = addr; + client.connection = tx.clone(); + // I don't think this will fail + let match_id = client.current_match.unwrap(); + let client_color = client.color; + + drop(client); + + let mut clients_guard = self.clients.write().await; + clients_guard.remove(&old_addr); + clients_guard.insert(addr, client_guard.clone()); + drop(clients_guard); + + let tournament_guard = self.tournament.read().await; + if tournament_guard.is_some() { + let tourney = tournament_guard.clone().unwrap(); + tourney.write().await.inform_reconnect(old_addr, addr); + } + drop(tournament_guard); + + let matches_guard = self.matches.read().await; + let mut the_match = matches_guard.get(&match_id).unwrap().write().await; + if the_match.demo_mode { + drop(the_match); + drop(matches_guard); + self.terminate_match(match_id).await; + return Ok(()); + } else { + the_match.ledger.clear(); + the_match.board = vec![vec![Color::None; 6]; 7]; + let opponent_addr = if the_match.player1 == addr { + the_match.player2 + } else { + the_match.player1 + }; + + if the_match.wait_thread.is_some() { + the_match.wait_thread.as_ref().unwrap().abort(); + } + + if the_match.timeout_thread.is_some() { + the_match.timeout_thread.as_ref().unwrap().abort(); + } + + let clients_guard = self.clients.read().await; + let opponent = clients_guard.get(&opponent_addr).unwrap().read().await; + let _ = send(&opponent.connection, "GAME:TERMINATED"); + let _ = send( + &tx, + &format!("GAME:START:{}", bool::from(client_color) as u8), + ); + let _ = send( + &opponent.connection, + &format!("GAME:START:{}", bool::from(opponent.color) as u8), + ); + } + + Ok(()) + } + + pub async fn handle_reconnect_cmd( + &self, + addr: SocketAddr, + tx: UnboundedSender, + requested_username: String, + ) -> Result<(), anyhow::Error> { + let clients_guard = self.clients.read().await; + let disconnected_guard = self.disconnected_clients.read().await; + let mut found_client = None; + + for client in clients_guard.values() { + if requested_username == client.read().await.username { + if disconnected_guard.contains(&requested_username) { + found_client = Some(client.clone()); + } + break; + } + } + + drop(clients_guard); + drop(disconnected_guard); + + if let Some(client_guard) = found_client { + self.disconnected_clients.write().await.retain(|name| name != &requested_username); + let mut client = client_guard.write().await; + let old_addr = client.addr; + client.addr = addr; + client.connection = tx.clone(); + + let mut clients_guard = self.clients.write().await; + clients_guard.remove(&old_addr); + clients_guard.insert(addr, client_guard.clone()); + drop(clients_guard); + + let _ = send(&tx, "RECONNECT:ACK"); + + let matches_guard = self.matches.read().await; + let the_match = matches_guard.get(&client.current_match.unwrap()).unwrap().read().await; + + let last = the_match.ledger.last(); + if last.is_some() && last.unwrap().0 != client.color { + let _ = send( + &tx, + &format!("OPPONENT:{}", the_match.ledger.last().unwrap().1), + ); + } + } else { + return Err(anyhow::anyhow!(format!( + "ERROR:INVALID:RECONNECT:{}", + requested_username + ))); + } + Ok(()) } @@ -219,26 +354,20 @@ impl Server { let matches_guard = self.matches.read().await; let current_match = matches_guard.get(&client.current_match.unwrap()).unwrap().read().await; - let opponent_addr = if addr == current_match.player1 { - current_match.player2 - } else { - current_match.player1 - }; + let opponent = { + let mut result = None; - let opponent_connection = if current_match.demo_mode { - None - } else if addr == current_match.player1 { - Some(clients_guard.get(¤t_match.player2).unwrap().read().await.connection.clone()) - } else { - Some(clients_guard.get(¤t_match.player1).unwrap().read().await.connection.clone()) - }; + if !current_match.demo_mode { + let opponent_addr = if addr == current_match.player1 { + current_match.player2 + } else { + current_match.player1 + }; - let opponent_username = if current_match.demo_mode { - SERVER_PLAYER_USERNAME.to_string() - } else if addr == current_match.player1 { - clients_guard.get(¤t_match.player2).unwrap().read().await.username.clone() - } else { - clients_guard.get(¤t_match.player1).unwrap().read().await.username.clone() + result = Some(clients_guard.get(&opponent_addr).cloned().unwrap()); + } + + result }; // Check if it's their move @@ -284,9 +413,17 @@ impl Server { self.terminate_match(current_match_id).await; tx.send(Message::Close(None))?; } else { + let opponent = opponent.unwrap(); + let mut opponent = opponent.write().await; + let _ = send(&tx, "GAME:LOSS"); - let _ = send(&opponent_connection.unwrap(), "GAME:WINS"); - self.broadcast_message(&viewers, &format!("GAME:WIN:{}", opponent_username)).await; + let _ = send(&opponent.connection, "GAME:WINS"); + self.broadcast_message(&viewers, &format!("GAME:WIN:{}", opponent.username)).await; + + opponent.current_match = None; + opponent.color = Color::None; + let opponent_addr = opponent.addr; + drop(opponent); let mut clients_guard = self.clients.write().await; let mut client = clients_guard.get_mut(&addr).unwrap().write().await; @@ -294,11 +431,6 @@ impl Server { client.color = Color::None; drop(client); - let mut opponent = clients_guard.get_mut(&opponent_addr).unwrap().write().await; - opponent.current_match = None; - opponent.color = Color::None; - drop(opponent); - let mut tournament_guard = self.tournament.write().await; let tourney = tournament_guard.as_mut().unwrap(); tourney.write().await.inform_winner(opponent_addr, false); @@ -327,13 +459,17 @@ impl Server { if winner == client.color { let _ = send(&tx, "GAME:WINS"); if !current_match.demo_mode { - let _ = send(&opponent_connection.as_ref().unwrap(), "GAME:LOSS"); + let opponent = opponent.clone().unwrap(); + let opponent = opponent.read().await; + let _ = send(&opponent.connection, "GAME:LOSS"); } viewer_messages.push(format!("GAME:WIN:{}", client.username)); } else if filled { let _ = send(&tx, "GAME:DRAW"); if !current_match.demo_mode { - let _ = send(&opponent_connection.as_ref().unwrap(), "GAME:DRAW"); + let opponent = opponent.clone().unwrap(); + let opponent = opponent.read().await; + let _ = send(&opponent.connection, "GAME:DRAW"); } viewer_messages.push("GAME:DRAW".to_string()); } @@ -354,7 +490,8 @@ impl Server { drop(client); if !is_demo_mode { - let mut opponent = clients_guard.get(&opponent_addr).unwrap().write().await; + let opponent = opponent.clone().unwrap(); + let mut opponent = opponent.write().await; opponent.current_match = None; opponent.color = Color::None; drop(opponent); @@ -376,7 +513,9 @@ impl Server { } else if self.tournament.read().await.is_none() { let _ = send(&tx, "TOURNAMENT:END"); if !is_demo_mode { - let _ = send(&opponent_connection.unwrap(), "TOURNAMENT:END"); + let opponent = opponent.clone().unwrap(); + let opponent = opponent.read().await; + let _ = send(&opponent.connection, "TOURNAMENT:END"); } } @@ -405,7 +544,7 @@ impl Server { let demo_move = random_move(¤t_match.board); let no_winner = winner == Color::None && !filled; let observers = self.observers.clone(); - let opp_connection_move = opponent_connection.clone(); + let opponent_move = opponent.clone(); let client_tx = tx.clone(); if current_match.demo_mode { current_match.ledger.push((!client.color, demo_move, Instant::now())); @@ -416,8 +555,10 @@ impl Server { tokio::time::sleep(tokio::time::Duration::from_millis(adjusted_waiting as u64)).await; if !demo_mode && no_winner { + let opponent = opponent_move.unwrap(); + let opponent = opponent.read().await; let _ = send( - &opp_connection_move.as_ref().unwrap(), + &opponent.connection.clone(), &format!("OPPONENT:{}", column), ); } @@ -449,6 +590,7 @@ impl Server { let client_addr = addr.clone(); let observers = self.observers.clone(); let viewers = current_match.viewers.clone(); + let opponent_move = opponent.clone(); current_match.timeout_thread = Some(tokio::spawn(async move { if demo_mode { return; @@ -463,7 +605,10 @@ impl Server { if the_match.ledger.len() == ledger_size { // forfeit the match let _ = send(&client_tx, "GAME:WINS"); - let _ = send(&opponent_connection.unwrap(), "GAME:LOSS"); + let opponent = opponent_move.clone().unwrap(); + let opponent = opponent.read().await; + let _ = send(&opponent.connection, "GAME:LOSS"); + drop(opponent); broadcast_message( &observers, &viewers, @@ -477,7 +622,8 @@ impl Server { client.color = Color::None; drop(client); - let mut opponent = clients_guard.get_mut(&opponent_addr).unwrap().write().await; + let opponent = opponent_move.unwrap(); + let mut opponent = opponent.write().await; opponent.current_match = None; opponent.color = Color::None; drop(opponent); diff --git a/src/tournaments/mod.rs b/src/tournaments/mod.rs index d76d1d7..62d95f4 100644 --- a/src/tournaments/mod.rs +++ b/src/tournaments/mod.rs @@ -16,6 +16,8 @@ pub trait Tournament { async fn start(&mut self, server: &Server); async fn cancel(&mut self, server: &Server); fn inform_winner(&mut self, winner: SocketAddr, is_tie: bool); + fn inform_reconnect(&mut self, old_addr: SocketAddr, new_addr: SocketAddr); + fn contains_player(&self, addr: SocketAddr) -> bool; fn is_completed(&self) -> bool; fn get_type(&self) -> String; } diff --git a/src/tournaments/round_robin.rs b/src/tournaments/round_robin.rs index 8a4d508..18c814f 100644 --- a/src/tournaments/round_robin.rs +++ b/src/tournaments/round_robin.rs @@ -111,6 +111,24 @@ impl Tournament for RoundRobin { } } + fn inform_reconnect(&mut self, old_addr: SocketAddr, new_addr: SocketAddr) { + for (_, (player_addr, _)) in self.players.iter_mut() { + if *player_addr == old_addr { + *player_addr = new_addr; + break; + } + } + } + + fn contains_player(&self, addr: SocketAddr) -> bool { + for (_, (player_addr, _)) in self.players.iter() { + if *player_addr == addr { + return true; + } + } + false + } + async fn next(&mut self, server: &Server) { if self.is_completed { return; diff --git a/src/types.rs b/src/types.rs index 86dbcfb..fef36f7 100644 --- a/src/types.rs +++ b/src/types.rs @@ -24,6 +24,16 @@ impl ops::Not for Color { } } +impl From 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, -- 2.49.1 From 8538068442480c5442eb359af4bb603d8d732b92 Mon Sep 17 00:00:00 2001 From: Rem's Little Helper Date: Fri, 6 Mar 2026 00:49:50 -0500 Subject: [PATCH 4/5] feat: add admin award winner command (#23) Co-authored-by: Joshua Higgins Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/main.rs | 15 +++++++ src/server.rs | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/src/main.rs b/src/main.rs index 027b74e..ae5bb6a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -168,6 +168,21 @@ async fn handle_connection( let _ = send(&tx, "ERROR:INVALID:TERMINATE"); } } + } else if parts.get(1) == Some(&"AWARD") && parts.len() > 3 { + match parts[2].parse::() { + Ok(match_id) => { + let winner = parts[3].to_string(); + if let Err(e) = + sd.handle_game_award_winner(addr, match_id, winner).await + { + error!("handle_game_award_winner: {}", e); + let _ = send(&tx, e.to_string().as_str()); + } + } + Err(_) => { + let _ = send(&tx, "ERROR:INVALID:AWARD"); + } + } } else { let _ = send(&tx, "ERROR:INVALID:GAME"); } diff --git a/src/server.rs b/src/server.rs index b8cc9c9..f4384b7 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,3 +1,4 @@ +use anyhow::anyhow; use std::time::Instant; use crate::{tournaments::*, types::*, *}; @@ -818,6 +819,127 @@ impl Server { Ok(()) } + pub async fn handle_game_award_winner( + &self, + addr: SocketAddr, + match_id: u32, + winner_username: String, + ) -> Result<(), anyhow::Error> { + if !self.auth_check(addr).await { + return Err(anyhow!("ERROR:INVALID:AUTH")); + } + + let matches_guard = self.matches.read().await; + let found_match = + matches_guard.get(&match_id).ok_or_else(|| anyhow!("ERROR:INVALID:AWARD"))?.clone(); + drop(matches_guard); + + let the_match = found_match.read().await; + + // Validate that the declared winner is actually one of the players in this match + let clients_guard = self.clients.read().await; + let player1_client = clients_guard.get(&the_match.player1); + let player2_client = clients_guard.get(&the_match.player2); + + // If we cannot resolve both players, or the winner username doesn't match either, reject + if let (Some(p1_arc), Some(p2_arc)) = (player1_client, player2_client) { + let p1 = p1_arc.read().await; + let p2 = p2_arc.read().await; + + if winner_username != p1.username && winner_username != p2.username { + return Err(anyhow!("ERROR:INVALID:AWARD")); + } + } else { + return Err(anyhow!("ERROR:INVALID:AWARD")); + } + drop(clients_guard); + + self.matches.write().await.remove(&match_id); + + if let Some(wait_thread) = &the_match.wait_thread { + wait_thread.abort(); + } + + if let Some(timeout_thread) = &the_match.timeout_thread { + timeout_thread.abort(); + } + + self.broadcast_message(&the_match.viewers, &format!("GAME:WIN:{}", winner_username)).await; + + let clients_guard = self.clients.read().await; + if the_match.demo_mode { + let player_win = if winner_username != SERVER_PLAYER_USERNAME { + "WINS" + } else { + "LOSS" + }; + let mut the_player = if the_match.player1 != SERVER_PLAYER_ADDR.parse()? { + clients_guard.get(&the_match.player1).unwrap().write().await + } else { + clients_guard.get(&the_match.player2).unwrap().write().await + }; + + let _ = send(&the_player.connection, &format!("GAME:{}", player_win)); + let _ = send(&the_player.connection, "TOURNAMENT:END"); + + the_player.color = Color::None; + the_player.current_match = None; + + return Ok(()); + } + + let mut player1 = clients_guard.get(&the_match.player1).unwrap().write().await; + let mut player2 = clients_guard.get(&the_match.player2).unwrap().write().await; + + player1.current_match = None; + player1.color = Color::None; + + player2.current_match = None; + player2.color = Color::None; + + let winner_tx = if player1.username == winner_username { + player1.connection.clone() + } else { + player2.connection.clone() + }; + + let loser_tx = if player1.username != winner_username { + player1.connection.clone() + } else { + player2.connection.clone() + }; + + let winner_addr = if player1.username == winner_username { + player1.addr.clone() + } else { + player2.addr.clone() + }; + + drop(player1); + drop(player2); + drop(clients_guard); + + let _ = send(&winner_tx, "GAME:WINS"); + let _ = send(&loser_tx, "GAME:LOSS"); + + if self.tournament.read().await.is_some() { + let mut tournament_guard = self.tournament.write().await; + let tourney = tournament_guard.as_mut().unwrap(); + tourney.write().await.inform_winner(winner_addr, false); + if self.matches.read().await.is_empty() { + tourney.write().await.next(&self).await; + if tourney.read().await.is_completed() { + *tournament_guard = None; + } + } + } else { + let _ = send(&winner_tx, "TOURNAMENT:END"); + let _ = send(&loser_tx, "TOURNAMENT:END"); + } + + Ok(()) + } + pub async fn handle_tournament_start( &self, addr: SocketAddr, -- 2.49.1 From 5f858878b92ba62bc590a2d090c1fdf5f2b6fb64 Mon Sep 17 00:00:00 2001 From: Joshua Higgins Date: Fri, 6 Mar 2026 00:55:16 -0500 Subject: [PATCH 5/5] misc: bump packages --- Cargo.lock | 361 +++++++++++++++++++++++++++++++++++++++++++++++------ Cargo.toml | 16 +-- 2 files changed, 331 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18c1285..6b17b55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" @@ -52,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" @@ -60,7 +71,7 @@ dependencies = [ "async-trait", "futures-util", "local-ip-address", - "rand", + "rand 0.10.0", "tokio", "tokio-tungstenite", "tracing", @@ -76,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" @@ -174,6 +194,12 @@ 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" @@ -181,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", @@ -199,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", ] @@ -242,10 +273,24 @@ 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" @@ -258,6 +303,27 @@ dependencies = [ "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" @@ -275,12 +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" @@ -293,6 +377,12 @@ 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" @@ -301,9 +391,9 @@ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "local-ip-address" -version = "0.6.9" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92488bc8a0f99ee9f23577bdd06526d49657df8bd70504c61f812337cdad01ab" +checksum = "79ef8c257c92ade496781a32a581d43e3d512cf8ce714ecf04ea80f93ed0ff4a" dependencies = [ "libc", "neli", @@ -325,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" @@ -338,9 +434,9 @@ dependencies = [ [[package]] name = "neli" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e23bebbf3e157c402c4d5ee113233e5e0610cc27453b2f07eefce649c7365dcc" +checksum = "22f9786d56d972959e1408b6a93be6af13b9c1392036c5c1fafa08a1b0c6ee87" dependencies = [ "bitflags", "byteorder", @@ -409,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" @@ -424,6 +514,16 @@ 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" @@ -470,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" @@ -477,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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.0", ] [[package]] @@ -487,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]] @@ -496,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" @@ -514,6 +637,12 @@ 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" @@ -544,6 +673,19 @@ dependencies = [ "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" @@ -551,7 +693,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -643,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", @@ -683,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", @@ -705,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", @@ -749,7 +891,7 @@ dependencies = [ "http", "httparse", "log", - "rand", + "rand 0.9.2", "sha1", "thiserror", "utf-8", @@ -767,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" @@ -797,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]] @@ -895,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" @@ -914,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" diff --git a/Cargo.toml b/Cargo.toml index 35e4cbf..d215a8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +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" -local-ip-address = "0.6.9" +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" -- 2.49.1