399 lines
13 KiB
Rust
399 lines
13 KiB
Rust
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<SocketAddr, usize>,
|
|
pub players: Vec<Player>,
|
|
pub active_player: usize,
|
|
pub has_started: bool,
|
|
pub board: Board,
|
|
pub validated_board: Board,
|
|
pub deck: RngDeck,
|
|
pub leaderboard: Arc<dyn Leaderboard>,
|
|
pub successive_skipped_turns: usize,
|
|
}
|
|
|
|
impl Room {
|
|
pub fn with_leaderboard(leaderboard: Arc<dyn Leaderboard>) -> Self {
|
|
Self {
|
|
leaderboard,
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
pub fn add_player(
|
|
&mut self,
|
|
addr: SocketAddr,
|
|
player_name: String,
|
|
tx: UnboundedSender<ServerMessage>,
|
|
) -> 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::<Vec<Tile>>();
|
|
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<Mutex<Room>>;
|
|
|
|
#[derive(Clone)]
|
|
pub struct RoomHandle {
|
|
pub write: UnboundedSender<TaggedClientMessage>,
|
|
pub room: RoomPtr,
|
|
}
|
|
|
|
impl RoomHandle {
|
|
pub async fn run_room(&mut self, mut read: UnboundedReceiver<TaggedClientMessage>) {
|
|
while let Some((addr, msg)) = read.next().await {
|
|
if !self.room.lock().unwrap().on_message(addr, msg) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub type Rooms = HashMap<String, RoomHandle>;
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|