From 15ffa880f7cfa5d63e17d3a5930c675f74c8ab55 Mon Sep 17 00:00:00 2001 From: Joshua Higgins Date: Thu, 26 Mar 2026 12:52:49 -0400 Subject: [PATCH] feat: knockout brackets --- debug_client.js | 2 +- src/server.rs | 147 +++++++++++--- src/tournaments/knockout_bracket.rs | 292 ++++++++++++++++++++++++++++ src/tournaments/mod.rs | 13 +- src/tournaments/round_robin.rs | 86 ++++---- 5 files changed, 460 insertions(+), 80 deletions(-) create mode 100644 src/tournaments/knockout_bracket.rs diff --git a/debug_client.js b/debug_client.js index c4146af..b6b65fe 100644 --- a/debug_client.js +++ b/debug_client.js @@ -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; diff --git a/src/server.rs b/src/server.rs index e7f5dae..8df245a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -429,6 +429,9 @@ impl Server { invalid = true; } + let player1 = current_match.player1.clone(); + let player2 = current_match.player2.clone(); + // Terminate games if a player makes an invalid move if invalid { let current_match_id = current_match.id; @@ -468,7 +471,16 @@ impl Server { let mut tournament_guard = self.tournament.write().await; let tourney = tournament_guard.as_mut().unwrap(); - tourney.write().await.inform_winner(opponent_username, false); + tourney + .write() + .await + .inform_winner( + opponent_username, + current_match_id, + player1.clone(), + player2.clone(), + ) + .await; drop(tournament_guard); self.matches.write().await.remove(¤t_match_id).unwrap(); @@ -539,32 +551,55 @@ impl Server { matches_guard.remove(¤t_match_id).unwrap(); - if self.tournament.read().await.is_some() && matches_guard.is_empty() { - drop(matches_guard); - drop(clients_guard); - + 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(username.clone(), filled); - tourney.write().await.next(&self).await; - if tourney.read().await.is_completed() { - *tournament_guard = None; - } - } else if self.tournament.read().await.is_none() { - let _ = send(&tx, "TOURNAMENT:END"); - if !is_demo_mode { - let opponent = opponent.clone().unwrap(); - let opponent = opponent.read().await; - let _ = send(&opponent.connection, "TOURNAMENT:END"); + let winner = if filled { + String::new() + } else { + username.clone() + }; + + tourney + .write() + .await + .inform_winner(winner, current_match_id, player1.clone(), player2.clone()) + .await; + + if matches_guard.is_empty() { + drop(matches_guard); + drop(clients_guard); + + tourney.write().await.next(&self).await; + if tourney.read().await.is_completed() { + let tournament_players = tourney.read().await.get_players(); + let clients_guard = self.clients.read().await; + + for player in tournament_players { + let player = clients_guard.get(&player); + + if player.is_none() { + continue; + } + + let player = player.unwrap().read().await; + let _ = send(&player.connection, "TOURNAMENT:END"); + } + + *tournament_guard = None; + + self.broadcast("TOURNAMENT:END").await; + } } } return Ok(()); } - let default_waiting_time = *self.waiting_timeout.read().await; - let mut adjusted_waiting = - default_waiting_time as i64 + (rand::rng().random_range(0..=50) - 25); + let set_waiting_time = *self.waiting_timeout.read().await; + let mut variance = rand::rng().random_range(0..=(set_waiting_time / 200)) as i64; + variance *= rand::rng().random_range(0..=2) - 1; + let mut adjusted_waiting = set_waiting_time as i64 + variance; let current_move_time = Instant::now(); if current_match.ledger.is_empty() { @@ -609,7 +644,7 @@ impl Server { } if demo_mode && no_winner { - tokio::time::sleep(tokio::time::Duration::from_millis(default_waiting_time)).await; + tokio::time::sleep(tokio::time::Duration::from_millis(set_waiting_time)).await; let _ = send(&client_tx, &format!("OPPONENT:{}", demo_move)); let observers_guard = observers.read().await; let msg = format!( @@ -640,7 +675,8 @@ impl Server { tokio::time::sleep(tokio::time::Duration::from_millis(max_timeout as u64)).await; let matches_guard = matches.read().await; - let the_match = matches_guard.get(&match_id); + let the_match = matches_guard.get(&match_id).cloned(); + drop(matches_guard); if let Some(the_match) = the_match { let the_match = the_match.read().await; if the_match.ledger.len() == ledger_size { @@ -670,7 +706,7 @@ impl Server { let mut tournament_guard = tournament.write().await; let tourney = tournament_guard.as_mut().unwrap(); - tourney.write().await.inform_winner(client_username, false); + tourney.write().await.inform_winner(client_username, match_id, player1, player2).await; drop(tournament_guard); matches.write().await.remove(&match_id).unwrap(); @@ -820,7 +856,23 @@ impl Server { let tourney = tournament_guard.as_mut().unwrap(); tourney.write().await.next(&self).await; if tourney.read().await.is_completed() { + let tournament_players = tourney.read().await.get_players(); + let clients_guard = self.clients.read().await; + + for player in tournament_players { + let player = clients_guard.get(&player); + + if player.is_none() { + continue; + } + + let player = player.unwrap().read().await; + let _ = send(&player.connection, "TOURNAMENT:END"); + } + *tournament_guard = None; + + self.broadcast("TOURNAMENT:END").await; } } Ok(()) @@ -926,11 +978,36 @@ impl Server { 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_username, false); + tourney + .write() + .await + .inform_winner( + winner_username, + match_id, + the_match.player1.clone(), + the_match.player2.clone(), + ) + .await; if self.matches.read().await.is_empty() { tourney.write().await.next(&self).await; if tourney.read().await.is_completed() { + let tournament_players = tourney.read().await.get_players(); + let clients_guard = self.clients.read().await; + + for player in tournament_players { + let player = clients_guard.get(&player); + + if player.is_none() { + continue; + } + + let player = player.unwrap().read().await; + let _ = send(&player.connection, "TOURNAMENT:END"); + } + *tournament_guard = None; + + self.broadcast("TOURNAMENT:END").await; } } } else { @@ -968,14 +1045,26 @@ impl Server { drop(clients_guard); - let mut tourney = match tournament_type.as_str() { - "RoundRobin" => RoundRobin::new(&ready_players), - &_ => RoundRobin::new(&ready_players), - }; - tourney.start(&self).await; + let tourney: Option>> = + match tournament_type.as_str() { + "RoundRobin" => Some(Arc::new(RwLock::new( + RoundRobin::new(&ready_players, &self).await, + ))), + "KnockoutBracket" => Some(Arc::new(RwLock::new( + KnockoutBracket::new(&ready_players, &self).await, + ))), + &_ => None, + }; + + if tourney.is_none() { + return Err(anyhow::anyhow!("ERROR:INVALID:TOURNAMENT")); + } + + let tourney = tourney.unwrap(); + tourney.write().await.start(&self).await; let mut tournament_guard = self.tournament.write().await; - *tournament_guard = Some(Arc::new(RwLock::new(tourney))); + *tournament_guard = Some(tourney); // Clear any pending reservations when a tournament starts self.reservations.write().await.clear(); diff --git a/src/tournaments/knockout_bracket.rs b/src/tournaments/knockout_bracket.rs new file mode 100644 index 0000000..7a50856 --- /dev/null +++ b/src/tournaments/knockout_bracket.rs @@ -0,0 +1,292 @@ +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, + pub current_matches: Vec, + pub previous_wait: u64, + pub completed: bool, + pub started: bool, + pub clients: Clients, + pub matches: Matches, + pub usernames: Vec, +} + +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[i + 1].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; + + 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; + + 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()); + + 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(); + + *server.waiting_timeout.write().await = 5; + + 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, + clients: server.clients.clone(), + matches: server.matches.clone(), + usernames: ready_players.to_vec(), + } + } + + 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 { + *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| b.1.cmp(&a.1)); + self.players = players; + + for player in &self.players { + self.pairings.push(player.0.clone()); + } + + self.create_matches().await; + + self.started = true; + return; + } + + if self.pairings.len() == 1 { + self.completed = true; + } else { + self.pairings.retain(|p| !p.is_empty()); + self.create_matches().await; + } + } + + async fn start(&mut self, server: &Server) { + 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 { + if player1_track.1 < player2_track.1 { + winner = player2_track.0.clone(); + } else { + winner = player1_track.0.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.clone(), + player2.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(match_id); + player1.ready = false; + + 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(match_id); + player2.ready = false; + + 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.current_matches.push(match_id); + self.matches.write().await.insert(match_id, new_match.clone()); + } + + 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 { + self.usernames.clone() + } + + fn get_type(&self) -> String { + "KnockoutBracket".to_string() + } +} diff --git a/src/tournaments/mod.rs b/src/tournaments/mod.rs index 813173d..ba8881e 100644 --- a/src/tournaments/mod.rs +++ b/src/tournaments/mod.rs @@ -4,17 +4,26 @@ 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 { - fn new(ready_players: &[String]) -> Self + 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); - fn inform_winner(&mut self, winner: String, is_tie: bool); + 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; fn get_type(&self) -> String; } diff --git a/src/tournaments/round_robin.rs b/src/tournaments/round_robin.rs index fc1c597..e6a3301 100644 --- a/src/tournaments/round_robin.rs +++ b/src/tournaments/round_robin.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use async_trait::async_trait; +use tracing::info; use crate::{server::Server, *}; @@ -12,11 +13,13 @@ pub struct RoundRobin { pub players: HashMap, pub top_half: Vec, pub bottom_half: Vec, - pub is_completed: bool, + pub completed: bool, + pub current_matches: Vec, + pub usernames: Vec, } impl RoundRobin { - async fn create_matches(&self, clients: &Clients, matches: &Matches) { + 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 player1_username = self.players.get(id).unwrap(); @@ -35,6 +38,8 @@ impl RoundRobin { 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; @@ -73,12 +78,14 @@ impl RoundRobin { #[async_trait] impl Tournament for RoundRobin { - fn new(ready_players: &[String]) -> RoundRobin { + async fn new(ready_players: &[String], _: &Server) -> RoundRobin { let mut result = RoundRobin { players: HashMap::new(), top_half: Vec::new(), bottom_half: Vec::new(), - is_completed: false, + completed: false, + current_matches: Vec::new(), + usernames: ready_players.to_vec(), }; let size = ready_players.len(); @@ -98,8 +105,10 @@ impl Tournament for RoundRobin { result } - fn inform_winner(&mut self, winner: String, is_tie: bool) { - if is_tie { + async fn inform_winner(&mut self, winner: String, match_id: u32, _: String, _: String) { + info!("RoundRobin: told winner was \"{}\"", winner); + + if winner.is_empty() { return; } @@ -109,24 +118,17 @@ impl Tournament for RoundRobin { break; } } - } - fn contains_player(&self, username: String) -> bool { - for (_, (player_username, _)) in self.players.iter() { - if *player_username == username { - return true; - } - } - false + self.current_matches.retain(|id| !(*id == match_id)); } async fn next(&mut self, server: &Server) { - if self.is_completed { + if self.completed { return; } if self.top_half.len() <= 1 || self.bottom_half.is_empty() { - self.is_completed = true; + self.completed = true; return; } @@ -138,20 +140,20 @@ impl Tournament for RoundRobin { 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; + self.completed = true; } let clients_guard = server.clients.read().await; let mut player_scores: Vec<(String, u32)> = Vec::new(); for (_, player_addr) in self.players.iter() { let player = clients_guard.get(&player_addr.0).unwrap().read().await; - let _ = send(&player.connection.clone(), "TOURNAMENT:END"); player_scores.push((player.username.clone(), player_addr.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)) @@ -160,15 +162,7 @@ impl Tournament for RoundRobin { server.broadcast(&message).await; - if self.is_completed() { - // Send scores - let clients_guard = server.clients.read().await; - for (_, player_addr) in self.players.iter() { - let player = clients_guard.get(&player_addr.0).unwrap().read().await; - let _ = send(&player.connection.clone(), "TOURNAMENT:END"); - } - } else { - // Create next matches + if !self.is_completed() { self.create_matches(&server.clients, &server.matches).await; } } @@ -178,36 +172,32 @@ impl Tournament for RoundRobin { } async fn cancel(&mut self, server: &Server) { - for (_, addr) in self.players.iter() { - let clients_guard = server.clients.read().await; + for match_id in &self.current_matches { + server.terminate_match(*match_id).await; + } - let client = clients_guard.get(&addr.0); + 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 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); - - server.terminate_match(match_id).await; - - if !client_ready { - let _ = send(&client_connection, "TOURNAMENT:END"); - } + let _ = send(&client.connection, "TOURNAMENT:END"); } } + fn contains_player(&self, username: String) -> bool { + self.usernames.contains(&username) + } + fn is_completed(&self) -> bool { - self.is_completed + self.completed + } + + fn get_players(&self) -> Vec { + self.usernames.clone() } fn get_type(&self) -> String {