diff --git a/src/main.rs b/src/main.rs index c39099c..b485e61 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,12 @@ mod types; +mod random_ai; use crate::types::{Color, Match}; use futures_util::{SinkExt, StreamExt}; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; +use rand::Rng; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::mpsc::error::SendError; use tokio::sync::mpsc::UnboundedSender; @@ -12,10 +14,11 @@ use tokio::sync::RwLock; use tokio_tungstenite::{accept_async, tungstenite::Message}; use tracing::{error, info, warn}; use types::Client; +use crate::random_ai::random_move; -type Clients = Arc>>>; +type Clients = Arc>>>>; type Observers = Arc>>>; -type Matches = Arc>>>; +type Matches = Arc>>>>; #[tokio::main] @@ -23,6 +26,9 @@ async fn main() -> Result<(), anyhow::Error> { // Initialize logging tracing_subscriber::fmt::init(); + let args: Vec = std::env::args().collect(); + let demo_mode = args.get(1).is_some() && args.get(1).unwrap() == "demo"; + let addr = "0.0.0.0:8080"; let listener = TcpListener::bind(&addr).await?; info!("WebSocket server listening on: {}", addr); @@ -38,6 +44,7 @@ async fn main() -> Result<(), anyhow::Error> { clients.clone(), observers.clone(), matches.clone(), + demo_mode )); } @@ -50,6 +57,7 @@ async fn handle_connection( clients: Clients, observers: Observers, matches: Matches, + demo_mode: bool, ) -> Result<(), anyhow::Error> { info!("New WebSocket connection from: {}", addr); @@ -58,12 +66,7 @@ async fn handle_connection( let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); // Store the client - { - observers - .write() - .await - .insert(addr, tx.clone()); - } + observers.write().await.insert(addr, tx.clone()); // Spawn task to handle outgoing messages let mut send_task = tokio::spawn(async move { @@ -80,11 +83,17 @@ async fn handle_connection( Ok(Message::Text(text)) => { info!("Received text from {}: {}", addr, text); - if text.starts_with("CONNECT") { + 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; for client in clients.read().await.values() { - if requested_username == client.username { + if requested_username == client.read().await.username { let _ = send(&tx, &format!("ERROR:INVALID:ID:{}", requested_username)); is_taken = true; break; @@ -95,45 +104,75 @@ async fn handle_connection( // not taken observers.write().await.remove(&addr); - clients.write().await.insert(addr.to_string().parse()?, Client::new(requested_username, tx.clone())); + 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.starts_with("READY") { + else if text == "READY" { if clients.read().await.get(&addr).is_none() { let _ = send(&tx, "ERROR:INVALID"); continue; } - if clients.read().await.get(&addr).unwrap().ready { + if clients.read().await.get(&addr).unwrap().read().await.ready { let _ = send(&tx, "ERROR:INVALID"); continue; } - clients.write().await.get_mut(&addr).unwrap().ready = true; + clients.write().await.get_mut(&addr).unwrap().write().await.ready = true; let _ = send(&tx,"READY:ACK"); + + if demo_mode { + let match_id: u32 = rand::rng().random_range(100000..=999999); + let new_match = Arc::new(RwLock::new(Match::new(match_id, addr.to_string().parse()?, addr.to_string().parse()?))); + matches.write().await.insert(match_id, new_match.clone()); + clients.write().await.get_mut(&addr).unwrap().write().await.ready = false; + clients + .write().await + .get_mut(&addr).unwrap() + .write().await.current_match = Some(match_id); + clients + .write().await + .get_mut(&addr).unwrap() + .write().await.color = Color::Red; + let _ = send(&tx, "GAME:START:1"); + } } - else if text.starts_with("PLAY") { - let read = clients.read().await; - let client = read.get(&addr); + else if text.starts_with("PLAY:") { + let clients_guard = clients.read().await; + let client_option = clients_guard.get(&addr); // Check if client is valid - if client.is_none() || client.unwrap().current_match.is_none() { + if client_option.is_none() || client_option.unwrap().read().await.current_match.is_none() { let _ = send(&tx, "ERROR:INVALID:MOVE"); continue; } - let current_match = client.unwrap().current_match.unwrap(); - let current_player = if client.unwrap().username == current_match.player1.username - { current_match.player1 } else { current_match.player2 }; - let opponent = if client.unwrap().username == current_match.player1.username - { current_match.player2 } else { current_match.player1 }; + let client = client_option.unwrap().read().await; + + let matches_guard = matches.read().await; + let current_match = matches_guard.get(&client.current_match.unwrap()).unwrap().read().await; + + let opponent = { + let result = + if addr == current_match.player1 + { clients_guard.get(¤t_match.player2).unwrap().read().await } + else + { clients_guard.get(¤t_match.player1).unwrap().read().await }; + + result + }; // Check if it's their move if (current_match.ledger.is_empty() && - current_match.first.username != client.unwrap().username) || - (client.unwrap().current_match.unwrap().ledger.last().unwrap().0.username == client.unwrap().username) + current_match.first != addr) || + (current_match.ledger.last().is_some() && current_match.ledger.last().unwrap().0 == client.color) { let _ = send(&tx, "ERROR:INVALID:MOVE"); continue; @@ -141,6 +180,12 @@ async fn handle_connection( let column_parse = text.split(":").collect::>()[1].parse::(); + drop(current_match); + drop(matches_guard); + + let mut matches_guard = 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 current_match.board[column][4] != Color::None { @@ -149,21 +194,19 @@ async fn handle_connection( } // Place it - for i in 0..current_match.board[column].len() { - if current_match.board[column][i] == Color::None { - matches - .write() - .await - .get_mut(¤t_match.id).unwrap() - .board[column][i] = current_player.color.clone(); - break; - } - } + current_match.place_token(client.color.clone(), column) } else { let _ = send(&tx, "ERROR:INVALID:MOVE"); continue; } + // broadcast the move to viewers + broadcast_message( + ¤t_match.viewers, + &observers, + &format!("GAME:MOVE:{}:{}", client.username, column_parse.clone()?)) + .await; + // Check game end conditions let (winner, filled) = { let mut result = (Color::None, false); @@ -171,31 +214,32 @@ async fn handle_connection( let mut any_empty = true; for x in 0..6 { for y in 0..5 { - let color = ¤t_match.board[x][y]; + let color = current_match.board[x][y].clone(); let mut horizontal_end = true; let mut vertical_end = true; let mut diagonal_end = true; - if any_empty && *color == Color::None { + if any_empty && color == Color::None { any_empty = false; } for i in 0..4 { - if x + 3 < 6 && current_match.board[x + i][y] != *color { + if x + i < 6 && current_match.board[x + i][y] != color && horizontal_end { horizontal_end = false; } - if y + 3 < 5 && current_match.board[x + i][y] != *color { + if y + i < 5 && current_match.board[x][y + i] != color && vertical_end{ vertical_end = false; } - if x + 3 < 6 && y + 3 < 5 && current_match.board[x + i][y + i] != *color { + if x + i < 6 && y + i < 5 && current_match.board[x + i][y + i] != color && diagonal_end { diagonal_end = false; } } - if horizontal_end || vertical_end || diagonal_end { result = (color.clone(), false) } + if horizontal_end || vertical_end || diagonal_end { result = (color.clone(), false); break; } } + if result.0 != Color::None { break; } } if any_empty && result.0 == Color::None { result.1 = true; } @@ -204,29 +248,89 @@ async fn handle_connection( }; if winner != Color::None { - if winner == current_player.color { + if winner == client.color { let _ = send(&tx, "GAME:WINS"); - let _ = send(&opponent.connection, "GAME:LOSS"); - + if !demo_mode { + let _ = send(&opponent.connection, "GAME:LOSS"); + } + broadcast_message(¤t_match.viewers, &observers, &format!("GAME:WIN:{}", client.username)).await; } else { let _ = send(&tx, "GAME:LOSS"); - let _ = send(&opponent.connection, "GAME:WINS"); - + if !demo_mode { + let _ = send(&opponent.connection, "GAME:WINS"); + } + broadcast_message(¤t_match.viewers, &observers, &format!("GAME:WIN:{}", opponent.username)).await; } } else if filled { let _ = send(&tx, "GAME:DRAW"); - let _ = send(&opponent.connection, "GAME:DRAW"); - } - else { - let _ = send(&opponent.connection, &format!("OPPONENT:{}", column_parse?)); + if !demo_mode { + let _ = send(&opponent.connection, "GAME:DRAW"); + } + broadcast_message(¤t_match.viewers, &observers, "GAME:DRAW").await; + } + + + // remove match from matchmaker + if winner != Color::None || filled { + let opponent_addr = opponent.addr; + let current_match_id = current_match.id; + + drop(client); + drop(opponent); + drop(current_match); + drop(clients_guard); + + let mut clients_guard = clients.write().await; + let mut client = clients_guard.get_mut(&addr).unwrap().write().await; + client.current_match = None; + drop(client); + + let mut opponent = clients_guard.get_mut(&opponent_addr).unwrap().write().await; + + if !demo_mode { + opponent.current_match = None; + } + matches_guard.remove(¤t_match_id).unwrap(); + continue; + } + + if !demo_mode{ + // TODO: delay/autoplay/continue behavior + let _ = send(&opponent.connection, &format!("OPPONENT:{}", column_parse.clone()?)); + } else { + let random_move = random_move(¤t_match.board); + current_match.place_token(Color::Blue, random_move); + let _ = send(&tx, &format!("OPPONENT:{}", random_move)); } - // TODO: remove match from matchmaker - // TODO: broadcast moves to viewers } + + else if text == "GAME:LIST" { + todo!() + } + else if text.starts_with("GAME:WATCH:") { + todo!() + } + + else if text.starts_with("ADMIN:AUTH:") { + todo!() + } + else if text.starts_with("ADMIN:KICK:") { + todo!() + } + else if text == "GAME:TERMINATE" { + todo!() + } + else if text.starts_with("GAME:AUTOPLAY:") { + todo!() + } + else if text == "GAME:CONTINUE" { + todo!() + } + else { - let _ = send(&tx, "GAME:UNKNOWN"); + let _ = send(&tx, "ERROR:UNKNOWN"); } } Ok(Message::Close(_)) => { @@ -234,7 +338,7 @@ async fn handle_connection( break; } Ok(_) => { - let _ = send(&tx, "GAME:UNKNOWN"); + let _ = send(&tx, "ERROR:UNKNOWN"); } Err(e) => { error!("WebSocket error for {}: {}", addr, e); @@ -255,19 +359,18 @@ async fn handle_connection( Ok(()) } -async fn broadcast_message(observers: &Observers, msg: &str) { - let observers = observers.read().await; - for (_, tx) in observers.iter() { - let _ = send(tx, msg); +async fn broadcast_message(addrs: &Vec, observers: &Observers, msg: &str) { + for addr in addrs { + let _ = send(observers.read().await.get(addr).unwrap(), msg); } } async fn watch(matches: &Matches, new_match_id: u32, addr: SocketAddr) { for a_match in &mut matches.write().await.values_mut() { let mut found = false; - for i in 0..a_match.viewers.len() { - if a_match.viewers[i] == addr { - a_match.viewers.remove(i); + for i in 0..a_match.write().await.viewers.len() { + if a_match.write().await.viewers[i] == addr { + a_match.write().await.viewers.remove(i); found = true; break; } @@ -276,7 +379,11 @@ async fn watch(matches: &Matches, new_match_id: u32, addr: SocketAddr) { if found { break; } } - matches.write().await.get_mut(&new_match_id).unwrap().viewers.push(addr); + matches + .write().await + .get_mut(&new_match_id).unwrap() + .write().await + .viewers.push(addr); } fn send(tx: &UnboundedSender, text: &str) -> Result<(), SendError> { diff --git a/src/random_ai.rs b/src/random_ai.rs new file mode 100644 index 0000000..c7a98f1 --- /dev/null +++ b/src/random_ai.rs @@ -0,0 +1,11 @@ +use rand::Rng; +use crate::types::{Client, Color}; + +pub fn random_move(board: &[Vec]) -> usize { + let mut random = rand::rng().random_range(0..6); + while board[random][4] != Color::None { + random = rand::rng().random_range(0..6); + } + + random +} \ No newline at end of file diff --git a/src/types.rs b/src/types.rs index 77a2d1a..f27c87f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::net::SocketAddr; use std::vec; use rand::Rng; @@ -13,22 +12,27 @@ pub enum Color { None, } -pub struct Client<'a> { +#[derive(Clone)] +pub struct Client { pub username: String, pub connection: UnboundedSender, pub ready: bool, pub color: Color, - pub current_match: Option<&'a Match<'static>>, + pub current_match: Option, + pub demo: bool, + pub addr: SocketAddr, } -impl Client<'static> { - pub fn new(username: String, connection: UnboundedSender) -> Client<'static> { +impl Client { + pub fn new(username: String, connection: UnboundedSender, addr: SocketAddr) -> Client { Client { username, connection, ready: false, color: Color::None, current_match: None, + demo: false, + addr, } } @@ -37,19 +41,28 @@ impl Client<'static> { } } -pub struct Match<'a> { +pub struct Match { pub id: u32, pub board: Vec>, pub viewers: Vec, - pub ledger: Vec<(&'a Client<'a>, usize)>, - pub first: &'a Client<'a>, - pub player1: &'a Client<'a>, - pub player2: &'a Client<'a>, + pub ledger: Vec<(Color, usize)>, + pub first: SocketAddr, + pub player1: SocketAddr, + pub player2: SocketAddr, } -impl<'a> Match<'a> { - pub fn new(id: u32, player1: &'a Client, player2: &'a Client) -> Match<'a> { - let first = if rand::rng().random_range(0..=1) == 0 { player1 } else { player2 }; +impl Match { + pub fn new(id: u32, player1: SocketAddr, player2: SocketAddr) -> Match { + let first = if rand::rng().random_range(0..=1) == 0 { player1.clone() } else { player2.clone() }; Match { id, board: vec![vec![Color::None; 5]; 6], viewers: Vec::new(), ledger: Vec::new(), first, player1, player2 } } + + pub fn place_token(&mut self, color: Color, column: usize) { + for i in 0..5 { + if self.board[column][i] == Color::None { + self.board[column][i] = color; + break; + } + } + } } \ No newline at end of file