From 7820555fa8f1e058fac5b1f3d822b728b847e5b2 Mon Sep 17 00:00:00 2001 From: htom Date: Wed, 26 Nov 2025 11:18:58 +0100 Subject: [PATCH 01/10] added new packages to ui and removed unused stuff from server --- server/src/main.rs | 5 +- server/src/matchmaking.rs | 7 +- ui/Cargo.toml | 13 +++- ui/src/main.rs | 154 ++++++++++++++++++++++++-------------- 4 files changed, 112 insertions(+), 67 deletions(-) diff --git a/server/src/main.rs b/server/src/main.rs index e3876a5..1ff68ca 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,8 +1,7 @@ mod connection; mod matchmaking; -use env_logger::{Env, Logger}; -use log::{error, info, warn}; -use std::env; +use env_logger::Env; +use log::{error, info}; use tokio::net::TcpListener; #[tokio::main] diff --git a/server/src/matchmaking.rs b/server/src/matchmaking.rs index b1514c9..640dca5 100644 --- a/server/src/matchmaking.rs +++ b/server/src/matchmaking.rs @@ -26,12 +26,8 @@ impl MatchmakingSystem { } } - pub async fn clean_up(&self, match_id: Uuid) { - self.matches.lock().await.remove(&match_id); - } - async fn try_create_match(&self) { - info!("Checking for new matches!"); + //info!("Checking for new matches!"); let mut queue = self.waiting_queue.lock().await; while queue.len() >= 2 { @@ -72,6 +68,7 @@ impl MatchmakingSystem { } if let Some(player) = conn_map.get_mut(&black_player) { player.current_match = Some(match_id); + //TODO: at the end of a match delete this from player } else { error!("Could not store match id for black player"); } diff --git a/ui/Cargo.toml b/ui/Cargo.toml index 310b0ce..5cf216b 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -6,5 +6,16 @@ edition = "2024" [dependencies] eframe = "0.33.0" egui = "0.33.0" -tokio-tungstenite = "0.28.0" winit = "0.30.12" +tokio = { version = "1", features = ["full"] } +tokio-tungstenite = "0.21" +tungstenite = "0.21" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +futures-util = "0.3.31" +url = "2.5.7" +uuid = {version = "1.18.1", features = ["v4", "serde"] } +engine = {path = "../engine/"} +log = {version = "0.4.28"} +env_logger = "0.11.8" +local-ip-address = "0.6.5" diff --git a/ui/src/main.rs b/ui/src/main.rs index cc8c7c4..cc61876 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -1,7 +1,17 @@ use eframe::egui; +use env_logger::Env; +use log::{error, info}; fn main() -> eframe::Result<()> { - let options = eframe::NativeOptions{ + //set up for logging + let env = Env::default().filter_or("MY_LOG_LEVEL", "INFO"); + env_logger::init_from_env(env); + info!("Initialized logger"); + + //create a TCPlistener with tokio and bind machine ip for connection + //for this we need to query the ip + + let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_fullscreen(true) .with_min_inner_size(egui::vec2(800.0, 600.0)) // Minimum width, height @@ -9,23 +19,23 @@ fn main() -> eframe::Result<()> { ..Default::default() }; eframe::run_native( - "Knightly", - options, - Box::new(|cc| { - let mut fonts = egui::FontDefinitions::default(); - fonts.font_data.insert( - "symbols".to_owned(), - egui::FontData::from_static(include_bytes!("../fonts/DejaVuSans.ttf")).into(), - ); - fonts - .families - .entry(egui::FontFamily::Proportional) - .or_default() - .insert(0, "symbols".to_owned()); - cc.egui_ctx.set_fonts(fonts); - Ok(Box::new(ChessApp::default())) - }), -) + "Knightly", + options, + Box::new(|cc| { + let mut fonts = egui::FontDefinitions::default(); + fonts.font_data.insert( + "symbols".to_owned(), + egui::FontData::from_static(include_bytes!("../fonts/DejaVuSans.ttf")).into(), + ); + fonts + .families + .entry(egui::FontFamily::Proportional) + .or_default() + .insert(0, "symbols".to_owned()); + cc.egui_ctx.set_fonts(fonts); + Ok(Box::new(ChessApp::default())) + }), + ) } #[derive(Clone, Copy, PartialEq, Debug)] @@ -169,13 +179,14 @@ impl ChessApp { self.fullscreen = self.pending_settings.fullscreen; self.selected_resolution = self.pending_settings.selected_resolution; self.server_port = self.pending_settings.server_port.clone(); - + if let Some(resolution) = self.resolutions.get(self.selected_resolution) { - ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize( - egui::Vec2::new(resolution.0 as f32, resolution.1 as f32) - )); + ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(egui::Vec2::new( + resolution.0 as f32, + resolution.1 as f32, + ))); } - + ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(self.fullscreen)); } @@ -196,16 +207,22 @@ impl eframe::App for ChessApp { ui.heading("♞ Knightly ♞"); ui.add_space(30.0); - if ui.add_sized([300.0, 60.0], egui::Button::new("Play")).clicked() { + if ui + .add_sized([300.0, 60.0], egui::Button::new("Play")) + .clicked() + { self.state = AppState::InGame; } ui.add_space(8.0); - - if ui.add_sized([300.0, 60.0], egui::Button::new("Settings")).clicked() { + + if ui + .add_sized([300.0, 60.0], egui::Button::new("Settings")) + .clicked() + { self.enter_settings(); } ui.add_space(8.0); - + if ui .add_sized([300.0, 60.0], egui::Button::new("Quit")) .clicked() @@ -225,7 +242,10 @@ impl eframe::App for ChessApp { // Fullscreen toggle ui.horizontal(|ui| { ui.label("Fullscreen:"); - if ui.checkbox(&mut self.pending_settings.fullscreen, "").changed() { + if ui + .checkbox(&mut self.pending_settings.fullscreen, "") + .changed() + { // If enabling fullscreen, we might want to disable resolution selection } }); @@ -241,7 +261,8 @@ impl eframe::App for ChessApp { self.resolutions[self.pending_settings.selected_resolution].1 )) .show_ui(ui, |ui| { - for (i, &(width, height)) in self.resolutions.iter().enumerate() { + for (i, &(width, height)) in self.resolutions.iter().enumerate() + { ui.selectable_value( &mut self.pending_settings.selected_resolution, i, @@ -255,20 +276,28 @@ impl eframe::App for ChessApp { // Server port input field ui.horizontal(|ui| { ui.label("Local Server Port:"); - ui.add(egui::TextEdit::singleline(&mut self.pending_settings.server_port) - .desired_width(100.0) - .hint_text("8080")); + ui.add( + egui::TextEdit::singleline(&mut self.pending_settings.server_port) + .desired_width(100.0) + .hint_text("8080"), + ); }); ui.add_space(30.0); // Apply and Cancel buttons ui.horizontal(|ui| { - if ui.add_sized([140.0, 40.0], egui::Button::new("Apply")).clicked() { + if ui + .add_sized([140.0, 40.0], egui::Button::new("Apply")) + .clicked() + { self.apply_settings(ctx); self.state = AppState::MainMenu; } - - if ui.add_sized([140.0, 40.0], egui::Button::new("Cancel")).clicked() { + + if ui + .add_sized([140.0, 40.0], egui::Button::new("Cancel")) + .clicked() + { self.state = AppState::MainMenu; } }); @@ -293,24 +322,24 @@ impl eframe::App for ChessApp { ui.label(format!("Turn: {:?}", self.turn)); }); }); - + egui::CentralPanel::default().show(ctx, |ui| { ui.vertical_centered(|ui| { let full_avail = ui.available_rect_before_wrap(); let board_tile = (full_avail.width().min(full_avail.height())) / 8.0; let board_size = board_tile * 8.0; - + // Create a child UI at the board position let (response, painter) = ui.allocate_painter( egui::Vec2::new(board_size, board_size), - egui::Sense::click() + egui::Sense::click(), ); - + let board_rect = egui::Rect::from_center_size( full_avail.center(), - egui::vec2(board_size, board_size) + egui::vec2(board_size, board_size), ); - + // Draw the chess board let tile_size = board_size / 8.0; for row in 0..8 { @@ -320,17 +349,17 @@ impl eframe::App for ChessApp { } else { egui::Color32::from_rgb(217, 217, 217) }; - + let rect = egui::Rect::from_min_size( egui::Pos2::new( board_rect.min.x + col as f32 * tile_size, - board_rect.min.y + row as f32 * tile_size + board_rect.min.y + row as f32 * tile_size, ), - egui::Vec2::new(tile_size, tile_size) + egui::Vec2::new(tile_size, tile_size), ); - + painter.rect_filled(rect, 0.0, color); - + // Draw piece let piece = self.board[row][col]; if piece != Piece::Empty { @@ -341,27 +370,36 @@ impl eframe::App for ChessApp { egui::Align2::CENTER_CENTER, symbol, font_id, - if matches!(piece, Piece::King('w') | Piece::Queen('w') | Piece::Rook('w') | Piece::Bishop('w') | Piece::Knight('w') | Piece::Pawn('w')) { + if matches!( + piece, + Piece::King('w') + | Piece::Queen('w') + | Piece::Rook('w') + | Piece::Bishop('w') + | Piece::Knight('w') + | Piece::Pawn('w') + ) { egui::Color32::WHITE } else { egui::Color32::BLACK - } + }, ); } - + // Draw selection highlight if self.selected == Some((row, col)) { painter.rect_stroke( - rect, - 0.0, + rect, + 0.0, egui::Stroke::new(3.0, egui::Color32::RED), - egui::StrokeKind::Inside + egui::StrokeKind::Inside, ); } - + // Handle clicks if ui.ctx().input(|i| i.pointer.primary_clicked()) { - let click_pos = ui.ctx().input(|i| i.pointer.interact_pos()).unwrap(); + let click_pos = + ui.ctx().input(|i| i.pointer.interact_pos()).unwrap(); if rect.contains(click_pos) { self.handle_click(row, col); } @@ -387,14 +425,14 @@ mod tests { assert!(matches!(app.board[1][0], Piece::Pawn('b'))); assert!(matches!(app.board[6][0], Piece::Pawn('w'))); } - + #[test] fn test_piece_symbols() { assert_eq!(Piece::King('w').symbol(), "♔"); assert_eq!(Piece::King('b').symbol(), "♚"); assert_eq!(Piece::Empty.symbol(), ""); } - + #[test] fn test_piece_selection() { let mut app = ChessApp::default(); @@ -403,7 +441,7 @@ mod tests { app.handle_click(6, 0); assert_eq!(app.selected, None); } - + #[test] fn test_piece_movement() { let mut app = ChessApp::default(); @@ -422,7 +460,7 @@ mod tests { app.handle_click(5, 0); // White moves assert_eq!(app.turn, Turn::Black); // Should now be Black's turn } - + #[test] fn test_server_port_default() { let app = ChessApp::default(); From 60911f93f6d9f26dc89dd1ca7d6217ae409192f9 Mon Sep 17 00:00:00 2001 From: htom Date: Wed, 26 Nov 2025 13:55:16 +0100 Subject: [PATCH 02/10] removed unused stuff from test client --- server/src/bin/client.rs | 20 +++----------------- server/src/connection.rs | 1 - 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/server/src/bin/client.rs b/server/src/bin/client.rs index d74f60b..815f92c 100644 --- a/server/src/bin/client.rs +++ b/server/src/bin/client.rs @@ -7,12 +7,6 @@ use tokio_tungstenite::{connect_async, tungstenite::Message}; use url::Url; use uuid::Uuid; -#[derive(Serialize, Deserialize, Debug)] -struct Step { - from: String, - to: String, -} - #[derive(Serialize, Deserialize)] #[serde(tag = "type")] enum ClientMessage { @@ -24,17 +18,6 @@ enum ClientMessage { RequestLegalMoves { fen: 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, -} - #[derive(Serialize, Deserialize)] pub enum ServerMessage2 { GameEnd { @@ -48,6 +31,9 @@ pub enum ServerMessage2 { color: String, opponent_name: String, }, + Ok { + response: Result<(), String>, + }, } #[tokio::main] diff --git a/server/src/connection.rs b/server/src/connection.rs index 828f0cf..6b2a5c1 100644 --- a/server/src/connection.rs +++ b/server/src/connection.rs @@ -6,7 +6,6 @@ use engine::{get_available_moves, is_game_over}; use futures_util::{SinkExt, StreamExt}; use log::{error, info, warn}; use serde::{Deserialize, Serialize}; -use std::char::from_u32_unchecked; use std::collections::{HashMap, VecDeque}; use std::sync::Arc; use tokio::net::TcpStream; From 5deebb0621fbf57f10f09b243d199b3b4822562a Mon Sep 17 00:00:00 2001 From: htom Date: Wed, 26 Nov 2025 13:55:39 +0100 Subject: [PATCH 03/10] connecting with websocket to local hosted server instance --- ui/Cargo.toml | 1 + ui/src/connection.rs | 96 ++++++++++++++++++++++++++++++++++++++++++++ ui/src/main.rs | 28 ++++++++----- 3 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 ui/src/connection.rs diff --git a/ui/Cargo.toml b/ui/Cargo.toml index 5cf216b..97f9116 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -19,3 +19,4 @@ engine = {path = "../engine/"} log = {version = "0.4.28"} env_logger = "0.11.8" local-ip-address = "0.6.5" +anyhow = "1.0.100" diff --git a/ui/src/connection.rs b/ui/src/connection.rs new file mode 100644 index 0000000..1e20221 --- /dev/null +++ b/ui/src/connection.rs @@ -0,0 +1,96 @@ +use engine::{chessmove::ChessMove, gameend::GameEnd}; +use futures_util::StreamExt; +use local_ip_address::local_ip; +use log::{error, info, warn}; +use serde::{Deserialize, Serialize}; +use std::{ + error::Error, + net::{IpAddr, Ipv4Addr}, +}; +use tokio_tungstenite::connect_async; +use url::Url; +use uuid::Uuid; + +#[derive(Serialize, Deserialize)] +pub enum ServerMessage2 { + GameEnd { + winner: GameEnd, + }, + UIUpdate { + fen: String, + }, + MatchFound { + match_id: Uuid, + color: String, + opponent_name: String, + }, + Ok { + response: Result<(), String>, + }, +} + +#[derive(Serialize, Deserialize)] +#[serde(tag = "type")] +enum ClientMessage { + Join { username: String }, + FindMatch, + Move { step: ChessMove, fen: String }, + Resign, + Chat { text: String }, + RequestLegalMoves { fen: String }, +} + +fn get_ip_address() -> IpAddr { + let ip = local_ip().unwrap_or(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))); + + ip +} + +pub async fn handle_connection(server_port: &str) -> anyhow::Result<()> { + let address = get_ip_address(); + + //start main loop + let server_address = String::from("ws://") + &address.to_string() + ":" + server_port; + warn!( + "Machine IpAddress is bound for listener. Ip: {}", + server_address + ); + + let url = Url::parse(&server_address)?; + + let (ws_stream, _) = connect_async(url).await?; + let (mut write, mut read) = ws_stream.split(); + + let read_handle = while let Some(message) = read.next().await { + info!("connection"); + match message { + Ok(msg) => { + if msg.is_text() { + let text = msg.to_text().unwrap(); + info!("text: {}", text); + + if let Ok(parsed) = serde_json::from_str::(text) { + match parsed { + ServerMessage2::GameEnd { winner } => {} + ServerMessage2::UIUpdate { fen } => {} + ServerMessage2::MatchFound { + match_id, + color, + opponent_name, + } => {} + ServerMessage2::Ok { response } => {} + _ => { + error!("Received unkown servermessage2"); + } + } + } + } + } + Err(e) => { + error!("Error receiving message: {}", e); + } + } + }; + + Ok(()) +} diff --git a/ui/src/main.rs b/ui/src/main.rs index cc61876..fa89e55 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -1,19 +1,20 @@ use eframe::egui; use env_logger::Env; -use log::{error, info}; +use log::{error, info, warn}; -fn main() -> eframe::Result<()> { +use crate::connection::handle_connection; +mod connection; + +#[tokio::main] +async fn main() -> anyhow::Result<(), eframe::Error> { //set up for logging let env = Env::default().filter_or("MY_LOG_LEVEL", "INFO"); env_logger::init_from_env(env); - info!("Initialized logger"); - - //create a TCPlistener with tokio and bind machine ip for connection - //for this we need to query the ip + warn!("Initialized logger"); let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() - .with_fullscreen(true) + .with_fullscreen(false) .with_min_inner_size(egui::vec2(800.0, 600.0)) // Minimum width, height .with_inner_size(egui::vec2(7680.0, 4320.0)), // Initial size ..Default::default() @@ -119,7 +120,7 @@ impl Default for ChessApp { selected: None, turn: Turn::White, pending_settings: PendingSettings::default(), - server_port: "8080".to_string(), // Default port + server_port: "9001".to_string(), // Default port } } } @@ -211,6 +212,15 @@ impl eframe::App for ChessApp { .add_sized([300.0, 60.0], egui::Button::new("Play")) .clicked() { + let port = self.server_port.clone(); + info!("\nstarting connection\n"); + + //create a TCPlistener with tokio and bind machine ip for connection + tokio::spawn(async move { + info!("tokoi"); + handle_connection(&port).await + }); + self.state = AppState::InGame; } ui.add_space(8.0); @@ -279,7 +289,7 @@ impl eframe::App for ChessApp { ui.add( egui::TextEdit::singleline(&mut self.pending_settings.server_port) .desired_width(100.0) - .hint_text("8080"), + .hint_text("9001"), ); }); ui.add_space(30.0); From 933bb46f17af54245652580c35880279817a1c07 Mon Sep 17 00:00:00 2001 From: Bence <76205860+kbence04@users.noreply.github.com> Date: Wed, 26 Nov 2025 15:22:23 +0100 Subject: [PATCH 04/10] Board squares flipping --- ui/src/main.rs | 90 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 8 deletions(-) diff --git a/ui/src/main.rs b/ui/src/main.rs index fa89e55..06cc5fb 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -53,12 +53,12 @@ enum Piece { impl Piece { fn symbol(&self) -> &'static str { match self { - Piece::King('w') => "♔", - Piece::Queen('w') => "♕", - Piece::Rook('w') => "♖", - Piece::Bishop('w') => "♗", - Piece::Knight('w') => "♘", - Piece::Pawn('w') => "♙", + Piece::King('w') => "♚", + Piece::Queen('w') => "♛", + Piece::Rook('w') => "♜", + Piece::Bishop('w') => "♝", + Piece::Knight('w') => "♞", + Piece::Pawn('w') => "♟︎", Piece::King('b') => "♚", Piece::Queen('b') => "♛", Piece::Rook('b') => "♜", @@ -351,13 +351,15 @@ impl eframe::App for ChessApp { ); // Draw the chess board + let player = "black"; + if player =="white"{ let tile_size = board_size / 8.0; for row in 0..8 { for col in 0..8 { let color = if (row + col) % 2 == 0 { - egui::Color32::from_rgb(100, 97, 97) - } else { egui::Color32::from_rgb(217, 217, 217) + } else { + egui::Color32::from_rgb(100, 97, 97) }; let rect = egui::Rect::from_min_size( @@ -416,6 +418,78 @@ impl eframe::App for ChessApp { } } } + } + if player=="black"{ + { + let tile_size = board_size / 8.0; + for row in 0..8 { + for col in 0..8 { + let color = if (row + col) % 2 == 0 { + egui::Color32::from_rgb(217, 217, 217) + } else { + egui::Color32::from_rgb(100, 97, 97) + }; + + let rect = egui::Rect::from_min_size( + egui::Pos2::new( + board_rect.min.x + col as f32 * tile_size, + board_rect.min.y + row as f32 * tile_size, + ), + egui::Vec2::new(tile_size, tile_size), + ); + + painter.rect_filled(rect, 0.0, color); + + // Draw piece + let piece = self.board[row][col]; + if piece != Piece::Empty { + let symbol = piece.symbol(); + let font_id = egui::FontId::proportional(tile_size * 0.75); + painter.text( + rect.center(), + egui::Align2::CENTER_CENTER, + symbol, + font_id, + if matches!( + piece, + Piece::King('w') + | Piece::Queen('w') + | Piece::Rook('w') + | Piece::Bishop('w') + | Piece::Knight('w') + | Piece::Pawn('w') + ) { + egui::Color32::BLACK + } else { + egui::Color32::WHITE + }, + ); + } + + // Draw selection highlight + if self.selected == Some((row, col)) { + painter.rect_stroke( + rect, + 0.0, + egui::Stroke::new(3.0, egui::Color32::RED), + egui::StrokeKind::Inside, + ); + } + + // Handle clicks + if ui.ctx().input(|i| i.pointer.primary_clicked()) { + let click_pos = + ui.ctx().input(|i| i.pointer.interact_pos()).unwrap(); + if rect.contains(click_pos) { + self.handle_click(row, col); + } + } + } + } + } + } + + }); }); } From 2112109470a2359cc05570bb8dc89ae18d83c15d Mon Sep 17 00:00:00 2001 From: htom Date: Wed, 26 Nov 2025 17:53:12 +0100 Subject: [PATCH 05/10] added optional parameters that the ui will receive from server --- ui/src/main.rs | 260 +++++++++++++++++++++++++------------------------ 1 file changed, 134 insertions(+), 126 deletions(-) diff --git a/ui/src/main.rs b/ui/src/main.rs index 06cc5fb..865b630 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -1,6 +1,7 @@ use eframe::egui; use env_logger::Env; use log::{error, info, warn}; +use uuid::Uuid; use crate::connection::handle_connection; mod connection; @@ -83,6 +84,7 @@ enum AppState { Settings, } +#[derive(Clone)] struct ChessApp { fullscreen: bool, resolutions: Vec<(u32, u32)>, @@ -93,6 +95,9 @@ struct ChessApp { turn: Turn, pending_settings: PendingSettings, server_port: String, + player_color: Option, + match_id: Option, + opponent_name: Option, } #[derive(Default)] @@ -121,6 +126,9 @@ impl Default for ChessApp { turn: Turn::White, pending_settings: PendingSettings::default(), server_port: "9001".to_string(), // Default port + player_color: None, + match_id: None, + opponent_name: None, } } } @@ -218,7 +226,7 @@ impl eframe::App for ChessApp { //create a TCPlistener with tokio and bind machine ip for connection tokio::spawn(async move { info!("tokoi"); - handle_connection(&port).await + handle_connection(&port, self).await }); self.state = AppState::InGame; @@ -351,145 +359,145 @@ impl eframe::App for ChessApp { ); // Draw the chess board - let player = "black"; - if player =="white"{ - let tile_size = board_size / 8.0; - for row in 0..8 { - for col in 0..8 { - let color = if (row + col) % 2 == 0 { - egui::Color32::from_rgb(217, 217, 217) - } else { - egui::Color32::from_rgb(100, 97, 97) - }; + if self.player_color == Some("white".to_string()) { + let tile_size = board_size / 8.0; + for row in 0..8 { + for col in 0..8 { + let color = if (row + col) % 2 == 0 { + egui::Color32::from_rgb(217, 217, 217) + } else { + egui::Color32::from_rgb(100, 97, 97) + }; - let rect = egui::Rect::from_min_size( - egui::Pos2::new( - board_rect.min.x + col as f32 * tile_size, - board_rect.min.y + row as f32 * tile_size, - ), - egui::Vec2::new(tile_size, tile_size), - ); - - painter.rect_filled(rect, 0.0, color); - - // Draw piece - let piece = self.board[row][col]; - if piece != Piece::Empty { - let symbol = piece.symbol(); - let font_id = egui::FontId::proportional(tile_size * 0.75); - painter.text( - rect.center(), - egui::Align2::CENTER_CENTER, - symbol, - font_id, - if matches!( - piece, - Piece::King('w') - | Piece::Queen('w') - | Piece::Rook('w') - | Piece::Bishop('w') - | Piece::Knight('w') - | Piece::Pawn('w') - ) { - egui::Color32::WHITE - } else { - egui::Color32::BLACK - }, + let rect = egui::Rect::from_min_size( + egui::Pos2::new( + board_rect.min.x + col as f32 * tile_size, + board_rect.min.y + row as f32 * tile_size, + ), + egui::Vec2::new(tile_size, tile_size), ); - } - // Draw selection highlight - if self.selected == Some((row, col)) { - painter.rect_stroke( - rect, - 0.0, - egui::Stroke::new(3.0, egui::Color32::RED), - egui::StrokeKind::Inside, - ); - } + painter.rect_filled(rect, 0.0, color); - // Handle clicks - if ui.ctx().input(|i| i.pointer.primary_clicked()) { - let click_pos = - ui.ctx().input(|i| i.pointer.interact_pos()).unwrap(); - if rect.contains(click_pos) { - self.handle_click(row, col); + // Draw piece + let piece = self.board[row][col]; + if piece != Piece::Empty { + let symbol = piece.symbol(); + let font_id = egui::FontId::proportional(tile_size * 0.75); + painter.text( + rect.center(), + egui::Align2::CENTER_CENTER, + symbol, + font_id, + if matches!( + piece, + Piece::King('w') + | Piece::Queen('w') + | Piece::Rook('w') + | Piece::Bishop('w') + | Piece::Knight('w') + | Piece::Pawn('w') + ) { + egui::Color32::WHITE + } else { + egui::Color32::BLACK + }, + ); + } + + // Draw selection highlight + if self.selected == Some((row, col)) { + painter.rect_stroke( + rect, + 0.0, + egui::Stroke::new(3.0, egui::Color32::RED), + egui::StrokeKind::Inside, + ); + } + + // Handle clicks + if ui.ctx().input(|i| i.pointer.primary_clicked()) { + let click_pos = + ui.ctx().input(|i| i.pointer.interact_pos()).unwrap(); + if rect.contains(click_pos) { + self.handle_click(row, col); + } } } } } - } - if player=="black"{ - { - let tile_size = board_size / 8.0; - for row in 0..8 { - for col in 0..8 { - let color = if (row + col) % 2 == 0 { - egui::Color32::from_rgb(217, 217, 217) - } else { - egui::Color32::from_rgb(100, 97, 97) - }; - - let rect = egui::Rect::from_min_size( - egui::Pos2::new( - board_rect.min.x + col as f32 * tile_size, - board_rect.min.y + row as f32 * tile_size, - ), - egui::Vec2::new(tile_size, tile_size), - ); - - painter.rect_filled(rect, 0.0, color); - - // Draw piece - let piece = self.board[row][col]; - if piece != Piece::Empty { - let symbol = piece.symbol(); - let font_id = egui::FontId::proportional(tile_size * 0.75); - painter.text( - rect.center(), - egui::Align2::CENTER_CENTER, - symbol, - font_id, - if matches!( - piece, - Piece::King('w') - | Piece::Queen('w') - | Piece::Rook('w') - | Piece::Bishop('w') - | Piece::Knight('w') - | Piece::Pawn('w') - ) { - egui::Color32::BLACK + if self.player_color == Some("black".to_string()) { + { + let tile_size = board_size / 8.0; + for row in 0..8 { + for col in 0..8 { + let color = if (row + col) % 2 == 0 { + egui::Color32::from_rgb(217, 217, 217) } else { - egui::Color32::WHITE - }, - ); - } + egui::Color32::from_rgb(100, 97, 97) + }; - // Draw selection highlight - if self.selected == Some((row, col)) { - painter.rect_stroke( - rect, - 0.0, - egui::Stroke::new(3.0, egui::Color32::RED), - egui::StrokeKind::Inside, - ); - } + let rect = egui::Rect::from_min_size( + egui::Pos2::new( + board_rect.min.x + col as f32 * tile_size, + board_rect.min.y + row as f32 * tile_size, + ), + egui::Vec2::new(tile_size, tile_size), + ); - // Handle clicks - if ui.ctx().input(|i| i.pointer.primary_clicked()) { - let click_pos = - ui.ctx().input(|i| i.pointer.interact_pos()).unwrap(); - if rect.contains(click_pos) { - self.handle_click(row, col); + painter.rect_filled(rect, 0.0, color); + + // Draw piece + let piece = self.board[row][col]; + if piece != Piece::Empty { + let symbol = piece.symbol(); + let font_id = + egui::FontId::proportional(tile_size * 0.75); + painter.text( + rect.center(), + egui::Align2::CENTER_CENTER, + symbol, + font_id, + if matches!( + piece, + Piece::King('w') + | Piece::Queen('w') + | Piece::Rook('w') + | Piece::Bishop('w') + | Piece::Knight('w') + | Piece::Pawn('w') + ) { + egui::Color32::BLACK + } else { + egui::Color32::WHITE + }, + ); + } + + // Draw selection highlight + if self.selected == Some((row, col)) { + painter.rect_stroke( + rect, + 0.0, + egui::Stroke::new(3.0, egui::Color32::RED), + egui::StrokeKind::Inside, + ); + } + + // Handle clicks + if ui.ctx().input(|i| i.pointer.primary_clicked()) { + let click_pos = ui + .ctx() + .input(|i| i.pointer.interact_pos()) + .unwrap(); + if rect.contains(click_pos) { + self.handle_click(row, col); + } + } } } } } - } - } - - }); }); } @@ -548,6 +556,6 @@ mod tests { #[test] fn test_server_port_default() { let app = ChessApp::default(); - assert_eq!(app.server_port, "8080"); + assert_eq!(app.server_port, "9001"); } } From 5763716848b022040bf46a8b016c239973c09957 Mon Sep 17 00:00:00 2001 From: htom Date: Thu, 27 Nov 2025 16:31:48 +0100 Subject: [PATCH 06/10] fixed error: sending both match notification to the same player and removed old server message struct --- server/src/connection.rs | 12 ------------ server/src/matchmaking.rs | 4 ++-- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/server/src/connection.rs b/server/src/connection.rs index 6b2a5c1..246f032 100644 --- a/server/src/connection.rs +++ b/server/src/connection.rs @@ -45,18 +45,6 @@ pub struct Step { pub to: 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, - response: Option, -}*/ - #[derive(Serialize, Deserialize)] pub enum ServerMessage2 { GameEnd { diff --git a/server/src/matchmaking.rs b/server/src/matchmaking.rs index 640dca5..c43448a 100644 --- a/server/src/matchmaking.rs +++ b/server/src/matchmaking.rs @@ -1,5 +1,5 @@ use crate::connection::ServerMessage2; -use crate::connection::{ConnectionMap, GameMatch, MatchMap, WaitingQueue, broadcast_to_match}; +use crate::connection::{ConnectionMap, GameMatch, MatchMap, WaitingQueue}; use log::{error, info, warn}; use rand::random; use uuid::Uuid; @@ -119,7 +119,7 @@ impl MatchmakingSystem { }; let _ = crate::connection::send_message_to_player_connection( - conn_map.get_mut(&white), + conn_map.get_mut(&black), &serde_json::to_string(&message).unwrap(), ) .await; From df02c2eee1cd3b9fb97895810d77159204520136 Mon Sep 17 00:00:00 2001 From: htom Date: Thu, 27 Nov 2025 16:33:08 +0100 Subject: [PATCH 07/10] updated ui code with partial rewriting, now if there are two players they will join a match and the board will be drawn --- ui/src/connection.rs | 35 +- ui/src/main.rs | 818 +++++++++++++++++++++---------------------- 2 files changed, 429 insertions(+), 424 deletions(-) diff --git a/ui/src/connection.rs b/ui/src/connection.rs index 1e20221..006c88c 100644 --- a/ui/src/connection.rs +++ b/ui/src/connection.rs @@ -1,16 +1,19 @@ use engine::{chessmove::ChessMove, gameend::GameEnd}; -use futures_util::StreamExt; +use futures_util::{SinkExt, StreamExt}; use local_ip_address::local_ip; use log::{error, info, warn}; use serde::{Deserialize, Serialize}; use std::{ - error::Error, net::{IpAddr, Ipv4Addr}, + sync::{Arc, Mutex}, }; use tokio_tungstenite::connect_async; +use tungstenite::Message; use url::Url; use uuid::Uuid; +use crate::{ChessApp, ClientEvent, SharedGameState}; + #[derive(Serialize, Deserialize)] pub enum ServerMessage2 { GameEnd { @@ -46,7 +49,11 @@ fn get_ip_address() -> IpAddr { ip } -pub async fn handle_connection(server_port: &str) -> anyhow::Result<()> { +pub async fn handle_connection( + server_port: &str, + shared_state: SharedGameState, + ui_events: Arc>>, +) -> anyhow::Result<()> { let address = get_ip_address(); //start main loop @@ -61,7 +68,7 @@ pub async fn handle_connection(server_port: &str) -> anyhow::Result<()> { let (ws_stream, _) = connect_async(url).await?; let (mut write, mut read) = ws_stream.split(); - let read_handle = while let Some(message) = read.next().await { + while let Some(message) = read.next().await { info!("connection"); match message { Ok(msg) => { @@ -69,7 +76,7 @@ pub async fn handle_connection(server_port: &str) -> anyhow::Result<()> { let text = msg.to_text().unwrap(); info!("text: {}", text); - if let Ok(parsed) = serde_json::from_str::(text) { + /*if let Ok(parsed) = serde_json::from_str::(text) { match parsed { ServerMessage2::GameEnd { winner } => {} ServerMessage2::UIUpdate { fen } => {} @@ -77,12 +84,26 @@ pub async fn handle_connection(server_port: &str) -> anyhow::Result<()> { match_id, color, opponent_name, - } => {} + } => { + //chess_app.player_color = Some(color); + } ServerMessage2::Ok { response } => {} _ => { error!("Received unkown servermessage2"); } } + }*/ + + if let Ok(parsed) = serde_json::from_str::(text) { + // Update shared state with server message + shared_state.update_from_server_message(parsed); + } + + // Send UI events to server + let events = ui_events.lock().unwrap().drain(..).collect::>(); + for event in events { + let message = serde_json::to_string(&event)?; + write.send(Message::Text(message)).await?; } } } @@ -90,7 +111,7 @@ pub async fn handle_connection(server_port: &str) -> anyhow::Result<()> { error!("Error receiving message: {}", e); } } - }; + } Ok(()) } diff --git a/ui/src/main.rs b/ui/src/main.rs index 865b630..c516ea0 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -1,14 +1,18 @@ use eframe::egui; +use engine::{boardsquare::BoardSquare, chessmove::ChessMove}; use env_logger::Env; +use futures_util::{SinkExt, StreamExt}; use log::{error, info, warn}; +use serde::{Deserialize, Serialize}; +use std::sync::{Arc, Mutex}; +use tokio::sync::mpsc; +use tokio_tungstenite::{connect_async, tungstenite::Message}; +use url::Url; use uuid::Uuid; -use crate::connection::handle_connection; -mod connection; - #[tokio::main] async fn main() -> anyhow::Result<(), eframe::Error> { - //set up for logging + // Set up logging let env = Env::default().filter_or("MY_LOG_LEVEL", "INFO"); env_logger::init_from_env(env); warn!("Initialized logger"); @@ -16,10 +20,11 @@ async fn main() -> anyhow::Result<(), eframe::Error> { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_fullscreen(false) - .with_min_inner_size(egui::vec2(800.0, 600.0)) // Minimum width, height - .with_inner_size(egui::vec2(7680.0, 4320.0)), // Initial size + .with_min_inner_size(egui::vec2(800.0, 600.0)) + .with_inner_size(egui::vec2(1920.0, 1080.0)), ..Default::default() }; + eframe::run_native( "Knightly", options, @@ -40,175 +45,312 @@ async fn main() -> anyhow::Result<(), eframe::Error> { ) } -#[derive(Clone, Copy, PartialEq, Debug)] -enum Piece { - King(char), - Queen(char), - Rook(char), - Bishop(char), - Knight(char), - Pawn(char), - Empty, +// Server message types (from your connection.rs) +#[derive(Serialize, Deserialize, Debug)] +pub enum ServerMessage2 { + GameEnd { + winner: String, + }, + UIUpdate { + fen: String, + }, + MatchFound { + match_id: Uuid, + color: String, + opponent_name: String, + }, + Ok { + response: Result<(), String>, + }, } -impl Piece { - fn symbol(&self) -> &'static str { - match self { - Piece::King('w') => "♚", - Piece::Queen('w') => "♛", - Piece::Rook('w') => "♜", - Piece::Bishop('w') => "♝", - Piece::Knight('w') => "♞", - Piece::Pawn('w') => "♟︎", - Piece::King('b') => "♚", - Piece::Queen('b') => "♛", - Piece::Rook('b') => "♜", - Piece::Bishop('b') => "♝", - Piece::Knight('b') => "♞", - Piece::Pawn('b') => "♟︎", - Piece::Empty => "", - _ => "", +// Client event types (from your connection.rs) +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type")] +pub enum ClientEvent { + Join { username: String }, + FindMatch, + Move { step: ChessMove }, + Resign, + Chat { text: String }, + RequestLegalMoves { fen: String }, +} + +// Game state +#[derive(Debug, Clone)] +struct GameState { + fen: String, + player_color: Option, + opponent_name: Option, + match_id: Option, + game_over: Option, +} + +impl Default for GameState { + fn default() -> Self { + Self { + fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1".to_string(), + player_color: None, + opponent_name: None, + match_id: None, + game_over: None, } } } -#[derive(PartialEq, Debug)] -enum Turn { - White, - Black, -} - +// UI state enum AppState { MainMenu, + Connecting, + FindingMatch, InGame, - Settings, + GameOver, } -#[derive(Clone)] struct ChessApp { - fullscreen: bool, - resolutions: Vec<(u32, u32)>, - selected_resolution: usize, state: AppState, - board: [[Piece; 8]; 8], - selected: Option<(usize, usize)>, - turn: Turn, - pending_settings: PendingSettings, + game_state: Arc>, server_port: String, - player_color: Option, - match_id: Option, - opponent_name: Option, -} + username: String, -#[derive(Default)] -struct PendingSettings { - fullscreen: bool, - selected_resolution: usize, - server_port: String, + // Channels for communication with network tasks + tx_to_network: Option>, + rx_from_network: Option>, + + // UI state + selected_square: Option<(usize, usize)>, } impl Default for ChessApp { fn default() -> Self { Self { - fullscreen: true, - resolutions: vec![ - (1280, 720), - (1600, 900), - (1920, 1080), - (2560, 1440), - (3840, 2160), - (7680, 4320), - ], - selected_resolution: 2, // Default to 1920x1080 state: AppState::MainMenu, - board: Self::starting_board(), - selected: None, - turn: Turn::White, - pending_settings: PendingSettings::default(), - server_port: "9001".to_string(), // Default port - player_color: None, - match_id: None, - opponent_name: None, + game_state: Arc::new(Mutex::new(GameState::default())), + server_port: "9001".to_string(), + username: "Player".to_string(), + tx_to_network: None, + rx_from_network: None, + selected_square: None, } } } impl ChessApp { - fn starting_board() -> [[Piece; 8]; 8] { - use Piece::*; - [ - [ - Rook('b'), - Knight('b'), - Bishop('b'), - Queen('b'), - King('b'), - Bishop('b'), - Knight('b'), - Rook('b'), - ], - [Pawn('b'); 8], - [Empty; 8], - [Empty; 8], - [Empty; 8], - [Empty; 8], - [Pawn('w'); 8], - [ - Rook('w'), - Knight('w'), - Bishop('w'), - Queen('w'), - King('w'), - Bishop('w'), - Knight('w'), - Rook('w'), - ], - ] + fn connect_to_server(&mut self) { + let server_port = self.server_port.clone(); + let username = self.username.clone(); + let game_state = self.game_state.clone(); + + // Create channels for communication + let (tx_to_network, rx_from_ui) = mpsc::unbounded_channel(); + let (tx_to_ui, rx_from_network) = mpsc::unbounded_channel(); + + self.tx_to_network = Some(tx_to_network); + self.rx_from_network = Some(rx_from_network); + + self.state = AppState::Connecting; + + // Spawn network connection task + tokio::spawn(async move { + if let Err(e) = + Self::network_handler(server_port, username, rx_from_ui, tx_to_ui, game_state).await + { + error!("Network handler error: {}", e); + } + }); + } + + async fn network_handler( + server_port: String, + username: String, + mut rx_from_ui: mpsc::UnboundedReceiver, + tx_to_ui: mpsc::UnboundedSender, + game_state: Arc>, + ) -> anyhow::Result<()> { + // Build WebSocket URL + let server_address = format!("ws://127.0.0.1:{}", server_port); + let url = Url::parse(&server_address)?; + + info!("Connecting to: {}", server_address); + let (ws_stream, _) = connect_async(url).await?; + let (mut write, mut read) = ws_stream.split(); + + // Send initial join message and immediately send FindMatch + let join_event = ClientEvent::Join { username }; + write + .send(Message::Text(serde_json::to_string(&join_event)?)) + .await?; + info!("Sent Join event"); + + // Send FindMatch immediately after joining + let find_match_event = ClientEvent::FindMatch; + write + .send(Message::Text(serde_json::to_string(&find_match_event)?)) + .await?; + info!("Sent FindMatch event"); + + // Spawn reader task + let tx_to_ui_clone = tx_to_ui.clone(); + let game_state_clone = game_state.clone(); + let reader_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(); + info!("Received: {}", text); + + if let Ok(server_msg) = serde_json::from_str::(text) { + // Update game state + if let Ok(mut state) = game_state_clone.lock() { + match &server_msg { + ServerMessage2::UIUpdate { fen } => { + state.fen = fen.clone(); + } + ServerMessage2::MatchFound { + color, + opponent_name, + .. + } => { + state.player_color = Some(color.clone()); + state.opponent_name = Some(opponent_name.clone()); + } + ServerMessage2::GameEnd { winner } => { + state.game_over = Some(winner.clone()); + } + _ => {} + } + } + + // Send to UI + if let Err(e) = tx_to_ui_clone.send(server_msg) { + error!("Failed to send to UI: {}", e); + break; + } + } + } + Err(e) => { + error!("WebSocket error: {}", e); + break; + } + _ => {} + } + } + }); + + // Writer task (main thread) + while let Some(event) = rx_from_ui.recv().await { + let message = serde_json::to_string(&event)?; + write.send(Message::Text(message)).await?; + info!("Sent event to server: {:?}", event); + } + + // Wait for reader to finish + let _ = reader_handle.await; + + Ok(()) } fn handle_click(&mut self, row: usize, col: usize) { - if let Some((r, c)) = self.selected { - let piece = self.board[r][c]; - self.board[r][c] = Piece::Empty; - self.board[row][col] = piece; - self.selected = None; - self.turn = if self.turn == Turn::White { - Turn::Black - } else { - Turn::White - }; + if let Some((from_row, from_col)) = self.selected_square { + // Send move to server + if let Some(tx) = &self.tx_to_network { + let chess_move = ChessMove::Quiet { + piece_type: engine::piecetype::PieceType::WhiteKing, + from_square: BoardSquare { x: 0, y: 1 }, + to_square: BoardSquare { x: 2, y: 2 }, + promotion_piece: None, + }; + let move_event = ClientEvent::Move { step: chess_move }; + let _ = tx.send(move_event); + } + self.selected_square = None; } else { - if self.board[row][col] != Piece::Empty { - self.selected = Some((row, col)); + // Select square + self.selected_square = Some((row, col)); + } + } + + fn fen_to_board(&self, fen: &str) -> [[char; 8]; 8] { + let mut board = [[' '; 8]; 8]; + let parts: Vec<&str> = fen.split_whitespace().collect(); + let board_str = parts[0]; + + let mut row = 0; + let mut col = 0; + + for c in board_str.chars() { + if c == '/' { + row += 1; + col = 0; + } else if c.is_digit(10) { + col += c.to_digit(10).unwrap() as usize; + } else { + if row < 8 && col < 8 { + board[row][col] = c; + } + col += 1; } } + + board } - fn apply_settings(&mut self, ctx: &egui::Context) { - self.fullscreen = self.pending_settings.fullscreen; - self.selected_resolution = self.pending_settings.selected_resolution; - self.server_port = self.pending_settings.server_port.clone(); - - if let Some(resolution) = self.resolutions.get(self.selected_resolution) { - ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(egui::Vec2::new( - resolution.0 as f32, - resolution.1 as f32, - ))); + fn chess_char_to_piece(&self, c: char) -> &'static str { + match c { + 'K' => "♔", + 'Q' => "♕", + 'R' => "♖", + 'B' => "♗", + 'N' => "♘", + 'P' => "♙", + 'k' => "♚", + 'q' => "♛", + 'r' => "♜", + 'b' => "♝", + 'n' => "♞", + 'p' => "♟︎", + _ => "", } - - ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(self.fullscreen)); } - fn enter_settings(&mut self) { - self.pending_settings.fullscreen = self.fullscreen; - self.pending_settings.selected_resolution = self.selected_resolution; - self.pending_settings.server_port = self.server_port.clone(); - self.state = AppState::Settings; + fn process_network_messages(&mut self) { + if let Some(rx) = &mut self.rx_from_network { + while let Ok(msg) = rx.try_recv() { + match msg { + ServerMessage2::MatchFound { .. } => { + info!("Match found! Transitioning to InGame state"); + self.state = AppState::InGame; + } + ServerMessage2::GameEnd { .. } => { + info!("Game over! Transitioning to GameOver state"); + self.state = AppState::GameOver; + } + ServerMessage2::Ok { response } => { + info!("Server OK response: {:?}", response); + // When we get the OK response, transition to FindingMatch state + // This shows the "Finding Match..." screen while we wait + if matches!(self.state, AppState::Connecting) { + self.state = AppState::FindingMatch; + } + } + ServerMessage2::UIUpdate { fen } => { + info!("Board updated with FEN: {}", fen); + // UI will automatically redraw with new FEN + } + } + } + } } } impl eframe::App for ChessApp { - fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // Process incoming network messages + self.process_network_messages(); + + // Get current game state + let game_state = self.game_state.lock().unwrap().clone(); + match self.state { AppState::MainMenu => { egui::CentralPanel::default().show(ctx, |ui| { @@ -216,283 +358,158 @@ impl eframe::App for ChessApp { ui.heading("♞ Knightly ♞"); ui.add_space(30.0); - if ui - .add_sized([300.0, 60.0], egui::Button::new("Play")) - .clicked() - { - let port = self.server_port.clone(); - info!("\nstarting connection\n"); + ui.horizontal(|ui| { + ui.label("Username:"); + ui.text_edit_singleline(&mut self.username); + }); - //create a TCPlistener with tokio and bind machine ip for connection - tokio::spawn(async move { - info!("tokoi"); - handle_connection(&port, self).await - }); + ui.horizontal(|ui| { + ui.label("Server Port:"); + ui.text_edit_singleline(&mut self.server_port); + }); - self.state = AppState::InGame; + ui.add_space(20.0); + + if ui.button("Connect & Play").clicked() { + self.connect_to_server(); } - ui.add_space(8.0); - if ui - .add_sized([300.0, 60.0], egui::Button::new("Settings")) - .clicked() - { - self.enter_settings(); - } - ui.add_space(8.0); - - if ui - .add_sized([300.0, 60.0], egui::Button::new("Quit")) - .clicked() - { + if ui.button("Quit").clicked() { std::process::exit(0); } }); }); } - AppState::Settings => { + AppState::Connecting => { egui::CentralPanel::default().show(ctx, |ui| { ui.vertical_centered(|ui| { - ui.heading("Settings"); - ui.add_space(30.0); + ui.heading("Connecting to Server..."); + ui.add_space(20.0); + ui.spinner(); + }); + }); + } - // Fullscreen toggle - ui.horizontal(|ui| { - ui.label("Fullscreen:"); - if ui - .checkbox(&mut self.pending_settings.fullscreen, "") - .changed() - { - // If enabling fullscreen, we might want to disable resolution selection - } - }); - ui.add_space(10.0); - - // Resolution dropdown - ui.horizontal(|ui| { - ui.label("Resolution:"); - egui::ComboBox::new("resolution_combo", "") - .selected_text(format!( - "{}x{}", - self.resolutions[self.pending_settings.selected_resolution].0, - self.resolutions[self.pending_settings.selected_resolution].1 - )) - .show_ui(ui, |ui| { - for (i, &(width, height)) in self.resolutions.iter().enumerate() - { - ui.selectable_value( - &mut self.pending_settings.selected_resolution, - i, - format!("{}x{}", width, height), - ); - } - }); - }); - ui.add_space(10.0); - - // Server port input field - ui.horizontal(|ui| { - ui.label("Local Server Port:"); - ui.add( - egui::TextEdit::singleline(&mut self.pending_settings.server_port) - .desired_width(100.0) - .hint_text("9001"), - ); - }); - ui.add_space(30.0); - - // Apply and Cancel buttons - ui.horizontal(|ui| { - if ui - .add_sized([140.0, 40.0], egui::Button::new("Apply")) - .clicked() - { - self.apply_settings(ctx); - self.state = AppState::MainMenu; - } - - if ui - .add_sized([140.0, 40.0], egui::Button::new("Cancel")) - .clicked() - { - self.state = AppState::MainMenu; - } - }); + AppState::FindingMatch => { + egui::CentralPanel::default().show(ctx, |ui| { + ui.vertical_centered(|ui| { + ui.heading("Finding Match..."); + ui.add_space(20.0); + ui.label("Waiting for an opponent..."); + ui.spinner(); }); }); } AppState::InGame => { + // Draw menu bar egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { ui.horizontal(|ui| { if ui.button("Main Menu").clicked() { - self.state = AppState::MainMenu; - } - if ui.button("Settings").clicked() { - self.enter_settings(); - } - if ui.button("New Game").clicked() { *self = ChessApp::default(); - self.state = AppState::InGame; } + + if ui.button("Resign").clicked() { + if let Some(tx) = &self.tx_to_network { + let _ = tx.send(ClientEvent::Resign); + } + } + ui.separator(); - ui.label(format!("Turn: {:?}", self.turn)); + + if let Some(color) = &game_state.player_color { + ui.label(format!("You are: {}", color)); + } + + if let Some(opponent) = &game_state.opponent_name { + ui.label(format!("vs: {}", opponent)); + } }); }); + // Draw chess board egui::CentralPanel::default().show(ctx, |ui| { ui.vertical_centered(|ui| { - let full_avail = ui.available_rect_before_wrap(); - let board_tile = (full_avail.width().min(full_avail.height())) / 8.0; - let board_size = board_tile * 8.0; + let board = self.fen_to_board(&game_state.fen); + let is_white = game_state + .player_color + .as_ref() + .map_or(true, |c| c == "white"); + + let available_size = ui.available_size(); + let board_size = available_size.x.min(available_size.y) * 0.9; + let tile_size = board_size / 8.0; - // Create a child UI at the board position let (response, painter) = ui.allocate_painter( egui::Vec2::new(board_size, board_size), egui::Sense::click(), ); - let board_rect = egui::Rect::from_center_size( - full_avail.center(), - egui::vec2(board_size, board_size), - ); + let board_top_left = response.rect.left_top(); - // Draw the chess board - if self.player_color == Some("white".to_string()) { - let tile_size = board_size / 8.0; - for row in 0..8 { - for col in 0..8 { - let color = if (row + col) % 2 == 0 { - egui::Color32::from_rgb(217, 217, 217) + // Draw board and pieces + for row in 0..8 { + for col in 0..8 { + let (display_row, display_col) = if is_white { + (7 - row, col) + } else { + (row, 7 - col) + }; + + let color = if (row + col) % 2 == 0 { + egui::Color32::from_rgb(240, 217, 181) // Light + } else { + egui::Color32::from_rgb(181, 136, 99) // Dark + }; + + let rect = egui::Rect::from_min_size( + egui::Pos2::new( + board_top_left.x + col as f32 * tile_size, + board_top_left.y + row as f32 * tile_size, + ), + egui::Vec2::new(tile_size, tile_size), + ); + + painter.rect_filled(rect, 0.0, color); + + // Draw piece + let piece_char = board[display_row][display_col]; + if piece_char != ' ' { + let symbol = self.chess_char_to_piece(piece_char); + let font_id = egui::FontId::proportional(tile_size * 0.8); + let text_color = if piece_char.is_uppercase() { + egui::Color32::WHITE } else { - egui::Color32::from_rgb(100, 97, 97) + egui::Color32::BLACK }; - let rect = egui::Rect::from_min_size( - egui::Pos2::new( - board_rect.min.x + col as f32 * tile_size, - board_rect.min.y + row as f32 * tile_size, - ), - egui::Vec2::new(tile_size, tile_size), + painter.text( + rect.center(), + egui::Align2::CENTER_CENTER, + symbol, + font_id, + text_color, ); + } - painter.rect_filled(rect, 0.0, color); - - // Draw piece - let piece = self.board[row][col]; - if piece != Piece::Empty { - let symbol = piece.symbol(); - let font_id = egui::FontId::proportional(tile_size * 0.75); - painter.text( - rect.center(), - egui::Align2::CENTER_CENTER, - symbol, - font_id, - if matches!( - piece, - Piece::King('w') - | Piece::Queen('w') - | Piece::Rook('w') - | Piece::Bishop('w') - | Piece::Knight('w') - | Piece::Pawn('w') - ) { - egui::Color32::WHITE - } else { - egui::Color32::BLACK - }, - ); - } - - // Draw selection highlight - if self.selected == Some((row, col)) { + // Draw selection + if let Some((sel_row, sel_col)) = self.selected_square { + if sel_row == display_row && sel_col == display_col { painter.rect_stroke( rect, 0.0, egui::Stroke::new(3.0, egui::Color32::RED), - egui::StrokeKind::Inside, + egui::StrokeKind::Middle, ); } - - // Handle clicks - if ui.ctx().input(|i| i.pointer.primary_clicked()) { - let click_pos = - ui.ctx().input(|i| i.pointer.interact_pos()).unwrap(); - if rect.contains(click_pos) { - self.handle_click(row, col); - } - } } - } - } - if self.player_color == Some("black".to_string()) { - { - let tile_size = board_size / 8.0; - for row in 0..8 { - for col in 0..8 { - let color = if (row + col) % 2 == 0 { - egui::Color32::from_rgb(217, 217, 217) - } else { - egui::Color32::from_rgb(100, 97, 97) - }; - let rect = egui::Rect::from_min_size( - egui::Pos2::new( - board_rect.min.x + col as f32 * tile_size, - board_rect.min.y + row as f32 * tile_size, - ), - egui::Vec2::new(tile_size, tile_size), - ); - - painter.rect_filled(rect, 0.0, color); - - // Draw piece - let piece = self.board[row][col]; - if piece != Piece::Empty { - let symbol = piece.symbol(); - let font_id = - egui::FontId::proportional(tile_size * 0.75); - painter.text( - rect.center(), - egui::Align2::CENTER_CENTER, - symbol, - font_id, - if matches!( - piece, - Piece::King('w') - | Piece::Queen('w') - | Piece::Rook('w') - | Piece::Bishop('w') - | Piece::Knight('w') - | Piece::Pawn('w') - ) { - egui::Color32::BLACK - } else { - egui::Color32::WHITE - }, - ); - } - - // Draw selection highlight - if self.selected == Some((row, col)) { - painter.rect_stroke( - rect, - 0.0, - egui::Stroke::new(3.0, egui::Color32::RED), - egui::StrokeKind::Inside, - ); - } - - // Handle clicks - if ui.ctx().input(|i| i.pointer.primary_clicked()) { - let click_pos = ui - .ctx() - .input(|i| i.pointer.interact_pos()) - .unwrap(); - if rect.contains(click_pos) { - self.handle_click(row, col); - } + // Handle clicks + if response.clicked() { + if let Some(click_pos) = ui.ctx().pointer_interact_pos() { + if rect.contains(click_pos) { + self.handle_click(display_row, display_col); } } } @@ -501,61 +518,28 @@ impl eframe::App for ChessApp { }); }); } + + AppState::GameOver => { + egui::CentralPanel::default().show(ctx, |ui| { + ui.vertical_centered(|ui| { + ui.heading("Game Over"); + ui.add_space(20.0); + + if let Some(reason) = &game_state.game_over { + ui.label(format!("Result: {}", reason)); + } + + ui.add_space(20.0); + + if ui.button("Back to Main Menu").clicked() { + *self = ChessApp::default(); + } + }); + }); + } } - } -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_initial_board_setup() { - let app = ChessApp::default(); - assert!(matches!(app.board[0][0], Piece::Rook('b'))); - assert!(matches!(app.board[7][0], Piece::Rook('w'))); - - assert!(matches!(app.board[1][0], Piece::Pawn('b'))); - assert!(matches!(app.board[6][0], Piece::Pawn('w'))); - } - - #[test] - fn test_piece_symbols() { - assert_eq!(Piece::King('w').symbol(), "♔"); - assert_eq!(Piece::King('b').symbol(), "♚"); - assert_eq!(Piece::Empty.symbol(), ""); - } - - #[test] - fn test_piece_selection() { - let mut app = ChessApp::default(); - app.handle_click(6, 0); - assert_eq!(app.selected, Some((6, 0))); - app.handle_click(6, 0); - assert_eq!(app.selected, None); - } - - #[test] - fn test_piece_movement() { - let mut app = ChessApp::default(); - // Select and move a piece - app.handle_click(6, 0); // Select white pawn - app.handle_click(5, 0); // Move to empty square - assert_eq!(app.board[6][0], Piece::Empty); - assert!(matches!(app.board[5][0], Piece::Pawn('w'))); - } - - #[test] - fn test_turn_switching() { - let mut app = ChessApp::default(); - assert_eq!(app.turn, Turn::White); - app.handle_click(6, 0); // White selects - app.handle_click(5, 0); // White moves - assert_eq!(app.turn, Turn::Black); // Should now be Black's turn - } - - #[test] - fn test_server_port_default() { - let app = ChessApp::default(); - assert_eq!(app.server_port, "9001"); + + // Request repaint to keep UI responsive + ctx.request_repaint(); } } From 75c39ccbe433ad8385792eb1ad401952d998a523 Mon Sep 17 00:00:00 2001 From: htom Date: Thu, 27 Nov 2025 16:45:40 +0100 Subject: [PATCH 08/10] added new tests for the new code --- ui/src/main.rs | 350 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) diff --git a/ui/src/main.rs b/ui/src/main.rs index c516ea0..54b77d9 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -336,6 +336,9 @@ impl ChessApp { ServerMessage2::UIUpdate { fen } => { info!("Board updated with FEN: {}", fen); // UI will automatically redraw with new FEN + if let Ok(mut game_state) = self.game_state.lock() { + game_state.fen = fen; + } } } } @@ -543,3 +546,350 @@ impl eframe::App for ChessApp { ctx.request_repaint(); } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_default_game_state() { + let game_state = GameState::default(); + assert_eq!( + game_state.fen, + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" + ); + assert_eq!(game_state.player_color, None); + assert_eq!(game_state.opponent_name, None); + assert_eq!(game_state.match_id, None); + assert_eq!(game_state.game_over, None); + } + + #[test] + fn test_fen_to_board_starting_position() { + let app = ChessApp::default(); + let board = app.fen_to_board("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"); + + // Test black pieces on rank 0 + assert_eq!(board[0][0], 'r'); + assert_eq!(board[0][1], 'n'); + assert_eq!(board[0][2], 'b'); + assert_eq!(board[0][3], 'q'); + assert_eq!(board[0][4], 'k'); + assert_eq!(board[0][5], 'b'); + assert_eq!(board[0][6], 'n'); + assert_eq!(board[0][7], 'r'); + + // Test black pawns on rank 1 + for col in 0..8 { + assert_eq!(board[1][col], 'p'); + } + + // Test empty squares in the middle + for row in 2..6 { + for col in 0..8 { + assert_eq!(board[row][col], ' '); + } + } + + // Test white pawns on rank 6 + for col in 0..8 { + assert_eq!(board[6][col], 'P'); + } + + // Test white pieces on rank 7 + assert_eq!(board[7][0], 'R'); + assert_eq!(board[7][1], 'N'); + assert_eq!(board[7][2], 'B'); + assert_eq!(board[7][3], 'Q'); + assert_eq!(board[7][4], 'K'); + assert_eq!(board[7][5], 'B'); + assert_eq!(board[7][6], 'N'); + assert_eq!(board[7][7], 'R'); + } + + #[test] + fn test_fen_to_board_with_numbers() { + let app = ChessApp::default(); + let board = app.fen_to_board("4k3/8/8/8/8/8/8/4K3"); + + // Test empty squares around kings + for row in 0..8 { + for col in 0..8 { + if (row == 0 && col == 4) || (row == 7 && col == 4) { + continue; // Skip king positions + } + assert_eq!(board[row][col], ' '); + } + } + + // Test king positions + assert_eq!(board[0][4], 'k'); // black king + assert_eq!(board[7][4], 'K'); // white king + } + + #[test] + fn test_chess_char_to_piece() { + let app = ChessApp::default(); + + // Test white pieces + assert_eq!(app.chess_char_to_piece('K'), "♔"); + assert_eq!(app.chess_char_to_piece('Q'), "♕"); + assert_eq!(app.chess_char_to_piece('R'), "♖"); + assert_eq!(app.chess_char_to_piece('B'), "♗"); + assert_eq!(app.chess_char_to_piece('N'), "♘"); + assert_eq!(app.chess_char_to_piece('P'), "♙"); + + // Test black pieces + assert_eq!(app.chess_char_to_piece('k'), "♚"); + assert_eq!(app.chess_char_to_piece('q'), "♛"); + assert_eq!(app.chess_char_to_piece('r'), "♜"); + assert_eq!(app.chess_char_to_piece('b'), "♝"); + assert_eq!(app.chess_char_to_piece('n'), "♞"); + assert_eq!(app.chess_char_to_piece('p'), "♟︎"); + + // Test invalid piece + assert_eq!(app.chess_char_to_piece('X'), ""); + assert_eq!(app.chess_char_to_piece(' '), ""); + } + + #[test] + fn test_chess_app_default() { + let app = ChessApp::default(); + + assert_eq!(app.server_port, "9001"); + assert_eq!(app.username, "Player"); + assert!(app.tx_to_network.is_none()); + assert!(app.rx_from_network.is_none()); + assert!(app.selected_square.is_none()); + + // Verify initial state is MainMenu + match app.state { + AppState::MainMenu => (), + _ => panic!("Expected initial state to be MainMenu"), + } + } + + #[test] + fn test_game_state_clone() { + let mut original = GameState::default(); + original.player_color = Some("white".to_string()); + original.opponent_name = Some("Opponent".to_string()); + original.match_id = Some(Uuid::new_v4()); + original.game_over = Some("Checkmate".to_string()); + + let cloned = original.clone(); + + assert_eq!(original.fen, cloned.fen); + assert_eq!(original.player_color, cloned.player_color); + assert_eq!(original.opponent_name, cloned.opponent_name); + assert_eq!(original.match_id, cloned.match_id); + assert_eq!(original.game_over, cloned.game_over); + } + + #[test] + fn test_handle_click_selection() { + let mut app = ChessApp::default(); + + // Initially no square should be selected + assert_eq!(app.selected_square, None); + + // Click on a square should select it + app.handle_click(3, 4); + assert_eq!(app.selected_square, Some((3, 4))); + + // Click on another square should deselect and send move (if tx exists) + // Since we don't have a real tx in tests, we just verify the selection is cleared + app.handle_click(4, 4); + assert_eq!(app.selected_square, None); + } + + #[test] + fn test_process_network_messages_match_found() { + let mut app = ChessApp::default(); + let (tx, mut rx) = mpsc::unbounded_channel(); + app.rx_from_network = Some(rx); + + // Send a MatchFound message + let match_id = Uuid::new_v4(); + let message = ServerMessage2::MatchFound { + match_id, + color: "white".to_string(), + opponent_name: "TestOpponent".to_string(), + }; + + tx.send(message).unwrap(); + + // Process the message + app.process_network_messages(); + + // State should transition to InGame + match app.state { + AppState::InGame => (), + _ => panic!("Expected state to transition to InGame"), + } + } + + #[test] + fn test_process_network_messages_game_over() { + let mut app = ChessApp::default(); + let (tx, mut rx) = mpsc::unbounded_channel(); + app.rx_from_network = Some(rx); + + // Send a GameEnd message + let message = ServerMessage2::GameEnd { + winner: "White won by checkmate".to_string(), + }; + + tx.send(message).unwrap(); + + // Process the message + app.process_network_messages(); + + // State should transition to GameOver + match app.state { + AppState::GameOver => (), + _ => panic!("Expected state to transition to GameOver"), + } + } + + #[test] + fn test_process_network_messages_ui_update() { + let mut app = ChessApp::default(); + let (tx, mut rx) = mpsc::unbounded_channel(); + app.rx_from_network = Some(rx); + + // Send a UIUpdate message + let new_fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1".to_string(); + let message = ServerMessage2::UIUpdate { + fen: new_fen.clone(), + }; + + tx.send(message).unwrap(); + + // Process the message + app.process_network_messages(); + + // Game state should be updated with new FEN + let game_state = app.game_state.lock().unwrap(); + assert_eq!(game_state.fen, new_fen); + } + + #[test] + fn test_process_network_messages_ok_response() { + let mut app = ChessApp::default(); + app.state = AppState::Connecting; + let (tx, mut rx) = mpsc::unbounded_channel(); + app.rx_from_network = Some(rx); + + // Send an Ok message + let message = ServerMessage2::Ok { response: Ok(()) }; + + tx.send(message).unwrap(); + + // Process the message + app.process_network_messages(); + + // State should transition to FindingMatch when in Connecting state + match app.state { + AppState::FindingMatch => (), + _ => panic!("Expected state to transition to FindingMatch"), + } + } + + #[test] + fn test_fen_edge_cases() { + let app = ChessApp::default(); + + // Test empty board + let empty_board = app.fen_to_board("8/8/8/8/8/8/8/8"); + for row in 0..8 { + for col in 0..8 { + assert_eq!(empty_board[row][col], ' '); + } + } + + // Test FEN with multiple digit numbers + let board = app.fen_to_board("k7/8/8/8/8/8/8/7K"); + assert_eq!(board[0][0], 'k'); + assert_eq!(board[7][7], 'K'); + + // Test FEN with mixed pieces and numbers + let board = app.fen_to_board("r3k2r/8/8/8/8/8/8/R3K2R"); + assert_eq!(board[0][0], 'r'); + assert_eq!(board[0][4], 'k'); + assert_eq!(board[0][7], 'r'); + assert_eq!(board[7][0], 'R'); + assert_eq!(board[7][4], 'K'); + assert_eq!(board[7][7], 'R'); + } + + #[tokio::test] + async fn test_client_event_serialization() { + // Test Join event + let join_event = ClientEvent::Join { + username: "test".to_string(), + }; + let serialized = serde_json::to_string(&join_event).unwrap(); + assert!(serialized.contains("Join")); + assert!(serialized.contains("test")); + + // Test FindMatch event + let find_match_event = ClientEvent::FindMatch; + let serialized = serde_json::to_string(&find_match_event).unwrap(); + assert!(serialized.contains("FindMatch")); + + // Test Move event + let chess_move = ChessMove::Quiet { + piece_type: engine::piecetype::PieceType::WhiteKing, + from_square: BoardSquare { x: 0, y: 1 }, + to_square: BoardSquare { x: 2, y: 2 }, + promotion_piece: None, + }; + let move_event = ClientEvent::Move { step: chess_move }; + let serialized = serde_json::to_string(&move_event).unwrap(); + assert!(serialized.contains("Move")); + + // Test Resign event + let resign_event = ClientEvent::Resign; + let serialized = serde_json::to_string(&resign_event).unwrap(); + assert!(serialized.contains("Resign")); + } + + #[test] + fn test_server_message_deserialization() { + // Test Ok message + let ok_json = r#"{"Ok":{"response":{"Ok":null}}}"#; + let message: ServerMessage2 = serde_json::from_str(ok_json).unwrap(); + match message { + ServerMessage2::Ok { response } => { + assert!(response.is_ok()); + } + _ => panic!("Expected Ok message"), + } + + // Test MatchFound message + let match_found_json = r#"{"MatchFound":{"match_id":"12345678-1234-1234-1234-123456789012","color":"white","opponent_name":"Test"}}"#; + let message: ServerMessage2 = serde_json::from_str(match_found_json).unwrap(); + match message { + ServerMessage2::MatchFound { + match_id, + color, + opponent_name, + } => { + assert_eq!(color, "white"); + assert_eq!(opponent_name, "Test"); + } + _ => panic!("Expected MatchFound message"), + } + + // Test UIUpdate message + let ui_update_json = + r#"{"UIUpdate":{"fen":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"}}"#; + let message: ServerMessage2 = serde_json::from_str(ui_update_json).unwrap(); + match message { + ServerMessage2::UIUpdate { fen } => { + assert!(fen.contains("rnbqkbnr")); + } + _ => panic!("Expected UIUpdate message"), + } + } +} From bdbe93932d271f52ce4f68167315ecfa8136beb4 Mon Sep 17 00:00:00 2001 From: htom Date: Thu, 27 Nov 2025 21:17:53 +0100 Subject: [PATCH 09/10] fixed unicode characters, fixed queen and king position, new min windows size --- ui/src/main.rs | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/ui/src/main.rs b/ui/src/main.rs index 54b77d9..6b3fd40 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -20,7 +20,7 @@ async fn main() -> anyhow::Result<(), eframe::Error> { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_fullscreen(false) - .with_min_inner_size(egui::vec2(800.0, 600.0)) + .with_min_inner_size(egui::vec2(800.0, 800.0)) .with_inner_size(egui::vec2(1920.0, 1080.0)), ..Default::default() }; @@ -45,7 +45,6 @@ async fn main() -> anyhow::Result<(), eframe::Error> { ) } -// Server message types (from your connection.rs) #[derive(Serialize, Deserialize, Debug)] pub enum ServerMessage2 { GameEnd { @@ -64,7 +63,6 @@ pub enum ServerMessage2 { }, } -// Client event types (from your connection.rs) #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type")] pub enum ClientEvent { @@ -209,10 +207,11 @@ impl ChessApp { ServerMessage2::MatchFound { color, opponent_name, - .. + match_id, } => { state.player_color = Some(color.clone()); state.opponent_name = Some(opponent_name.clone()); + state.match_id = Some(match_id.clone()); } ServerMessage2::GameEnd { winner } => { state.game_over = Some(winner.clone()); @@ -254,6 +253,7 @@ impl ChessApp { if let Some((from_row, from_col)) = self.selected_square { // Send move to server if let Some(tx) = &self.tx_to_network { + // TODO: kinyerni a tenyleges kivalasztott babut let chess_move = ChessMove::Quiet { piece_type: engine::piecetype::PieceType::WhiteKing, from_square: BoardSquare { x: 0, y: 1 }, @@ -297,12 +297,12 @@ impl ChessApp { fn chess_char_to_piece(&self, c: char) -> &'static str { match c { - 'K' => "♔", - 'Q' => "♕", - 'R' => "♖", - 'B' => "♗", - 'N' => "♘", - 'P' => "♙", + 'K' => "♚", + 'Q' => "♛", + 'R' => "♜", + 'B' => "♝", + 'N' => "♞", + 'P' => "♟︎", 'k' => "♚", 'q' => "♛", 'r' => "♜", @@ -386,6 +386,7 @@ impl eframe::App for ChessApp { AppState::Connecting => { egui::CentralPanel::default().show(ctx, |ui| { + // TODO: go back to menu btn ui.vertical_centered(|ui| { ui.heading("Connecting to Server..."); ui.add_space(20.0); @@ -396,11 +397,18 @@ impl eframe::App for ChessApp { AppState::FindingMatch => { egui::CentralPanel::default().show(ctx, |ui| { + // TODO: go back to menu btn + // TODO: how do we delete the player from the waiting queue ui.vertical_centered(|ui| { ui.heading("Finding Match..."); ui.add_space(20.0); ui.label("Waiting for an opponent..."); ui.spinner(); + + ui.add_space(20.0); + if ui.button("cancel").clicked() { + std::process::exit(0); + } }); }); } @@ -455,9 +463,9 @@ impl eframe::App for ChessApp { for row in 0..8 { for col in 0..8 { let (display_row, display_col) = if is_white { - (7 - row, col) + (row, col) } else { - (row, 7 - col) + (7 - row, 7 - col) }; let color = if (row + col) % 2 == 0 { From a0bca32733cf52d835b8ec1489f23f82f42911b6 Mon Sep 17 00:00:00 2001 From: htom Date: Thu, 27 Nov 2025 21:45:38 +0100 Subject: [PATCH 10/10] fixed test conditions --- ui/src/main.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ui/src/main.rs b/ui/src/main.rs index 6b3fd40..0a12141 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -639,12 +639,12 @@ mod tests { let app = ChessApp::default(); // Test white pieces - assert_eq!(app.chess_char_to_piece('K'), "♔"); - assert_eq!(app.chess_char_to_piece('Q'), "♕"); - assert_eq!(app.chess_char_to_piece('R'), "♖"); - assert_eq!(app.chess_char_to_piece('B'), "♗"); - assert_eq!(app.chess_char_to_piece('N'), "♘"); - assert_eq!(app.chess_char_to_piece('P'), "♙"); + assert_eq!(app.chess_char_to_piece('K'), "♚"); + assert_eq!(app.chess_char_to_piece('Q'), "♛"); + assert_eq!(app.chess_char_to_piece('R'), "♜"); + assert_eq!(app.chess_char_to_piece('B'), "♝"); + assert_eq!(app.chess_char_to_piece('N'), "♞"); + assert_eq!(app.chess_char_to_piece('P'), "♟︎"); // Test black pieces assert_eq!(app.chess_char_to_piece('k'), "♚");