updated ui code with partial rewriting, now if there are two players they will join a match and the board will be drawn

This commit is contained in:
2025-11-27 16:33:08 +01:00
parent 5763716848
commit df02c2eee1
2 changed files with 429 additions and 424 deletions

View File

@@ -1,16 +1,19 @@
use engine::{chessmove::ChessMove, gameend::GameEnd}; use engine::{chessmove::ChessMove, gameend::GameEnd};
use futures_util::StreamExt; use futures_util::{SinkExt, StreamExt};
use local_ip_address::local_ip; use local_ip_address::local_ip;
use log::{error, info, warn}; use log::{error, info, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
error::Error,
net::{IpAddr, Ipv4Addr}, net::{IpAddr, Ipv4Addr},
sync::{Arc, Mutex},
}; };
use tokio_tungstenite::connect_async; use tokio_tungstenite::connect_async;
use tungstenite::Message;
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
use crate::{ChessApp, ClientEvent, SharedGameState};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub enum ServerMessage2 { pub enum ServerMessage2 {
GameEnd { GameEnd {
@@ -46,7 +49,11 @@ fn get_ip_address() -> IpAddr {
ip 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<Mutex<Vec<ClientEvent>>>,
) -> anyhow::Result<()> {
let address = get_ip_address(); let address = get_ip_address();
//start main loop //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 (ws_stream, _) = connect_async(url).await?;
let (mut write, mut read) = ws_stream.split(); 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"); info!("connection");
match message { match message {
Ok(msg) => { Ok(msg) => {
@@ -69,7 +76,7 @@ pub async fn handle_connection(server_port: &str) -> anyhow::Result<()> {
let text = msg.to_text().unwrap(); let text = msg.to_text().unwrap();
info!("text: {}", text); info!("text: {}", text);
if let Ok(parsed) = serde_json::from_str::<ServerMessage2>(text) { /*if let Ok(parsed) = serde_json::from_str::<ServerMessage2>(text) {
match parsed { match parsed {
ServerMessage2::GameEnd { winner } => {} ServerMessage2::GameEnd { winner } => {}
ServerMessage2::UIUpdate { fen } => {} ServerMessage2::UIUpdate { fen } => {}
@@ -77,12 +84,26 @@ pub async fn handle_connection(server_port: &str) -> anyhow::Result<()> {
match_id, match_id,
color, color,
opponent_name, opponent_name,
} => {} } => {
//chess_app.player_color = Some(color);
}
ServerMessage2::Ok { response } => {} ServerMessage2::Ok { response } => {}
_ => { _ => {
error!("Received unkown servermessage2"); error!("Received unkown servermessage2");
} }
} }
}*/
if let Ok(parsed) = serde_json::from_str::<ServerMessage2>(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::<Vec<_>>();
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); error!("Error receiving message: {}", e);
} }
} }
}; }
Ok(()) Ok(())
} }

View File

@@ -1,14 +1,18 @@
use eframe::egui; use eframe::egui;
use engine::{boardsquare::BoardSquare, chessmove::ChessMove};
use env_logger::Env; use env_logger::Env;
use futures_util::{SinkExt, StreamExt};
use log::{error, info, warn}; 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 uuid::Uuid;
use crate::connection::handle_connection;
mod connection;
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<(), eframe::Error> { async fn main() -> anyhow::Result<(), eframe::Error> {
//set up for logging // Set up logging
let env = Env::default().filter_or("MY_LOG_LEVEL", "INFO"); let env = Env::default().filter_or("MY_LOG_LEVEL", "INFO");
env_logger::init_from_env(env); env_logger::init_from_env(env);
warn!("Initialized logger"); warn!("Initialized logger");
@@ -16,10 +20,11 @@ async fn main() -> anyhow::Result<(), eframe::Error> {
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default() viewport: egui::ViewportBuilder::default()
.with_fullscreen(false) .with_fullscreen(false)
.with_min_inner_size(egui::vec2(800.0, 600.0)) // Minimum width, height .with_min_inner_size(egui::vec2(800.0, 600.0))
.with_inner_size(egui::vec2(7680.0, 4320.0)), // Initial size .with_inner_size(egui::vec2(1920.0, 1080.0)),
..Default::default() ..Default::default()
}; };
eframe::run_native( eframe::run_native(
"Knightly", "Knightly",
options, options,
@@ -40,175 +45,312 @@ async fn main() -> anyhow::Result<(), eframe::Error> {
) )
} }
#[derive(Clone, Copy, PartialEq, Debug)] // Server message types (from your connection.rs)
enum Piece { #[derive(Serialize, Deserialize, Debug)]
King(char), pub enum ServerMessage2 {
Queen(char), GameEnd {
Rook(char), winner: String,
Bishop(char), },
Knight(char), UIUpdate {
Pawn(char), fen: String,
Empty, },
MatchFound {
match_id: Uuid,
color: String,
opponent_name: String,
},
Ok {
response: Result<(), String>,
},
} }
impl Piece { // Client event types (from your connection.rs)
fn symbol(&self) -> &'static str { #[derive(Serialize, Deserialize, Debug)]
match self { #[serde(tag = "type")]
Piece::King('w') => "", pub enum ClientEvent {
Piece::Queen('w') => "", Join { username: String },
Piece::Rook('w') => "", FindMatch,
Piece::Bishop('w') => "", Move { step: ChessMove },
Piece::Knight('w') => "", Resign,
Piece::Pawn('w') => "♟︎", Chat { text: String },
Piece::King('b') => "", RequestLegalMoves { fen: String },
Piece::Queen('b') => "", }
Piece::Rook('b') => "",
Piece::Bishop('b') => "", // Game state
Piece::Knight('b') => "", #[derive(Debug, Clone)]
Piece::Pawn('b') => "♟︎", struct GameState {
Piece::Empty => "", fen: String,
_ => "", player_color: Option<String>,
opponent_name: Option<String>,
match_id: Option<Uuid>,
game_over: Option<String>,
}
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)] // UI state
enum Turn {
White,
Black,
}
enum AppState { enum AppState {
MainMenu, MainMenu,
Connecting,
FindingMatch,
InGame, InGame,
Settings, GameOver,
} }
#[derive(Clone)]
struct ChessApp { struct ChessApp {
fullscreen: bool,
resolutions: Vec<(u32, u32)>,
selected_resolution: usize,
state: AppState, state: AppState,
board: [[Piece; 8]; 8], game_state: Arc<Mutex<GameState>>,
selected: Option<(usize, usize)>,
turn: Turn,
pending_settings: PendingSettings,
server_port: String, server_port: String,
player_color: Option<String>, username: String,
match_id: Option<Uuid>,
opponent_name: Option<String>,
}
#[derive(Default)] // Channels for communication with network tasks
struct PendingSettings { tx_to_network: Option<mpsc::UnboundedSender<ClientEvent>>,
fullscreen: bool, rx_from_network: Option<mpsc::UnboundedReceiver<ServerMessage2>>,
selected_resolution: usize,
server_port: String, // UI state
selected_square: Option<(usize, usize)>,
} }
impl Default for ChessApp { impl Default for ChessApp {
fn default() -> Self { fn default() -> Self {
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, state: AppState::MainMenu,
board: Self::starting_board(), game_state: Arc::new(Mutex::new(GameState::default())),
selected: None, server_port: "9001".to_string(),
turn: Turn::White, username: "Player".to_string(),
pending_settings: PendingSettings::default(), tx_to_network: None,
server_port: "9001".to_string(), // Default port rx_from_network: None,
player_color: None, selected_square: None,
match_id: None,
opponent_name: None,
} }
} }
} }
impl ChessApp { impl ChessApp {
fn starting_board() -> [[Piece; 8]; 8] { fn connect_to_server(&mut self) {
use Piece::*; let server_port = self.server_port.clone();
[ let username = self.username.clone();
[ let game_state = self.game_state.clone();
Rook('b'),
Knight('b'), // Create channels for communication
Bishop('b'), let (tx_to_network, rx_from_ui) = mpsc::unbounded_channel();
Queen('b'), let (tx_to_ui, rx_from_network) = mpsc::unbounded_channel();
King('b'),
Bishop('b'), self.tx_to_network = Some(tx_to_network);
Knight('b'), self.rx_from_network = Some(rx_from_network);
Rook('b'),
], self.state = AppState::Connecting;
[Pawn('b'); 8],
[Empty; 8], // Spawn network connection task
[Empty; 8], tokio::spawn(async move {
[Empty; 8], if let Err(e) =
[Empty; 8], Self::network_handler(server_port, username, rx_from_ui, tx_to_ui, game_state).await
[Pawn('w'); 8], {
[ error!("Network handler error: {}", e);
Rook('w'), }
Knight('w'), });
Bishop('w'), }
Queen('w'),
King('w'), async fn network_handler(
Bishop('w'), server_port: String,
Knight('w'), username: String,
Rook('w'), mut rx_from_ui: mpsc::UnboundedReceiver<ClientEvent>,
], tx_to_ui: mpsc::UnboundedSender<ServerMessage2>,
] game_state: Arc<Mutex<GameState>>,
) -> 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::<ServerMessage2>(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) { fn handle_click(&mut self, row: usize, col: usize) {
if let Some((r, c)) = self.selected { if let Some((from_row, from_col)) = self.selected_square {
let piece = self.board[r][c]; // Send move to server
self.board[r][c] = Piece::Empty; if let Some(tx) = &self.tx_to_network {
self.board[row][col] = piece; let chess_move = ChessMove::Quiet {
self.selected = None; piece_type: engine::piecetype::PieceType::WhiteKing,
self.turn = if self.turn == Turn::White { from_square: BoardSquare { x: 0, y: 1 },
Turn::Black to_square: BoardSquare { x: 2, y: 2 },
} else { promotion_piece: None,
Turn::White
}; };
let move_event = ClientEvent::Move { step: chess_move };
let _ = tx.send(move_event);
}
self.selected_square = None;
} else { } else {
if self.board[row][col] != Piece::Empty { // Select square
self.selected = Some((row, col)); self.selected_square = Some((row, col));
}
} }
} }
fn apply_settings(&mut self, ctx: &egui::Context) { fn fen_to_board(&self, fen: &str) -> [[char; 8]; 8] {
self.fullscreen = self.pending_settings.fullscreen; let mut board = [[' '; 8]; 8];
self.selected_resolution = self.pending_settings.selected_resolution; let parts: Vec<&str> = fen.split_whitespace().collect();
self.server_port = self.pending_settings.server_port.clone(); let board_str = parts[0];
if let Some(resolution) = self.resolutions.get(self.selected_resolution) { let mut row = 0;
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(egui::Vec2::new( let mut col = 0;
resolution.0 as f32,
resolution.1 as f32, 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;
}
} }
ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(self.fullscreen)); board
} }
fn enter_settings(&mut self) { fn chess_char_to_piece(&self, c: char) -> &'static str {
self.pending_settings.fullscreen = self.fullscreen; match c {
self.pending_settings.selected_resolution = self.selected_resolution; 'K' => "",
self.pending_settings.server_port = self.server_port.clone(); 'Q' => "",
self.state = AppState::Settings; 'R' => "",
'B' => "",
'N' => "",
'P' => "",
'k' => "",
'q' => "",
'r' => "",
'b' => "",
'n' => "",
'p' => "♟︎",
_ => "",
}
}
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 { 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 { match self.state {
AppState::MainMenu => { AppState::MainMenu => {
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
@@ -216,163 +358,115 @@ impl eframe::App for ChessApp {
ui.heading("♞ Knightly ♞"); ui.heading("♞ Knightly ♞");
ui.add_space(30.0); ui.add_space(30.0);
if ui ui.horizontal(|ui| {
.add_sized([300.0, 60.0], egui::Button::new("Play")) ui.label("Username:");
.clicked() ui.text_edit_singleline(&mut self.username);
{
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, self).await
}); });
self.state = AppState::InGame; ui.horizontal(|ui| {
} ui.label("Server Port:");
ui.add_space(8.0); ui.text_edit_singleline(&mut self.server_port);
});
if ui ui.add_space(20.0);
.add_sized([300.0, 60.0], egui::Button::new("Settings"))
.clicked()
{
self.enter_settings();
}
ui.add_space(8.0);
if ui if ui.button("Connect & Play").clicked() {
.add_sized([300.0, 60.0], egui::Button::new("Quit")) self.connect_to_server();
.clicked() }
{
if ui.button("Quit").clicked() {
std::process::exit(0); std::process::exit(0);
} }
}); });
}); });
} }
AppState::Settings => { AppState::Connecting => {
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.heading("Settings"); ui.heading("Connecting to Server...");
ui.add_space(30.0); 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 AppState::FindingMatch => {
.add_sized([140.0, 40.0], egui::Button::new("Cancel")) egui::CentralPanel::default().show(ctx, |ui| {
.clicked() ui.vertical_centered(|ui| {
{ ui.heading("Finding Match...");
self.state = AppState::MainMenu; ui.add_space(20.0);
} ui.label("Waiting for an opponent...");
}); ui.spinner();
}); });
}); });
} }
AppState::InGame => { AppState::InGame => {
// Draw menu bar
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("Main Menu").clicked() { 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 = 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.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| { egui::CentralPanel::default().show(ctx, |ui| {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
let full_avail = ui.available_rect_before_wrap(); let board = self.fen_to_board(&game_state.fen);
let board_tile = (full_avail.width().min(full_avail.height())) / 8.0; let is_white = game_state
let board_size = board_tile * 8.0; .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( let (response, painter) = ui.allocate_painter(
egui::Vec2::new(board_size, board_size), egui::Vec2::new(board_size, board_size),
egui::Sense::click(), egui::Sense::click(),
); );
let board_rect = egui::Rect::from_center_size( let board_top_left = response.rect.left_top();
full_avail.center(),
egui::vec2(board_size, board_size),
);
// Draw the chess board // Draw board and pieces
if self.player_color == Some("white".to_string()) {
let tile_size = board_size / 8.0;
for row in 0..8 { for row in 0..8 {
for col in 0..8 { for col in 0..8 {
let color = if (row + col) % 2 == 0 { let (display_row, display_col) = if is_white {
egui::Color32::from_rgb(217, 217, 217) (7 - row, col)
} else { } else {
egui::Color32::from_rgb(100, 97, 97) (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( let rect = egui::Rect::from_min_size(
egui::Pos2::new( egui::Pos2::new(
board_rect.min.x + col as f32 * tile_size, board_top_left.x + col as f32 * tile_size,
board_rect.min.y + row as f32 * tile_size, board_top_left.y + row as f32 * tile_size,
), ),
egui::Vec2::new(tile_size, tile_size), egui::Vec2::new(tile_size, tile_size),
); );
@@ -380,119 +474,42 @@ impl eframe::App for ChessApp {
painter.rect_filled(rect, 0.0, color); painter.rect_filled(rect, 0.0, color);
// Draw piece // Draw piece
let piece = self.board[row][col]; let piece_char = board[display_row][display_col];
if piece != Piece::Empty { if piece_char != ' ' {
let symbol = piece.symbol(); let symbol = self.chess_char_to_piece(piece_char);
let font_id = egui::FontId::proportional(tile_size * 0.75); let font_id = egui::FontId::proportional(tile_size * 0.8);
painter.text( let text_color = if piece_char.is_uppercase() {
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 egui::Color32::WHITE
} else { } else {
egui::Color32::BLACK 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 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( painter.text(
rect.center(), rect.center(),
egui::Align2::CENTER_CENTER, egui::Align2::CENTER_CENTER,
symbol, symbol,
font_id, font_id,
if matches!( text_color,
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 // Draw selection
if self.selected == Some((row, col)) { if let Some((sel_row, sel_col)) = self.selected_square {
if sel_row == display_row && sel_col == display_col {
painter.rect_stroke( painter.rect_stroke(
rect, rect,
0.0, 0.0,
egui::Stroke::new(3.0, egui::Color32::RED), egui::Stroke::new(3.0, egui::Color32::RED),
egui::StrokeKind::Inside, egui::StrokeKind::Middle,
); );
} }
}
// Handle clicks // Handle clicks
if ui.ctx().input(|i| i.pointer.primary_clicked()) { if response.clicked() {
let click_pos = ui if let Some(click_pos) = ui.ctx().pointer_interact_pos() {
.ctx()
.input(|i| i.pointer.interact_pos())
.unwrap();
if rect.contains(click_pos) { if rect.contains(click_pos) {
self.handle_click(row, col); 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);
#[cfg(test)] if ui.button("Back to Main Menu").clicked() {
mod tests { *self = ChessApp::default();
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')));
// Request repaint to keep UI responsive
assert!(matches!(app.board[1][0], Piece::Pawn('b'))); ctx.request_repaint();
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");
} }
} }