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 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(())
|
||||||
}
|
}
|
||||||
|
|||||||
762
ui/src/main.rs
762
ui/src/main.rs
@@ -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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user