From b5d71051a69c361898f937f143a559cb588c0c18 Mon Sep 17 00:00:00 2001 From: Joshua Higgins Date: Tue, 30 Dec 2025 21:39:43 -0500 Subject: [PATCH 1/4] misc(refactor): moved tournament to seperate module --- src/lib.rs | 127 +++++++++++++++++ src/main.rs | 140 +------------------ src/tournaments/mod.rs | 17 +++ src/tournaments/round_robin.rs | 174 +++++++++++++++++++++++ src/types.rs | 243 +++++---------------------------- 5 files changed, 359 insertions(+), 342 deletions(-) create mode 100644 src/lib.rs create mode 100644 src/tournaments/mod.rs create mode 100644 src/tournaments/round_robin.rs diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..6d97f53 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,127 @@ +use std::{collections::HashMap, net::SocketAddr, sync::Arc}; + +use rand::Rng; +use tokio::sync::{RwLock, mpsc::{UnboundedSender, error::SendError}}; +use tokio_tungstenite::tungstenite::Message; +use tracing::error; + +use crate::{tournaments::Tournament, types::{Client, Color, Match}}; + +pub mod types; +pub mod tournaments; + +pub type Clients = Arc>>>>; +pub type Usernames = Arc>>; +pub type Observers = Arc>>>; +pub type Matches = Arc>>>>; +pub type WrappedTournament = Arc>>>>; + +pub async fn broadcast_message(addrs: &Vec, observers: &Observers, msg: &str) { + for addr in addrs { + let observers_guard = observers.read().await; + let tx = observers_guard.get(addr); + if tx.is_none() { continue; } + let _ = send(tx.unwrap(), msg); + } +} + +pub async fn broadcast_message_all_observers(observers: &Observers, msg: &str) { + let observers_guard = observers.read().await; + for (_, tx) in observers_guard.iter() { + let _ = send(tx, msg); + } +} + +pub async fn watch(matches: &Matches, new_match_id: u32, addr: SocketAddr) -> Result<(), String> { + let matches_guard = matches.read().await; + + for match_guard in matches_guard.values() { + let mut found = false; + let mut a_match = match_guard.write().await; + for i in 0..a_match.viewers.len() { + if a_match.viewers[i] == addr { + a_match.viewers.remove(i); + found = true; + break; + } + } + + if found { + break; + } + } + + let result = matches_guard.get(&new_match_id); + if result.is_none() { + return Err("Match not found".to_string()); + } + result.unwrap().write().await.viewers.push(addr); + + Ok(()) +} + +pub async fn auth_check(admin: &Arc>>, addr: SocketAddr) -> bool { + if admin.read().await.is_none() || admin.read().await.unwrap() != addr { + return false; + } + true +} + +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 async fn terminate_match(match_id: u32, matches: &Matches, clients: &Clients, observers: &Observers, demo_mode: bool) { + let matches_guard = matches.read().await; + let the_match = matches_guard.get(&match_id); + if the_match.is_none() { + error!("Tried to call terminate_match on invalid matchID: {}", match_id); + } + let the_match = the_match.unwrap().read().await; + + if the_match.wait_thread.is_some() { + the_match.wait_thread.as_ref().unwrap().abort(); + } + + broadcast_message(&the_match.viewers, observers, "GAME:TERMINATED").await; + + let clients_guard = clients.read().await; + let mut player1 = clients_guard.get(&the_match.player1).unwrap().write().await; + let _ = send(&player1.connection, "GAME:TERMINATED"); + player1.current_match = None; + player1.color = Color::None; + drop(player1); + + if !demo_mode { + let mut player2 = clients_guard.get(&the_match.player2).unwrap().write().await; + let _ = send(&player2.connection, "GAME:TERMINATED"); + player2.current_match = None; + player2.color = Color::None; + drop(player2); + } + + drop(clients_guard); + + drop(the_match); + drop(matches_guard); + + matches.write().await.remove(&match_id); +} + +pub fn random_move(board: &[Vec]) -> 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, text: &str) -> Result<(), SendError> { + tx.send(Message::text(text)) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 8b01f69..ae25d82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,12 @@ -mod types; - -use crate::types::{*}; +use connect4_moderator_server::{*}; +use connect4_moderator_server::types::{*}; +use connect4_moderator_server::tournaments::{*}; use futures_util::{SinkExt, StreamExt}; use rand::Rng; -use std::collections::HashMap; use std::env; use std::net::SocketAddr; use std::sync::Arc; use tokio::net::{TcpListener, TcpStream}; -use tokio::sync::mpsc::error::SendError; -use tokio::sync::mpsc::UnboundedSender; use tokio::sync::RwLock; use tokio_tungstenite::{accept_async, tungstenite::Message}; use tracing::{error, info, warn}; @@ -44,27 +41,8 @@ async fn main() -> Result<(), anyhow::Error> { let listener = TcpListener::bind(&addr).await?; info!("WebSocket server listening on: {}", addr); - let clients: Clients = Arc::new(RwLock::new(HashMap::new())); - let usernames: Usernames = Arc::new(RwLock::new(HashMap::new())); - let observers: Observers = Arc::new(RwLock::new(HashMap::new())); - let matches: Matches = Arc::new(RwLock::new(HashMap::new())); - let admin: Arc>> = Arc::new(RwLock::new(None)); - let tournament: WrappedTournament = Arc::new(RwLock::new(None)); - let waiting_timeout: Arc> = Arc::new(RwLock::new(5000)); - let server_data = Arc::new( - Server { - clients, - usernames, - observers, - matches, - admin, - admin_password, - tournament, - waiting_timeout, - demo_mode, - tournament_type, - } + Server::new(admin_password.as_ref().clone(), demo_mode, tournament_type) ); while let Ok((stream, addr)) = listener.accept().await { @@ -725,113 +703,3 @@ fn end_game_check(board: &[Vec]) -> (Color, bool) { result } - -async fn broadcast_message(addrs: &Vec, observers: &Observers, msg: &str) { - for addr in addrs { - let observers_guard = observers.read().await; - let tx = observers_guard.get(addr); - if tx.is_none() { continue; } - let _ = send(tx.unwrap(), msg); - } -} - -async fn broadcast_message_all_observers(observers: &Observers, msg: &str) { - let observers_guard = observers.read().await; - for (_, tx) in observers_guard.iter() { - let _ = send(tx, msg); - } -} - -async fn watch(matches: &Matches, new_match_id: u32, addr: SocketAddr) -> Result<(), String> { - let matches_guard = matches.read().await; - - for match_guard in matches_guard.values() { - let mut found = false; - let mut a_match = match_guard.write().await; - for i in 0..a_match.viewers.len() { - if a_match.viewers[i] == addr { - a_match.viewers.remove(i); - found = true; - break; - } - } - - if found { - break; - } - } - - let result = matches_guard.get(&new_match_id); - if result.is_none() { - return Err("Match not found".to_string()); - } - result.unwrap().write().await.viewers.push(addr); - - Ok(()) -} - -async fn auth_check(admin: &Arc>>, addr: SocketAddr) -> bool { - if admin.read().await.is_none() || admin.read().await.unwrap() != addr { - return false; - } - true -} - -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 -} - -async fn terminate_match(match_id: u32, matches: &Matches, clients: &Clients, observers: &Observers, demo_mode: bool) { - let matches_guard = matches.read().await; - let the_match = matches_guard.get(&match_id); - if the_match.is_none() { - error!("Tried to call terminate_match on invalid matchID: {}", match_id); - } - let the_match = the_match.unwrap().read().await; - - if the_match.wait_thread.is_some() { - the_match.wait_thread.as_ref().unwrap().abort(); - } - - broadcast_message(&the_match.viewers, observers, "GAME:TERMINATED").await; - - let clients_guard = clients.read().await; - let mut player1 = clients_guard.get(&the_match.player1).unwrap().write().await; - let _ = send(&player1.connection, "GAME:TERMINATED"); - player1.current_match = None; - player1.color = Color::None; - drop(player1); - - if !demo_mode { - let mut player2 = clients_guard.get(&the_match.player2).unwrap().write().await; - let _ = send(&player2.connection, "GAME:TERMINATED"); - player2.current_match = None; - player2.color = Color::None; - drop(player2); - } - - drop(clients_guard); - - drop(the_match); - drop(matches_guard); - - matches.write().await.remove(&match_id); -} - -fn random_move(board: &[Vec]) -> usize { - let mut random = rand::rng().random_range(0..7); - while board[random][5] != Color::None { - random = rand::rng().random_range(0..7); - } - - random -} - -fn send(tx: &UnboundedSender, text: &str) -> Result<(), SendError> { - tx.send(Message::text(text)) -} diff --git a/src/tournaments/mod.rs b/src/tournaments/mod.rs new file mode 100644 index 0000000..d7fec4b --- /dev/null +++ b/src/tournaments/mod.rs @@ -0,0 +1,17 @@ +use std::net::SocketAddr; + +use async_trait::async_trait; + +use crate::{*}; + +pub mod round_robin; +pub use round_robin::RoundRobin; + +#[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; +} \ No newline at end of file diff --git a/src/tournaments/round_robin.rs b/src/tournaments/round_robin.rs new file mode 100644 index 0000000..2811719 --- /dev/null +++ b/src/tournaments/round_robin.rs @@ -0,0 +1,174 @@ +use std::{collections::HashMap, net::SocketAddr}; + +use async_trait::async_trait; + +use crate::{*}; + +#[derive(Clone)] +pub struct RoundRobin { + pub players: HashMap, + pub top_half: Vec, + pub bottom_half: Vec, + 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 } +} diff --git a/src/types.rs b/src/types.rs index 65bc0e9..33e0e32 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,37 +1,46 @@ +use crate::*; use rand::Rng; -use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; use std::vec; -use async_trait::async_trait; 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>>>>; -pub type Usernames = Arc>>; -pub type Observers = Arc>>>; -pub type Matches = Arc>>>>; -pub type WrappedTournament = Arc>>>>; pub struct Server { - pub clients: Clients, - pub usernames: Usernames, - pub observers: Observers, - pub matches: Matches, - pub admin: Arc>>, - pub admin_password: Arc, - pub tournament: WrappedTournament, - pub waiting_timeout: Arc>, - pub demo_mode: bool, - pub tournament_type: String, + pub clients: Clients, + pub usernames: Usernames, + pub observers: Observers, + pub matches: Matches, + pub admin: Arc>>, + pub admin_password: Arc, + pub tournament: WrappedTournament, + pub waiting_timeout: Arc>, + pub demo_mode: bool, + pub tournament_type: String, +} + +impl Server { + pub fn new(admin_password: String, demo_mode: bool, tournament_type: String) -> Server { + Server { + clients: Arc::new(RwLock::new(HashMap::new())), + usernames: Arc::new(RwLock::new(HashMap::new())), + observers: Arc::new(RwLock::new(HashMap::new())), + matches: Arc::new(RwLock::new(HashMap::new())), + admin: Arc::new(RwLock::new(None)), + admin_password: Arc::new(admin_password), + tournament: Arc::new(RwLock::new(None)), + waiting_timeout: Arc::new(RwLock::new(5000)), + demo_mode, + tournament_type, + } + } } #[derive(PartialEq, Clone)] pub enum Color { Red, - Yellow, + Yellow, None, } @@ -42,7 +51,7 @@ pub struct Client { pub ready: bool, pub color: Color, pub current_match: Option, - pub round_robin_id: u32, + pub round_robin_id: u32, pub score: u32, pub addr: SocketAddr, } @@ -55,198 +64,20 @@ impl Client { ready: false, color: Color::None, current_match: None, - round_robin_id: 0, + round_robin_id: 0, score: 0, addr, } } } -#[async_trait] -pub trait Tournament { - fn new(ready_players: &[SocketAddr]) -> Self where Self: Sized; - async fn next(&mut self, clients: &Clients, matches: &Matches, observers: &Observers); - async fn start(&mut self, clients: &Clients, matches: &Matches); - async fn cancel(&mut self, clients: &Clients, matches: &Matches, observers: &Observers); - fn is_completed(&self) -> bool; -} - -#[derive(Clone)] -pub struct RoundRobin { - pub players: HashMap, - pub top_half: Vec, - pub bottom_half: Vec, - 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>, pub viewers: Vec, pub ledger: Vec<(Color, usize)>, - pub move_to_dispatch: (Color, usize), - pub wait_thread: Option>, + pub move_to_dispatch: (Color, usize), + pub wait_thread: Option>, pub player1: SocketAddr, pub player2: SocketAddr, } @@ -264,10 +95,10 @@ impl Match { 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}, + move_to_dispatch: (Color::None, 0), + wait_thread: None, + player1: if player1 == first { player1 } else { player2 }, + player2: if player1 == first { player2 } else { player1 }, } } From b416dc17ad6a39b0bfeffde1e11a2be036db389e Mon Sep 17 00:00:00 2001 From: Joshua Higgins Date: Tue, 30 Dec 2025 22:26:26 -0500 Subject: [PATCH 2/4] misc(refactor): extract command handlers and centralize error handling - Move command-specific logic out of main.rs into Server::handle_* methods (parsing remains in main and handlers accept already-parsed input). - Handlers now return Result and return Err("ERROR:...") instead of sending error messages directly to clients. - main.rs now logs handler errors to console and sends the error string to the client (removed `continue` statements after error sends). - Move end_game_check from main.rs into lib.rs for reuse. - Update handler implementations to follow the new error-returning pattern. --- src/lib.rs | 193 +++++--- src/main.rs | 789 ++++++++------------------------- src/tournaments/mod.rs | 16 +- src/tournaments/round_robin.rs | 259 +++++------ src/types.rs | 764 +++++++++++++++++++++++++++---- 5 files changed, 1143 insertions(+), 878 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6d97f53..f93b62d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,20 @@ use std::{collections::HashMap, net::SocketAddr, sync::Arc}; use rand::Rng; -use tokio::sync::{RwLock, mpsc::{UnboundedSender, error::SendError}}; +use tokio::sync::{ + mpsc::{error::SendError, UnboundedSender}, + RwLock, +}; use tokio_tungstenite::tungstenite::Message; use tracing::error; -use crate::{tournaments::Tournament, types::{Client, Color, Match}}; +use crate::{ + tournaments::Tournament, + types::{Client, Color, Match}, +}; -pub mod types; pub mod tournaments; +pub mod types; pub type Clients = Arc>>>>; pub type Usernames = Arc>>; @@ -18,26 +24,28 @@ pub type WrappedTournament = Arc, observers: &Observers, msg: &str) { for addr in addrs { - let observers_guard = observers.read().await; - let tx = observers_guard.get(addr); - if tx.is_none() { continue; } + let observers_guard = observers.read().await; + let tx = observers_guard.get(addr); + if tx.is_none() { + continue; + } let _ = send(tx.unwrap(), msg); } } pub async fn broadcast_message_all_observers(observers: &Observers, msg: &str) { - let observers_guard = observers.read().await; - for (_, tx) in observers_guard.iter() { - let _ = send(tx, msg); - } + let observers_guard = observers.read().await; + for (_, tx) in observers_guard.iter() { + let _ = send(tx, msg); + } } pub async fn watch(matches: &Matches, new_match_id: u32, addr: SocketAddr) -> Result<(), String> { - let matches_guard = matches.read().await; + let matches_guard = matches.read().await; for match_guard in matches_guard.values() { let mut found = false; - let mut a_match = match_guard.write().await; + let mut a_match = match_guard.write().await; for i in 0..a_match.viewers.len() { if a_match.viewers[i] == addr { a_match.viewers.remove(i); @@ -51,77 +59,140 @@ pub async fn watch(matches: &Matches, new_match_id: u32, addr: SocketAddr) -> Re } } - let result = matches_guard.get(&new_match_id); - if result.is_none() { - return Err("Match not found".to_string()); - } + let result = matches_guard.get(&new_match_id); + if result.is_none() { + return Err("Match not found".to_string()); + } result.unwrap().write().await.viewers.push(addr); - Ok(()) + Ok(()) } pub async fn auth_check(admin: &Arc>>, addr: SocketAddr) -> bool { - if admin.read().await.is_none() || admin.read().await.unwrap() != addr { - return false; - } - true + if admin.read().await.is_none() || admin.read().await.unwrap() != addr { + return false; + } + true } 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 + 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 async fn terminate_match(match_id: u32, matches: &Matches, clients: &Clients, observers: &Observers, demo_mode: bool) { - let matches_guard = matches.read().await; - let the_match = matches_guard.get(&match_id); - if the_match.is_none() { - error!("Tried to call terminate_match on invalid matchID: {}", match_id); - } - let the_match = the_match.unwrap().read().await; +pub async fn terminate_match( + match_id: u32, + matches: &Matches, + clients: &Clients, + observers: &Observers, + demo_mode: bool, +) { + let matches_guard = matches.read().await; + let the_match = matches_guard.get(&match_id); + if the_match.is_none() { + error!( + "Tried to call terminate_match on invalid matchID: {}", + match_id + ); + } + let the_match = the_match.unwrap().read().await; - if the_match.wait_thread.is_some() { - the_match.wait_thread.as_ref().unwrap().abort(); - } + if the_match.wait_thread.is_some() { + the_match.wait_thread.as_ref().unwrap().abort(); + } - broadcast_message(&the_match.viewers, observers, "GAME:TERMINATED").await; + broadcast_message(&the_match.viewers, observers, "GAME:TERMINATED").await; - let clients_guard = clients.read().await; - let mut player1 = clients_guard.get(&the_match.player1).unwrap().write().await; - let _ = send(&player1.connection, "GAME:TERMINATED"); - player1.current_match = None; - player1.color = Color::None; - drop(player1); + let clients_guard = clients.read().await; + let mut player1 = clients_guard.get(&the_match.player1).unwrap().write().await; + let _ = send(&player1.connection, "GAME:TERMINATED"); + player1.current_match = None; + player1.color = Color::None; + drop(player1); - if !demo_mode { - let mut player2 = clients_guard.get(&the_match.player2).unwrap().write().await; - let _ = send(&player2.connection, "GAME:TERMINATED"); - player2.current_match = None; - player2.color = Color::None; - drop(player2); - } + if !demo_mode { + let mut player2 = clients_guard.get(&the_match.player2).unwrap().write().await; + let _ = send(&player2.connection, "GAME:TERMINATED"); + player2.current_match = None; + player2.color = Color::None; + drop(player2); + } - drop(clients_guard); + drop(clients_guard); - drop(the_match); - drop(matches_guard); + drop(the_match); + drop(matches_guard); - matches.write().await.remove(&match_id); + matches.write().await.remove(&match_id); } pub fn random_move(board: &[Vec]) -> usize { - let mut random = rand::rng().random_range(0..7); - while board[random][5] != Color::None { - random = rand::rng().random_range(0..7); - } + let mut random = rand::rng().random_range(0..7); + while board[random][5] != Color::None { + random = rand::rng().random_range(0..7); + } - random + random +} + +pub fn end_game_check(board: &[Vec]) -> (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 = 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 || board[x + i][y] != color && horizontal_end { + horizontal_end = false; + } + + if y + i >= 6 || board[x][y + i] != color && vertical_end { + vertical_end = false; + } + + if x + i >= 7 || y + i >= 6 || board[x + i][y + i] != color && diagonal_end_up { + diagonal_end_up = false; + } + + if x + i >= 7 + || (y as i32 - i as i32) < 0 + || board[x + i][y - i] != color && diagonal_end_down + { + diagonal_end_down = false; + } + } + + if horizontal_end || vertical_end || diagonal_end_up || diagonal_end_down { + result = (color.clone(), false); + break; + } + } + if result.0 != Color::None { + break; + } + } + + if any_empty && result.0 == Color::None { + result.1 = true; + } + + result } pub fn send(tx: &UnboundedSender, text: &str) -> Result<(), SendError> { tx.send(Message::text(text)) -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index ae25d82..827aca2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,12 @@ -use connect4_moderator_server::{*}; -use connect4_moderator_server::types::{*}; -use connect4_moderator_server::tournaments::{*}; +use connect4_moderator_server::types::*; +use connect4_moderator_server::*; use futures_util::{SinkExt, StreamExt}; -use rand::Rng; use std::env; use std::net::SocketAddr; use std::sync::Arc; use tokio::net::{TcpListener, TcpStream}; -use tokio::sync::RwLock; use tokio_tungstenite::{accept_async, tungstenite::Message}; -use tracing::{error, info, warn}; +use tracing::{error, info}; // TODO: Allow random "player1" in demo mode // TODO: Support reconnecting behaviors @@ -26,31 +23,31 @@ async fn main() -> Result<(), anyhow::Error> { let args: Vec = env::args().collect(); let demo_mode = args.get(1).is_some() && args.get(1).unwrap() == "demo"; - let tournament_type = if !demo_mode { - if let Some(tourney) = args.get(1) { - tourney.clone() - } else { - "round_robin".to_string() - } - } else { "round_robin".to_string() }; - let admin_password = env::var("ADMIN_AUTH").unwrap_or_else(|_| String::from("admin")); - info!("Admin password: {}", admin_password); - let admin_password = Arc::new(admin_password); + let tournament_type = if !demo_mode { + if let Some(tourney) = args.get(1) { + tourney.clone() + } else { + "round_robin".to_string() + } + } else { + "round_robin".to_string() + }; + let admin_password = env::var("ADMIN_AUTH").unwrap_or_else(|_| String::from("admin")); + info!("Admin password: {}", admin_password); + let admin_password = Arc::new(admin_password); let addr = "0.0.0.0:8080"; let listener = TcpListener::bind(&addr).await?; info!("WebSocket server listening on: {}", addr); - let server_data = Arc::new( - Server::new(admin_password.as_ref().clone(), demo_mode, tournament_type) - ); + let server_data = Arc::new(Server::new( + admin_password.as_ref().clone(), + demo_mode, + tournament_type, + )); while let Ok((stream, addr)) = listener.accept().await { - tokio::spawn(handle_connection( - stream, - addr, - server_data.clone(), - )); + tokio::spawn(handle_connection(stream, addr, server_data.clone())); } Ok(()) @@ -84,534 +81,163 @@ async fn handle_connection( match msg { Ok(Message::Text(text)) => { info!("Received text from {}: {}", addr, text); - - if text.starts_with("CONNECT:") { - let requested_username = text.split(":").collect::>()[1].to_string(); - - if requested_username.is_empty() { - let _ = send(&tx, &format!("ERROR:INVALID:ID:{}", requested_username)); - continue; - } - - let mut is_taken = false; - let clients_guard = sd.clients.read().await; - for client in clients_guard.values() { - if requested_username == client.read().await.username { - let _ = send(&tx, &format!("ERROR:INVALID:ID:{}", requested_username)); - is_taken = true; - break; - } - } - - if is_taken { - continue; - } - - drop(clients_guard); - - // not taken - sd.observers.write().await.remove(&addr); - sd.usernames.write().await.insert(requested_username.clone(), addr); - sd.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"); - } - else if text == "READY" { - let clients_guard = sd.clients.read().await; - if clients_guard.get(&addr).is_none() { - let _ = send(&tx, "ERROR:INVALID"); - continue; - } - - if clients_guard.get(&addr).unwrap().read().await.ready { - let _ = send(&tx, "ERROR:INVALID"); - continue; - } - - let mut client = clients_guard.get(&addr).unwrap().write().await; - client.ready = true; - let _ = send(&tx, "READY:ACK"); - - if sd.demo_mode { - let match_id: u32 = gen_match_id(&sd.matches).await; - let new_match = Arc::new(RwLock::new(Match::new( - match_id, - addr.to_string().parse()?, - addr.to_string().parse()?, - ))); - sd.matches.write().await.insert(match_id, new_match.clone()); - client.ready = false; - client.current_match = Some(match_id); - client.color = Color::Red; - let _ = send(&tx, "GAME:START:1"); - } - } - else if text.starts_with("PLAY:") { - let clients_guard = sd.clients.read().await; - let client = clients_guard.get(&addr); - - // Check if client is valid - if client.is_none() || client.unwrap().read().await.current_match.is_none() - { - let _ = send(&tx, "ERROR:INVALID:MOVE"); - continue; - } - let client = client.unwrap().read().await; - - let matches_guard = sd.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_connection = - if addr == current_match.player1 { - clients_guard.get(¤t_match.player2).unwrap().read().await.connection.clone() - } else { - clients_guard.get(¤t_match.player1).unwrap().read().await.connection.clone() - }; - - let opponent_username = - 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() - }; - - - // Check if it's their move - let mut invalid = false; - if (current_match.ledger.is_empty() && current_match.player1 != addr) || - (current_match.ledger.last().is_some() && current_match.ledger.last().unwrap().0 == client.color) - { - let _ = send(&tx, "ERROR:INVALID:MOVE"); - invalid = true; - } - - let column_parse = text.split(":").collect::>()[1].parse::(); - - drop(current_match); - drop(matches_guard); - - let mut matches_guard = sd.matches.write().await; - let mut current_match = matches_guard - .get_mut(&client.current_match.unwrap()) - .unwrap() - .write() - .await; - - // Check if valid move - if let Ok(column) = column_parse { - if column >= 7 && !invalid { - let _ = send(&tx, "ERROR:INVALID:MOVE"); - invalid = true; - } - - if current_match.board[column][5] != Color::None && !invalid { - let _ = send(&tx, "ERROR:INVALID:MOVE"); - invalid = true; - } - - - } else { - let _ = send(&tx, "ERROR:INVALID:MOVE"); - invalid = true; - } - - // Terminate games if a player makes an invalid move - if invalid { - let current_match_id = current_match.id; - let viewers = current_match.viewers.clone(); - - drop(current_match); - drop(matches_guard); - drop(client); - drop(clients_guard); - - if sd.demo_mode { - terminate_match(current_match_id, &sd.matches, &sd.clients, &sd.observers, sd.demo_mode).await; - tx.send(Message::Close(None))?; - } else { - let _ = send(&tx, "GAME:LOSS"); - let _ = send(&opponent_connection, "GAME:WINS"); - broadcast_message(&viewers, &sd.observers, &format!("GAME:WIN:{}", opponent_username)).await; - - let mut clients_guard = sd.clients.write().await; - let mut client = clients_guard.get_mut(&addr).unwrap().write().await; - client.current_match = None; - 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; - opponent.score += 1; - drop(opponent); - - sd.matches.write().await.remove(¤t_match_id).unwrap(); - } - continue; - } else { - // Place it - current_match.place_token(client.color.clone(), column_parse.clone()?); - current_match.move_to_dispatch = (client.color.clone(), column_parse.clone()?); - } - - // broadcast the move to viewers - broadcast_message( - ¤t_match.viewers, - &sd.observers, - &format!("GAME:MOVE:{}:{}", client.username, column_parse.clone()?), - ) - .await; - - // Check game end conditions - let (winner, filled) = end_game_check(¤t_match.board); - - if winner != Color::None { - if winner == client.color { - let _ = send(&tx, "GAME:WINS"); - if !sd.demo_mode { - let _ = send(&opponent_connection, "GAME:LOSS"); + let parts: Vec<&str> = text.split(':').collect(); + let cmd = parts[0]; + match cmd { + "CONNECT" => { + if parts.len() > 1 { + let requested_username = parts[1].to_string(); + if let Err(e) = + sd.handle_connect(addr, tx.clone(), requested_username).await + { + error!("handle_connect: {}", e); + let _ = send(&tx, e.to_string().as_str()); } - broadcast_message( - ¤t_match.viewers, - &sd.observers, - &format!("GAME:WIN:{}", client.username), - ) - .await; } else { - let _ = send(&tx, "GAME:LOSS"); - if !sd.demo_mode { - let _ = send(&opponent_connection, "GAME:WINS"); + let _ = send(&tx, "ERROR:INVALID:ID:"); + } + } + "READY" => { + if let Err(e) = sd.handle_ready(addr, tx.clone()).await { + error!("handle_ready: {}", e); + let _ = send(&tx, e.to_string().as_str()); + } + } + "PLAY" => { + if parts.len() > 1 { + match parts[1].parse::() { + Ok(column) => { + if let Err(e) = sd.handle_play(addr, tx.clone(), column).await { + error!("handle_play: {}", e); + let _ = send(&tx, e.to_string().as_str()); + } + } + Err(_) => { + let _ = send(&tx, "ERROR:INVALID:MOVE"); + } } - broadcast_message( - ¤t_match.viewers, - &sd.observers, - &format!("GAME:WIN:{}", opponent_username), - ) - .await; + } else { + let _ = send(&tx, "ERROR:INVALID:MOVE"); } - } else if filled { - let _ = send(&tx, "GAME:DRAW"); - if !sd.demo_mode { - let _ = send(&opponent_connection, "GAME:DRAW"); + } + "PLAYER" => { + if parts.get(1) == Some(&"LIST") { + if let Err(e) = sd.handle_player_list(tx.clone()).await { + error!("handle_player_list: {}", e); + let _ = send(&tx, e.to_string().as_str()); + } } - broadcast_message(¤t_match.viewers, &sd.observers, "GAME:DRAW").await; } - - // remove match from matchmaker - if winner != Color::None || filled { - let current_match_id = current_match.id; - - drop(client); - drop(current_match); - drop(clients_guard); - - let clients_guard = sd.clients.read().await; - let mut client = clients_guard.get(&addr).unwrap().write().await; - if client.color == winner { - client.score += 1; - } - client.current_match = None; - client.color = Color::None; - drop(client); - - let mut opponent = clients_guard.get(&opponent_addr).unwrap().write().await; - if opponent.color == winner { - opponent.score += 1; - } - opponent.current_match = None; - opponent.color = Color::None; - drop(opponent); - matches_guard.remove(¤t_match_id).unwrap(); - - if !sd.demo_mode && matches_guard.is_empty() { - drop(matches_guard); - drop(clients_guard); - - let mut tournament_guard = sd.tournament.write().await; - let tourney = tournament_guard.as_mut().unwrap(); - tourney.write().await.next(&sd.clients, &sd.matches, &sd.observers).await; - if tourney.read().await.is_completed() { - *tournament_guard = None; - } - } - - continue; + "GAME" => { + if parts.get(1) == Some(&"LIST") { + if let Err(e) = sd.handle_game_list(tx.clone()).await { + error!("handle_game_list: {}", e); + let _ = send(&tx, e.to_string().as_str()); + } + } else if parts.get(1) == Some(&"WATCH") && parts.len() > 2 { + match parts[2].parse::() { + Ok(match_id) => { + if let Err(e) = + sd.handle_game_watch(tx.clone(), match_id, addr).await + { + error!("handle_game_watch: {}", e); + let _ = send(&tx, e.to_string().as_str()); + } + } + Err(_) => { + let _ = send(&tx, "ERROR:INVALID:WATCH"); + } + } + } else if parts.get(1) == Some(&"TERMINATE") && parts.len() > 2 { + match parts[2].parse::() { + Ok(match_id) => { + if let Err(e) = + sd.handle_game_terminate(tx.clone(), addr, match_id).await + { + error!("handle_game_terminate: {}", e); + let _ = send(&tx, e.to_string().as_str()); + } + } + Err(_) => { + let _ = send(&tx, "ERROR:INVALID:TERMINATE"); + } + } + } + } + "ADMIN" => { + if parts.get(1) == Some(&"AUTH") && parts.len() > 2 { + if let Err(e) = + sd.handle_admin_auth(tx.clone(), addr, parts[2].to_string()).await + { + error!("handle_admin_auth: {}", e); + let _ = send(&tx, e.to_string().as_str()); + } + } else if parts.get(1) == Some(&"KICK") && parts.len() > 2 { + if let Err(e) = + sd.handle_admin_kick(tx.clone(), addr, parts[2].to_string()).await + { + error!("handle_admin_kick: {}", e); + let _ = send(&tx, e.to_string().as_str()); + } + } + } + "TOURNAMENT" => { + if parts.get(1) == Some(&"START") { + if let Err(e) = sd.handle_tournament_start(tx.clone(), addr).await { + error!("handle_tournament_start: {}", e); + let _ = send(&tx, e.to_string().as_str()); + } + } else if parts.get(1) == Some(&"CANCEL") { + if let Err(e) = sd.handle_tournament_cancel(tx.clone(), addr).await { + error!("handle_tournament_cancel: {}", e); + let _ = send(&tx, e.to_string().as_str()); + } + } else if parts.get(1) == Some(&"WAIT") && parts.len() > 2 { + match parts[2].parse::() { + Ok(v) => { + if let Err(e) = + sd.handle_tournament_wait(tx.clone(), addr, v).await + { + error!("handle_tournament_wait: {}", e); + let _ = send(&tx, e.to_string().as_str()); + } + } + Err(_) => { + let _ = send(&tx, "ERROR:INVALID:TOURNAMENT"); + } + } + } + } + "GET" => { + if parts.get(1) == Some(&"MOVE_WAIT") { + if let Err(e) = sd.handle_get_move_wait(tx.clone()).await { + error!("handle_get_move_wait: {}", e); + let _ = send(&tx, e.to_string().as_str()); + } + } else if parts.get(1) == Some(&"TOURNAMENT_STATUS") { + if let Err(e) = sd.handle_get_tournament_status(tx.clone()).await { + error!("handle_get_tournament_status: {}", e); + let _ = send(&tx, e.to_string().as_str()); + } + } + } + _ => { + let _ = send(&tx, "ERROR:UNKNOWN"); } - - let connection_to_send = if !sd.demo_mode { opponent_connection.clone() } else { tx.clone() }; - let column = if !sd.demo_mode { column_parse.clone()? } else { random_move(¤t_match.board) }; - if sd.demo_mode { - let move_to_dispatch = current_match.move_to_dispatch.clone(); - current_match.ledger.push(move_to_dispatch); - current_match.move_to_dispatch = (Color::Yellow, column); - current_match.place_token(Color::Yellow, column); - } - - let waiting = *sd.waiting_timeout.read().await as i64 + (rand::rng().random_range(0..=50) - 25); - let matches_move = sd.matches.clone(); - let observers_move = sd.observers.clone(); - let match_id_move = current_match.id; - let demo_mode_move = sd.demo_mode; - current_match.wait_thread = Some(tokio::spawn(async move { - tokio::time::sleep(tokio::time::Duration::from_millis(waiting as u64)).await; - - let mut matches_guard = matches_move.write().await; - let mut current_match = matches_guard.get_mut(&match_id_move).unwrap().write().await; - let move_to_dispatch = current_match.move_to_dispatch.clone(); - current_match.ledger.push(move_to_dispatch); - current_match.move_to_dispatch = (Color::None, 0); - - if demo_mode_move { - broadcast_message( - ¤t_match.viewers, - &observers_move, - &format!("GAME:MOVE:{}:{}", "demo", column), - ).await; - } - - drop(current_match); - drop(matches_guard); - - let _ = send(&connection_to_send, &format!("OPPONENT:{}", column)); - })); - } - - else if text == "PLAYER:LIST" { - let clients_guard = sd.clients.read().await; - let mut to_send = "PLAYER:LIST:".to_string(); - for client_guard in clients_guard.values() { - let player = client_guard.read().await; - to_send += player.username.as_str(); to_send += ","; - to_send += if player.ready { "true" } else { "false" }; to_send += ","; - to_send += if player.current_match.is_some() { "true" } else { "false" }; - to_send += "|"; - } - - to_send.remove(to_send.len() - 1); - - let _ = send(&tx, to_send.as_str()); - } - else if text == "GAME:LIST" { - let matches_guard = sd.matches.read().await; - let clients_guard = sd.clients.read().await; - let mut to_send = "GAME:LIST:".to_string(); - for match_guard in matches_guard.values() { - let a_match = match_guard.read().await; - let player1 = clients_guard.get(&a_match.player1).unwrap().read().await; - let player2 = clients_guard.get(&a_match.player2).unwrap().read().await; - to_send += a_match.id.to_string().as_str(); - to_send += ","; to_send += player1.username.as_str(); to_send += ","; - to_send += if player1.username == player2.username { "demo" } else { player2.username.as_str() }; - to_send += "|"; - } - - to_send.remove(to_send.len() - 1); - - let _ = send(&tx, to_send.as_str()); - } - else if text.starts_with("GAME:WATCH:") { - let match_id_parse = text.split(":").collect::>()[2].parse::(); - match match_id_parse { - Ok(match_id) => { - let result = watch(&sd.matches, match_id, addr).await; - if result.is_err() { let _ = send(&tx, "ERROR:INVALID:WATCH"); continue; } - - let clients_guard = sd.clients.read().await; - let matches_guard = sd.matches.read().await; - let the_match = matches_guard.get(&match_id).unwrap().read().await; - let player1 = clients_guard.get(&the_match.player1).unwrap().read().await.username.clone(); - let mut player2 = clients_guard.get(&the_match.player2).unwrap().read().await.username.clone(); - if sd.demo_mode { player2 = "demo".to_string(); } - let ledger = the_match.ledger.clone(); - - drop(clients_guard); - drop(the_match); - drop(matches_guard); - - let mut message = format!("GAME:WATCH:ACK:{},{},{}|", match_id, player1, player2); - - for a_move in ledger { - if a_move.0 == Color::Red { - message += &format!("{},{}|", player1, a_move.1); - } else { - message += &format!("{},{}|", player2, a_move.1); - } - } - - message.pop(); - - let _ = send(&tx, &message); - } - Err(_) => { let _ = send(&tx, "ERROR:INVALID:WATCH"); continue; } - } - } - - else if text.starts_with("ADMIN:AUTH:") { - if sd.admin.read().await.is_some() { - let _ = send(&tx, "ERROR:INVALID:AUTH"); - continue; - } - - let password_parse = text.split(":").collect::>()[2]; - if password_parse != *sd.admin_password { - let _ = send(&tx, "ERROR:INVALID:AUTH"); - continue; - } - - let mut admin_guard = sd.admin.write().await; - *admin_guard = Some(addr.to_string().parse()?); - let _ = send(&tx, "ADMIN:AUTH:ACK"); - } - else if text.starts_with("ADMIN:KICK:") { - if !auth_check(&sd.admin, addr).await { - let _ = send(&tx, "ERROR:INVALID:AUTH"); - } - - let kick_username = text.split(":").collect::>()[2]; - - let usernames_guard = sd.usernames.read().await; - let clients_guard = sd.clients.read().await; - - let kick_addr_result = usernames_guard.get(kick_username); - match kick_addr_result { - Some(kick_addr) => { - let kick_client = clients_guard.get(kick_addr).unwrap().read().await; - kick_client.connection.send(Message::Close(None))?; - }, - None => { - let _ = send(&tx, "ERROR:INVALID:KICK"); - continue - } - } - } - else if text.starts_with("GAME:TERMINATE:") { - if !auth_check(&sd.admin, addr).await { - let _ = send(&tx, "ERROR:INVALID:AUTH"); - } - - let match_id_parse = text.split(":").collect::>()[2].parse::(); - if match_id_parse.is_err() { let _ = send(&tx, "ERROR:INVALID:TERMINATE"); continue; } - - terminate_match(match_id_parse?, &sd.matches, &sd.clients, &sd.observers, sd.demo_mode).await; - - if !sd.demo_mode && sd.matches.read().await.is_empty() { - let mut tournament_guard = sd.tournament.write().await; - let tourney = tournament_guard.as_mut().unwrap(); - tourney.write().await.next(&sd.clients, &sd.matches, &sd.observers).await; - if tourney.read().await.is_completed() { - *tournament_guard = None; - } - } - } - - else if text == "TOURNAMENT:START" { - if !auth_check(&sd.admin, addr).await { - let _ = send(&tx, "ERROR:INVALID:AUTH"); - } - - if sd.tournament.read().await.is_some() || sd.demo_mode { - let _ = send(&tx, "ERROR:INVALID:TOURNAMENT"); - continue; - } - - let mut clients_guard = sd.clients.write().await; - let mut ready_players = Vec::new(); - for (client_addr, client_guard) in clients_guard.iter_mut() { - if client_guard.read().await.ready { - ready_players.push(*client_addr); - } - } - - if ready_players.len() < 3 { - let _ = send(&tx, "ERROR:INVALID:TOURNAMENT"); - continue; - } - - drop(clients_guard); - - let mut tourney = match sd.tournament_type.as_str() { - "round_robin" => RoundRobin::new(&ready_players), - &_ => RoundRobin::new(&ready_players), - }; - tourney.start(&sd.clients, &sd.matches).await; - - let mut tournament_guard = sd.tournament.write().await; - *tournament_guard = Some(Arc::new(RwLock::new(tourney))); - - let _ = send(&tx, "TOURNAMENT:START:ACK"); - } - else if text == "TOURNAMENT:CANCEL" { - if !auth_check(&sd.admin, addr).await { - let _ = send(&tx, "ERROR:INVALID:AUTH"); - } - - if sd.tournament.read().await.is_none() || sd.demo_mode { - let _ = send(&tx, "ERROR:INVALID:TOURNAMENT"); - continue; - } - - let mut tournament_guard = sd.tournament.write().await; - let tourney = tournament_guard.as_mut().unwrap(); - tourney.write().await.cancel(&sd.clients, &sd.matches, &sd.observers).await; - *tournament_guard = None; - - let _ = send(&tx, "TOURNAMENT:CANCEL:ACK"); - } - else if text.starts_with("TOURNAMENT:WAIT:") { - if !auth_check(&sd.admin, addr).await { - let _ = send(&tx, "ERROR:INVALID:AUTH"); - } - - let new_timeout = text.split(":").collect::>()[2].parse::()?; - *sd.waiting_timeout.write().await = (new_timeout * 1000.0) as u64; - } - - else if text == "GET:MOVE_WAIT" { - let mut msg = "GET:MOVE_WAIT:".to_string(); - msg += &(*sd.waiting_timeout.read().await as f64 / 1000f64).to_string(); - let _ = send(&tx, &msg); - } - - else if text == "GET:TOURNAMENT_STATUS" { - let status = sd.tournament.read().await.is_some(); - if sd.demo_mode { - let _ = send(&tx, "GET:TOURNAMENT_STATUS:DEMO"); - } else { - let mut msg = "GET:TOURNAMENT_STATUS:".to_string(); - msg += status.to_string().as_str(); - let _ = send(&tx, &msg); - } - } - - else { - let _ = send(&tx, "ERROR:UNKNOWN"); } } Ok(Message::Close(_)) => { info!("Client {} disconnected", addr); break; } - Ok(Message::Binary(_)) => { let _ = send(&tx, "ERROR:UNKNOWN"); } - Ok(_) => {}, // Ping packets, we can ignore, they get handled for us + Ok(Message::Binary(_)) => { + let _ = send(&tx, "ERROR:UNKNOWN"); + } + Ok(_) => {} // Ping packets, we can ignore, they get handled for us Err(e) => { error!("WebSocket error for {}: {}", addr, e); break; - }, + } } } @@ -619,25 +245,32 @@ async fn handle_connection( send_task.abort(); // Remove and terminate any matches - // We may not be a client disconnecting, do this check - let clients_guard = sd.clients.read().await; - 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); - terminate_match(match_id, &sd.matches, &sd.clients, &sd.observers, sd.demo_mode).await; - } else { - drop(client); - } + // We may not be a client disconnecting, do this check + let clients_guard = sd.clients.read().await; + 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); + terminate_match( + match_id, + &sd.matches, + &sd.clients, + &sd.observers, + sd.demo_mode, + ) + .await; + } else { + drop(client); + } - drop(clients_guard); + drop(clients_guard); - sd.clients.write().await.remove(&addr); - sd.usernames.write().await.remove(&username); - } + sd.clients.write().await.remove(&addr); + sd.usernames.write().await.remove(&username); + } - sd.observers.write().await.remove(&addr); + sd.observers.write().await.remove(&addr); let mut admin_guard = sd.admin.write().await; if let Some(admin_addr) = *admin_guard { @@ -651,55 +284,3 @@ async fn handle_connection( Ok(()) } - -fn end_game_check(board: &[Vec]) -> (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 = 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 || board[x + i][y] != color && horizontal_end { - horizontal_end = false; - } - - if y + i >= 6 || board[x][y + i] != color && vertical_end { - vertical_end = false; - } - - if x + i >= 7 || y + i >= 6 || board[x + i][y + i] != color && diagonal_end_up { - diagonal_end_up = false; - } - - if x + i >= 7 || (y as i32 - i as i32) < 0 || - board[x + i][y - i] != color && diagonal_end_down { - diagonal_end_down = false; - } - } - - if horizontal_end || vertical_end || diagonal_end_up || diagonal_end_down { - result = (color.clone(), false); - break; - } - } - if result.0 != Color::None { - break; - } - } - - if any_empty && result.0 == Color::None { - result.1 = true; - } - - result -} diff --git a/src/tournaments/mod.rs b/src/tournaments/mod.rs index d7fec4b..5014e6e 100644 --- a/src/tournaments/mod.rs +++ b/src/tournaments/mod.rs @@ -2,16 +2,18 @@ use std::net::SocketAddr; use async_trait::async_trait; -use crate::{*}; +use crate::*; pub mod round_robin; pub use round_robin::RoundRobin; #[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; -} \ No newline at end of file + 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; +} diff --git a/src/tournaments/round_robin.rs b/src/tournaments/round_robin.rs index 2811719..ca696a5 100644 --- a/src/tournaments/round_robin.rs +++ b/src/tournaments/round_robin.rs @@ -2,173 +2,180 @@ use std::{collections::HashMap, net::SocketAddr}; use async_trait::async_trait; -use crate::{*}; +use crate::*; #[derive(Clone)] pub struct RoundRobin { - pub players: HashMap, - pub top_half: Vec, - pub bottom_half: Vec, - pub is_completed: bool, + pub players: HashMap, + pub top_half: Vec, + pub bottom_half: Vec, + 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()); + 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(); + 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_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; + 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; + 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"); - } + 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); + drop(player1); - let mut player2 = clients_guard.get(player2_addr).unwrap().write().await; + let mut player2 = clients_guard.get(player2_addr).unwrap().write().await; - player2.current_match = Some(match_id); - player2.ready = false; + 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"); - } + 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); + drop(player2); - matches.write().await.insert(match_id, new_match.clone()); - } - } + 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, - }; + 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(); + let size = ready_players.len(); - for (id, player) in ready_players.iter().enumerate() { - result.players.insert(id as u32, *player); - } + 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 0..size / 2 { + result.top_half.push(i as u32); + } - for i in size / 2..size { - result.bottom_half.push(i as u32); - } + for i in size / 2..size { + result.bottom_half.push(i as u32); + } - result - } + result + } - async fn next(&mut self, clients: &Clients, matches: &Matches, observers: &Observers) { - if self.is_completed { - return; - } + 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; - } + 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); + 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.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; - } + 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; - } + 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)); + 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(); + 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; - } - } + 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 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; + 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 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(); + let match_id = client.current_match; + if match_id.is_none() { + continue; + } + let match_id = match_id.unwrap(); - drop(client); - drop(clients_guard); + drop(client); + drop(clients_guard); - terminate_match(match_id, matches, clients, observers, false).await; + terminate_match(match_id, matches, clients, observers, false).await; - if !client_ready { - let _ = send(&client_connection, "TOURNAMENT:END"); - } - } - } + if !client_ready { + let _ = send(&client_connection, "TOURNAMENT:END"); + } + } + } - fn is_completed(&self) -> bool { self.is_completed } + fn is_completed(&self) -> bool { + self.is_completed + } } diff --git a/src/types.rs b/src/types.rs index 33e0e32..255bda4 100644 --- a/src/types.rs +++ b/src/types.rs @@ -8,106 +8,710 @@ use tokio::sync::RwLock; use tokio_tungstenite::tungstenite::Message; pub struct Server { - pub clients: Clients, - pub usernames: Usernames, - pub observers: Observers, - pub matches: Matches, - pub admin: Arc>>, - pub admin_password: Arc, - pub tournament: WrappedTournament, - pub waiting_timeout: Arc>, - pub demo_mode: bool, - pub tournament_type: String, + pub clients: Clients, + pub usernames: Usernames, + pub observers: Observers, + pub matches: Matches, + pub admin: Arc>>, + pub admin_password: Arc, + pub tournament: WrappedTournament, + pub waiting_timeout: Arc>, + pub demo_mode: bool, + pub tournament_type: String, } impl Server { - pub fn new(admin_password: String, demo_mode: bool, tournament_type: String) -> Server { - Server { - clients: Arc::new(RwLock::new(HashMap::new())), - usernames: Arc::new(RwLock::new(HashMap::new())), - observers: Arc::new(RwLock::new(HashMap::new())), - matches: Arc::new(RwLock::new(HashMap::new())), - admin: Arc::new(RwLock::new(None)), - admin_password: Arc::new(admin_password), - tournament: Arc::new(RwLock::new(None)), - waiting_timeout: Arc::new(RwLock::new(5000)), - demo_mode, - tournament_type, - } - } + pub fn new(admin_password: String, demo_mode: bool, tournament_type: String) -> Server { + Server { + clients: Arc::new(RwLock::new(HashMap::new())), + usernames: Arc::new(RwLock::new(HashMap::new())), + observers: Arc::new(RwLock::new(HashMap::new())), + matches: Arc::new(RwLock::new(HashMap::new())), + admin: Arc::new(RwLock::new(None)), + admin_password: Arc::new(admin_password), + tournament: Arc::new(RwLock::new(None)), + waiting_timeout: Arc::new(RwLock::new(5000)), + demo_mode, + tournament_type, + } + } + + // Handler for CONNECT: + pub async fn handle_connect( + &self, + addr: SocketAddr, + tx: UnboundedSender, + requested_username: String, + ) -> Result<(), anyhow::Error> { + if requested_username.is_empty() { + return Err(anyhow::anyhow!(format!( + "ERROR:INVALID:ID:{}", + requested_username + ))); + } + + let clients_guard = self.clients.read().await; + for client in clients_guard.values() { + if requested_username == client.read().await.username { + return Err(anyhow::anyhow!(format!( + "ERROR:INVALID:ID:{}", + requested_username + ))); + } + } + + drop(clients_guard); + + // 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 _ = crate::send(&tx, "CONNECT:ACK"); + Ok(()) + } + + // Handler for READY + pub async fn handle_ready( + &self, + addr: SocketAddr, + tx: UnboundedSender, + ) -> Result<(), anyhow::Error> { + let clients_guard = self.clients.read().await; + if clients_guard.get(&addr).is_none() { + return Err(anyhow::anyhow!("ERROR:INVALID")); + } + + if clients_guard.get(&addr).unwrap().read().await.ready { + return Err(anyhow::anyhow!("ERROR:INVALID")); + } + + let mut client = clients_guard.get(&addr).unwrap().write().await; + client.ready = true; + let _ = crate::send(&tx, "READY:ACK"); + + if self.demo_mode { + let match_id: u32 = crate::gen_match_id(&self.matches).await; + let new_match = Arc::new(RwLock::new(Match::new( + match_id, + addr.to_string().parse()?, + addr.to_string().parse()?, + ))); + self.matches.write().await.insert(match_id, new_match.clone()); + client.ready = false; + client.current_match = Some(match_id); + client.color = Color::Red; + let _ = crate::send(&tx, "GAME:START:1"); + } + + Ok(()) + } + + // Handler for PLAY (column already parsed) + pub async fn handle_play( + &self, + addr: SocketAddr, + tx: UnboundedSender, + column: usize, + ) -> Result<(), anyhow::Error> { + let clients_guard = self.clients.read().await; + let client_opt = clients_guard.get(&addr); + + // Check if client is valid + if client_opt.is_none() || client_opt.unwrap().read().await.current_match.is_none() { + return Err(anyhow::anyhow!("ERROR:INVALID:MOVE")); + } + let client = client_opt.unwrap().read().await; + + 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_connection = if addr == current_match.player1 { + clients_guard.get(¤t_match.player2).unwrap().read().await.connection.clone() + } else { + clients_guard.get(¤t_match.player1).unwrap().read().await.connection.clone() + }; + + let opponent_username = 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() + }; + + // Check if it's their move + let mut invalid = false; + if (current_match.ledger.is_empty() && current_match.player1 != addr) + || (current_match.ledger.last().is_some() + && current_match.ledger.last().unwrap().0 == client.color) + { + let _ = crate::send(&tx, "ERROR:INVALID:MOVE"); + invalid = true; + } + + drop(current_match); + drop(matches_guard); + + let mut matches_guard = self.matches.write().await; + let mut current_match = + matches_guard.get_mut(&client.current_match.unwrap()).unwrap().write().await; + + // Check if valid move + if column >= 7 && !invalid { + let _ = crate::send(&tx, "ERROR:INVALID:MOVE"); + invalid = true; + } + + if current_match.board[column][5] != Color::None && !invalid { + let _ = crate::send(&tx, "ERROR:INVALID:MOVE"); + invalid = true; + } + + // Terminate games if a player makes an invalid move + if invalid { + let current_match_id = current_match.id; + let viewers = current_match.viewers.clone(); + + drop(current_match); + drop(matches_guard); + drop(client); + drop(clients_guard); + + if self.demo_mode { + crate::terminate_match( + current_match_id, + &self.matches, + &self.clients, + &self.observers, + self.demo_mode, + ) + .await; + tx.send(Message::Close(None))?; + } else { + let _ = crate::send(&tx, "GAME:LOSS"); + let _ = crate::send(&opponent_connection, "GAME:WINS"); + crate::broadcast_message( + &viewers, + &self.observers, + &format!("GAME:WIN:{}", opponent_username), + ) + .await; + + let mut clients_guard = self.clients.write().await; + let mut client = clients_guard.get_mut(&addr).unwrap().write().await; + client.current_match = None; + 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; + opponent.score += 1; + drop(opponent); + + self.matches.write().await.remove(¤t_match_id).unwrap(); + } + return Ok(()); + } else { + // Place it + current_match.place_token(client.color.clone(), column); + current_match.move_to_dispatch = (client.color.clone(), column); + } + + // broadcast the move to viewers + crate::broadcast_message( + ¤t_match.viewers, + &self.observers, + &format!("GAME:MOVE:{}:{}", client.username, column), + ) + .await; + + // Check game end conditions + let (winner, filled) = crate::end_game_check(¤t_match.board); + + if winner != Color::None { + if winner == client.color { + let _ = crate::send(&tx, "GAME:WINS"); + if !self.demo_mode { + let _ = crate::send(&opponent_connection, "GAME:LOSS"); + } + crate::broadcast_message( + ¤t_match.viewers, + &self.observers, + &format!("GAME:WIN:{}", client.username), + ) + .await; + } else { + let _ = crate::send(&tx, "GAME:LOSS"); + if !self.demo_mode { + let _ = crate::send(&opponent_connection, "GAME:WINS"); + } + crate::broadcast_message( + ¤t_match.viewers, + &self.observers, + &format!("GAME:WIN:{}", opponent_username), + ) + .await; + } + } else if filled { + let _ = crate::send(&tx, "GAME:DRAW"); + if !self.demo_mode { + let _ = crate::send(&opponent_connection, "GAME:DRAW"); + } + crate::broadcast_message(¤t_match.viewers, &self.observers, "GAME:DRAW").await; + } + + // remove match from matchmaker + if winner != Color::None || filled { + let current_match_id = current_match.id; + + drop(client); + drop(current_match); + drop(clients_guard); + + let clients_guard = self.clients.read().await; + let mut client = clients_guard.get(&addr).unwrap().write().await; + if client.color == winner { + client.score += 1; + } + client.current_match = None; + client.color = Color::None; + drop(client); + + let mut opponent = clients_guard.get(&opponent_addr).unwrap().write().await; + if opponent.color == winner { + opponent.score += 1; + } + opponent.current_match = None; + opponent.color = Color::None; + drop(opponent); + matches_guard.remove(¤t_match_id).unwrap(); + + if !self.demo_mode && matches_guard.is_empty() { + drop(matches_guard); + drop(clients_guard); + + let mut tournament_guard = self.tournament.write().await; + let tourney = tournament_guard.as_mut().unwrap(); + tourney.write().await.next(&self.clients, &self.matches, &self.observers).await; + if tourney.read().await.is_completed() { + *tournament_guard = None; + } + } + + return Ok(()); + } + + let connection_to_send = if !self.demo_mode { + opponent_connection.clone() + } else { + tx.clone() + }; + let column_to_use = if !self.demo_mode { + column + } else { + crate::random_move(¤t_match.board) + }; + if self.demo_mode { + let move_to_dispatch = current_match.move_to_dispatch.clone(); + current_match.ledger.push(move_to_dispatch); + current_match.move_to_dispatch = (Color::Yellow, column_to_use); + current_match.place_token(Color::Yellow, column_to_use); + } + + let waiting = + *self.waiting_timeout.read().await as i64 + (rand::rng().random_range(0..=50) - 25); + let matches_move = self.matches.clone(); + let observers_move = self.observers.clone(); + let match_id_move = current_match.id; + let demo_mode_move = self.demo_mode; + current_match.wait_thread = Some(tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_millis(waiting as u64)).await; + + let mut matches_guard = matches_move.write().await; + let mut current_match = matches_guard.get_mut(&match_id_move).unwrap().write().await; + let move_to_dispatch = current_match.move_to_dispatch.clone(); + current_match.ledger.push(move_to_dispatch); + current_match.move_to_dispatch = (Color::None, 0); + + if demo_mode_move { + crate::broadcast_message( + ¤t_match.viewers, + &observers_move, + &format!("GAME:MOVE:{}:{}", "demo", column_to_use), + ) + .await; + } + + drop(current_match); + drop(matches_guard); + + let _ = crate::send(&connection_to_send, &format!("OPPONENT:{}", column_to_use)); + })); + + Ok(()) + } + + pub async fn handle_player_list( + &self, + tx: UnboundedSender, + ) -> Result<(), anyhow::Error> { + let clients_guard = self.clients.read().await; + let mut to_send = "PLAYER:LIST:".to_string(); + for client_guard in clients_guard.values() { + let player = client_guard.read().await; + to_send += player.username.as_str(); + to_send += ","; + to_send += if player.ready { "true" } else { "false" }; + to_send += ","; + to_send += if player.current_match.is_some() { + "true" + } else { + "false" + }; + to_send += "|"; + } + + if !to_send.ends_with(":") { + to_send.remove(to_send.len() - 1); + } + + let _ = crate::send(&tx, to_send.as_str()); + Ok(()) + } + + pub async fn handle_game_list( + &self, + tx: UnboundedSender, + ) -> Result<(), anyhow::Error> { + let matches_guard = self.matches.read().await; + let clients_guard = self.clients.read().await; + let mut to_send = "GAME:LIST:".to_string(); + for match_guard in matches_guard.values() { + let a_match = match_guard.read().await; + let player1 = clients_guard.get(&a_match.player1).unwrap().read().await; + let player2 = clients_guard.get(&a_match.player2).unwrap().read().await; + to_send += a_match.id.to_string().as_str(); + to_send += ","; + to_send += player1.username.as_str(); + to_send += ","; + to_send += if player1.username == player2.username { + "demo" + } else { + player2.username.as_str() + }; + to_send += "|"; + } + + if !to_send.ends_with(":") { + to_send.remove(to_send.len() - 1); + } + + let _ = crate::send(&tx, to_send.as_str()); + Ok(()) + } + + pub async fn handle_game_watch( + &self, + tx: UnboundedSender, + match_id: u32, + addr: SocketAddr, + ) -> Result<(), anyhow::Error> { + let result = crate::watch(&self.matches, match_id, addr).await; + if result.is_err() { + return Err(anyhow::anyhow!("ERROR:INVALID:WATCH")); + } + + let clients_guard = self.clients.read().await; + let matches_guard = self.matches.read().await; + let the_match = matches_guard.get(&match_id).unwrap().read().await; + let player1 = clients_guard.get(&the_match.player1).unwrap().read().await.username.clone(); + let mut player2 = + clients_guard.get(&the_match.player2).unwrap().read().await.username.clone(); + if self.demo_mode { + player2 = "demo".to_string(); + } + let ledger = the_match.ledger.clone(); + + drop(clients_guard); + drop(the_match); + drop(matches_guard); + + let mut message = format!("GAME:WATCH:ACK:{},{},{}|", match_id, player1, player2); + + for a_move in ledger { + if a_move.0 == Color::Red { + message += &format!("{},{}|", player1, a_move.1); + } else { + message += &format!("{},{}|", player2, a_move.1); + } + } + + message.pop(); + + let _ = crate::send(&tx, &message); + Ok(()) + } + + pub async fn handle_admin_auth( + &self, + tx: UnboundedSender, + addr: SocketAddr, + password: String, + ) -> Result<(), anyhow::Error> { + if self.admin.read().await.is_some() { + return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); + } + + if password != *self.admin_password { + return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); + } + + let mut admin_guard = self.admin.write().await; + *admin_guard = Some(addr.to_string().parse()?); + let _ = crate::send(&tx, "ADMIN:AUTH:ACK"); + Ok(()) + } + + pub async fn handle_admin_kick( + &self, + tx: UnboundedSender, + addr: SocketAddr, + kick_username: String, + ) -> Result<(), anyhow::Error> { + if !crate::auth_check(&self.admin, addr).await { + return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); + } + + let usernames_guard = self.usernames.read().await; + let clients_guard = self.clients.read().await; + + let kick_addr_result = usernames_guard.get(&kick_username); + match kick_addr_result { + Some(kick_addr) => { + let kick_client = clients_guard.get(kick_addr).unwrap().read().await; + kick_client.connection.send(Message::Close(None))?; + } + None => return Err(anyhow::anyhow!("ERROR:INVALID:KICK")), + } + Ok(()) + } + + pub async fn handle_game_terminate( + &self, + tx: UnboundedSender, + addr: SocketAddr, + match_id: u32, + ) -> Result<(), anyhow::Error> { + if !crate::auth_check(&self.admin, addr).await { + return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); + } + + crate::terminate_match( + match_id, + &self.matches, + &self.clients, + &self.observers, + self.demo_mode, + ) + .await; + + if !self.demo_mode && self.matches.read().await.is_empty() { + let mut tournament_guard = self.tournament.write().await; + let tourney = tournament_guard.as_mut().unwrap(); + tourney.write().await.next(&self.clients, &self.matches, &self.observers).await; + if tourney.read().await.is_completed() { + *tournament_guard = None; + } + } + Ok(()) + } + + pub async fn handle_tournament_start( + &self, + tx: UnboundedSender, + addr: SocketAddr, + ) -> Result<(), anyhow::Error> { + if !crate::auth_check(&self.admin, addr).await { + return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); + } + + if self.tournament.read().await.is_some() || self.demo_mode { + return Err(anyhow::anyhow!("ERROR:INVALID:TOURNAMENT")); + } + + let mut clients_guard = self.clients.write().await; + let mut ready_players = Vec::new(); + for (client_addr, client_guard) in clients_guard.iter_mut() { + if client_guard.read().await.ready { + ready_players.push(*client_addr); + } + } + + if ready_players.len() < 3 { + return Err(anyhow::anyhow!("ERROR:INVALID:TOURNAMENT")); + } + + drop(clients_guard); + + let mut tourney = match self.tournament_type.as_str() { + "round_robin" => crate::tournaments::round_robin::RoundRobin::new(&ready_players), + &_ => crate::tournaments::round_robin::RoundRobin::new(&ready_players), + }; + tourney.start(&self.clients, &self.matches).await; + + let mut tournament_guard = self.tournament.write().await; + *tournament_guard = Some(Arc::new(RwLock::new(tourney))); + + let _ = crate::send(&tx, "TOURNAMENT:START:ACK"); + Ok(()) + } + + pub async fn handle_tournament_cancel( + &self, + tx: UnboundedSender, + addr: SocketAddr, + ) -> Result<(), anyhow::Error> { + if !crate::auth_check(&self.admin, addr).await { + return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); + } + + if self.tournament.read().await.is_none() || self.demo_mode { + return Err(anyhow::anyhow!("ERROR:INVALID:TOURNAMENT")); + } + + let mut tournament_guard = self.tournament.write().await; + let tourney = tournament_guard.as_mut().unwrap(); + tourney.write().await.cancel(&self.clients, &self.matches, &self.observers).await; + *tournament_guard = None; + + let _ = crate::send(&tx, "TOURNAMENT:CANCEL:ACK"); + Ok(()) + } + + pub async fn handle_tournament_wait( + &self, + tx: UnboundedSender, + addr: SocketAddr, + new_timeout: f64, + ) -> Result<(), anyhow::Error> { + if !crate::auth_check(&self.admin, addr).await { + return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); + } + + *self.waiting_timeout.write().await = (new_timeout * 1000.0) as u64; + Ok(()) + } + + pub async fn handle_get_move_wait( + &self, + tx: UnboundedSender, + ) -> Result<(), anyhow::Error> { + let mut msg = "GET:MOVE_WAIT:".to_string(); + msg += &(*self.waiting_timeout.read().await as f64 / 1000f64).to_string(); + let _ = crate::send(&tx, &msg); + Ok(()) + } + + pub async fn handle_get_tournament_status( + &self, + tx: UnboundedSender, + ) -> Result<(), anyhow::Error> { + let status = self.tournament.read().await.is_some(); + if self.demo_mode { + let _ = crate::send(&tx, "GET:TOURNAMENT_STATUS:DEMO"); + } else { + let mut msg = "GET:TOURNAMENT_STATUS:".to_string(); + msg += status.to_string().as_str(); + let _ = crate::send(&tx, &msg); + } + Ok(()) + } } #[derive(PartialEq, Clone)] pub enum Color { - Red, - Yellow, - None, + Red, + Yellow, + None, } #[derive(Clone)] pub struct Client { - pub username: String, - pub connection: UnboundedSender, - pub ready: bool, - pub color: Color, - pub current_match: Option, - pub round_robin_id: u32, - pub score: u32, - pub addr: SocketAddr, + pub username: String, + pub connection: UnboundedSender, + pub ready: bool, + pub color: Color, + pub current_match: Option, + pub round_robin_id: u32, + pub score: u32, + pub addr: SocketAddr, } impl Client { - pub fn new(username: String, connection: UnboundedSender, 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, addr: SocketAddr) -> Client { + Client { + username, + connection, + ready: false, + color: Color::None, + current_match: None, + round_robin_id: 0, + score: 0, + addr, + } + } } pub struct Match { - pub id: u32, - pub board: Vec>, - pub viewers: Vec, - pub ledger: Vec<(Color, usize)>, - pub move_to_dispatch: (Color, usize), - pub wait_thread: Option>, - pub player1: SocketAddr, - pub player2: SocketAddr, + pub id: u32, + pub board: Vec>, + pub viewers: Vec, + pub ledger: Vec<(Color, usize)>, + pub move_to_dispatch: (Color, usize), + pub wait_thread: Option>, + pub player1: SocketAddr, + pub player2: SocketAddr, } 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: 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() + }; - 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, + 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 }, + } + } - 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 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; + } + } + } } From 23feed3f214e513f2d04d7dd445a3ca846f8316e Mon Sep 17 00:00:00 2001 From: Joshua Higgins Date: Tue, 30 Dec 2025 22:51:52 -0500 Subject: [PATCH 3/4] misc(refactor): move Server type to its own module --- src/lib.rs | 161 +------ src/main.rs | 24 +- src/server.rs | 703 ++++++++++++++++++++++++++++ src/tournaments/mod.rs | 8 +- src/tournaments/round_robin.rs | 20 +- src/types.rs | 806 +++++---------------------------- 6 files changed, 846 insertions(+), 876 deletions(-) create mode 100644 src/server.rs diff --git a/src/lib.rs b/src/lib.rs index f93b62d..172a7f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ use crate::{ types::{Client, Color, Match}, }; +pub mod server; pub mod tournaments; pub mod types; @@ -22,57 +23,15 @@ pub type Observers = Arc>>>; pub type Matches = Arc>>>>; pub type WrappedTournament = Arc>>>>; -pub async fn broadcast_message(addrs: &Vec, observers: &Observers, msg: &str) { - for addr in addrs { - let observers_guard = observers.read().await; - let tx = observers_guard.get(addr); - if tx.is_none() { - continue; - } - let _ = send(tx.unwrap(), msg); - } -} - -pub async fn broadcast_message_all_observers(observers: &Observers, msg: &str) { - let observers_guard = observers.read().await; - for (_, tx) in observers_guard.iter() { - let _ = send(tx, msg); - } -} - -pub async fn watch(matches: &Matches, new_match_id: u32, addr: SocketAddr) -> Result<(), String> { - let matches_guard = matches.read().await; - - for match_guard in matches_guard.values() { - let mut found = false; - let mut a_match = match_guard.write().await; - for i in 0..a_match.viewers.len() { - if a_match.viewers[i] == addr { - a_match.viewers.remove(i); - found = true; - break; - } - } - - if found { - break; - } - } - - let result = matches_guard.get(&new_match_id); - if result.is_none() { - return Err("Match not found".to_string()); - } - result.unwrap().write().await.viewers.push(addr); - - Ok(()) -} - -pub async fn auth_check(admin: &Arc>>, addr: SocketAddr) -> bool { - if admin.read().await.is_none() || admin.read().await.unwrap() != addr { - return false; - } - true +pub async fn broadcast_message(observers: &Observers, addrs: &Vec, msg: &str) { + for addr in addrs { + let observers_guard = observers.read().await; + let tx = observers_guard.get(addr); + if tx.is_none() { + continue; + } + let _ = send(tx.unwrap(), msg); + } } pub async fn gen_match_id(matches: &Matches) -> u32 { @@ -84,52 +43,6 @@ pub async fn gen_match_id(matches: &Matches) -> u32 { result } -pub async fn terminate_match( - match_id: u32, - matches: &Matches, - clients: &Clients, - observers: &Observers, - demo_mode: bool, -) { - let matches_guard = matches.read().await; - let the_match = matches_guard.get(&match_id); - if the_match.is_none() { - error!( - "Tried to call terminate_match on invalid matchID: {}", - match_id - ); - } - let the_match = the_match.unwrap().read().await; - - if the_match.wait_thread.is_some() { - the_match.wait_thread.as_ref().unwrap().abort(); - } - - broadcast_message(&the_match.viewers, observers, "GAME:TERMINATED").await; - - let clients_guard = clients.read().await; - let mut player1 = clients_guard.get(&the_match.player1).unwrap().write().await; - let _ = send(&player1.connection, "GAME:TERMINATED"); - player1.current_match = None; - player1.color = Color::None; - drop(player1); - - if !demo_mode { - let mut player2 = clients_guard.get(&the_match.player2).unwrap().write().await; - let _ = send(&player2.connection, "GAME:TERMINATED"); - player2.current_match = None; - player2.color = Color::None; - drop(player2); - } - - drop(clients_guard); - - drop(the_match); - drop(matches_guard); - - matches.write().await.remove(&match_id); -} - pub fn random_move(board: &[Vec]) -> usize { let mut random = rand::rng().random_range(0..7); while board[random][5] != Color::None { @@ -139,60 +52,6 @@ pub fn random_move(board: &[Vec]) -> usize { random } -pub fn end_game_check(board: &[Vec]) -> (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 = 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 || board[x + i][y] != color && horizontal_end { - horizontal_end = false; - } - - if y + i >= 6 || board[x][y + i] != color && vertical_end { - vertical_end = false; - } - - if x + i >= 7 || y + i >= 6 || board[x + i][y + i] != color && diagonal_end_up { - diagonal_end_up = false; - } - - if x + i >= 7 - || (y as i32 - i as i32) < 0 - || board[x + i][y - i] != color && diagonal_end_down - { - diagonal_end_down = false; - } - } - - if horizontal_end || vertical_end || diagonal_end_up || diagonal_end_down { - result = (color.clone(), false); - break; - } - } - if result.0 != Color::None { - break; - } - } - - if any_empty && result.0 == Color::None { - result.1 = true; - } - - result -} - pub fn send(tx: &UnboundedSender, text: &str) -> Result<(), SendError> { tx.send(Message::text(text)) } diff --git a/src/main.rs b/src/main.rs index 827aca2..327aa97 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ -use connect4_moderator_server::types::*; -use connect4_moderator_server::*; +use connect4_moderator_server::{server::Server, *}; use futures_util::{SinkExt, StreamExt}; use std::env; use std::net::SocketAddr; @@ -151,9 +150,7 @@ async fn handle_connection( } else if parts.get(1) == Some(&"TERMINATE") && parts.len() > 2 { match parts[2].parse::() { Ok(match_id) => { - if let Err(e) = - sd.handle_game_terminate(tx.clone(), addr, match_id).await - { + if let Err(e) = sd.handle_game_terminate(addr, match_id).await { error!("handle_game_terminate: {}", e); let _ = send(&tx, e.to_string().as_str()); } @@ -173,9 +170,7 @@ async fn handle_connection( let _ = send(&tx, e.to_string().as_str()); } } else if parts.get(1) == Some(&"KICK") && parts.len() > 2 { - if let Err(e) = - sd.handle_admin_kick(tx.clone(), addr, parts[2].to_string()).await - { + if let Err(e) = sd.handle_admin_kick(addr, parts[2].to_string()).await { error!("handle_admin_kick: {}", e); let _ = send(&tx, e.to_string().as_str()); } @@ -194,9 +189,9 @@ async fn handle_connection( } } else if parts.get(1) == Some(&"WAIT") && parts.len() > 2 { match parts[2].parse::() { - Ok(v) => { + Ok(new_timeout) => { if let Err(e) = - sd.handle_tournament_wait(tx.clone(), addr, v).await + sd.handle_tournament_wait(addr, new_timeout).await { error!("handle_tournament_wait: {}", e); let _ = send(&tx, e.to_string().as_str()); @@ -252,14 +247,7 @@ async fn handle_connection( let username = client.username.clone(); if let Some(match_id) = client.current_match { drop(client); - terminate_match( - match_id, - &sd.matches, - &sd.clients, - &sd.observers, - sd.demo_mode, - ) - .await; + sd.terminate_match(match_id).await; } else { drop(client); } diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..e9e3e62 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,703 @@ +use crate::{tournaments::Tournament, types::*, *}; + +pub struct Server { + pub clients: Clients, + pub usernames: Usernames, + pub observers: Observers, + pub matches: Matches, + pub admin: Arc>>, + pub admin_password: Arc, + pub tournament: WrappedTournament, + pub waiting_timeout: Arc>, + pub demo_mode: bool, + pub tournament_type: String, +} + +impl Server { + pub fn new(admin_password: String, demo_mode: bool, tournament_type: String) -> Server { + Server { + clients: Arc::new(RwLock::new(HashMap::new())), + usernames: Arc::new(RwLock::new(HashMap::new())), + observers: Arc::new(RwLock::new(HashMap::new())), + matches: Arc::new(RwLock::new(HashMap::new())), + admin: Arc::new(RwLock::new(None)), + admin_password: Arc::new(admin_password), + tournament: Arc::new(RwLock::new(None)), + waiting_timeout: Arc::new(RwLock::new(5000)), + demo_mode, + tournament_type, + } + } + + // Handler for CONNECT: + pub async fn handle_connect( + &self, + addr: SocketAddr, + tx: UnboundedSender, + requested_username: String, + ) -> Result<(), anyhow::Error> { + if requested_username.is_empty() { + return Err(anyhow::anyhow!(format!( + "ERROR:INVALID:ID:{}", + requested_username + ))); + } + + let clients_guard = self.clients.read().await; + for client in clients_guard.values() { + if requested_username == client.read().await.username { + return Err(anyhow::anyhow!(format!( + "ERROR:INVALID:ID:{}", + requested_username + ))); + } + } + + drop(clients_guard); + + // 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 _ = crate::send(&tx, "CONNECT:ACK"); + Ok(()) + } + + // Handler for READY + pub async fn handle_ready( + &self, + addr: SocketAddr, + tx: UnboundedSender, + ) -> Result<(), anyhow::Error> { + let clients_guard = self.clients.read().await; + if clients_guard.get(&addr).is_none() { + return Err(anyhow::anyhow!("ERROR:INVALID")); + } + + if clients_guard.get(&addr).unwrap().read().await.ready { + return Err(anyhow::anyhow!("ERROR:INVALID")); + } + + let mut client = clients_guard.get(&addr).unwrap().write().await; + client.ready = true; + let _ = crate::send(&tx, "READY:ACK"); + + if self.demo_mode { + let match_id: u32 = crate::gen_match_id(&self.matches).await; + let new_match = Arc::new(RwLock::new(Match::new( + match_id, + addr.to_string().parse()?, + addr.to_string().parse()?, + ))); + self.matches.write().await.insert(match_id, new_match.clone()); + client.ready = false; + client.current_match = Some(match_id); + client.color = Color::Red; + let _ = crate::send(&tx, "GAME:START:1"); + } + + Ok(()) + } + + // Handler for PLAY (column already parsed) + pub async fn handle_play( + &self, + addr: SocketAddr, + tx: UnboundedSender, + column: usize, + ) -> Result<(), anyhow::Error> { + let clients_guard = self.clients.read().await; + let client_opt = clients_guard.get(&addr); + + // Check if client is valid + if client_opt.is_none() || client_opt.unwrap().read().await.current_match.is_none() { + return Err(anyhow::anyhow!("ERROR:INVALID:MOVE")); + } + let client = client_opt.unwrap().read().await; + + 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_connection = if addr == current_match.player1 { + clients_guard.get(¤t_match.player2).unwrap().read().await.connection.clone() + } else { + clients_guard.get(¤t_match.player1).unwrap().read().await.connection.clone() + }; + + let opponent_username = 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() + }; + + // Check if it's their move + let mut invalid = false; + if (current_match.ledger.is_empty() && current_match.player1 != addr) + || (current_match.ledger.last().is_some() + && current_match.ledger.last().unwrap().0 == client.color) + { + let _ = crate::send(&tx, "ERROR:INVALID:MOVE"); + invalid = true; + } + + drop(current_match); + drop(matches_guard); + + let mut matches_guard = self.matches.write().await; + let mut current_match = + matches_guard.get_mut(&client.current_match.unwrap()).unwrap().write().await; + + // Check if valid move + if column >= 7 && !invalid { + let _ = crate::send(&tx, "ERROR:INVALID:MOVE"); + invalid = true; + } + + if current_match.board[column][5] != Color::None && !invalid { + let _ = crate::send(&tx, "ERROR:INVALID:MOVE"); + invalid = true; + } + + // Terminate games if a player makes an invalid move + if invalid { + let current_match_id = current_match.id; + let viewers = current_match.viewers.clone(); + + drop(current_match); + drop(matches_guard); + drop(client); + drop(clients_guard); + + if self.demo_mode { + self.terminate_match(current_match_id).await; + tx.send(Message::Close(None))?; + } else { + let _ = crate::send(&tx, "GAME:LOSS"); + let _ = crate::send(&opponent_connection, "GAME:WINS"); + self.broadcast_message(&viewers, &format!("GAME:WIN:{}", opponent_username)).await; + + let mut clients_guard = self.clients.write().await; + let mut client = clients_guard.get_mut(&addr).unwrap().write().await; + client.current_match = None; + 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; + opponent.score += 1; + drop(opponent); + + self.matches.write().await.remove(¤t_match_id).unwrap(); + } + return Ok(()); + } else { + // Place it + current_match.place_token(client.color.clone(), column); + current_match.move_to_dispatch = (client.color.clone(), column); + } + + // broadcast the move to viewers + self.broadcast_message( + ¤t_match.viewers, + &format!("GAME:MOVE:{}:{}", client.username, column), + ) + .await; + + // Check game end conditions + let (winner, filled) = current_match.end_game_check(); + + if winner != Color::None { + if winner == client.color { + let _ = crate::send(&tx, "GAME:WINS"); + if !self.demo_mode { + let _ = crate::send(&opponent_connection, "GAME:LOSS"); + } + self.broadcast_message( + ¤t_match.viewers, + &format!("GAME:WIN:{}", client.username), + ) + .await; + } else { + let _ = crate::send(&tx, "GAME:LOSS"); + if !self.demo_mode { + let _ = crate::send(&opponent_connection, "GAME:WINS"); + } + self.broadcast_message( + ¤t_match.viewers, + &format!("GAME:WIN:{}", opponent_username), + ) + .await; + } + } else if filled { + let _ = crate::send(&tx, "GAME:DRAW"); + if !self.demo_mode { + let _ = crate::send(&opponent_connection, "GAME:DRAW"); + } + self.broadcast_message(¤t_match.viewers, "GAME:DRAW").await; + } + + // remove match from matchmaker + if winner != Color::None || filled { + let current_match_id = current_match.id; + + drop(client); + drop(current_match); + drop(clients_guard); + + let clients_guard = self.clients.read().await; + let mut client = clients_guard.get(&addr).unwrap().write().await; + if client.color == winner { + client.score += 1; + } + client.current_match = None; + client.color = Color::None; + drop(client); + + let mut opponent = clients_guard.get(&opponent_addr).unwrap().write().await; + if opponent.color == winner { + opponent.score += 1; + } + opponent.current_match = None; + opponent.color = Color::None; + drop(opponent); + matches_guard.remove(¤t_match_id).unwrap(); + + if !self.demo_mode && matches_guard.is_empty() { + drop(matches_guard); + drop(clients_guard); + + let mut tournament_guard = self.tournament.write().await; + let tourney = tournament_guard.as_mut().unwrap(); + tourney.write().await.next(&self).await; + if tourney.read().await.is_completed() { + *tournament_guard = None; + } + } + + return Ok(()); + } + + let connection_to_send = if !self.demo_mode { + opponent_connection.clone() + } else { + tx.clone() + }; + let column_to_use = if !self.demo_mode { + column + } else { + crate::random_move(¤t_match.board) + }; + if self.demo_mode { + let move_to_dispatch = current_match.move_to_dispatch.clone(); + current_match.ledger.push(move_to_dispatch); + current_match.move_to_dispatch = (Color::Yellow, column_to_use); + current_match.place_token(Color::Yellow, column_to_use); + } + + let waiting = + *self.waiting_timeout.read().await as i64 + (rand::rng().random_range(0..=50) - 25); + let matches_move = self.matches.clone(); + let observers_move = self.observers.clone(); + let match_id_move = current_match.id; + let demo_mode_move = self.demo_mode; + current_match.wait_thread = Some(tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_millis(waiting as u64)).await; + + let mut matches_guard = matches_move.write().await; + let mut current_match = matches_guard.get_mut(&match_id_move).unwrap().write().await; + let move_to_dispatch = current_match.move_to_dispatch.clone(); + current_match.ledger.push(move_to_dispatch); + current_match.move_to_dispatch = (Color::None, 0); + + if demo_mode_move { + crate::broadcast_message( + &observers_move, + ¤t_match.viewers, + &format!("GAME:MOVE:{}:{}", "demo", column_to_use), + ) + .await; + } + + drop(current_match); + drop(matches_guard); + + let _ = crate::send(&connection_to_send, &format!("OPPONENT:{}", column_to_use)); + })); + + Ok(()) + } + + pub async fn handle_player_list( + &self, + tx: UnboundedSender, + ) -> Result<(), anyhow::Error> { + let clients_guard = self.clients.read().await; + let mut to_send = "PLAYER:LIST:".to_string(); + for client_guard in clients_guard.values() { + let player = client_guard.read().await; + to_send += player.username.as_str(); + to_send += ","; + to_send += if player.ready { "true" } else { "false" }; + to_send += ","; + to_send += if player.current_match.is_some() { + "true" + } else { + "false" + }; + to_send += "|"; + } + + if !to_send.ends_with(":") { + to_send.remove(to_send.len() - 1); + } + + let _ = crate::send(&tx, to_send.as_str()); + Ok(()) + } + + pub async fn handle_game_list( + &self, + tx: UnboundedSender, + ) -> Result<(), anyhow::Error> { + let matches_guard = self.matches.read().await; + let clients_guard = self.clients.read().await; + let mut to_send = "GAME:LIST:".to_string(); + for match_guard in matches_guard.values() { + let a_match = match_guard.read().await; + let player1 = clients_guard.get(&a_match.player1).unwrap().read().await; + let player2 = clients_guard.get(&a_match.player2).unwrap().read().await; + to_send += a_match.id.to_string().as_str(); + to_send += ","; + to_send += player1.username.as_str(); + to_send += ","; + to_send += if player1.username == player2.username { + "demo" + } else { + player2.username.as_str() + }; + to_send += "|"; + } + + if !to_send.ends_with(":") { + to_send.remove(to_send.len() - 1); + } + + let _ = crate::send(&tx, to_send.as_str()); + Ok(()) + } + + pub async fn handle_game_watch( + &self, + tx: UnboundedSender, + match_id: u32, + addr: SocketAddr, + ) -> Result<(), anyhow::Error> { + let result = self.watch(match_id, addr).await; + if result.is_err() { + return Err(anyhow::anyhow!("ERROR:INVALID:WATCH")); + } + + let clients_guard = self.clients.read().await; + let matches_guard = self.matches.read().await; + let the_match = matches_guard.get(&match_id).unwrap().read().await; + let player1 = clients_guard.get(&the_match.player1).unwrap().read().await.username.clone(); + let mut player2 = + clients_guard.get(&the_match.player2).unwrap().read().await.username.clone(); + if self.demo_mode { + player2 = "demo".to_string(); + } + let ledger = the_match.ledger.clone(); + + drop(clients_guard); + drop(the_match); + drop(matches_guard); + + let mut message = format!("GAME:WATCH:ACK:{},{},{}|", match_id, player1, player2); + + for a_move in ledger { + if a_move.0 == Color::Red { + message += &format!("{},{}|", player1, a_move.1); + } else { + message += &format!("{},{}|", player2, a_move.1); + } + } + + message.pop(); + + let _ = crate::send(&tx, &message); + Ok(()) + } + + pub async fn handle_admin_auth( + &self, + tx: UnboundedSender, + addr: SocketAddr, + password: String, + ) -> Result<(), anyhow::Error> { + if self.admin.read().await.is_some() { + return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); + } + + if password != *self.admin_password { + return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); + } + + let mut admin_guard = self.admin.write().await; + *admin_guard = Some(addr.to_string().parse()?); + let _ = crate::send(&tx, "ADMIN:AUTH:ACK"); + Ok(()) + } + + pub async fn handle_admin_kick( + &self, + addr: SocketAddr, + kick_username: String, + ) -> Result<(), anyhow::Error> { + if !self.auth_check(addr).await { + return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); + } + + let usernames_guard = self.usernames.read().await; + let clients_guard = self.clients.read().await; + + let kick_addr_result = usernames_guard.get(&kick_username); + match kick_addr_result { + Some(kick_addr) => { + let kick_client = clients_guard.get(kick_addr).unwrap().read().await; + kick_client.connection.send(Message::Close(None))?; + } + None => return Err(anyhow::anyhow!("ERROR:INVALID:KICK")), + } + Ok(()) + } + + pub async fn handle_game_terminate( + &self, + addr: SocketAddr, + match_id: u32, + ) -> Result<(), anyhow::Error> { + if !self.auth_check(addr).await { + return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); + } + + self.terminate_match(match_id).await; + + if !self.demo_mode && self.matches.read().await.is_empty() { + let mut tournament_guard = self.tournament.write().await; + let tourney = tournament_guard.as_mut().unwrap(); + tourney.write().await.next(&self).await; + if tourney.read().await.is_completed() { + *tournament_guard = None; + } + } + Ok(()) + } + + pub async fn handle_tournament_start( + &self, + tx: UnboundedSender, + addr: SocketAddr, + ) -> Result<(), anyhow::Error> { + if !self.auth_check(addr).await { + return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); + } + + if self.tournament.read().await.is_some() || self.demo_mode { + return Err(anyhow::anyhow!("ERROR:INVALID:TOURNAMENT")); + } + + let mut clients_guard = self.clients.write().await; + let mut ready_players = Vec::new(); + for (client_addr, client_guard) in clients_guard.iter_mut() { + if client_guard.read().await.ready { + ready_players.push(*client_addr); + } + } + + if ready_players.len() < 3 { + return Err(anyhow::anyhow!("ERROR:INVALID:TOURNAMENT")); + } + + drop(clients_guard); + + let mut tourney = match self.tournament_type.as_str() { + "round_robin" => crate::tournaments::round_robin::RoundRobin::new(&ready_players), + &_ => crate::tournaments::round_robin::RoundRobin::new(&ready_players), + }; + tourney.start(&self).await; + + let mut tournament_guard = self.tournament.write().await; + *tournament_guard = Some(Arc::new(RwLock::new(tourney))); + + let _ = crate::send(&tx, "TOURNAMENT:START:ACK"); + Ok(()) + } + + pub async fn handle_tournament_cancel( + &self, + tx: UnboundedSender, + addr: SocketAddr, + ) -> Result<(), anyhow::Error> { + if !self.auth_check(addr).await { + return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); + } + + if self.tournament.read().await.is_none() || self.demo_mode { + return Err(anyhow::anyhow!("ERROR:INVALID:TOURNAMENT")); + } + + let mut tournament_guard = self.tournament.write().await; + let tourney = tournament_guard.as_mut().unwrap(); + tourney.write().await.cancel(&self).await; + *tournament_guard = None; + + let _ = crate::send(&tx, "TOURNAMENT:CANCEL:ACK"); + Ok(()) + } + + pub async fn handle_tournament_wait( + &self, + addr: SocketAddr, + new_timeout: f64, + ) -> Result<(), anyhow::Error> { + if !self.auth_check(addr).await { + return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); + } + + *self.waiting_timeout.write().await = (new_timeout * 1000.0) as u64; + Ok(()) + } + + pub async fn handle_get_move_wait( + &self, + tx: UnboundedSender, + ) -> Result<(), anyhow::Error> { + let mut msg = "GET:MOVE_WAIT:".to_string(); + msg += &(*self.waiting_timeout.read().await as f64 / 1000f64).to_string(); + let _ = crate::send(&tx, &msg); + Ok(()) + } + + pub async fn handle_get_tournament_status( + &self, + tx: UnboundedSender, + ) -> Result<(), anyhow::Error> { + let status = self.tournament.read().await.is_some(); + if self.demo_mode { + let _ = crate::send(&tx, "GET:TOURNAMENT_STATUS:DEMO"); + } else { + let mut msg = "GET:TOURNAMENT_STATUS:".to_string(); + msg += status.to_string().as_str(); + let _ = crate::send(&tx, &msg); + } + Ok(()) + } + + pub async fn watch(&self, new_match_id: u32, addr: SocketAddr) -> Result<(), String> { + let matches_guard = self.matches.read().await; + + for match_guard in matches_guard.values() { + let mut found = false; + let mut a_match = match_guard.write().await; + for i in 0..a_match.viewers.len() { + if a_match.viewers[i] == addr { + a_match.viewers.remove(i); + found = true; + break; + } + } + + if found { + break; + } + } + + let result = matches_guard.get(&new_match_id); + if result.is_none() { + return Err("Match not found".to_string()); + } + result.unwrap().write().await.viewers.push(addr); + + Ok(()) + } + + pub async fn terminate_match(&self, match_id: u32) { + let matches_guard = self.matches.read().await; + let the_match = matches_guard.get(&match_id); + if the_match.is_none() { + error!( + "Tried to call terminate_match on invalid matchID: {}", + match_id + ); + } + let the_match = the_match.unwrap().read().await; + + if the_match.wait_thread.is_some() { + the_match.wait_thread.as_ref().unwrap().abort(); + } + + self.broadcast_message(&the_match.viewers, "GAME:TERMINATED").await; + + let clients_guard = self.clients.read().await; + let mut player1 = clients_guard.get(&the_match.player1).unwrap().write().await; + let _ = send(&player1.connection, "GAME:TERMINATED"); + player1.current_match = None; + player1.color = Color::None; + drop(player1); + + if !self.demo_mode { + let mut player2 = clients_guard.get(&the_match.player2).unwrap().write().await; + let _ = send(&player2.connection, "GAME:TERMINATED"); + player2.current_match = None; + player2.color = Color::None; + drop(player2); + } + + drop(clients_guard); + + drop(the_match); + drop(matches_guard); + + self.matches.write().await.remove(&match_id); + } + + pub async fn broadcast_message(&self, addrs: &Vec, msg: &str) { + for addr in addrs { + let observers_guard = self.observers.read().await; + let tx = observers_guard.get(addr); + if tx.is_none() { + continue; + } + let _ = send(tx.unwrap(), msg); + } + } + + pub async fn broadcast_message_all_observers(&self, msg: &str) { + let observers_guard = self.observers.read().await; + for (_, tx) in observers_guard.iter() { + let _ = send(tx, msg); + } + } + + pub async fn auth_check(&self, addr: SocketAddr) -> bool { + if self.admin.read().await.is_none() || self.admin.read().await.unwrap() != addr { + return false; + } + true + } +} diff --git a/src/tournaments/mod.rs b/src/tournaments/mod.rs index 5014e6e..724f149 100644 --- a/src/tournaments/mod.rs +++ b/src/tournaments/mod.rs @@ -2,7 +2,7 @@ use std::net::SocketAddr; use async_trait::async_trait; -use crate::*; +use crate::server::Server; pub mod round_robin; pub use round_robin::RoundRobin; @@ -12,8 +12,8 @@ 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); + async fn next(&mut self, server: &Server); + async fn start(&mut self, server: &Server); + async fn cancel(&mut self, server: &Server); fn is_completed(&self) -> bool; } diff --git a/src/tournaments/round_robin.rs b/src/tournaments/round_robin.rs index ca696a5..9060874 100644 --- a/src/tournaments/round_robin.rs +++ b/src/tournaments/round_robin.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, net::SocketAddr}; use async_trait::async_trait; -use crate::*; +use crate::{server::Server, *}; #[derive(Clone)] pub struct RoundRobin { @@ -94,7 +94,7 @@ impl Tournament for RoundRobin { result } - async fn next(&mut self, clients: &Clients, matches: &Matches, observers: &Observers) { + async fn next(&mut self, server: &Server) { if self.is_completed { return; } @@ -117,7 +117,7 @@ impl Tournament for RoundRobin { if self.is_completed() { // Send scores - let clients_guard = clients.read().await; + let clients_guard = server.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; @@ -135,20 +135,20 @@ impl Tournament for RoundRobin { } message.pop(); - broadcast_message_all_observers(observers, &message).await; + server.broadcast_message_all_observers(&message).await; } else { // Create next matches - self.create_matches(clients, matches).await; + self.create_matches(&server.clients, &server.matches).await; } } - async fn start(&mut self, clients: &Clients, matches: &Matches) { - self.create_matches(clients, matches).await; + async fn start(&mut self, server: &Server) { + self.create_matches(&server.clients, &server.matches).await; } - async fn cancel(&mut self, clients: &Clients, matches: &Matches, observers: &Observers) { + async fn cancel(&mut self, server: &Server) { for (_, addr) in self.players.iter() { - let clients_guard = clients.read().await; + let clients_guard = server.clients.read().await; let client = clients_guard.get(addr); if client.is_none() { @@ -167,7 +167,7 @@ impl Tournament for RoundRobin { drop(client); drop(clients_guard); - terminate_match(match_id, matches, clients, observers, false).await; + server.terminate_match(match_id).await; if !client_ready { let _ = send(&client_connection, "TOURNAMENT:END"); diff --git a/src/types.rs b/src/types.rs index 255bda4..8a9686a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,717 +1,137 @@ -use crate::*; use rand::Rng; use std::net::SocketAddr; -use std::sync::Arc; use std::vec; use tokio::sync::mpsc::UnboundedSender; -use tokio::sync::RwLock; use tokio_tungstenite::tungstenite::Message; -pub struct Server { - pub clients: Clients, - pub usernames: Usernames, - pub observers: Observers, - pub matches: Matches, - pub admin: Arc>>, - pub admin_password: Arc, - pub tournament: WrappedTournament, - pub waiting_timeout: Arc>, - pub demo_mode: bool, - pub tournament_type: String, -} - -impl Server { - pub fn new(admin_password: String, demo_mode: bool, tournament_type: String) -> Server { - Server { - clients: Arc::new(RwLock::new(HashMap::new())), - usernames: Arc::new(RwLock::new(HashMap::new())), - observers: Arc::new(RwLock::new(HashMap::new())), - matches: Arc::new(RwLock::new(HashMap::new())), - admin: Arc::new(RwLock::new(None)), - admin_password: Arc::new(admin_password), - tournament: Arc::new(RwLock::new(None)), - waiting_timeout: Arc::new(RwLock::new(5000)), - demo_mode, - tournament_type, - } - } - - // Handler for CONNECT: - pub async fn handle_connect( - &self, - addr: SocketAddr, - tx: UnboundedSender, - requested_username: String, - ) -> Result<(), anyhow::Error> { - if requested_username.is_empty() { - return Err(anyhow::anyhow!(format!( - "ERROR:INVALID:ID:{}", - requested_username - ))); - } - - let clients_guard = self.clients.read().await; - for client in clients_guard.values() { - if requested_username == client.read().await.username { - return Err(anyhow::anyhow!(format!( - "ERROR:INVALID:ID:{}", - requested_username - ))); - } - } - - drop(clients_guard); - - // 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 _ = crate::send(&tx, "CONNECT:ACK"); - Ok(()) - } - - // Handler for READY - pub async fn handle_ready( - &self, - addr: SocketAddr, - tx: UnboundedSender, - ) -> Result<(), anyhow::Error> { - let clients_guard = self.clients.read().await; - if clients_guard.get(&addr).is_none() { - return Err(anyhow::anyhow!("ERROR:INVALID")); - } - - if clients_guard.get(&addr).unwrap().read().await.ready { - return Err(anyhow::anyhow!("ERROR:INVALID")); - } - - let mut client = clients_guard.get(&addr).unwrap().write().await; - client.ready = true; - let _ = crate::send(&tx, "READY:ACK"); - - if self.demo_mode { - let match_id: u32 = crate::gen_match_id(&self.matches).await; - let new_match = Arc::new(RwLock::new(Match::new( - match_id, - addr.to_string().parse()?, - addr.to_string().parse()?, - ))); - self.matches.write().await.insert(match_id, new_match.clone()); - client.ready = false; - client.current_match = Some(match_id); - client.color = Color::Red; - let _ = crate::send(&tx, "GAME:START:1"); - } - - Ok(()) - } - - // Handler for PLAY (column already parsed) - pub async fn handle_play( - &self, - addr: SocketAddr, - tx: UnboundedSender, - column: usize, - ) -> Result<(), anyhow::Error> { - let clients_guard = self.clients.read().await; - let client_opt = clients_guard.get(&addr); - - // Check if client is valid - if client_opt.is_none() || client_opt.unwrap().read().await.current_match.is_none() { - return Err(anyhow::anyhow!("ERROR:INVALID:MOVE")); - } - let client = client_opt.unwrap().read().await; - - 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_connection = if addr == current_match.player1 { - clients_guard.get(¤t_match.player2).unwrap().read().await.connection.clone() - } else { - clients_guard.get(¤t_match.player1).unwrap().read().await.connection.clone() - }; - - let opponent_username = 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() - }; - - // Check if it's their move - let mut invalid = false; - if (current_match.ledger.is_empty() && current_match.player1 != addr) - || (current_match.ledger.last().is_some() - && current_match.ledger.last().unwrap().0 == client.color) - { - let _ = crate::send(&tx, "ERROR:INVALID:MOVE"); - invalid = true; - } - - drop(current_match); - drop(matches_guard); - - let mut matches_guard = self.matches.write().await; - let mut current_match = - matches_guard.get_mut(&client.current_match.unwrap()).unwrap().write().await; - - // Check if valid move - if column >= 7 && !invalid { - let _ = crate::send(&tx, "ERROR:INVALID:MOVE"); - invalid = true; - } - - if current_match.board[column][5] != Color::None && !invalid { - let _ = crate::send(&tx, "ERROR:INVALID:MOVE"); - invalid = true; - } - - // Terminate games if a player makes an invalid move - if invalid { - let current_match_id = current_match.id; - let viewers = current_match.viewers.clone(); - - drop(current_match); - drop(matches_guard); - drop(client); - drop(clients_guard); - - if self.demo_mode { - crate::terminate_match( - current_match_id, - &self.matches, - &self.clients, - &self.observers, - self.demo_mode, - ) - .await; - tx.send(Message::Close(None))?; - } else { - let _ = crate::send(&tx, "GAME:LOSS"); - let _ = crate::send(&opponent_connection, "GAME:WINS"); - crate::broadcast_message( - &viewers, - &self.observers, - &format!("GAME:WIN:{}", opponent_username), - ) - .await; - - let mut clients_guard = self.clients.write().await; - let mut client = clients_guard.get_mut(&addr).unwrap().write().await; - client.current_match = None; - 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; - opponent.score += 1; - drop(opponent); - - self.matches.write().await.remove(¤t_match_id).unwrap(); - } - return Ok(()); - } else { - // Place it - current_match.place_token(client.color.clone(), column); - current_match.move_to_dispatch = (client.color.clone(), column); - } - - // broadcast the move to viewers - crate::broadcast_message( - ¤t_match.viewers, - &self.observers, - &format!("GAME:MOVE:{}:{}", client.username, column), - ) - .await; - - // Check game end conditions - let (winner, filled) = crate::end_game_check(¤t_match.board); - - if winner != Color::None { - if winner == client.color { - let _ = crate::send(&tx, "GAME:WINS"); - if !self.demo_mode { - let _ = crate::send(&opponent_connection, "GAME:LOSS"); - } - crate::broadcast_message( - ¤t_match.viewers, - &self.observers, - &format!("GAME:WIN:{}", client.username), - ) - .await; - } else { - let _ = crate::send(&tx, "GAME:LOSS"); - if !self.demo_mode { - let _ = crate::send(&opponent_connection, "GAME:WINS"); - } - crate::broadcast_message( - ¤t_match.viewers, - &self.observers, - &format!("GAME:WIN:{}", opponent_username), - ) - .await; - } - } else if filled { - let _ = crate::send(&tx, "GAME:DRAW"); - if !self.demo_mode { - let _ = crate::send(&opponent_connection, "GAME:DRAW"); - } - crate::broadcast_message(¤t_match.viewers, &self.observers, "GAME:DRAW").await; - } - - // remove match from matchmaker - if winner != Color::None || filled { - let current_match_id = current_match.id; - - drop(client); - drop(current_match); - drop(clients_guard); - - let clients_guard = self.clients.read().await; - let mut client = clients_guard.get(&addr).unwrap().write().await; - if client.color == winner { - client.score += 1; - } - client.current_match = None; - client.color = Color::None; - drop(client); - - let mut opponent = clients_guard.get(&opponent_addr).unwrap().write().await; - if opponent.color == winner { - opponent.score += 1; - } - opponent.current_match = None; - opponent.color = Color::None; - drop(opponent); - matches_guard.remove(¤t_match_id).unwrap(); - - if !self.demo_mode && matches_guard.is_empty() { - drop(matches_guard); - drop(clients_guard); - - let mut tournament_guard = self.tournament.write().await; - let tourney = tournament_guard.as_mut().unwrap(); - tourney.write().await.next(&self.clients, &self.matches, &self.observers).await; - if tourney.read().await.is_completed() { - *tournament_guard = None; - } - } - - return Ok(()); - } - - let connection_to_send = if !self.demo_mode { - opponent_connection.clone() - } else { - tx.clone() - }; - let column_to_use = if !self.demo_mode { - column - } else { - crate::random_move(¤t_match.board) - }; - if self.demo_mode { - let move_to_dispatch = current_match.move_to_dispatch.clone(); - current_match.ledger.push(move_to_dispatch); - current_match.move_to_dispatch = (Color::Yellow, column_to_use); - current_match.place_token(Color::Yellow, column_to_use); - } - - let waiting = - *self.waiting_timeout.read().await as i64 + (rand::rng().random_range(0..=50) - 25); - let matches_move = self.matches.clone(); - let observers_move = self.observers.clone(); - let match_id_move = current_match.id; - let demo_mode_move = self.demo_mode; - current_match.wait_thread = Some(tokio::spawn(async move { - tokio::time::sleep(tokio::time::Duration::from_millis(waiting as u64)).await; - - let mut matches_guard = matches_move.write().await; - let mut current_match = matches_guard.get_mut(&match_id_move).unwrap().write().await; - let move_to_dispatch = current_match.move_to_dispatch.clone(); - current_match.ledger.push(move_to_dispatch); - current_match.move_to_dispatch = (Color::None, 0); - - if demo_mode_move { - crate::broadcast_message( - ¤t_match.viewers, - &observers_move, - &format!("GAME:MOVE:{}:{}", "demo", column_to_use), - ) - .await; - } - - drop(current_match); - drop(matches_guard); - - let _ = crate::send(&connection_to_send, &format!("OPPONENT:{}", column_to_use)); - })); - - Ok(()) - } - - pub async fn handle_player_list( - &self, - tx: UnboundedSender, - ) -> Result<(), anyhow::Error> { - let clients_guard = self.clients.read().await; - let mut to_send = "PLAYER:LIST:".to_string(); - for client_guard in clients_guard.values() { - let player = client_guard.read().await; - to_send += player.username.as_str(); - to_send += ","; - to_send += if player.ready { "true" } else { "false" }; - to_send += ","; - to_send += if player.current_match.is_some() { - "true" - } else { - "false" - }; - to_send += "|"; - } - - if !to_send.ends_with(":") { - to_send.remove(to_send.len() - 1); - } - - let _ = crate::send(&tx, to_send.as_str()); - Ok(()) - } - - pub async fn handle_game_list( - &self, - tx: UnboundedSender, - ) -> Result<(), anyhow::Error> { - let matches_guard = self.matches.read().await; - let clients_guard = self.clients.read().await; - let mut to_send = "GAME:LIST:".to_string(); - for match_guard in matches_guard.values() { - let a_match = match_guard.read().await; - let player1 = clients_guard.get(&a_match.player1).unwrap().read().await; - let player2 = clients_guard.get(&a_match.player2).unwrap().read().await; - to_send += a_match.id.to_string().as_str(); - to_send += ","; - to_send += player1.username.as_str(); - to_send += ","; - to_send += if player1.username == player2.username { - "demo" - } else { - player2.username.as_str() - }; - to_send += "|"; - } - - if !to_send.ends_with(":") { - to_send.remove(to_send.len() - 1); - } - - let _ = crate::send(&tx, to_send.as_str()); - Ok(()) - } - - pub async fn handle_game_watch( - &self, - tx: UnboundedSender, - match_id: u32, - addr: SocketAddr, - ) -> Result<(), anyhow::Error> { - let result = crate::watch(&self.matches, match_id, addr).await; - if result.is_err() { - return Err(anyhow::anyhow!("ERROR:INVALID:WATCH")); - } - - let clients_guard = self.clients.read().await; - let matches_guard = self.matches.read().await; - let the_match = matches_guard.get(&match_id).unwrap().read().await; - let player1 = clients_guard.get(&the_match.player1).unwrap().read().await.username.clone(); - let mut player2 = - clients_guard.get(&the_match.player2).unwrap().read().await.username.clone(); - if self.demo_mode { - player2 = "demo".to_string(); - } - let ledger = the_match.ledger.clone(); - - drop(clients_guard); - drop(the_match); - drop(matches_guard); - - let mut message = format!("GAME:WATCH:ACK:{},{},{}|", match_id, player1, player2); - - for a_move in ledger { - if a_move.0 == Color::Red { - message += &format!("{},{}|", player1, a_move.1); - } else { - message += &format!("{},{}|", player2, a_move.1); - } - } - - message.pop(); - - let _ = crate::send(&tx, &message); - Ok(()) - } - - pub async fn handle_admin_auth( - &self, - tx: UnboundedSender, - addr: SocketAddr, - password: String, - ) -> Result<(), anyhow::Error> { - if self.admin.read().await.is_some() { - return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); - } - - if password != *self.admin_password { - return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); - } - - let mut admin_guard = self.admin.write().await; - *admin_guard = Some(addr.to_string().parse()?); - let _ = crate::send(&tx, "ADMIN:AUTH:ACK"); - Ok(()) - } - - pub async fn handle_admin_kick( - &self, - tx: UnboundedSender, - addr: SocketAddr, - kick_username: String, - ) -> Result<(), anyhow::Error> { - if !crate::auth_check(&self.admin, addr).await { - return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); - } - - let usernames_guard = self.usernames.read().await; - let clients_guard = self.clients.read().await; - - let kick_addr_result = usernames_guard.get(&kick_username); - match kick_addr_result { - Some(kick_addr) => { - let kick_client = clients_guard.get(kick_addr).unwrap().read().await; - kick_client.connection.send(Message::Close(None))?; - } - None => return Err(anyhow::anyhow!("ERROR:INVALID:KICK")), - } - Ok(()) - } - - pub async fn handle_game_terminate( - &self, - tx: UnboundedSender, - addr: SocketAddr, - match_id: u32, - ) -> Result<(), anyhow::Error> { - if !crate::auth_check(&self.admin, addr).await { - return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); - } - - crate::terminate_match( - match_id, - &self.matches, - &self.clients, - &self.observers, - self.demo_mode, - ) - .await; - - if !self.demo_mode && self.matches.read().await.is_empty() { - let mut tournament_guard = self.tournament.write().await; - let tourney = tournament_guard.as_mut().unwrap(); - tourney.write().await.next(&self.clients, &self.matches, &self.observers).await; - if tourney.read().await.is_completed() { - *tournament_guard = None; - } - } - Ok(()) - } - - pub async fn handle_tournament_start( - &self, - tx: UnboundedSender, - addr: SocketAddr, - ) -> Result<(), anyhow::Error> { - if !crate::auth_check(&self.admin, addr).await { - return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); - } - - if self.tournament.read().await.is_some() || self.demo_mode { - return Err(anyhow::anyhow!("ERROR:INVALID:TOURNAMENT")); - } - - let mut clients_guard = self.clients.write().await; - let mut ready_players = Vec::new(); - for (client_addr, client_guard) in clients_guard.iter_mut() { - if client_guard.read().await.ready { - ready_players.push(*client_addr); - } - } - - if ready_players.len() < 3 { - return Err(anyhow::anyhow!("ERROR:INVALID:TOURNAMENT")); - } - - drop(clients_guard); - - let mut tourney = match self.tournament_type.as_str() { - "round_robin" => crate::tournaments::round_robin::RoundRobin::new(&ready_players), - &_ => crate::tournaments::round_robin::RoundRobin::new(&ready_players), - }; - tourney.start(&self.clients, &self.matches).await; - - let mut tournament_guard = self.tournament.write().await; - *tournament_guard = Some(Arc::new(RwLock::new(tourney))); - - let _ = crate::send(&tx, "TOURNAMENT:START:ACK"); - Ok(()) - } - - pub async fn handle_tournament_cancel( - &self, - tx: UnboundedSender, - addr: SocketAddr, - ) -> Result<(), anyhow::Error> { - if !crate::auth_check(&self.admin, addr).await { - return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); - } - - if self.tournament.read().await.is_none() || self.demo_mode { - return Err(anyhow::anyhow!("ERROR:INVALID:TOURNAMENT")); - } - - let mut tournament_guard = self.tournament.write().await; - let tourney = tournament_guard.as_mut().unwrap(); - tourney.write().await.cancel(&self.clients, &self.matches, &self.observers).await; - *tournament_guard = None; - - let _ = crate::send(&tx, "TOURNAMENT:CANCEL:ACK"); - Ok(()) - } - - pub async fn handle_tournament_wait( - &self, - tx: UnboundedSender, - addr: SocketAddr, - new_timeout: f64, - ) -> Result<(), anyhow::Error> { - if !crate::auth_check(&self.admin, addr).await { - return Err(anyhow::anyhow!("ERROR:INVALID:AUTH")); - } - - *self.waiting_timeout.write().await = (new_timeout * 1000.0) as u64; - Ok(()) - } - - pub async fn handle_get_move_wait( - &self, - tx: UnboundedSender, - ) -> Result<(), anyhow::Error> { - let mut msg = "GET:MOVE_WAIT:".to_string(); - msg += &(*self.waiting_timeout.read().await as f64 / 1000f64).to_string(); - let _ = crate::send(&tx, &msg); - Ok(()) - } - - pub async fn handle_get_tournament_status( - &self, - tx: UnboundedSender, - ) -> Result<(), anyhow::Error> { - let status = self.tournament.read().await.is_some(); - if self.demo_mode { - let _ = crate::send(&tx, "GET:TOURNAMENT_STATUS:DEMO"); - } else { - let mut msg = "GET:TOURNAMENT_STATUS:".to_string(); - msg += status.to_string().as_str(); - let _ = crate::send(&tx, &msg); - } - Ok(()) - } -} - #[derive(PartialEq, Clone)] pub enum Color { - Red, - Yellow, - None, + Red, + Yellow, + None, } #[derive(Clone)] pub struct Client { - pub username: String, - pub connection: UnboundedSender, - pub ready: bool, - pub color: Color, - pub current_match: Option, - pub round_robin_id: u32, - pub score: u32, - pub addr: SocketAddr, + pub username: String, + pub connection: UnboundedSender, + pub ready: bool, + pub color: Color, + pub current_match: Option, + pub round_robin_id: u32, + pub score: u32, + pub addr: SocketAddr, } impl Client { - pub fn new(username: String, connection: UnboundedSender, 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, addr: SocketAddr) -> Client { + Client { + username, + connection, + ready: false, + color: Color::None, + current_match: None, + round_robin_id: 0, + score: 0, + addr, + } + } } pub struct Match { - pub id: u32, - pub board: Vec>, - pub viewers: Vec, - pub ledger: Vec<(Color, usize)>, - pub move_to_dispatch: (Color, usize), - pub wait_thread: Option>, - pub player1: SocketAddr, - pub player2: SocketAddr, + pub id: u32, + pub board: Vec>, + pub viewers: Vec, + pub ledger: Vec<(Color, usize)>, + pub move_to_dispatch: (Color, usize), + pub wait_thread: Option>, + pub player1: SocketAddr, + pub player2: SocketAddr, } 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: 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() + }; - 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, + 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 }, + } + } - 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 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; + } + } + + if any_empty && result.0 == Color::None { + result.1 = true; + } + + result + } } From 46db2f7cc8fdab82d2b02b21b35201980e3d85a0 Mon Sep 17 00:00:00 2001 From: Joshua Higgins Date: Tue, 30 Dec 2025 23:56:33 -0500 Subject: [PATCH 4/4] misc(refactor): put back error messages for malformed commands --- src/main.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main.rs b/src/main.rs index 327aa97..c61adc9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -125,6 +125,8 @@ async fn handle_connection( error!("handle_player_list: {}", e); let _ = send(&tx, e.to_string().as_str()); } + } else { + let _ = send(&tx, "ERROR:INVALID:PLAYER"); } } "GAME" => { @@ -159,6 +161,8 @@ async fn handle_connection( let _ = send(&tx, "ERROR:INVALID:TERMINATE"); } } + } else { + let _ = send(&tx, "ERROR:INVALID:GAME"); } } "ADMIN" => { @@ -174,6 +178,8 @@ async fn handle_connection( error!("handle_admin_kick: {}", e); let _ = send(&tx, e.to_string().as_str()); } + } else { + let _ = send(&tx, "ERROR:INVALID:ADMIN"); } } "TOURNAMENT" => { @@ -201,6 +207,8 @@ async fn handle_connection( let _ = send(&tx, "ERROR:INVALID:TOURNAMENT"); } } + } else { + let _ = send(&tx, "ERROR:INVALID:TOURNAMENT"); } } "GET" => { @@ -214,6 +222,8 @@ async fn handle_connection( error!("handle_get_tournament_status: {}", e); let _ = send(&tx, e.to_string().as_str()); } + } else { + let _ = send(&tx, "ERROR:INVALID:GET"); } } _ => {