diff --git a/board-network/src/protocol.rs b/board-network/src/protocol.rs index 0d8337b..07caf7d 100644 --- a/board-network/src/protocol.rs +++ b/board-network/src/protocol.rs @@ -2,7 +2,7 @@ use crate::types::{Position2dRef, TileRef}; use board_shared::{position::Position2d, tile::Tile}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Debug, Deserialize, Serialize)] pub enum ClientMessage { /// Creates a new room and join it with the given player name. /// @@ -12,9 +12,10 @@ pub enum ClientMessage { Disconnected, TileUse(#[serde(with = "Position2dRef")] Position2d, usize), TileTake(usize), + Validate, } -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub enum ServerMessage { /// Informs that a room has been joined. JoinedRoom { @@ -22,6 +23,7 @@ pub enum ServerMessage { players: Vec<(String, u32, bool)>, active_player: usize, }, + JoinFailed(String), /// Notify that new player has joined the game. PlayerConnected(String), /// Notify that new player has rejoined the game. @@ -30,6 +32,8 @@ pub enum ServerMessage { PlayerDisconnected(usize), /// Change the current player PlayerTurn(usize), + /// Update the current hand of the player + SyncHand(#[serde(with = "TileRef")] Tile), // TODO: Vec /// Informs that a tile has been placed TilePlaced( #[serde(with = "Position2dRef")] Position2d, @@ -37,4 +41,5 @@ pub enum ServerMessage { ), /// Informs that a tile has been removed TileRemoved(#[serde(with = "Position2dRef")] Position2d), + TurnRejected(String), } diff --git a/board-network/src/types.rs b/board-network/src/types.rs index 7c5574a..f6ecc51 100644 --- a/board-network/src/types.rs +++ b/board-network/src/types.rs @@ -2,7 +2,7 @@ use board_shared::position::Position2d; use board_shared::tile::{Digit, Operator, Tile}; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(remote = "Tile")] pub enum TileRef { Digit(#[serde(with = "DigitRef")] Digit), @@ -10,7 +10,7 @@ pub enum TileRef { Equals, } -#[derive(Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(remote = "Digit")] pub struct DigitRef { pub value: i8, @@ -18,7 +18,7 @@ pub struct DigitRef { pub has_right_parenthesis: bool, } -#[derive(Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(remote = "Operator")] pub enum OperatorRef { Add, @@ -27,7 +27,7 @@ pub enum OperatorRef { Divide, } -#[derive(Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(remote = "Position2d")] pub struct Position2dRef { pub x: usize, diff --git a/board-server/src/main.rs b/board-server/src/main.rs index e8ac007..8ae1539 100644 --- a/board-server/src/main.rs +++ b/board-server/src/main.rs @@ -4,7 +4,7 @@ mod room; use crate::room::{generate_room_name, Room, RoomHandle}; use anyhow::Result; use async_tungstenite::WebSocketStream; -use board_network::protocol::ClientMessage; +use board_network::protocol::{ClientMessage, ServerMessage}; use futures::channel::mpsc::{unbounded, UnboundedSender}; use futures::future::join; use futures::{future, SinkExt, StreamExt}; @@ -94,6 +94,23 @@ async fn handle_connection( return Ok(()); } + ClientMessage::JoinRoom(room_name, player_name) => { + let handle = rooms.lock().unwrap().get_mut(&room_name).cloned(); + if let Some(h) = handle { + println!("[{addr}] Joining room '{room_name}' for player '{player_name}'"); + run_player(player_name, addr, h, ws_stream).await; + return Ok(()); + } else { + ws_stream + .send(WebsocketMessage::Text( + serde_json::to_string(&ServerMessage::JoinFailed( + "Could not find room".to_string(), + )) + .unwrap(), + )) + .await?; + } + } msg => eprintln!("[{addr}] Received illegal message {msg:?}"), } } diff --git a/board-server/src/player.rs b/board-server/src/player.rs index d0a788e..8360c16 100644 --- a/board-server/src/player.rs +++ b/board-server/src/player.rs @@ -1,9 +1,11 @@ use board_network::protocol::ServerMessage; +use board_shared::game::Hand; use futures::channel::mpsc::UnboundedSender; #[derive(Debug)] pub struct Player { pub name: String, pub score: u32, + pub hand: Hand, pub ws: Option>, } diff --git a/board-server/src/room.rs b/board-server/src/room.rs index a4697b1..07109b9 100644 --- a/board-server/src/room.rs +++ b/board-server/src/room.rs @@ -1,5 +1,10 @@ use crate::player::Player; use board_network::protocol::{ClientMessage, ServerMessage}; +use board_shared::board::Board; +use board_shared::deck::RngDeck; +use board_shared::expr::is_valid_guess; +use board_shared::game::Hand; +use board_shared::position::Position2d; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::StreamExt; use rand::distributions::{Alphanumeric, DistString}; @@ -16,6 +21,9 @@ pub struct Room { pub connections: HashMap, pub players: Vec, pub active_player: usize, + pub board: Board, + pub validated_board: Board, + pub deck: RngDeck, } impl Room { @@ -43,9 +51,13 @@ impl Room { self.broadcast(ServerMessage::PlayerConnected(player_name.clone())); player_index = Some(self.players.len()); + let mut hand = Hand::default(); + hand.complete(&mut self.deck)?; + self.players.push(Player { name: player_name, score: 0, + hand, ws: Some(tx.clone()), }); } @@ -67,17 +79,91 @@ impl Room { Ok(()) } + pub fn next_player(&mut self) { + if self.connections.is_empty() { + return; + } + + loop { + self.active_player = (self.active_player + 1) % self.players.len(); + if self.players[self.active_player].ws.is_some() { + break; + } + } + + self.broadcast(ServerMessage::PlayerTurn(self.active_player)); + } + pub fn on_message(&mut self, addr: SocketAddr, msg: ClientMessage) -> bool { match msg { ClientMessage::Disconnected => self.on_client_disconnected(addr), ClientMessage::CreateRoom(_) | ClientMessage::JoinRoom(_, _) => { eprintln!("[{}] Illegal client message {:?}", self.name, msg); } + ClientMessage::TileUse(pos, tile_idx) => { + if let Some(p) = self.connections.get(&addr) { + if *p == self.active_player { + self.on_tile_use(pos, tile_idx); + } + } + } + ClientMessage::Validate => { + if let Some(p) = self.connections.get(&addr) { + if *p == self.active_player { + self.on_validate(); + } + } + } _ => todo!(), } !self.connections.is_empty() } + fn on_tile_use(&mut self, pos: Position2d, tile_idx: usize) { + let hand = &mut self.players[self.active_player].hand; + if let Some(tile) = hand.remove(tile_idx) { + self.board.set(pos.x, pos.y, tile); + } + } + + fn on_validate(&mut self) -> anyhow::Result<()> { + let diff = self.board.difference(&self.validated_board); + if !Board::has_alignment(&diff) { + self.reset_player_moves(); + self.send( + self.active_player, + ServerMessage::TurnRejected("Move is not aligned".to_string()), + ); + return Ok(()); + } + let is_valid = self + .board + .find_chains(&diff) + .iter() + .all(|chain| is_valid_guess(&self.board, chain) == Ok(true)); + + if is_valid { + self.players[self.active_player] + .hand + .complete(&mut self.deck)?; + self.next_player(); + } else { + self.send( + self.active_player, + ServerMessage::TurnRejected("Invalid expressions found".to_string()), + ); + } + Ok(()) + } + + fn reset_player_moves(&mut self) { + let diff = self.board.difference(&self.validated_board); + for pos in diff { + self.broadcast(ServerMessage::TileRemoved(pos)); + } + self.board = self.validated_board.clone(); + } + fn on_client_disconnected(&mut self, addr: SocketAddr) { if let Some(p) = self.connections.remove(&addr) { self.players[p].ws = None; @@ -97,6 +183,19 @@ impl Room { } } } + + fn send(&self, i: usize, s: ServerMessage) { + if let Some(p) = self.players[i].ws.as_ref() { + if let Err(e) = p.unbounded_send(s) { + eprintln!( + "[{}] Failed to send message to {}: {}", + self.name, self.players[i].name, e + ); + } + } else { + eprintln!("[{}] Tried sending message to inactive player", self.name); + } + } } type RoomPtr = Arc>; diff --git a/board-shared/src/deck.rs b/board-shared/src/deck.rs index bdecb90..b120e65 100644 --- a/board-shared/src/deck.rs +++ b/board-shared/src/deck.rs @@ -1,3 +1,4 @@ +use std::error::Error; use crate::tile::Operator; use enum_map::EnumMap; use rand::{thread_rng, Rng}; @@ -6,7 +7,14 @@ type DeckSize = u16; type DigitDeck = [DeckSize; 19]; /// When a deck is empty, new tiles cannot be retrieved. -pub type EmptyDeckError = (); +#[derive(Debug)] +pub struct EmptyDeckError; +impl Error for EmptyDeckError {} +impl std::fmt::Display for EmptyDeckError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Deck is empty") + } +} /// A entire deck of tiles. #[derive(Debug, Clone, Default, PartialEq)] diff --git a/board-shared/src/game.rs b/board-shared/src/game.rs index 4098185..c766baa 100644 --- a/board-shared/src/game.rs +++ b/board-shared/src/game.rs @@ -35,12 +35,20 @@ impl Hand { pub fn complete(&mut self, deck: &mut RngDeck) -> Result<(), EmptyDeckError> { for _ in 0..self.count_missing_operators() { self.tiles - .push(Tile::Operator(deck.rand_operator().ok_or(())?)); + .push(Tile::Operator(deck.rand_operator().ok_or(EmptyDeckError.into())?)); } for _ in 0..self.count_missing_numbers() { self.tiles - .push(Tile::Digit(Digit::new(deck.rand_digit().ok_or(())?))); + .push(Tile::Digit(Digit::new(deck.rand_digit().ok_or(EmptyDeckError.into())?))); } Ok(()) } + + pub fn remove(&mut self, idx: usize) -> Option { + if idx < self.tiles.len() { + Some(self.tiles.remove(idx)) + } else { + None + } + } }