diff --git a/Cargo.lock b/Cargo.lock index 963aa0c..65f0382 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,7 @@ version = "0.1.0" dependencies = [ "anyhow", "futures-util", + "rand", "serde", "serde_json", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 24315a1..8bc126d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,5 @@ serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" tracing = "0.1.41" tracing-subscriber = "0.3.20" -anyhow = "1.0.100" \ No newline at end of file +anyhow = "1.0.100" +rand = "0.9.2" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 122813f..0567c05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,8 @@ use crate::types::{Color, MatchMaker, Role, AI}; use futures_util::{SinkExt, StreamExt}; use std::collections::HashMap; use std::net::SocketAddr; +use std::num::ParseIntError; +use std::str::FromStr; use std::sync::Arc; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::RwLock; @@ -11,7 +13,7 @@ use tokio_tungstenite::{accept_async, tungstenite::Message}; use tracing::{error, info, warn}; use types::Client; -type Clients = Arc>>; +type Clients = Arc>>>; #[tokio::main] async fn main() -> Result<(), anyhow::Error> { @@ -140,6 +142,7 @@ async fn handle_connection( .as_ref() .unwrap(), Color::None, + addr.to_string() )); let _ = clients @@ -150,15 +153,166 @@ async fn handle_connection( .send("READY:ACK"); } else if text.starts_with("PLAY") { - // TODO! - // Check if client is valid - // Check if it's their move - // Check if valid move + let read = clients.read().await; + let client = read.get(&addr).unwrap(); + + // Check if client is valid + if client.role != Role::Player || client.current_match.is_none() { + let _ = clients + .read() + .await + .get(&addr) + .unwrap() + .send("ERROR:INVALID:MOVE"); + continue; + } + + let current_match = client.current_match.unwrap(); + let ai = if *client.username.as_ref().unwrap() == current_match.player1.username + { ¤t_match.player1 } else { ¤t_match.player2 }; + let opponent = if *client.username.as_ref().unwrap() == current_match.player1.username + { ¤t_match.player2 } else { ¤t_match.player1 }; + + // Check if it's their move + if (current_match.ledger.is_empty() && + current_match.first.username != *client.username.as_ref().unwrap()) || + (client.current_match.unwrap().ledger.last().unwrap().0.username == *client.username.as_ref().unwrap()) + { + let _ = clients + .read() + .await + .get(&addr) + .unwrap() + .send("ERROR:INVALID:MOVE"); + continue; + } + + // Check if valid move + if let Ok(column) = text.split(":").collect::>()[1].parse::() { + if current_match.board[column][4] != Color::None { + let _ = clients + .read() + .await + .get(&addr) + .unwrap() + .send("ERROR:INVALID:MOVE"); + continue; + } + + // Place it + for i in 0..current_match.board[column].len() { + if current_match.board[column][i] == Color::None { + match_maker.write().await.matches.get_mut(¤t_match.id).unwrap().board[column][i] = ai.color.clone(); + break; + } + } + } else { + let _ = clients + .read() + .await + .get(&addr) + .unwrap() + .send("ERROR:INVALID:MOVE"); + continue; + } - // Place it // Check game end conditions - // Broadcast move - } + let (winner, filled) = { + let mut result = (Color::None, false); + + let mut any_empty = true; + for x in 0..6 { + for y in 0..5 { + let color = ¤t_match.board[x][y]; + let mut horizontal_end = true; + let mut vertical_end = true; + let mut diagonal_end = true; + + 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 { + horizontal_end = false; + } + + if y + 3 < 5 && current_match.board[x + i][y] != *color { + vertical_end = false; + } + + if x + 3 < 6 && y + 3 < 5 && current_match.board[x + i][y + i] != *color { + diagonal_end = false; + } + } + + if horizontal_end || vertical_end || diagonal_end { result = (color.clone(), false) } + } + } + + if any_empty && result.0 == Color::None { result.1 = true; } + + result + }; + + if winner != Color::None { + if winner == ai.color { + let _ = clients + .read() + .await + .get(&addr) + .unwrap() + .send("GAME:WINS"); + + let _ = clients + .read() + .await + .get(&SocketAddr::from_str(&opponent.addr)?) + .unwrap() + .send("GAME:LOSS"); + } else { + let _ = clients + .read() + .await + .get(&addr) + .unwrap() + .send("GAME:LOSS"); + + let _ = clients + .read() + .await + .get(&SocketAddr::from_str(&opponent.addr)?) + .unwrap() + .send("GAME:WINS"); + } + } + else if filled { + let _ = clients + .read() + .await + .get(&addr) + .unwrap() + .send("GAME:DRAW"); + + let _ = clients + .read() + .await + .get(&SocketAddr::from_str(&opponent.addr)?) + .unwrap() + .send("GAME:DRAW"); + } + else { + let _ = clients + .read() + .await + .get(&SocketAddr::from_str(&opponent.addr)?) + .unwrap() + .send(&format!("OPPONENT:{}", text.split(":").collect::>()[1].parse::()?)); + } + + // TODO: remove match from matchmaker + // TODO: broadcast moves to viewers + } else { let _ = clients .read() diff --git a/src/types.rs b/src/types.rs index 342822c..7dfb1f3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; use std::net::SocketAddr; +use std::vec; +use rand::Rng; use tokio::sync::mpsc::error::SendError; use tokio::sync::mpsc::UnboundedSender; use tokio_tungstenite::tungstenite::Message; @@ -11,27 +13,29 @@ pub enum Role { Player, } -#[derive(PartialEq)] +#[derive(PartialEq, Clone)] pub enum Color { Red, Blue, None, } -pub struct Client { +pub struct Client<'a> { pub client_id: String, pub role: Role, pub connection: UnboundedSender, pub username: Option, + pub current_match: Option<&'a Match<'static>>, } -impl Client { - pub fn new(client_id: String, role: Role, connection: UnboundedSender) -> Client { +impl Client<'static> { + pub fn new(client_id: String, role: Role, connection: UnboundedSender) -> Client<'static> { Client { client_id, role, connection, username: None, + current_match: None, } } @@ -40,33 +44,39 @@ impl Client { } } +#[derive(Clone)] pub struct AI { pub username: String, pub color: Color, pub ready: bool, + pub addr: String, } impl AI { - pub fn new(username: &str, color: Color) -> AI { - AI { username: username.to_string(), color, ready: false } + pub fn new(username: &str, color: Color, addr: String) -> AI { + AI { username: username.to_string(), color, ready: false, addr } } } -pub struct Match { +pub struct Match<'a> { + pub id: u32, pub board: Vec>, pub viewers: Vec, + pub ledger: Vec<(&'a AI, usize)>, + pub first: AI, pub player1: AI, pub player2: AI, } -impl Match { - pub fn new(player1: AI, player2: AI) -> Match { - Match { board: Vec::new(), viewers: Vec::new(), player1, player2 } +impl Match<'static> { + pub fn new(id: u32, player1: AI, player2: AI) -> Match<'static> { + 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 struct MatchMaker { - pub matches: HashMap, + pub matches: HashMap>, pub ready_players: Vec, }