From 5f92bdde4e689659248edec3086ca7ab08518724 Mon Sep 17 00:00:00 2001 From: clfreville2 Date: Mon, 20 Mar 2023 19:03:26 +0100 Subject: [PATCH] End a game when everyone has skipped their turn --- board-network/src/protocol.rs | 2 + board-server/.dockerignore | 7 ++++ board-server/docker-compose.yml | 2 + board-server/src/leaderboard.rs | 31 +++++++++------ board-server/src/main.rs | 12 +++++- board-server/src/room.rs | 70 ++++++++++++++++++++++++++++++++- 6 files changed, 110 insertions(+), 14 deletions(-) create mode 100644 board-server/.dockerignore diff --git a/board-network/src/protocol.rs b/board-network/src/protocol.rs index b97eb3e..4cf239b 100644 --- a/board-network/src/protocol.rs +++ b/board-network/src/protocol.rs @@ -58,4 +58,6 @@ pub enum ServerMessage { /// Informs that a tile has been removed TileRemoved(Position2dRef), TurnRejected(String), + /// Notify that the game has ended. + GameOver, } diff --git a/board-server/.dockerignore b/board-server/.dockerignore new file mode 100644 index 0000000..2c1e155 --- /dev/null +++ b/board-server/.dockerignore @@ -0,0 +1,7 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/target +**/Dockerfile +**/docker-compose.yml diff --git a/board-server/docker-compose.yml b/board-server/docker-compose.yml index 2e42a11..fd2cb7d 100644 --- a/board-server/docker-compose.yml +++ b/board-server/docker-compose.yml @@ -5,6 +5,8 @@ services: build: context: .. dockerfile: board-server/Dockerfile + environment: + - REDIS_HOST=redis ports: - "8080:8080" depends_on: diff --git a/board-server/src/leaderboard.rs b/board-server/src/leaderboard.rs index 24d3429..f68d8c2 100644 --- a/board-server/src/leaderboard.rs +++ b/board-server/src/leaderboard.rs @@ -1,14 +1,18 @@ use async_trait::async_trait; use redis::{AsyncCommands, RedisError}; use std::env; +use std::sync::Arc; const LEADERBOARD: &str = "leaderboard"; -type LeaderboardEntry = (String, i32); +type LeaderboardEntry = (String, u32); #[async_trait] -trait Leaderboard { - async fn add_score(&self, player_name: &str, score: i32) -> Result<(), RedisError>; +pub trait Leaderboard: Send + Sync { + async fn add_score(&self, player_name: &str, score: u32) -> Result<(), RedisError>; async fn get_highscores(&self) -> Result, RedisError>; + fn driver(&self) -> &'static str { + "in-memory" + } } struct RedisLeaderboard { @@ -23,7 +27,7 @@ impl RedisLeaderboard { #[async_trait] impl Leaderboard for RedisLeaderboard { - async fn add_score(&self, player_name: &str, score: i32) -> Result<(), RedisError> { + async fn add_score(&self, player_name: &str, score: u32) -> Result<(), RedisError> { let mut con = self.client.get_async_connection().await?; con.zadd(LEADERBOARD, player_name, score).await?; Ok(()) @@ -36,13 +40,18 @@ impl Leaderboard for RedisLeaderboard { con.zrange_withscores(LEADERBOARD, 0, count - 1).await?; Ok(leaderboard) } + + fn driver(&self) -> &'static str { + "redis" + } } -struct InMemoryLeaderboard(); +#[derive(Debug, Default)] +pub struct InMemoryLeaderboard(); #[async_trait] impl Leaderboard for InMemoryLeaderboard { - async fn add_score(&self, _: &str, _: i32) -> Result<(), RedisError> { + async fn add_score(&self, _: &str, _: u32) -> Result<(), RedisError> { Ok(()) } @@ -51,12 +60,12 @@ impl Leaderboard for InMemoryLeaderboard { } } -async fn provide_leaderboard() -> Box { - match env::var("REDIS_HOSTNAME") { +pub fn provide_leaderboard() -> Arc { + match env::var("REDIS_HOST") { Ok(redis_host_name) => match redis::Client::open(format!("redis://{redis_host_name}/")) { - Ok(client) => Box::new(RedisLeaderboard::new(client)), - Err(_) => Box::new(InMemoryLeaderboard()), + Ok(client) => Arc::new(RedisLeaderboard::new(client)), + Err(_) => Arc::new(InMemoryLeaderboard::default()), }, - Err(_) => Box::new(InMemoryLeaderboard()), + Err(_) => Arc::new(InMemoryLeaderboard::default()), } } diff --git a/board-server/src/main.rs b/board-server/src/main.rs index c57d4bb..3f159bb 100644 --- a/board-server/src/main.rs +++ b/board-server/src/main.rs @@ -2,6 +2,7 @@ mod leaderboard; mod player; mod room; +use crate::leaderboard::{provide_leaderboard, Leaderboard}; use crate::room::{generate_room_name, Room, RoomHandle}; use anyhow::Result; use async_tungstenite::WebSocketStream; @@ -64,6 +65,7 @@ async fn run_player( async fn handle_connection( rooms: Rooms, + leaderboard: Arc, raw_stream: Async, addr: SocketAddr, mut close_room: UnboundedSender, @@ -77,7 +79,7 @@ async fn handle_connection( match msg { ClientMessage::CreateRoom(player_name) => { let (write, read) = unbounded(); - let room = Arc::new(Mutex::new(Room::default())); + let room = Arc::new(Mutex::new(Room::with_leaderboard(leaderboard))); let handle = RoomHandle { write, room }; let room_name = generate_room_name(&mut rooms.lock().unwrap(), handle.clone()); println!("[{addr}] Creating room '{room_name}' for player '{player_name}'"); @@ -127,6 +129,9 @@ fn main() -> Result<(), io::Error> { .parse::() .expect("Invalid address"); + let leaderboard = provide_leaderboard(); + println!("Using {} leaderboard.", leaderboard.driver()); + let rooms = Rooms::new(Mutex::new(HashMap::new())); let close_room = { @@ -149,8 +154,11 @@ fn main() -> Result<(), io::Error> { while let Ok((stream, addr)) = listener.accept().await { let close_room = close_room.clone(); let rooms = rooms.clone(); + let leaderboard = leaderboard.clone(); smol::spawn(async move { - if let Err(e) = handle_connection(rooms, stream, addr, close_room).await { + if let Err(e) = + handle_connection(rooms, leaderboard, stream, addr, close_room).await + { eprintln!("Failed to handle connection from {addr}: {e}"); } }) diff --git a/board-server/src/room.rs b/board-server/src/room.rs index b43228d..168ea10 100644 --- a/board-server/src/room.rs +++ b/board-server/src/room.rs @@ -1,3 +1,4 @@ +use crate::leaderboard::{InMemoryLeaderboard, Leaderboard}; use crate::player::Player; use board_network::protocol::{ClientMessage, ServerMessage}; use board_network::types::TileRef; @@ -9,16 +10,17 @@ 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); -#[derive(Debug, Default)] pub struct Room { pub name: String, pub connections: HashMap, @@ -28,9 +30,18 @@ pub struct Room { 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, @@ -87,6 +98,11 @@ impl Room { 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() { @@ -171,6 +187,10 @@ impl Room { 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(); + } if !Board::has_alignment(&diff) { self.reset_player_moves(); self.send( @@ -215,6 +235,12 @@ impl Room { 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) { @@ -226,6 +252,15 @@ impl Room { 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; @@ -273,6 +308,39 @@ impl Room { } } +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)]