diff --git a/.gitignore b/.gitignore index de2a1e4..56bee1d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,11 +18,9 @@ target # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ # Added by cargo -/target - -/.idea +/target \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index de0965c..122813f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,123 +1,209 @@ mod types; +use crate::types::{Color, MatchMaker, Role, AI}; +use futures_util::{SinkExt, StreamExt}; +use std::collections::HashMap; 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 futures_util::{SinkExt, StreamExt}; -use std::collections::HashMap; -use tracing::{info, error, warn}; +use tracing::{error, info, warn}; use types::Client; -use crate::types::{Color, Role, AI}; type Clients = Arc>>; #[tokio::main] async fn main() -> Result<(), anyhow::Error> { - // Initialize logging - tracing_subscriber::fmt::init(); + // Initialize logging + tracing_subscriber::fmt::init(); - let addr = "127.0.0.1:8080"; - let listener = TcpListener::bind(&addr).await?; - info!("WebSocket server listening on: {}", addr); + let addr = "0.0.0.0:8080"; + let listener = TcpListener::bind(&addr).await?; + info!("WebSocket server listening on: {}", addr); - let mut clients: Clients = Arc::new(RwLock::new(HashMap::new())); + let mut clients: Clients = Arc::new(RwLock::new(HashMap::new())); + let mut match_maker: Arc> = Arc::new(RwLock::new(MatchMaker::new())); - while let Ok((stream, addr)) = listener.accept().await { - tokio::spawn(handle_connection(stream, addr, clients.clone())); - } + while let Ok((stream, addr)) = listener.accept().await { + tokio::spawn(handle_connection( + stream, + addr, + clients.clone(), + match_maker.clone(), + )); + } - Ok(()) + Ok(()) } async fn handle_connection( - stream: TcpStream, - addr: SocketAddr, - clients: Clients, + stream: TcpStream, + addr: SocketAddr, + clients: Clients, + match_maker: Arc>, ) -> Result<(), anyhow::Error> { - info!("New WebSocket connection from: {}", addr); + info!("New WebSocket connection from: {}", addr); - let ws_stream = accept_async(stream).await?; - let (mut ws_sender, mut ws_receiver) = ws_stream.split(); - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + let ws_stream = accept_async(stream).await?; + let (mut ws_sender, mut ws_receiver) = ws_stream.split(); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - // Store the client - { - clients.write().await.insert(addr, Client::new(String::new(), Role::Observer, tx)); - } + // Store the client + { + clients + .write() + .await + .insert(addr, Client::new(String::new(), Role::Observer, tx)); + } - // Spawn task to handle outgoing messages - let mut send_task = tokio::spawn(async move { - while let Some(msg) = rx.recv().await { - if ws_sender.send(msg).await.is_err() { - break; - } - } - }); + // Spawn task to handle outgoing messages + let mut send_task = tokio::spawn(async move { + while let Some(msg) = rx.recv().await { + if ws_sender.send(msg).await.is_err() { + break; + } + } + }); - // Handle incoming messages - while let Some(msg) = ws_receiver.next().await { - match msg { - Ok(Message::Text(text)) => { - info!("Received text from {}: {}", addr, text); + // Handle incoming messages + while let Some(msg) = ws_receiver.next().await { + 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(); - let mut is_taken = false; - for client in clients.read().await.values() { - if let Some(username) = &client.username { - if *username == requested_username { - is_taken = true; - break; - } + if text.starts_with("CONNECT") { + let requested_username = text.split(":").collect::>()[1].to_string(); + let mut is_taken = false; + for client in clients.read().await.values() { + if let Some(username) = &client.username { + if *username == requested_username { + let _ = clients + .read() + .await + .get(&addr) + .unwrap() + .send(&format!("ERROR:INVALID:ID:{}", requested_username)); + is_taken = true; + break; + } + } + } + + if is_taken { continue; } + + // not taken + clients.write().await.get_mut(&addr).unwrap().role = Role::Player; + clients.write().await.get_mut(&addr).unwrap().username = Some(requested_username); + + let _ = clients + .read() + .await + .get(&addr) + .unwrap() + .send("CONNECT:ACK"); + } + else if text.starts_with("READY") { + if clients.read().await.get(&addr).unwrap().role != Role::Player { + let _ = clients + .read() + .await + .get(&addr) + .unwrap() + .send("ERROR:INVALID"); + continue; + } + + let mut already_ready = false; + for ready_player in &match_maker.read().await.ready_players { + if ready_player.username.eq(clients.read().await.get(&addr).unwrap().username.as_ref().unwrap()) { + let _ = clients + .read() + .await + .get(&addr) + .unwrap() + .send("ERROR:INVALID"); + already_ready = true; + break; } } - if is_taken { - let _ = clients.read().await.get(&addr).unwrap().send("ERROR:INVALID:ID:".to_string() + &*requested_username); - continue; - } + if already_ready { continue; } - // not taken - clients.write().await.get_mut(&addr).unwrap().role = Role::Player; - clients.write().await.get_mut(&addr).unwrap().username = Some(requested_username); - } - else if text.starts_with("READY") { - // TODO! - } + match_maker.write().await.ready_players.push(AI::new( + clients + .read() + .await + .get(&addr) + .unwrap() + .username + .as_ref() + .unwrap(), + Color::None, + )); + + let _ = clients + .read() + .await + .get(&addr) + .unwrap() + .send("READY:ACK"); + } else if text.starts_with("PLAY") { - // TODO! - } + // TODO! + // Check if client is valid + // Check if it's their move + // Check if valid move + + // Place it + // Check game end conditions + // Broadcast move + } else { - let _ = clients.read().await.get(&addr).unwrap().send("ERROR:UNKNOWN".to_string()); - } - } - Ok(Message::Close(_)) => { - info!("Client {} disconnected", addr); - break; - } - Ok(_) => { let _ = clients.read().await.get(&addr).unwrap().send("ERROR:UNKNOWN".to_string()); } - Err(e) => { - error!("WebSocket error for {}: {}", addr, e); - break; - } - } - } + let _ = clients + .read() + .await + .get(&addr) + .unwrap() + .send("ERROR:UNKNOWN"); + } + } + Ok(Message::Close(_)) => { + info!("Client {} disconnected", addr); + break; + } + Ok(_) => { + let _ = clients + .read() + .await + .get(&addr) + .unwrap() + .send("ERROR:UNKNOWN"); + } + Err(e) => { + error!("WebSocket error for {}: {}", addr, e); + break; + } + } + } - // Clean up - send_task.abort(); - clients.write().await.remove(&addr); - info!("Client {} removed", addr); + // Clean up + send_task.abort(); - Ok(()) + // TODO: Remove and terminate any matches + + clients.write().await.remove(&addr); + + info!("Client {} removed", addr); + + Ok(()) } async fn broadcast_message(clients: &Clients, msg: Message) { - let clients = clients.read().await; - for (_, client) in clients.iter() { - if client.role == Role::Admin || client.role == Role::Observer { - client.connection.send(msg.clone()).ok(); - } - } -} \ No newline at end of file + let clients = clients.read().await; + for (_, client) in clients.iter() { + if client.role == Role::Admin || client.role == Role::Observer { + client.connection.send(msg.clone()).ok(); + } + } +} diff --git a/src/types.rs b/src/types.rs index 30ca28b..342822c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; +use std::net::SocketAddr; use tokio::sync::mpsc::error::SendError; use tokio::sync::mpsc::UnboundedSender; use tokio_tungstenite::tungstenite::Message; @@ -33,7 +35,7 @@ impl Client { } } - pub fn send(&self, text: String) -> Result<(), SendError> { + pub fn send(&self, text: &str) -> Result<(), SendError> { self.connection.send(Message::text(text)) } } @@ -51,14 +53,42 @@ impl AI { } pub struct Match { - pub id: u32, pub board: Vec>, + pub viewers: Vec, pub player1: AI, pub player2: AI, } impl Match { - pub fn new(id: u32, player1: AI, player2: AI) -> Match { - Match { id, board: Vec::new(), player1, player2 } + pub fn new(player1: AI, player2: AI) -> Match { + Match { board: Vec::new(), viewers: Vec::new(), player1, player2 } } } + +pub struct MatchMaker { + pub matches: HashMap, + pub ready_players: Vec, +} + +impl MatchMaker { + pub fn new() -> MatchMaker { + MatchMaker { matches: HashMap::new(), ready_players: Vec::new() } + } + + pub fn watch(&mut self, viewer: SocketAddr, match_id: u32) { + for aMatch in &mut self.matches.values_mut() { + let mut found = false; + for i in 0..aMatch.viewers.len() { + if aMatch.viewers[i] == viewer { + aMatch.viewers.remove(i); + found = true; + break; + } + } + + if found { break; } + } + + self.matches.get_mut(&match_id).unwrap().viewers.push(viewer); + } +} \ No newline at end of file