diff --git a/.github/workflows/upload_data.yml b/.github/workflows/upload_data.yml index c0ab435..ca88108 100644 --- a/.github/workflows/upload_data.yml +++ b/.github/workflows/upload_data.yml @@ -20,7 +20,7 @@ jobs: echo "$GOOGLE_SERVICE_ACCOUNT_JSON" > service_account.json python <<'PYCODE' - import gspread, json, subprocess + import gspread, json, subprocess, time creds = json.load(open("service_account.json")) gc = gspread.service_account_from_dict(creds) @@ -38,6 +38,7 @@ jobs: for i, row in enumerate(rows_to_append): worksheet.insert_row(row, start_row + i) + time.sleep(1) with open(f"{v}/test_data.log", "r") as f: diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 15addf3..0ffa59b 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -4,4 +4,6 @@ version = "0.1.0" edition = "2024" [dependencies] -once_cell = "1.19" \ No newline at end of file +once_cell = "1.19" +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/engine/src/boardsquare.rs b/engine/src/boardsquare.rs index b4613fe..2587406 100644 --- a/engine/src/boardsquare.rs +++ b/engine/src/boardsquare.rs @@ -1,32 +1,31 @@ +use serde::{Deserialize, Serialize}; +#[derive(Serialize, Deserialize)] pub struct BoardSquare { - pub x: usize, - pub y: usize + pub x: usize, + pub y: usize, } impl BoardSquare { - - pub fn new() -> Self { - return Self{ - x: 0, - y: 0 - }; - } - - pub fn from_coord(x: usize, y: usize) -> Self { - - #[cfg(debug_assertions)] - { - if x > 7 { - println!("Warning: x coordinate of square is bigger than 7, it might not be on the board!"); - } - if y > 7 { - println!("Warning: y coordinate of square is bigger than 7, it might not be on the board!"); - } + pub fn new() -> Self { + return Self { x: 0, y: 0 }; } - return Self { - x: x, - y: y - }; - } -} \ No newline at end of file + + pub fn from_coord(x: usize, y: usize) -> Self { + #[cfg(debug_assertions)] + { + if x > 7 { + println!( + "Warning: x coordinate of square is bigger than 7, it might not be on the board!" + ); + } + if y > 7 { + println!( + "Warning: y coordinate of square is bigger than 7, it might not be on the board!" + ); + } + } + return Self { x: x, y: y }; + } +} + diff --git a/engine/src/chessmove.rs b/engine/src/chessmove.rs index cf92de1..ae46a6a 100644 --- a/engine/src/chessmove.rs +++ b/engine/src/chessmove.rs @@ -1,71 +1,71 @@ use crate::piecetype; use super::boardsquare::BoardSquare; -use super::piecetype::PieceType; use super::movetype::MoveType; +use super::piecetype::PieceType; +use serde::{Deserialize, Serialize}; - +#[derive(Serialize, Deserialize)] pub struct ChessMove { - pub move_type: MoveType, - pub piece_type: PieceType, - pub from_square: BoardSquare, - pub to_square: BoardSquare, - pub rook_from: BoardSquare, - pub rook_to: BoardSquare, - pub promotion_piece: Option + pub move_type: MoveType, + pub piece_type: PieceType, + pub from_square: BoardSquare, + pub to_square: BoardSquare, + pub rook_from: BoardSquare, + pub rook_to: BoardSquare, + pub promotion_piece: Option, } impl ChessMove { - - pub fn quiet( - piece_type: PieceType, - from_square: BoardSquare, - to_square: BoardSquare, - promotion_piece: Option - ) -> Self { - return Self { - move_type: MoveType::Quiet, - piece_type: piece_type, - from_square: from_square, - to_square: to_square, - rook_from: BoardSquare::new(), - rook_to: BoardSquare::new(), - promotion_piece: promotion_piece + pub fn quiet( + piece_type: PieceType, + from_square: BoardSquare, + to_square: BoardSquare, + promotion_piece: Option, + ) -> Self { + return Self { + move_type: MoveType::Quiet, + piece_type: piece_type, + from_square: from_square, + to_square: to_square, + rook_from: BoardSquare::new(), + rook_to: BoardSquare::new(), + promotion_piece: promotion_piece, + }; } - } - pub fn capture( - piece_type: PieceType, - from_square: BoardSquare, - to_square: BoardSquare, - promotion_piece: Option - ) -> Self { - return Self { - move_type: MoveType::Capture, - piece_type: piece_type, - from_square: from_square, - to_square: to_square, - rook_from: BoardSquare::new(), - rook_to: BoardSquare::new(), - promotion_piece: promotion_piece + pub fn capture( + piece_type: PieceType, + from_square: BoardSquare, + to_square: BoardSquare, + promotion_piece: Option, + ) -> Self { + return Self { + move_type: MoveType::Capture, + piece_type: piece_type, + from_square: from_square, + to_square: to_square, + rook_from: BoardSquare::new(), + rook_to: BoardSquare::new(), + promotion_piece: promotion_piece, + }; } - } - pub fn castle( - piece_type: PieceType, - from_square: BoardSquare, - to_square: BoardSquare, - rook_from: BoardSquare, - rook_to: BoardSquare - ) -> Self { - return Self { - move_type: MoveType::Quiet, - piece_type: piece_type, - from_square: from_square, - to_square: to_square, - rook_from: rook_from, - rook_to: rook_to, - promotion_piece: None + pub fn castle( + piece_type: PieceType, + from_square: BoardSquare, + to_square: BoardSquare, + rook_from: BoardSquare, + rook_to: BoardSquare, + ) -> Self { + return Self { + move_type: MoveType::Quiet, + piece_type: piece_type, + from_square: from_square, + to_square: to_square, + rook_from: rook_from, + rook_to: rook_to, + promotion_piece: None, + }; } - } -} \ No newline at end of file +} diff --git a/engine/src/movetype.rs b/engine/src/movetype.rs index ec045a5..3c531dd 100644 --- a/engine/src/movetype.rs +++ b/engine/src/movetype.rs @@ -1,7 +1,11 @@ +use serde::Deserialize; +use serde::Serialize; +#[derive(Serialize, Deserialize)] pub enum MoveType { - Quiet, - Capture, - Castle, - EnPassant -} \ No newline at end of file + Quiet, + Capture, + Castle, + EnPassant, +} + diff --git a/engine/src/piecetype.rs b/engine/src/piecetype.rs index 3eb23f1..b79d341 100644 --- a/engine/src/piecetype.rs +++ b/engine/src/piecetype.rs @@ -1,15 +1,18 @@ +use serde::{Deserialize, Serialize}; +#[derive(Serialize, Deserialize)] pub enum PieceType { - WhitePawn, - WhiteKnight, - WhiteBishop, - WhiteRook, - WhiteQueen, - WhiteKing, - BlackPawn, - BlackKnight, - BlackBishop, - BlackRook, - BlackQueen, - BlackKing -} \ No newline at end of file + WhitePawn, + WhiteKnight, + WhiteBishop, + WhiteRook, + WhiteQueen, + WhiteKing, + BlackPawn, + BlackKnight, + BlackBishop, + BlackRook, + BlackQueen, + BlackKing, +} + diff --git a/server/Cargo.toml b/server/Cargo.toml index c631dd0..ab5f7bf 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -14,3 +14,9 @@ url = "2.5.7" uuid = {version = "1.18.1", features = ["v4", "serde"] } anyhow = "1.0.100" rand = "0.9.2" +engine = {path = "../engine/"} + + +[[bin]] +name = "client" +path = "src/bin/client.rs" diff --git a/server/src/bin/client.rs b/server/src/bin/client.rs new file mode 100644 index 0000000..1b9edbf --- /dev/null +++ b/server/src/bin/client.rs @@ -0,0 +1,195 @@ +use futures_util::{SinkExt, StreamExt}; +use serde::{Deserialize, Serialize}; +use std::io::{self, Write}; +use tokio_tungstenite::{connect_async, tungstenite::Message}; +use url::Url; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type")] +enum ClientMessage { + Join { username: String }, + FindMatch, + Move { from: String, to: String }, + Resign, + Chat { text: String }, +} + +#[derive(Serialize, Deserialize, Debug)] +struct ServerMessage { + #[serde(rename = "type")] + message_type: String, + player_id: Option, + match_id: Option, + opponent: Option, + color: Option, + reason: Option, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("Knightly Chess Client"); + println!("========================"); + + // Get server address from user + print!("Enter server address [ws://127.0.0.1:9001]: "); + io::stdout().flush()?; + let mut server_addr = String::new(); + io::stdin().read_line(&mut server_addr)?; + let server_addr = server_addr.trim(); + let server_addr = if server_addr.is_empty() { + "ws://127.0.0.1:9001".to_string() + } else { + server_addr.to_string() + }; + + // Connect to server + println!("Connecting to {}...", server_addr); + let url = Url::parse(&server_addr)?; + let (ws_stream, _) = connect_async(url).await?; + println!("Connected to server!"); + + let (mut write, mut read) = ws_stream.split(); + + // Spawn a task to handle incoming messages + let read_handle = tokio::spawn(async move { + while let Some(message) = read.next().await { + match message { + Ok(msg) => { + if msg.is_text() { + let text = msg.to_text().unwrap(); + println!("\nServer: {}", text); + + // Try to parse as structured message + if let Ok(parsed) = serde_json::from_str::(text) { + match parsed.message_type.as_str() { + "welcome" => { + if let Some(player_id) = parsed.player_id { + println!("Welcome! Your player ID: {}", player_id); + } + } + _ => { + println!("cucc: {:?}", parsed); + } + } + } + } + } + Err(e) => { + eprintln!("Error receiving message: {}", e); + break; + } + } + } + }); + + // Main loop for sending messages + println!("\nAvailable commands:"); + println!(" join - Join the server"); + println!(" findmatch - Find a match"); + println!(" move - Make a move (e.g., move e2 e4)"); + println!(" chat - Send chat message"); + println!(" resign - Resign from current game"); + println!(" quit - Exit client"); + println!(); + + loop { + print!("āž”ļø Enter command: "); + io::stdout().flush()?; + + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + let input = input.trim(); + + if input.is_empty() { + continue; + } + + let parts: Vec<&str> = input.split_whitespace().collect(); + let command = parts[0].to_lowercase(); + + match command.as_str() { + "quit" | "exit" => { + println!("šŸ‘‹ Goodbye!"); + break; + } + "join" => { + if parts.len() >= 2 { + let username = parts[1..].join(" "); + let message = ClientMessage::Join { username }; + send_message(&mut write, &message).await?; + } else { + println!("Usage: join "); + } + } + "findmatch" | "find" => { + let message = ClientMessage::FindMatch; + send_message(&mut write, &message).await?; + println!("šŸ” Searching for a match..."); + } + "move" => { + if parts.len() >= 3 { + let from = parts[1].to_string(); + let to = parts[2].to_string(); + let message = ClientMessage::Move { from, to }; + send_message(&mut write, &message).await?; + println!("ā™Ÿļø Sent move: {} -> {}", parts[1], parts[2]); + } else { + println!("Usage: move (e.g., move e2 e4)"); + } + } + "chat" => { + if parts.len() >= 2 { + let text = parts[1..].join(" "); + let message = ClientMessage::Chat { text }; + send_message(&mut write, &message).await?; + } else { + println!("Usage: chat "); + } + } + "resign" => { + let message = ClientMessage::Resign; + send_message(&mut write, &message).await?; + println!("Resigned from current game"); + } + "help" => { + print_help(); + } + _ => { + println!( + "Unknown command: {}. Type 'help' for available commands.", + command + ); + } + } + } + + // Cleanup + read_handle.abort(); + Ok(()) +} + +async fn send_message( + write: &mut futures_util::stream::SplitSink< + tokio_tungstenite::WebSocketStream< + tokio_tungstenite::MaybeTlsStream, + >, + Message, + >, + message: &ClientMessage, +) -> Result<(), Box> { + let json = serde_json::to_string(message)?; + write.send(Message::Text(json)).await?; + Ok(()) +} + +fn print_help() { + println!("\nšŸ“– Available Commands:"); + println!(" join - Register with a username"); + println!(" findmatch - Enter matchmaking queue"); + println!(" move - Make a chess move"); + println!(" chat - Send chat to opponent"); + println!(" resign - Resign from current game"); + println!(" help - Show this help"); + println!(" quit - Exit the client"); + println!(); +} diff --git a/server/src/connection.rs b/server/src/connection.rs index 715ae88..0b9a534 100644 --- a/server/src/connection.rs +++ b/server/src/connection.rs @@ -1,3 +1,5 @@ +use crate::connection::ClientEvent::*; +use engine::get_available_moves; use futures_util::{SinkExt, StreamExt}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, VecDeque}; @@ -26,6 +28,28 @@ pub fn new_waiting_queue() -> WaitingQueue { Arc::new(Mutex::new(VecDeque::new())) } +#[derive(Serialize, Deserialize, Debug)] +pub struct Step { + pub from: String, + pub to: String, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type")] +enum ClientEvent { + Join { username: String }, + FindMatch, + Move { from: String, to: String }, + Resign, + Chat { text: String }, + RequestLegalMoves { fen: String }, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct EventResponse { + pub response: Result<(), String>, +} + #[derive(Debug)] pub struct PlayerConnection { pub id: Uuid, @@ -43,12 +67,6 @@ pub struct GameMatch { pub move_history: Vec, } -#[derive(Serialize, Deserialize, Debug)] -pub struct Step { - pub from: String, - pub to: String, -} - // Message sending utilities pub async fn send_message_to_player( connections: &ConnectionMap, @@ -102,7 +120,6 @@ pub async fn handle_connection( connections: ConnectionMap, matches: MatchMap, waiting_queue: WaitingQueue, - event_system: crate::events::EventSystem, ) -> anyhow::Result<()> { use tokio_tungstenite::accept_async; @@ -141,8 +158,52 @@ pub async fn handle_connection( let text = message.to_text()?; println!("Received from {}: {}", player_id, text); - // TODO: Parse and handle message with event system - // This will be implemented when we integrate the event system + let client_data: ClientEvent = serde_json::from_str(text) + .expect("Failed to convert data into json at handle_connection"); + + println!("client: {:?}", client_data); + + match client_data { + Join { username } => { + { + let mut conn_map = connections.lock().await; + let player = conn_map.get_mut(&player_id).unwrap(); + player.username = Some(username); + } + + //respone to client + let response: EventResponse = EventResponse { + response: core::result::Result::Ok(()), + }; + + println!("response: {:?}", response); + + let _ = send_message_to_player( + &connections, + player_id, + &serde_json::to_string(&response).unwrap(), + ) + .await; + } + FindMatch => { + let mut wait_queue = waiting_queue.lock().await; + wait_queue.push_back(player_id.clone()); + println!("Appended {} to the waiting queue", player_id); + println!("queue {:?}", wait_queue); + } + Move { from, to } => {} + RequestLegalMoves { fen } => { + let moves = get_available_moves(&fen); + let _ = send_message_to_player( + &connections, + player_id, + &serde_json::to_string(&moves).unwrap(), + ) + .await; + println!("Sent moves to player: {}", player_id); + } + _ => {} + } } } diff --git a/server/src/events.rs b/server/src/events.rs deleted file mode 100644 index 8cc43a4..0000000 --- a/server/src/events.rs +++ /dev/null @@ -1,126 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use tokio::sync::Mutex; -use tokio::sync::mpsc; -use uuid::Uuid; - -#[derive(Serialize, Deserialize, Debug)] -pub struct Step { - pub from: String, - pub to: String, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(tag = "type")] -pub enum ClientEvent { - Join { username: String }, - FindMatch, - Move { from: String, to: String }, - Resign, - Chat { text: String }, - RequestLegalMoves { fen: String }, -} - -#[derive(Debug)] -pub enum ServerEvent { - PlayerJoined(Uuid, String), - PlayerLeft(Uuid), - PlayerJoinedQueue(Uuid), - PlayerJoinedMatch(Uuid, Uuid), // player_id, match_id - PlayerMove(Uuid, Step), - PlayerResigned(Uuid), - MatchCreated(Uuid, Uuid, Uuid), // match_id, white_id, black_id -} - -pub struct EventSystem { - sender: mpsc::UnboundedSender<(Uuid, ClientEvent)>, - receiver: Arc>>, -} - -impl Clone for EventSystem { - fn clone(&self) -> Self { - Self { - sender: self.sender.clone(), - receiver: Arc::clone(&self.receiver), - } - } -} - -impl EventSystem { - pub fn new() -> Self { - let (sender, receiver) = mpsc::unbounded_channel(); - Self { - sender, - receiver: Arc::new(Mutex::new(receiver)), - } - } - - pub async fn send_event( - &self, - player_id: Uuid, - event: ClientEvent, - ) -> Result<(), Box> { - self.sender.send((player_id, event))?; - Ok(()) - } - - pub async fn next_event(&self) -> Option<(Uuid, ClientEvent)> { - let mut receiver = self.receiver.lock().await; - receiver.recv().await - } -} - -impl Default for EventSystem { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use uuid::Uuid; - - #[tokio::test] - async fn test_event_system_send_and_receive() { - let event_system = EventSystem::new(); - let player_id = Uuid::new_v4(); - - let join_event = ClientEvent::Join { - username: "test_user".to_string(), - }; - - let send_result = event_system.send_event(player_id, join_event).await; - assert!(send_result.is_ok(), "Should send event successfully"); - - let received = event_system.next_event().await; - assert!(received.is_some(), "Should receive sent event"); - - let (received_id, received_event) = received.unwrap(); - assert_eq!(received_id, player_id, "Should receive correct player ID"); - - match received_event { - ClientEvent::Join { username } => { - assert_eq!(username, "test_user", "Should receive correct username"); - } - _ => panic!("Should receive Join event"), - } - } - - #[tokio::test] - async fn test_event_system_clone() { - let event_system1 = EventSystem::new(); - let event_system2 = event_system1.clone(); - - let player_id = Uuid::new_v4(); - let event = ClientEvent::FindMatch; - - event_system1.send_event(player_id, event).await.unwrap(); - - let received = event_system2.next_event().await; - assert!( - received.is_some(), - "Cloned event system should receive events" - ); - } -} diff --git a/server/src/main.rs b/server/src/main.rs index 0d35baf..ad6ee97 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,6 +1,6 @@ mod connection; -mod events; mod matchmaking; +mod messages; use tokio::net::TcpListener; #[tokio::main] @@ -14,15 +14,11 @@ async fn main() -> anyhow::Result<()> { let matches = connection::new_match_map(); let waiting_queue = connection::new_waiting_queue(); - // Event system for communication between components - let event_system = events::EventSystem::new(); - // Start matchmaking background task let matchmaker = matchmaking::MatchmakingSystem::new( connections.clone(), matches.clone(), waiting_queue.clone(), - event_system.clone(), ); tokio::spawn(async move { matchmaker.run().await; @@ -33,17 +29,10 @@ async fn main() -> anyhow::Result<()> { let connections = connections.clone(); let matches = matches.clone(); let waiting_queue = waiting_queue.clone(); - let event_system = event_system.clone(); tokio::spawn(async move { - if let Err(e) = connection::handle_connection( - stream, - connections, - matches, - waiting_queue, - event_system, - ) - .await + if let Err(e) = + connection::handle_connection(stream, connections, matches, waiting_queue).await { eprintln!("Connection error: {}", e); } diff --git a/server/src/matchmaking.rs b/server/src/matchmaking.rs index 4f4cc27..de7eca0 100644 --- a/server/src/matchmaking.rs +++ b/server/src/matchmaking.rs @@ -1,5 +1,4 @@ use crate::connection::{ConnectionMap, GameMatch, MatchMap, WaitingQueue}; -use crate::events::EventSystem; use rand::random; use uuid::Uuid; @@ -7,21 +6,14 @@ pub struct MatchmakingSystem { connections: ConnectionMap, matches: MatchMap, waiting_queue: WaitingQueue, - event_system: EventSystem, } impl MatchmakingSystem { - pub fn new( - connections: ConnectionMap, - matches: MatchMap, - waiting_queue: WaitingQueue, - event_system: EventSystem, - ) -> Self { + pub fn new(connections: ConnectionMap, matches: MatchMap, waiting_queue: WaitingQueue) -> Self { Self { connections, matches, waiting_queue, - event_system, } } @@ -114,7 +106,6 @@ impl MatchmakingSystem { #[cfg(test)] mod tests { use super::*; - use crate::events::EventSystem; use uuid::Uuid; use crate::connection::new_connection_map; @@ -126,14 +117,9 @@ mod tests { let connections = new_connection_map(); let matches = new_match_map(); let waiting_queue = new_waiting_queue(); - let event_system = EventSystem::new(); - let matchmaking = MatchmakingSystem::new( - connections.clone(), - matches.clone(), - waiting_queue.clone(), - event_system.clone(), - ); + let matchmaking = + MatchmakingSystem::new(connections.clone(), matches.clone(), waiting_queue.clone()); let player1 = Uuid::new_v4(); let player2 = Uuid::new_v4(); @@ -172,14 +158,9 @@ mod tests { let connections = new_connection_map(); let matches = new_match_map(); let waiting_queue = new_waiting_queue(); - let event_system = EventSystem::new(); - let matchmaking = MatchmakingSystem::new( - connections.clone(), - matches.clone(), - waiting_queue.clone(), - event_system.clone(), - ); + let matchmaking = + MatchmakingSystem::new(connections.clone(), matches.clone(), waiting_queue.clone()); let player1 = Uuid::new_v4(); {