feat: knockout brackets

This commit is contained in:
2026-03-26 12:52:49 -04:00
Unverified
parent 12c5c675e2
commit 15ffa880f7
5 changed files with 460 additions and 80 deletions

View File

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

View File

@@ -429,6 +429,9 @@ impl Server {
invalid = true; invalid = true;
} }
let player1 = current_match.player1.clone();
let player2 = current_match.player2.clone();
// Terminate games if a player makes an invalid move // Terminate games if a player makes an invalid move
if invalid { if invalid {
let current_match_id = current_match.id; let current_match_id = current_match.id;
@@ -468,7 +471,16 @@ impl Server {
let mut tournament_guard = self.tournament.write().await; let mut tournament_guard = self.tournament.write().await;
let tourney = tournament_guard.as_mut().unwrap(); 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); drop(tournament_guard);
self.matches.write().await.remove(&current_match_id).unwrap(); self.matches.write().await.remove(&current_match_id).unwrap();
@@ -539,32 +551,55 @@ impl Server {
matches_guard.remove(&current_match_id).unwrap(); matches_guard.remove(&current_match_id).unwrap();
if self.tournament.read().await.is_some() && matches_guard.is_empty() { if self.tournament.read().await.is_some() {
drop(matches_guard);
drop(clients_guard);
let mut tournament_guard = self.tournament.write().await; let mut tournament_guard = self.tournament.write().await;
let tourney = tournament_guard.as_mut().unwrap(); let tourney = tournament_guard.as_mut().unwrap();
tourney.write().await.inform_winner(username.clone(), filled); let winner = if filled {
tourney.write().await.next(&self).await; String::new()
if tourney.read().await.is_completed() { } else {
*tournament_guard = None; username.clone()
} };
} else if self.tournament.read().await.is_none() {
let _ = send(&tx, "TOURNAMENT:END"); tourney
if !is_demo_mode { .write()
let opponent = opponent.clone().unwrap(); .await
let opponent = opponent.read().await; .inform_winner(winner, current_match_id, player1.clone(), player2.clone())
let _ = send(&opponent.connection, "TOURNAMENT:END"); .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(()); return Ok(());
} }
let default_waiting_time = *self.waiting_timeout.read().await; let set_waiting_time = *self.waiting_timeout.read().await;
let mut adjusted_waiting = let mut variance = rand::rng().random_range(0..=(set_waiting_time / 200)) as i64;
default_waiting_time as i64 + (rand::rng().random_range(0..=50) - 25); variance *= rand::rng().random_range(0..=2) - 1;
let mut adjusted_waiting = set_waiting_time as i64 + variance;
let current_move_time = Instant::now(); let current_move_time = Instant::now();
if current_match.ledger.is_empty() { if current_match.ledger.is_empty() {
@@ -609,7 +644,7 @@ impl Server {
} }
if demo_mode && no_winner { 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 _ = send(&client_tx, &format!("OPPONENT:{}", demo_move));
let observers_guard = observers.read().await; let observers_guard = observers.read().await;
let msg = format!( let msg = format!(
@@ -640,7 +675,8 @@ impl Server {
tokio::time::sleep(tokio::time::Duration::from_millis(max_timeout as u64)).await; tokio::time::sleep(tokio::time::Duration::from_millis(max_timeout as u64)).await;
let matches_guard = matches.read().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 { if let Some(the_match) = the_match {
let the_match = the_match.read().await; let the_match = the_match.read().await;
if the_match.ledger.len() == ledger_size { if the_match.ledger.len() == ledger_size {
@@ -670,7 +706,7 @@ impl Server {
let mut tournament_guard = tournament.write().await; let mut tournament_guard = tournament.write().await;
let tourney = tournament_guard.as_mut().unwrap(); 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); drop(tournament_guard);
matches.write().await.remove(&match_id).unwrap(); matches.write().await.remove(&match_id).unwrap();
@@ -820,7 +856,23 @@ impl Server {
let tourney = tournament_guard.as_mut().unwrap(); let tourney = tournament_guard.as_mut().unwrap();
tourney.write().await.next(&self).await; tourney.write().await.next(&self).await;
if tourney.read().await.is_completed() { 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; *tournament_guard = None;
self.broadcast("TOURNAMENT:END").await;
} }
} }
Ok(()) Ok(())
@@ -926,11 +978,36 @@ impl Server {
if self.tournament.read().await.is_some() { if self.tournament.read().await.is_some() {
let mut tournament_guard = self.tournament.write().await; let mut tournament_guard = self.tournament.write().await;
let tourney = tournament_guard.as_mut().unwrap(); 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() { if self.matches.read().await.is_empty() {
tourney.write().await.next(&self).await; tourney.write().await.next(&self).await;
if tourney.read().await.is_completed() { 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; *tournament_guard = None;
self.broadcast("TOURNAMENT:END").await;
} }
} }
} else { } else {
@@ -968,14 +1045,26 @@ impl Server {
drop(clients_guard); drop(clients_guard);
let mut tourney = match tournament_type.as_str() { let tourney: Option<Arc<RwLock<dyn Tournament + Send + Sync + 'static>>> =
"RoundRobin" => RoundRobin::new(&ready_players), match tournament_type.as_str() {
&_ => RoundRobin::new(&ready_players), "RoundRobin" => Some(Arc::new(RwLock::new(
}; RoundRobin::new(&ready_players, &self).await,
tourney.start(&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; 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 // Clear any pending reservations when a tournament starts
self.reservations.write().await.clear(); self.reservations.write().await.clear();

View File

@@ -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<String>,
pub current_matches: Vec<u32>,
pub previous_wait: u64,
pub completed: bool,
pub started: bool,
pub clients: Clients,
pub matches: Matches,
pub usernames: 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[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<String> {
self.usernames.clone()
}
fn get_type(&self) -> String {
"KnockoutBracket".to_string()
}
}

View File

@@ -4,17 +4,26 @@ use crate::server::Server;
pub mod round_robin; pub mod round_robin;
pub use round_robin::RoundRobin; pub use round_robin::RoundRobin;
pub mod knockout_bracket;
pub use knockout_bracket::KnockoutBracket;
#[async_trait] #[async_trait]
pub trait Tournament { pub trait Tournament {
fn new(ready_players: &[String]) -> Self async fn new(ready_players: &[String], server: &Server) -> Self
where where
Self: Sized; Self: Sized;
async fn next(&mut self, server: &Server); async fn next(&mut self, server: &Server);
async fn start(&mut self, server: &Server); async fn start(&mut self, server: &Server);
async fn cancel(&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 contains_player(&self, username: String) -> bool;
fn is_completed(&self) -> bool; fn is_completed(&self) -> bool;
fn get_players(&self) -> Vec<String>;
fn get_type(&self) -> String; fn get_type(&self) -> String;
} }

View File

@@ -1,6 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use async_trait::async_trait; use async_trait::async_trait;
use tracing::info;
use crate::{server::Server, *}; use crate::{server::Server, *};
@@ -12,11 +13,13 @@ pub struct RoundRobin {
pub players: HashMap<ID, (String, Score)>, pub players: HashMap<ID, (String, Score)>,
pub top_half: Vec<ID>, pub top_half: Vec<ID>,
pub bottom_half: Vec<ID>, pub bottom_half: Vec<ID>,
pub is_completed: bool, pub completed: bool,
pub current_matches: Vec<ID>,
pub usernames: Vec<String>,
} }
impl RoundRobin { 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; let clients_guard = clients.read().await;
for (i, id) in self.top_half.iter().enumerate() { for (i, id) in self.top_half.iter().enumerate() {
let player1_username = self.players.get(id).unwrap(); let player1_username = self.players.get(id).unwrap();
@@ -35,6 +38,8 @@ impl RoundRobin {
false, false,
))); )));
self.current_matches.push(match_id.clone());
let match_guard = new_match.read().await; let match_guard = new_match.read().await;
let mut player1 = clients_guard.get(&player1_username.0).unwrap().write().await; let mut player1 = clients_guard.get(&player1_username.0).unwrap().write().await;
@@ -73,12 +78,14 @@ impl RoundRobin {
#[async_trait] #[async_trait]
impl Tournament for RoundRobin { impl Tournament for RoundRobin {
fn new(ready_players: &[String]) -> RoundRobin { async fn new(ready_players: &[String], _: &Server) -> RoundRobin {
let mut result = RoundRobin { let mut result = RoundRobin {
players: HashMap::new(), players: HashMap::new(),
top_half: Vec::new(), top_half: Vec::new(),
bottom_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(); let size = ready_players.len();
@@ -98,8 +105,10 @@ impl Tournament for RoundRobin {
result result
} }
fn inform_winner(&mut self, winner: String, is_tie: bool) { async fn inform_winner(&mut self, winner: String, match_id: u32, _: String, _: String) {
if is_tie { info!("RoundRobin: told winner was \"{}\"", winner);
if winner.is_empty() {
return; return;
} }
@@ -109,24 +118,17 @@ impl Tournament for RoundRobin {
break; break;
} }
} }
}
fn contains_player(&self, username: String) -> bool { self.current_matches.retain(|id| !(*id == match_id));
for (_, (player_username, _)) in self.players.iter() {
if *player_username == username {
return true;
}
}
false
} }
async fn next(&mut self, server: &Server) { async fn next(&mut self, server: &Server) {
if self.is_completed { if self.completed {
return; return;
} }
if self.top_half.len() <= 1 || self.bottom_half.is_empty() { if self.top_half.len() <= 1 || self.bottom_half.is_empty() {
self.is_completed = true; self.completed = true;
return; return;
} }
@@ -138,20 +140,20 @@ impl Tournament for RoundRobin {
let expected_bottom_start = self.top_half.len() as u32; let expected_bottom_start = self.top_half.len() as u32;
if self.top_half[1] == 1 && self.bottom_half[0] == expected_bottom_start { 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 clients_guard = server.clients.read().await;
let mut player_scores: Vec<(String, u32)> = Vec::new(); let mut player_scores: Vec<(String, u32)> = Vec::new();
for (_, player_addr) in self.players.iter() { for (_, player_addr) in self.players.iter() {
let player = clients_guard.get(&player_addr.0).unwrap().read().await; 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)); player_scores.push((player.username.clone(), player_addr.1));
} }
drop(clients_guard); drop(clients_guard);
player_scores.sort_by(|a, b| b.1.cmp(&a.1)); player_scores.sort_by(|a, b| b.1.cmp(&a.1));
// Send scores
let mut message = "TOURNAMENT:SCORES:".to_string(); let mut message = "TOURNAMENT:SCORES:".to_string();
for (player, score) in player_scores.iter() { for (player, score) in player_scores.iter() {
message.push_str(&format!("{},{}|", player, score)) message.push_str(&format!("{},{}|", player, score))
@@ -160,15 +162,7 @@ impl Tournament for RoundRobin {
server.broadcast(&message).await; server.broadcast(&message).await;
if self.is_completed() { 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
self.create_matches(&server.clients, &server.matches).await; self.create_matches(&server.clients, &server.matches).await;
} }
} }
@@ -178,36 +172,32 @@ impl Tournament for RoundRobin {
} }
async fn cancel(&mut self, server: &Server) { async fn cancel(&mut self, server: &Server) {
for (_, addr) in self.players.iter() { for match_id in &self.current_matches {
let clients_guard = server.clients.read().await; 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() { if client.is_none() {
continue; continue;
} }
let client = client.unwrap().read().await; let client = client.unwrap().read().await;
let client_connection = client.connection.clone(); let _ = send(&client.connection, "TOURNAMENT:END");
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");
}
} }
} }
fn contains_player(&self, username: String) -> bool {
self.usernames.contains(&username)
}
fn is_completed(&self) -> bool { fn is_completed(&self) -> bool {
self.is_completed self.completed
}
fn get_players(&self) -> Vec<String> {
self.usernames.clone()
} }
fn get_type(&self) -> String { fn get_type(&self) -> String {