use crate::leaderboard::{InMemoryLeaderboard, Leaderboard}; 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 board_shared::score::calc_score; use board_shared::tile::Tile; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::executor::block_on; use futures::StreamExt; use rand::distributions::{Alphanumeric, DistString}; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fmt; use std::net::SocketAddr; use std::sync::{Arc, Mutex}; type TaggedClientMessage = (SocketAddr, ClientMessage); pub struct Room { pub name: String, pub connections: HashMap, pub players: Vec, pub active_player: usize, pub has_started: bool, pub board: Board, pub validated_board: Board, pub deck: RngDeck, pub leaderboard: Arc, pub successive_skipped_turns: usize, } impl Room { pub fn with_leaderboard(leaderboard: Arc) -> Self { Self { leaderboard, ..Self::default() } } pub fn add_player( &mut self, addr: SocketAddr, player_name: String, tx: UnboundedSender, ) -> anyhow::Result<()> { // If the player name matches an existing player, but with a dropped connection, // then replace this player. let mut player_index = None; for (i, p) in self.players.iter().enumerate() { if p.name == player_name && p.ws.is_none() { player_index = Some(i); break; } } let mut is_new_player = false; if let Some(i) = player_index { // Reclaim the player's spot self.broadcast(ServerMessage::PlayerReconnected(i)); self.players[i].ws = Some(tx.clone()); } else { self.broadcast(ServerMessage::PlayerConnected(player_name.clone())); player_index = Some(self.players.len()); is_new_player = true; self.players.push(Player { name: player_name, score: 0, hand: Hand::default(), ws: Some(tx.clone()), }); } let player_index = player_index.expect("A player index should have been attributed"); self.connections.insert(addr, player_index); // Send the player the current state of the room tx.unbounded_send(ServerMessage::JoinedRoom { room_name: self.name.clone(), players: self .players .iter() .map(|p| (p.name.clone(), p.score, p.ws.is_some())) .collect(), board: self.board.clone(), active_player: self.active_player, has_started: self.has_started, })?; if self.has_started && is_new_player { self.players[player_index] .hand .complete(&mut self.deck) .ok(); self.sync_hand(player_index); } Ok(()) } pub fn next_player(&mut self) { if self.connections.is_empty() { return; } if self.successive_skipped_turns >= self.players.len() { self.on_game_over(); 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::StartGame => self.on_start_game(), ClientMessage::TileUse(pos, tile_idx) => { if let Some(p) = self.connections.get(&addr) { if *p == self.active_player { self.on_tile_use(pos.into(), tile_idx); } } } ClientMessage::TilePlaceEqual(pos) => { if let Some(p) = self.connections.get(&addr) { if *p == self.active_player { self.on_tile_place(pos.into(), Tile::Equals); } } } ClientMessage::TileTake(pos) => { if let Some(p) = self.connections.get(&addr) { if *p == self.active_player { self.on_tile_take(pos.into()); } } } ClientMessage::Validate => { if let Some(p) = self.connections.get(&addr) { if *p == self.active_player { self.on_validate(); } } } } !self.connections.is_empty() } fn on_start_game(&mut self) { if self.has_started { return; } self.has_started = true; self.deck = RngDeck::new_complete(); for p in &mut self.players { p.hand .complete(&mut self.deck) .expect("Not enough tiles in deck"); } for i in 0..self.players.len() { self.sync_hand(i); } self.broadcast(ServerMessage::PlayerTurn(self.active_player)); } 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.on_tile_place(pos, tile); self.sync_hand(self.active_player); } else { self.send( self.active_player, ServerMessage::TurnRejected("Invalid tile index".to_string()), ); } } fn on_tile_place(&mut self, pos: Position2d, tile: Tile) { self.board.set(pos.x, pos.y, tile); self.broadcast(ServerMessage::TilePlaced(pos.into(), tile.into())); } fn on_tile_take(&mut self, pos: Position2d) { if self.board.get(pos.x, pos.y) == self.validated_board.get(pos.x, pos.y) { self.send( self.active_player, ServerMessage::TurnRejected("Cannot take already validated tile.".to_string()), ); } else if let Some(tile) = self.board.take(pos.x, pos.y) { self.players[self.active_player].hand.push(tile); self.sync_hand(self.active_player); self.broadcast(ServerMessage::TileRemoved(pos.into())); } } fn on_validate(&mut self) { let diff = self.board.difference(&self.validated_board); if diff.is_empty() { self.successive_skipped_turns += 1; self.next_player(); return; } if !Board::has_alignment(&diff) || !self.board.is_unique_chain(&diff) { self.reset_player_moves(); self.send( self.active_player, ServerMessage::TurnRejected("Move is not aligned".to_string()), ); return; } 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) .ok(); self.on_validated_move(self.active_player, &diff); self.next_player(); } else { self.send( self.active_player, ServerMessage::TurnRejected("Invalid expressions found".to_string()), ); } } fn on_validated_move(&mut self, player_id: usize, diff: &[Position2d]) { let tiles_placed = diff .iter() .map(|&pos| { self.board .get(pos.x, pos.y) .expect("A placed tile should not be empty.") }) .collect::>(); self.sync_hand(player_id); self.players[player_id].score += calc_score(&tiles_placed); self.broadcast(ServerMessage::SyncScore( player_id, self.players[player_id].score, )); self.validated_board = self.board.clone(); self.successive_skipped_turns = 0; } fn on_game_over(&mut self) { self.broadcast(ServerMessage::GameOver); block_on(self.update_global_leaderboard()); } fn reset_player_moves(&mut self) { let diff = self.board.difference(&self.validated_board); for pos in diff { self.broadcast(ServerMessage::TileRemoved(pos.into())); } self.board = self.validated_board.clone(); self.sync_hand(self.active_player); } async fn update_global_leaderboard(&mut self) { for player in &self.players { self.leaderboard .add_score(&player.name, player.score) .await .ok(); } } fn on_client_disconnected(&mut self, addr: SocketAddr) { if let Some(p) = self.connections.remove(&addr) { self.players[p].ws = None; self.broadcast(ServerMessage::PlayerDisconnected(p)); } } fn broadcast(&self, s: ServerMessage) { for c in self.connections.values() { if let Some(ws) = &self.players[*c].ws { if let Err(e) = ws.unbounded_send(s.clone()) { eprintln!( "[{}] Failed to send broadcast to {}: {e}", self.name, self.players[*c].name ); } } } } fn send(&self, player_id: usize, s: ServerMessage) { if let Some(p) = self.players[player_id].ws.as_ref() { if let Err(e) = p.unbounded_send(s) { eprintln!( "[{}] Failed to send message to {}: {e}", self.name, self.players[player_id].name ); } } else { eprintln!("[{}] Tried sending message to inactive player", self.name); } } fn sync_hand(&mut self, player_id: usize) { self.send( player_id, ServerMessage::SyncHand(self.players[player_id].hand.tiles.clone()), ); } } impl Default for Room { fn default() -> Self { Self { name: String::new(), connections: Default::default(), players: vec![], active_player: 0, has_started: false, board: Default::default(), validated_board: Default::default(), deck: Default::default(), leaderboard: Arc::new(InMemoryLeaderboard::default()), successive_skipped_turns: 0, } } } impl fmt::Debug for Room { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Room") .field("name", &self.name) .field("connections", &self.connections) .field("players", &self.players) .field("active_player", &self.active_player) .field("has_started", &self.has_started) .field("board", &self.board) .field("validated_board", &self.validated_board) .field("deck", &self.deck) .field("skipped_successive_turns", &self.successive_skipped_turns) .finish() } } type RoomPtr = Arc>; #[derive(Clone)] pub struct RoomHandle { pub write: UnboundedSender, pub room: RoomPtr, } impl RoomHandle { pub async fn run_room(&mut self, mut read: UnboundedReceiver) { while let Some((addr, msg)) = read.next().await { if !self.room.lock().unwrap().on_message(addr, msg) { break; } } } } pub type Rooms = HashMap; pub fn generate_room_name(rooms: &mut Rooms, room: RoomHandle) -> String { let mut rng = rand::thread_rng(); loop { let name = Alphanumeric.sample_string(&mut rng, 5); if let Entry::Vacant(v) = rooms.entry(name.clone()) { room.room.lock().unwrap().name = name.clone(); v.insert(room); return name; } } }