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:
@@ -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<Mutex<Vec<ClientEvent>>>,
|
||||
) -> 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::<ServerMessage2>(text) {
|
||||
/*if let Ok(parsed) = serde_json::from_str::<ServerMessage2>(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::<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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
756
ui/src/main.rs
756
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<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)]
|
||||
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<Mutex<GameState>>,
|
||||
server_port: String,
|
||||
player_color: Option<String>,
|
||||
match_id: Option<Uuid>,
|
||||
opponent_name: Option<String>,
|
||||
}
|
||||
username: String,
|
||||
|
||||
#[derive(Default)]
|
||||
struct PendingSettings {
|
||||
fullscreen: bool,
|
||||
selected_resolution: usize,
|
||||
server_port: String,
|
||||
// Channels for communication with network tasks
|
||||
tx_to_network: Option<mpsc::UnboundedSender<ClientEvent>>,
|
||||
rx_from_network: Option<mpsc::UnboundedReceiver<ServerMessage2>>,
|
||||
|
||||
// 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<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) {
|
||||
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 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();
|
||||
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];
|
||||
|
||||
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,
|
||||
)));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(self.fullscreen));
|
||||
board
|
||||
}
|
||||
|
||||
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 chess_char_to_piece(&self, c: char) -> &'static str {
|
||||
match c {
|
||||
'K' => "♔",
|
||||
'Q' => "♕",
|
||||
'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 {
|
||||
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,163 +358,115 @@ 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");
|
||||
|
||||
//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("Username:");
|
||||
ui.text_edit_singleline(&mut self.username);
|
||||
});
|
||||
|
||||
self.state = AppState::InGame;
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Server Port:");
|
||||
ui.text_edit_singleline(&mut self.server_port);
|
||||
});
|
||||
|
||||
if ui
|
||||
.add_sized([300.0, 60.0], egui::Button::new("Settings"))
|
||||
.clicked()
|
||||
{
|
||||
self.enter_settings();
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.add_space(20.0);
|
||||
|
||||
if ui
|
||||
.add_sized([300.0, 60.0], egui::Button::new("Quit"))
|
||||
.clicked()
|
||||
{
|
||||
if ui.button("Connect & Play").clicked() {
|
||||
self.connect_to_server();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 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.heading("Connecting to Server...");
|
||||
ui.add_space(20.0);
|
||||
ui.spinner();
|
||||
});
|
||||
});
|
||||
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;
|
||||
// Draw board and pieces
|
||||
for row in 0..8 {
|
||||
for col in 0..8 {
|
||||
let color = if (row + col) % 2 == 0 {
|
||||
egui::Color32::from_rgb(217, 217, 217)
|
||||
let (display_row, display_col) = if is_white {
|
||||
(7 - row, col)
|
||||
} 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(
|
||||
egui::Pos2::new(
|
||||
board_rect.min.x + col as f32 * tile_size,
|
||||
board_rect.min.y + row as f32 * tile_size,
|
||||
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),
|
||||
);
|
||||
@@ -380,119 +474,42 @@ impl eframe::App for ChessApp {
|
||||
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')
|
||||
) {
|
||||
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::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(
|
||||
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
|
||||
},
|
||||
text_color,
|
||||
);
|
||||
}
|
||||
|
||||
// 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 response.clicked() {
|
||||
if let Some(click_pos) = ui.ctx().pointer_interact_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);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user