diff --git a/Cargo.lock b/Cargo.lock index 72124fd..e48d032 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,17 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bitflags" version = "2.10.0" @@ -40,6 +51,7 @@ name = "connect4-moderator-server" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "futures-util", "rand", "tokio", @@ -476,9 +488,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -487,9 +499,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -498,9 +510,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -519,9 +531,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "nu-ansi-term", "sharded-slab", diff --git a/Cargo.toml b/Cargo.toml index 12f51a0..217059b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,8 @@ edition = "2021" tokio = { version = "1.48", features = ["full"] } tokio-tungstenite = "0.28" futures-util = "0.3.31" -tracing = "0.1.41" -tracing-subscriber = "0.3.20" +tracing = "0.1.43" +tracing-subscriber = "0.3.22" anyhow = "1.0.100" -rand = "0.9.2" \ No newline at end of file +rand = "0.9.2" +async-trait = "0.1.89" \ No newline at end of file diff --git a/gameloop.py b/gameloop.py index 13bf7ac..0a60ab0 100644 --- a/gameloop.py +++ b/gameloop.py @@ -24,12 +24,15 @@ async def gameloop(socket): if (message[1] == "WINS") | (message[1] == "LOSS") | (message[1] == "DRAW") | (message[1] == "TERMINATED"): print(message[0] + ":" + message[1]) player.reset() - await socket.send("READY") case "OPPONENT": # Opponent has gone; calculate next move col = player.calculate_move(message[1]) await socket.send(f"PLAY:{col}") # Send your move to the sever + + case "TOURNAMENT": + if message[1] == "END": + await socket.send("READY") case "ERROR": print(f"{message[0]}: {':'.join(message[1:])}") diff --git a/src/main.rs b/src/main.rs index 6dae673..1bc8fb7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ mod types; -use crate::types::{Color, Match, Tournament}; +use crate::types::{*}; use futures_util::{SinkExt, StreamExt}; use rand::Rng; use std::collections::HashMap; @@ -13,26 +13,12 @@ use tokio::sync::mpsc::UnboundedSender; use tokio::sync::RwLock; use tokio_tungstenite::{accept_async, tungstenite::Message}; use tracing::{error, info, warn}; -use types::Client; -type Clients = Arc>>>>; -type Usernames = Arc>>; -type Observers = Arc>>>; -type Matches = Arc>>>>; - -// TODO: allow random player1 in demo mode - -struct Server { - clients: Clients, - usernames: Usernames, - observers: Observers, - matches: Matches, - admin: Arc>>, - admin_password: Arc, - tournament: Arc>>, - waiting_timeout: Arc>, - demo_mode: bool, -} +// TODO: Allow random "player1" in demo mode +// TODO: Support reconnecting behaviors +// TODO: Other tournament types +// TODO: Move timeouts +// TODO: Send moves instantly, sleep only till waiting time #[tokio::main] async fn main() -> Result<(), anyhow::Error> { @@ -41,6 +27,13 @@ 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); @@ -54,7 +47,7 @@ async fn main() -> Result<(), anyhow::Error> { 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: 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( @@ -67,7 +60,8 @@ async fn main() -> Result<(), anyhow::Error> { admin_password, tournament, waiting_timeout, - demo_mode + demo_mode, + tournament_type, } ); @@ -197,9 +191,9 @@ async fn handle_connection( let opponent_addr = if addr == current_match.player1 { - current_match.player1 - } else { current_match.player2 + } else { + current_match.player1 }; let opponent_connection = @@ -218,11 +212,12 @@ async fn handle_connection( // 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"); - continue; + invalid = true; } let column_parse = text.split(":").collect::>()[1].parse::(); @@ -238,9 +233,8 @@ async fn handle_connection( .await; // Check if valid move - let mut invalid = false; if let Ok(column) = column_parse { - if column >= 7 { + if column >= 7 && !invalid { let _ = send(&tx, "ERROR:INVALID:MOVE"); invalid = true; } @@ -346,8 +340,8 @@ async fn handle_connection( drop(current_match); drop(clients_guard); - let mut clients_guard = sd.clients.write().await; - let mut client = clients_guard.get_mut(&addr).unwrap().write().await; + let clients_guard = sd.clients.read().await; + let mut client = clients_guard.get(&addr).unwrap().write().await; if client.color == winner { client.score += 1; } @@ -355,92 +349,24 @@ async fn handle_connection( client.color = Color::None; drop(client); - let mut opponent = - clients_guard.get_mut(&opponent_addr).unwrap().write().await; - - if !sd.demo_mode { - opponent.current_match = None; - if opponent.color == winner { - opponent.score += 1; - } - opponent.color = Color::None; - } - matches_guard.remove(¤t_match_id).unwrap(); + 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.next(); - if tourney.is_completed { - // Send scores - let mut player_scores: Vec<(String, u32)> = Vec::new(); - for (_, player_addr) in tourney.players.iter() { - let mut player = clients_guard.get_mut(player_addr).unwrap().write().await; - 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(&sd.observers, &message).await; - } else { - // Create next matches - // TODO: Make this a function - for (i, id) in tourney.top_half.iter().enumerate() { - let player1_addr = tourney.players.get(id).unwrap(); - let player2_addr = tourney.players.get(tourney.bottom_half.get(i).unwrap()); - - if player2_addr.is_none() { continue; } - let player2_addr = player2_addr.unwrap(); - - let match_id: u32 = gen_match_id(&sd.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_mut(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::Blue; - let _ = send(&player1.connection, "GAME:START:0"); - } - - drop(player1); - - let mut player2 = clients_guard.get_mut(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::Blue; - let _ = send(&player2.connection, "GAME:START:0"); - } - - drop(player2); - - matches_guard.insert(match_id, new_match.clone()); - } + tourney.write().await.next(&sd.clients, &sd.matches, &sd.observers).await; + if tourney.read().await.is_completed() { + *tournament_guard = None; } } @@ -452,8 +378,8 @@ async fn handle_connection( 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::Blue, column); - current_match.place_token(Color::Blue, column); + 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); @@ -602,6 +528,15 @@ async fn handle_connection( 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" { @@ -627,58 +562,36 @@ async fn handle_connection( continue; } - let tourney = Tournament::new(&ready_players); + 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(tourney.clone()); - - for (i, id) in tourney.top_half.iter().enumerate() { - let player1_addr = tourney.players.get(id).unwrap(); - let player2_addr = tourney.players.get(tourney.bottom_half.get(i).unwrap()).unwrap(); - let match_id: u32 = gen_match_id(&sd.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_mut(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::Blue; - let _ = send(&player1.connection, "GAME:START:0"); - } - - drop(player1); - - let mut player2 = clients_guard.get_mut(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::Blue; - let _ = send(&player2.connection, "GAME:START:0"); - } - - drop(player2); - - sd.matches.write().await.insert(match_id, new_match.clone()); - } + *tournament_guard = Some(Arc::new(RwLock::new(tourney))); let _ = send(&tx, "TOURNAMENT:START:ACK"); } - // TODO: Cancel Tournament + 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"); @@ -688,6 +601,23 @@ async fn handle_connection( *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"); } @@ -709,8 +639,6 @@ async fn handle_connection( // Clean up send_task.abort(); - // TODO: Support reconnecting behaviors - // Remove and terminate any matches // We may not be a client disconnecting, do this check let clients_guard = sd.clients.read().await; diff --git a/src/types.rs b/src/types.rs index 9d7634b..65bc0e9 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,14 +1,37 @@ 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, +} #[derive(PartialEq, Clone)] pub enum Color { Red, - Blue, + Yellow, None, } @@ -39,17 +62,80 @@ impl Client { } } +#[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 Tournament { +pub struct RoundRobin { pub players: HashMap, pub top_half: Vec, pub bottom_half: Vec, pub is_completed: bool, } -impl Tournament { - pub fn new(ready_players: &[SocketAddr]) -> Tournament { - let mut result = Tournament { +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(), @@ -73,7 +159,7 @@ impl Tournament { result } - pub fn next(&mut self) { + async fn next(&mut self, clients: &Clients, matches: &Matches, observers: &Observers) { if self.is_completed { return; } @@ -93,7 +179,65 @@ impl Tournament { 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 {