diff --git a/.github/workflows/dispatcher.yml b/.github/workflows/dispatcher.yml
index 6532bfd..cb168d7 100644
--- a/.github/workflows/dispatcher.yml
+++ b/.github/workflows/dispatcher.yml
@@ -16,7 +16,9 @@ jobs:
server: ${{ steps.check.outputs.server }}
ui: ${{ steps.check.outputs.ui }}
steps:
- - uses: actions/checkout@v4
+ - name: checkout repository
+ uses: actions/checkout@v4
+
- name: Determine which tests to run
id: check
run: |
diff --git a/Docs/Knightly rendszerterv v1.png b/Docs/Knightly rendszerterv v1.png
new file mode 100644
index 0000000..9826e93
Binary files /dev/null and b/Docs/Knightly rendszerterv v1.png differ
diff --git a/Docs/Knightly rendszerterv v1.xml b/Docs/Knightly rendszerterv v1.xml
new file mode 100644
index 0000000..063e792
--- /dev/null
+++ b/Docs/Knightly rendszerterv v1.xml
@@ -0,0 +1,287 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/engine/src/bitboard.rs b/engine/src/bitboard.rs
index 67d128d..504ae04 100644
--- a/engine/src/bitboard.rs
+++ b/engine/src/bitboard.rs
@@ -1,2 +1,7 @@
mod attackmaps;
-mod utils;
\ No newline at end of file
+mod utils;
+mod legality;
+mod checkinfo;
+mod attacks;
+
+pub mod board;
\ No newline at end of file
diff --git a/engine/src/bitboard/attacks.rs b/engine/src/bitboard/attacks.rs
new file mode 100644
index 0000000..a348d96
--- /dev/null
+++ b/engine/src/bitboard/attacks.rs
@@ -0,0 +1,89 @@
+use super::board::Board;
+use super::attackmaps::*;
+
+impl Board {
+
+ const RANK_2: u64 = 0x0000_0000_0000_FF00;
+ const RANK_7: u64 = 0x00FF_0000_0000_0000;
+
+ pub fn get_pseudo_pawn_moves(&self, sq: u32) -> u64 {
+ let pawn: u64 = 1 << sq;
+ let mut move_mask: u64 = 0u64;
+ let move_offset: i8 = 8 - 16 * self.side_to_move as i8;
+
+ let next_sq: u64 = if move_offset > 0 {pawn << move_offset} else {pawn >> -move_offset};
+ if (self.occupancy[2] & next_sq) == 0 {
+ move_mask |= next_sq;
+
+ if (self.side_to_move == 0 && pawn & Self::RANK_2 != 0)
+ || (self.side_to_move == 1 && pawn & Self::RANK_7 != 0) {
+
+ let next_sq: u64 = if move_offset > 0 {next_sq << move_offset} else {next_sq >> -move_offset};
+ if (self.occupancy[2] & next_sq) == 0 {
+ move_mask |= next_sq;
+ }
+ }
+ }
+
+ return move_mask;
+ }
+ #[inline]
+ pub fn get_pseudo_knight_moves(&self, sq: u32) -> u64 {
+ return KNIGHT_ATTACK_MAP[sq as usize];
+ }
+ #[inline]
+ pub fn get_pseudo_king_moves(&self, sq: u32) -> u64 {
+ return KING_ATTACK_MAP[sq as usize];
+ }
+ #[inline]
+ pub fn get_pseudo_pawn_captures(&self, sq: u32) -> u64 {
+ return PAWN_ATTACK_MAP[sq as usize][self.side_to_move as usize];
+ }
+ #[inline]
+ pub fn get_pseudo_opponent_pawn_captures(&self, sq: u32) -> u64 {
+ return PAWN_ATTACK_MAP[sq as usize][1 - self.side_to_move as usize];
+ }
+ #[inline]
+ pub fn get_pseudo_bishop_moves(&self, sq: u32) -> u64 {
+ let mut moves = 0u64;
+ let sq = sq as usize;
+ let occupancy = self.occupancy[2];
+ moves |= get_raycast_from_square_in_direction(occupancy, sq, 1);
+ moves |= get_raycast_from_square_in_direction(occupancy, sq, 3);
+ moves |= get_raycast_from_square_in_direction(occupancy, sq, 5);
+ moves |= get_raycast_from_square_in_direction(occupancy, sq, 7);
+
+ return moves;
+ }
+ #[inline]
+ pub fn get_pseudo_rook_moves(&self, sq: u32) -> u64 {
+ let mut moves: u64 = 0u64;
+ let occupancy = self.occupancy[2];
+ let sq = sq as usize;
+ moves |= get_raycast_from_square_in_direction(occupancy, sq, 0);
+ moves |= get_raycast_from_square_in_direction(occupancy, sq, 2);
+ moves |= get_raycast_from_square_in_direction(occupancy, sq, 4);
+ moves |= get_raycast_from_square_in_direction(occupancy, sq, 6);
+
+ return moves;
+ }
+ #[inline(always)]
+ pub fn get_pseudo_queen_moves(&self, sq: u32) -> u64 {
+ return self.get_pseudo_bishop_moves(sq) | self.get_pseudo_rook_moves(sq);
+ }
+}
+
+#[inline(always)]
+pub fn get_raycast_from_square_in_direction(occupancy: u64, sq: usize, dir: usize) -> u64 {
+ let is_up: bool = dir / 4 == 0;
+ let mut ray: u64 = RAY_TABLE[sq][dir];
+ let blockers: u64 = occupancy & ray;
+
+ if blockers != 0 {
+ let first_blocker: u32 = if is_up { blockers.trailing_zeros() } else { 63 - blockers.leading_zeros() };
+
+ ray &= !RAY_TABLE[first_blocker as usize][dir];
+ }
+
+ return ray;
+}
\ No newline at end of file
diff --git a/engine/src/bitboard/board.rs b/engine/src/bitboard/board.rs
new file mode 100644
index 0000000..11a85ce
--- /dev/null
+++ b/engine/src/bitboard/board.rs
@@ -0,0 +1,182 @@
+use super::utils::try_get_square_number_from_notation;
+
+pub struct Board {
+ pub(in super) bitboards: [u64; 12], // 0-5 -> white pieces (P, N, B, R, Q, K), 6-11 -> black pieces (p, n, b, r, q, k)
+ pub(in super) piece_board: [u8; 64], // same as board indexes, 12 -> empty square
+ pub(in super) occupancy: [u64; 3], // 0 -> white, 1 -> black, 2 -> combined
+ pub(in super) castling_rights: u8, // 0b0000_KQkq
+ pub(in super) pinned_squares: [u8; 64], // 0 -> E-W, 1 -> NE-SW, 2 -> N-S, 3 -> SE-NW, 4 -> no pin
+ pub(in super) pin_mask: u64, // 1 -> pin, 0 -> no pin
+ pub(in super) en_passant_square: u64, // 1 -> ep square, 0 -> no ep square
+ pub(in super) side_to_move: u8 // 0 -> white to play, 1 -> black to play
+}
+
+impl Board {
+
+ pub fn new_clear() -> Self {
+ let mut bit_board: Self = Self {
+ bitboards: [0x0000_0000_0000_0000; 12],
+ piece_board: [12; 64],
+ occupancy: [0x0000_0000_0000_0000; 3],
+ castling_rights: 0b0000_0000,
+ pinned_squares: [4; 64],
+ pin_mask: 0u64,
+ en_passant_square: 0x0000_0000_0000_0000,
+ side_to_move: 0
+ };
+
+ return bit_board;
+ }
+ pub fn new() -> Self {
+ let mut bit_board: Board = Self {
+ bitboards: [0x0000_0000_0000_FF00,
+ 0x0000_0000_0000_0042,
+ 0x0000_0000_0000_0024,
+ 0x0000_0000_0000_0081,
+ 0x0000_0000_0000_0008,
+ 0x0000_0000_0000_0010,
+ 0x00FF_0000_0000_0000,
+ 0x4200_0000_0000_0000,
+ 0x2400_0000_0000_0000,
+ 0x8100_0000_0000_0000,
+ 0x0800_0000_0000_0000,
+ 0x1000_0000_0000_0000],
+ piece_board: [12; 64],
+ occupancy: [0; 3],
+ castling_rights: 0b0000_1111,
+ pinned_squares: [4; 64],
+ pin_mask: 0u64,
+ en_passant_square: 0x0000_0000_0000_0000,
+ side_to_move: 0
+ };
+ bit_board.calc_occupancy();
+ bit_board.calc_piece_board();
+
+ return bit_board;
+ }
+ pub fn build(fen: &str) -> Self {
+ let mut board: Board = Board::new_clear();
+
+ let mut col: i32 = 0;
+ let mut row: i32 = 7;
+ let pieces: [char; 12] = ['p', 'n', 'b', 'r', 'q', 'k', 'P', 'N', 'B', 'R', 'Q', 'K'];
+ let mut coming_up: &str = fen;
+
+ for (i, c) in coming_up.chars().enumerate() {
+ if pieces.contains(&c) {
+ // board.place_piece(row*8 + col, c);
+ col += 1;
+ }
+ else if ('1'..='8').contains(&c) {
+ col += c.to_string().parse::().unwrap();
+ }
+ else if c == '/' {
+ row -= 1;
+ col = 0;
+ }
+ else {
+ coming_up = &coming_up[i+1..];
+ break;
+ }
+ }
+ board.calc_occupancy();
+
+ match coming_up.chars().next().unwrap() {
+ 'w' => board.side_to_move = 0,
+ 'b' => board.side_to_move = 1,
+ _ => panic!("invalid fen notation / to be handled later")
+ }
+ coming_up = &coming_up[2..];
+
+ for (i, c) in coming_up.chars().enumerate() {
+ match c {
+ 'K' => board.castling_rights |= 1 << 3,
+ 'Q' => board.castling_rights |= 1 << 2,
+ 'k' => board.castling_rights |= 1 << 1,
+ 'q' => board.castling_rights |= 1,
+ '-' => {
+ coming_up = &coming_up[i+2..];
+ break;
+ }
+ _ => {
+ coming_up = &coming_up[i+1..];
+ break;
+ }
+ }
+ }
+ match coming_up.chars().next().unwrap() {
+ '-' => {
+ coming_up = &coming_up[1..];
+ }
+ _ => {
+ let notation = coming_up.split(' ').next().unwrap();
+ if let Ok(epsq_index) = try_get_square_number_from_notation(notation) {
+ board.en_passant_square = 1 << epsq_index;
+ }
+ }
+ }
+ board.calc_pinned_squares();
+ board.calc_piece_board();
+
+ return board;
+ }
+
+ #[inline(always)]
+ pub fn bitboards(&self, index: usize) -> u64 {
+ return self.bitboards[index];
+ }
+ #[inline(always)]
+ pub fn piece_board(&self, sq: u8) -> u8 {
+ return self.piece_board[sq as usize];
+ }
+ #[inline(always)]
+ pub fn occupancy(&self, side: usize) -> u64 {
+ return self.occupancy[side];
+ }
+ #[inline(always)]
+ pub fn castling_rights(&self) -> u8 {
+ return self.castling_rights;
+ }
+ #[inline(always)]
+ pub fn pinned_squares(&self, sq: usize) -> u8 {
+ return self.pinned_squares[sq];
+ }
+ #[inline(always)]
+ pub fn pin_mask(&self) -> u64 {
+ return self.pin_mask;
+ }
+ #[inline(always)]
+ pub fn en_passant_square(&self) -> u64 {
+ return self.en_passant_square;
+ }
+ #[inline(always)]
+ pub fn side_to_move(&self) -> u8 {
+ return self.side_to_move;
+ }
+
+ #[inline(always)]
+ pub fn current_king_square(&self) -> u32 {
+ return if self.side_to_move == 0 { self.bitboards[5].trailing_zeros() } else { self.bitboards[11].trailing_zeros() };
+ }
+
+ fn calc_occupancy(&mut self) {
+ self.occupancy = [0u64; 3];
+ for b in 0..6 {
+ self.occupancy[0] |= self.bitboards[b];
+ }
+ for b in 6..12 {
+ self.occupancy[1] |= self.bitboards[b];
+ }
+ self.occupancy[2] = self.occupancy[0] | self.occupancy[1];
+ }
+ fn calc_piece_board(&mut self) {
+ for sq in 0..64 {
+ for b in 0..12 {
+ if (self.bitboards[b as usize] & 1 << sq) != 0 {
+ self.piece_board[sq] = b;
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/engine/src/bitboard/checkinfo.rs b/engine/src/bitboard/checkinfo.rs
new file mode 100644
index 0000000..b145321
--- /dev/null
+++ b/engine/src/bitboard/checkinfo.rs
@@ -0,0 +1,21 @@
+
+pub struct CheckInfo {
+ pub check_count: u8,
+ pub move_mask: u64
+}
+
+impl CheckInfo {
+
+ pub fn new() -> Self {
+ return Self {
+ check_count: 0,
+ move_mask: 0xFFFF_FFFF_FFFF_FFFF
+ }
+ }
+
+ #[inline(always)]
+ pub fn add_checker(&mut self, move_mask: u64) {
+ self.move_mask &= move_mask;
+ self.check_count += 1;
+ }
+}
\ No newline at end of file
diff --git a/engine/src/bitboard/legality.rs b/engine/src/bitboard/legality.rs
new file mode 100644
index 0000000..7d12e57
--- /dev/null
+++ b/engine/src/bitboard/legality.rs
@@ -0,0 +1,48 @@
+use super::board::Board;
+use super::attackmaps::RAY_TABLE;
+
+impl Board {
+
+ pub(in super) fn calc_pinned_squares(&mut self) {
+ self.pinned_squares = [4; 64];
+ self.pin_mask = 0u64;
+
+ let friendly_pieces: u64 = self.occupancy[self.side_to_move as usize];
+ let offset: usize = 6 * self.side_to_move as usize;
+ let king_board: u64 = self.bitboards[5 + offset];
+ let king_sq: u32 = king_board.trailing_zeros();
+ let opponent_queen_bishop_mask: u64 = self.bitboards[8 - offset] | self.bitboards[10 - offset];
+ let opponent_queen_rook_mask: u64 = self.bitboards[9 - offset] | self.bitboards[10 - offset];
+
+ // Queen-Rook directions
+ self.set_pinned_in_ray_direction(king_sq, friendly_pieces, opponent_queen_rook_mask, 0);
+ self.set_pinned_in_ray_direction(king_sq, friendly_pieces, opponent_queen_rook_mask, 2);
+ self.set_pinned_in_ray_direction(king_sq, friendly_pieces, opponent_queen_rook_mask, 4);
+ self.set_pinned_in_ray_direction(king_sq, friendly_pieces, opponent_queen_rook_mask, 6);
+
+ // Queen-Bishop directions
+ self.set_pinned_in_ray_direction(king_sq, friendly_pieces, opponent_queen_bishop_mask, 1);
+ self.set_pinned_in_ray_direction(king_sq, friendly_pieces, opponent_queen_bishop_mask, 3);
+ self.set_pinned_in_ray_direction(king_sq, friendly_pieces, opponent_queen_bishop_mask, 5);
+ self.set_pinned_in_ray_direction(king_sq, friendly_pieces, opponent_queen_bishop_mask, 7);
+ }
+
+ pub(in super) fn set_pinned_in_ray_direction(&mut self, king_sq: u32, friendly_pieces: u64, attackers: u64, dir: u8) {
+ let is_up: bool = dir / 4 == 0;
+ let mask: u64 = RAY_TABLE[king_sq as usize][dir as usize];
+ let blockers: u64 = self.occupancy[2] & mask;
+ if blockers == 0 { return; }
+ let first_blocker_sq: u32 = if is_up { blockers.trailing_zeros() } else { 63 - blockers.leading_zeros() };
+ if (friendly_pieces & 1 << first_blocker_sq) != 0 {
+ let blockers: u64 = blockers & !(1 << first_blocker_sq);
+ if blockers == 0 { return; }
+ let second_blocker_sq: u32 = if is_up { blockers.trailing_zeros() } else { 63 - blockers.leading_zeros() };
+
+ if (attackers & 1 << second_blocker_sq) != 0 {
+ self.pinned_squares[first_blocker_sq as usize] = dir % 4;
+ self.pin_mask |= 1 << first_blocker_sq;
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/engine/src/bitboard/utils.rs b/engine/src/bitboard/utils.rs
index a30e503..25d7ba8 100644
--- a/engine/src/bitboard/utils.rs
+++ b/engine/src/bitboard/utils.rs
@@ -27,6 +27,27 @@ pub fn notation_from_square_number(sq: u8) -> String {
return notation;
}
+pub fn try_get_square_number_from_notation(notation: &str) -> Result {
+
+ let file = match notation.chars().nth(0).unwrap() {
+ 'a' => 0,
+ 'b' => 1,
+ 'c' => 2,
+ 'd' => 3,
+ 'e' => 4,
+ 'f' => 5,
+ 'g' => 6,
+ 'h' => 7,
+ _ => { return Result::Err(()); }
+ };
+ if let Some(rank) = notation.chars().nth(1) {
+ return Result::Ok(file + 8 * (rank.to_digit(10).unwrap() as u8) - 8);
+ }
+ else {
+ return Result::Err(());
+ }
+}
+
// <----- TESTS ----->
diff --git a/server/Cargo.toml b/server/Cargo.toml
index ff06670..c631dd0 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -4,3 +4,13 @@ version = "0.1.0"
edition = "2024"
[dependencies]
+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"] }
+anyhow = "1.0.100"
+rand = "0.9.2"
diff --git a/server/src/connection.rs b/server/src/connection.rs
new file mode 100644
index 0000000..715ae88
--- /dev/null
+++ b/server/src/connection.rs
@@ -0,0 +1,218 @@
+use futures_util::{SinkExt, StreamExt};
+use serde::{Deserialize, Serialize};
+use std::collections::{HashMap, VecDeque};
+use std::sync::Arc;
+use tokio::net::TcpStream;
+use tokio::sync::Mutex;
+use tokio_tungstenite::{WebSocketStream, tungstenite::Message};
+use uuid::Uuid;
+
+// Type definitions
+pub type Tx = futures_util::stream::SplitSink, Message>;
+pub type ConnectionMap = Arc>>;
+pub type MatchMap = Arc>>;
+pub type WaitingQueue = Arc>>;
+
+// Helper functions to create new instances
+pub fn new_connection_map() -> ConnectionMap {
+ Arc::new(Mutex::new(HashMap::new()))
+}
+
+pub fn new_match_map() -> MatchMap {
+ Arc::new(Mutex::new(HashMap::new()))
+}
+
+pub fn new_waiting_queue() -> WaitingQueue {
+ Arc::new(Mutex::new(VecDeque::new()))
+}
+
+#[derive(Debug)]
+pub struct PlayerConnection {
+ pub id: Uuid,
+ pub username: Option,
+ pub tx: Tx,
+ pub current_match: Option,
+}
+
+#[derive(Debug, Clone)]
+pub struct GameMatch {
+ pub id: Uuid,
+ pub player_white: Uuid,
+ pub player_black: Uuid,
+ pub board_state: String,
+ pub move_history: Vec,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct Step {
+ pub from: String,
+ pub to: String,
+}
+
+// Message sending utilities
+pub async fn send_message_to_player(
+ connections: &ConnectionMap,
+ player_id: Uuid,
+ message: &str,
+) -> Result<(), Box> {
+ let mut connections_lock = connections.lock().await;
+ if let Some(connection) = connections_lock.get_mut(&player_id) {
+ connection
+ .tx
+ .send(Message::Text(message.to_string()))
+ .await?;
+ }
+ Ok(())
+}
+
+pub async fn broadcast_to_all(connections: &ConnectionMap, message: &str) {
+ let mut connections_lock = connections.lock().await;
+ let mut dead_connections = Vec::new();
+
+ for (id, connection) in connections_lock.iter_mut() {
+ if let Err(e) = connection.tx.send(Message::Text(message.to_string())).await {
+ eprintln!("Failed to send to {}: {}", id, e);
+ dead_connections.push(*id);
+ }
+ }
+
+ // Clean up dead connections
+ for dead_id in dead_connections {
+ connections_lock.remove(&dead_id);
+ }
+}
+
+pub async fn broadcast_to_match(
+ connections: &ConnectionMap,
+ matches: &MatchMap,
+ match_id: Uuid,
+ message: &str,
+) -> Result<(), Box> {
+ let matches_lock = matches.lock().await;
+ if let Some(game_match) = matches_lock.get(&match_id) {
+ send_message_to_player(connections, game_match.player_white, message).await?;
+ send_message_to_player(connections, game_match.player_black, message).await?;
+ }
+ Ok(())
+}
+
+// Connection handler
+pub async fn handle_connection(
+ stream: TcpStream,
+ connections: ConnectionMap,
+ matches: MatchMap,
+ waiting_queue: WaitingQueue,
+ event_system: crate::events::EventSystem,
+) -> anyhow::Result<()> {
+ use tokio_tungstenite::accept_async;
+
+ let ws_stream = accept_async(stream).await?;
+ let (write, mut read) = ws_stream.split();
+
+ let player_id = Uuid::new_v4();
+
+ // Store the connection
+ {
+ let mut conn_map = connections.lock().await;
+ conn_map.insert(
+ player_id,
+ PlayerConnection {
+ id: player_id,
+ username: None,
+ tx: write,
+ current_match: None,
+ },
+ );
+ }
+
+ println!("New connection: {}", player_id);
+
+ // Send welcome message
+ let _ = send_message_to_player(
+ &connections,
+ player_id,
+ &format!(r#"{{"type": "welcome", "player_id": "{}"}}"#, player_id),
+ )
+ .await;
+
+ // Message processing loop
+ while let Some(Ok(message)) = read.next().await {
+ if message.is_text() {
+ let text = message.to_text()?;
+ println!("Received from {}: {}", player_id, text);
+
+ // TODO: Parse and handle message with event system
+ // This will be implemented when we integrate the event system
+ }
+ }
+
+ // Cleanup on disconnect
+ cleanup_player(player_id, &connections, &matches, &waiting_queue).await;
+ println!("Connection {} closed", player_id);
+
+ Ok(())
+}
+
+async fn cleanup_player(
+ player_id: Uuid,
+ connections: &ConnectionMap,
+ _matches: &MatchMap,
+ waiting_queue: &WaitingQueue,
+) {
+ // Remove from waiting queue
+ waiting_queue.lock().await.retain(|&id| id != player_id);
+
+ // Remove from connections
+ connections.lock().await.remove(&player_id);
+
+ println!("Cleaned up player {}", player_id);
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use uuid::Uuid;
+
+ #[tokio::test]
+ async fn test_send_message_to_nonexistent_player() {
+ let connections = new_connection_map();
+ let player_id = Uuid::new_v4();
+
+ let result = send_message_to_player(&connections, player_id, "test message").await;
+ assert!(result.is_ok(), "Should handle missing player gracefully");
+ }
+
+ #[tokio::test]
+ async fn test_broadcast_to_empty_connections() {
+ let connections = new_connection_map();
+
+ broadcast_to_all(&connections, "test broadcast").await;
+
+ let conn_map = connections.lock().await;
+ assert!(conn_map.is_empty(), "Connections should still be empty");
+ }
+
+ #[tokio::test]
+ async fn test_connection_cleanup() {
+ let connections = new_connection_map();
+ let matches = new_match_map();
+ let waiting_queue = new_waiting_queue();
+
+ let player_id = Uuid::new_v4();
+
+ {
+ waiting_queue.lock().await.push_back(player_id);
+ assert_eq!(waiting_queue.lock().await.len(), 1);
+ }
+
+ cleanup_player(player_id, &connections, &matches, &waiting_queue).await;
+
+ {
+ let queue = waiting_queue.lock().await;
+ assert!(
+ !queue.contains(&player_id),
+ "Player should be removed from waiting queue"
+ );
+ }
+ }
+}
diff --git a/server/src/events.rs b/server/src/events.rs
new file mode 100644
index 0000000..8cc43a4
--- /dev/null
+++ b/server/src/events.rs
@@ -0,0 +1,126 @@
+use serde::{Deserialize, Serialize};
+use std::sync::Arc;
+use tokio::sync::Mutex;
+use tokio::sync::mpsc;
+use uuid::Uuid;
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct Step {
+ pub from: String,
+ pub to: String,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(tag = "type")]
+pub enum ClientEvent {
+ Join { username: String },
+ FindMatch,
+ Move { from: String, to: String },
+ Resign,
+ Chat { text: String },
+ RequestLegalMoves { fen: String },
+}
+
+#[derive(Debug)]
+pub enum ServerEvent {
+ PlayerJoined(Uuid, String),
+ PlayerLeft(Uuid),
+ PlayerJoinedQueue(Uuid),
+ PlayerJoinedMatch(Uuid, Uuid), // player_id, match_id
+ PlayerMove(Uuid, Step),
+ PlayerResigned(Uuid),
+ MatchCreated(Uuid, Uuid, Uuid), // match_id, white_id, black_id
+}
+
+pub struct EventSystem {
+ sender: mpsc::UnboundedSender<(Uuid, ClientEvent)>,
+ receiver: Arc>>,
+}
+
+impl Clone for EventSystem {
+ fn clone(&self) -> Self {
+ Self {
+ sender: self.sender.clone(),
+ receiver: Arc::clone(&self.receiver),
+ }
+ }
+}
+
+impl EventSystem {
+ pub fn new() -> Self {
+ let (sender, receiver) = mpsc::unbounded_channel();
+ Self {
+ sender,
+ receiver: Arc::new(Mutex::new(receiver)),
+ }
+ }
+
+ pub async fn send_event(
+ &self,
+ player_id: Uuid,
+ event: ClientEvent,
+ ) -> Result<(), Box> {
+ self.sender.send((player_id, event))?;
+ Ok(())
+ }
+
+ pub async fn next_event(&self) -> Option<(Uuid, ClientEvent)> {
+ let mut receiver = self.receiver.lock().await;
+ receiver.recv().await
+ }
+}
+
+impl Default for EventSystem {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use uuid::Uuid;
+
+ #[tokio::test]
+ async fn test_event_system_send_and_receive() {
+ let event_system = EventSystem::new();
+ let player_id = Uuid::new_v4();
+
+ let join_event = ClientEvent::Join {
+ username: "test_user".to_string(),
+ };
+
+ let send_result = event_system.send_event(player_id, join_event).await;
+ assert!(send_result.is_ok(), "Should send event successfully");
+
+ let received = event_system.next_event().await;
+ assert!(received.is_some(), "Should receive sent event");
+
+ let (received_id, received_event) = received.unwrap();
+ assert_eq!(received_id, player_id, "Should receive correct player ID");
+
+ match received_event {
+ ClientEvent::Join { username } => {
+ assert_eq!(username, "test_user", "Should receive correct username");
+ }
+ _ => panic!("Should receive Join event"),
+ }
+ }
+
+ #[tokio::test]
+ async fn test_event_system_clone() {
+ let event_system1 = EventSystem::new();
+ let event_system2 = event_system1.clone();
+
+ let player_id = Uuid::new_v4();
+ let event = ClientEvent::FindMatch;
+
+ event_system1.send_event(player_id, event).await.unwrap();
+
+ let received = event_system2.next_event().await;
+ assert!(
+ received.is_some(),
+ "Cloned event system should receive events"
+ );
+ }
+}
diff --git a/server/src/main.rs b/server/src/main.rs
index e7a11a9..0d35baf 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -1,3 +1,54 @@
-fn main() {
- println!("Hello, world!");
+mod connection;
+mod events;
+mod matchmaking;
+use tokio::net::TcpListener;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ let address = "0.0.0.0:9001";
+ let listener = TcpListener::bind(address).await?;
+ println!("Server running on ws://{}", address);
+
+ // Shared state initialization using the new helper functions
+ let connections = connection::new_connection_map();
+ let matches = connection::new_match_map();
+ let waiting_queue = connection::new_waiting_queue();
+
+ // Event system for communication between components
+ let event_system = events::EventSystem::new();
+
+ // Start matchmaking background task
+ let matchmaker = matchmaking::MatchmakingSystem::new(
+ connections.clone(),
+ matches.clone(),
+ waiting_queue.clone(),
+ event_system.clone(),
+ );
+ tokio::spawn(async move {
+ matchmaker.run().await;
+ });
+
+ // Main connection loop
+ while let Ok((stream, _)) = listener.accept().await {
+ let connections = connections.clone();
+ let matches = matches.clone();
+ let waiting_queue = waiting_queue.clone();
+ let event_system = event_system.clone();
+
+ tokio::spawn(async move {
+ if let Err(e) = connection::handle_connection(
+ stream,
+ connections,
+ matches,
+ waiting_queue,
+ event_system,
+ )
+ .await
+ {
+ eprintln!("Connection error: {}", e);
+ }
+ });
+ }
+
+ Ok(())
}
diff --git a/server/src/matchmaking.rs b/server/src/matchmaking.rs
new file mode 100644
index 0000000..4f4cc27
--- /dev/null
+++ b/server/src/matchmaking.rs
@@ -0,0 +1,202 @@
+use crate::connection::{ConnectionMap, GameMatch, MatchMap, WaitingQueue};
+use crate::events::EventSystem;
+use rand::random;
+use uuid::Uuid;
+
+pub struct MatchmakingSystem {
+ connections: ConnectionMap,
+ matches: MatchMap,
+ waiting_queue: WaitingQueue,
+ event_system: EventSystem,
+}
+
+impl MatchmakingSystem {
+ pub fn new(
+ connections: ConnectionMap,
+ matches: MatchMap,
+ waiting_queue: WaitingQueue,
+ event_system: EventSystem,
+ ) -> Self {
+ Self {
+ connections,
+ matches,
+ waiting_queue,
+ event_system,
+ }
+ }
+
+ pub async fn run(&self) {
+ loop {
+ self.try_create_match().await;
+ tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
+ }
+ }
+
+ async fn try_create_match(&self) {
+ let mut queue = self.waiting_queue.lock().await;
+
+ while queue.len() >= 2 {
+ let player1 = queue.pop_front().unwrap();
+ let player2 = queue.pop_front().unwrap();
+
+ let match_id = Uuid::new_v4();
+ let (white_player, black_player) = if random::() {
+ (player1, player2)
+ } else {
+ (player2, player1)
+ };
+
+ let game_match = GameMatch {
+ id: match_id,
+ player_white: white_player,
+ player_black: black_player,
+ board_state: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1".to_string(),
+ move_history: Vec::new(),
+ };
+
+ // Store the match
+ self.matches.lock().await.insert(match_id, game_match);
+
+ // Update player connections
+ {
+ let mut conn_map = self.connections.lock().await;
+ if let Some(player) = conn_map.get_mut(&white_player) {
+ player.current_match = Some(match_id);
+ }
+ if let Some(player) = conn_map.get_mut(&black_player) {
+ player.current_match = Some(match_id);
+ }
+ }
+
+ // Notify players
+ self.notify_players(white_player, black_player, match_id)
+ .await;
+ }
+ }
+
+ async fn notify_players(&self, white: Uuid, black: Uuid, match_id: Uuid) {
+ let conn_map = self.connections.lock().await;
+
+ // Get opponent names
+ let white_name = conn_map
+ .get(&black)
+ .and_then(|c| c.username.as_deref())
+ .unwrap_or("Opponent");
+ let black_name = conn_map
+ .get(&white)
+ .and_then(|c| c.username.as_deref())
+ .unwrap_or("Opponent");
+
+ // Notify white player
+ if let Some(_) = conn_map.get(&white) {
+ let message = format!(
+ r#"{{"type": "match_found", "match_id": "{}", "opponent": "{}", "color": "white"}}"#,
+ match_id, black_name
+ );
+ let _ =
+ crate::connection::send_message_to_player(&self.connections, white, &message).await;
+ }
+
+ // Notify black player
+ if let Some(_) = conn_map.get(&black) {
+ let message = format!(
+ r#"{{"type": "match_found", "match_id": "{}", "opponent": "{}", "color": "black"}}"#,
+ match_id, white_name
+ );
+ let _ =
+ crate::connection::send_message_to_player(&self.connections, black, &message).await;
+ }
+
+ println!("Match created: {} (white) vs {} (black)", white, black);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::events::EventSystem;
+ use uuid::Uuid;
+
+ use crate::connection::new_connection_map;
+ use crate::connection::new_match_map;
+ use crate::connection::new_waiting_queue;
+
+ #[tokio::test]
+ async fn test_matchmaking_creates_matches() {
+ let connections = new_connection_map();
+ let matches = new_match_map();
+ let waiting_queue = new_waiting_queue();
+ let event_system = EventSystem::new();
+
+ let matchmaking = MatchmakingSystem::new(
+ connections.clone(),
+ matches.clone(),
+ waiting_queue.clone(),
+ event_system.clone(),
+ );
+
+ let player1 = Uuid::new_v4();
+ let player2 = Uuid::new_v4();
+
+ {
+ waiting_queue.lock().await.push_back(player1);
+ waiting_queue.lock().await.push_back(player2);
+ }
+
+ matchmaking.try_create_match().await;
+
+ {
+ let matches_map = matches.lock().await;
+ assert_eq!(matches_map.len(), 1, "Should create one match");
+
+ let game_match = matches_map.values().next().unwrap();
+ assert!(game_match.player_white == player1 || game_match.player_white == player2);
+ assert!(game_match.player_black == player1 || game_match.player_black == player2);
+ assert_ne!(
+ game_match.player_white, game_match.player_black,
+ "Players should be different"
+ );
+ }
+
+ {
+ let queue = waiting_queue.lock().await;
+ assert!(
+ queue.is_empty(),
+ "Waiting queue should be empty after matchmaking"
+ );
+ }
+ }
+
+ #[tokio::test]
+ async fn test_matchmaking_with_odd_players() {
+ let connections = new_connection_map();
+ let matches = new_match_map();
+ let waiting_queue = new_waiting_queue();
+ let event_system = EventSystem::new();
+
+ let matchmaking = MatchmakingSystem::new(
+ connections.clone(),
+ matches.clone(),
+ waiting_queue.clone(),
+ event_system.clone(),
+ );
+
+ let player1 = Uuid::new_v4();
+ {
+ waiting_queue.lock().await.push_back(player1);
+ }
+
+ matchmaking.try_create_match().await;
+
+ {
+ let matches_map = matches.lock().await;
+ assert!(
+ matches_map.is_empty(),
+ "Should not create match with only one player"
+ );
+
+ let queue = waiting_queue.lock().await;
+ assert_eq!(queue.len(), 1, "Should keep single player in queue");
+ }
+ }
+}
diff --git a/server/src/messages.rs b/server/src/messages.rs
new file mode 100644
index 0000000..e6e957a
--- /dev/null
+++ b/server/src/messages.rs
@@ -0,0 +1,36 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Debug)]
+#[serde(tag = "type")]
+pub enum ServerMessage {
+ Welcome {
+ player_id: String,
+ },
+ MatchFound {
+ match_id: String,
+ opponent: String,
+ color: String,
+ },
+ GameStart {
+ fen: String,
+ white_time: u32,
+ black_time: u32,
+ },
+ MoveResult {
+ valid: bool,
+ from: String,
+ to: String,
+ new_fen: String,
+ },
+ OpponentMove {
+ from: String,
+ to: String,
+ },
+ GameEnd {
+ result: String,
+ reason: String,
+ },
+ Error {
+ reason: String,
+ },
+}