End a game when everyone has skipped their turn
This commit is contained in:
@@ -58,4 +58,6 @@ pub enum ServerMessage {
|
|||||||
/// Informs that a tile has been removed
|
/// Informs that a tile has been removed
|
||||||
TileRemoved(Position2dRef),
|
TileRemoved(Position2dRef),
|
||||||
TurnRejected(String),
|
TurnRejected(String),
|
||||||
|
/// Notify that the game has ended.
|
||||||
|
GameOver,
|
||||||
}
|
}
|
||||||
|
7
board-server/.dockerignore
Normal file
7
board-server/.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/target
|
||||||
|
**/Dockerfile
|
||||||
|
**/docker-compose.yml
|
@@ -5,6 +5,8 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: board-server/Dockerfile
|
dockerfile: board-server/Dockerfile
|
||||||
|
environment:
|
||||||
|
- REDIS_HOST=redis
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
@@ -1,14 +1,18 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use redis::{AsyncCommands, RedisError};
|
use redis::{AsyncCommands, RedisError};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
const LEADERBOARD: &str = "leaderboard";
|
const LEADERBOARD: &str = "leaderboard";
|
||||||
type LeaderboardEntry = (String, i32);
|
type LeaderboardEntry = (String, u32);
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
trait Leaderboard {
|
pub trait Leaderboard: Send + Sync {
|
||||||
async fn add_score(&self, player_name: &str, score: i32) -> Result<(), RedisError>;
|
async fn add_score(&self, player_name: &str, score: u32) -> Result<(), RedisError>;
|
||||||
async fn get_highscores(&self) -> Result<Vec<LeaderboardEntry>, RedisError>;
|
async fn get_highscores(&self) -> Result<Vec<LeaderboardEntry>, RedisError>;
|
||||||
|
fn driver(&self) -> &'static str {
|
||||||
|
"in-memory"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RedisLeaderboard {
|
struct RedisLeaderboard {
|
||||||
@@ -23,7 +27,7 @@ impl RedisLeaderboard {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Leaderboard for RedisLeaderboard {
|
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?;
|
let mut con = self.client.get_async_connection().await?;
|
||||||
con.zadd(LEADERBOARD, player_name, score).await?;
|
con.zadd(LEADERBOARD, player_name, score).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -36,13 +40,18 @@ impl Leaderboard for RedisLeaderboard {
|
|||||||
con.zrange_withscores(LEADERBOARD, 0, count - 1).await?;
|
con.zrange_withscores(LEADERBOARD, 0, count - 1).await?;
|
||||||
Ok(leaderboard)
|
Ok(leaderboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn driver(&self) -> &'static str {
|
||||||
|
"redis"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InMemoryLeaderboard();
|
#[derive(Debug, Default)]
|
||||||
|
pub struct InMemoryLeaderboard();
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Leaderboard for InMemoryLeaderboard {
|
impl Leaderboard for InMemoryLeaderboard {
|
||||||
async fn add_score(&self, _: &str, _: i32) -> Result<(), RedisError> {
|
async fn add_score(&self, _: &str, _: u32) -> Result<(), RedisError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,12 +60,12 @@ impl Leaderboard for InMemoryLeaderboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn provide_leaderboard() -> Box<dyn Leaderboard> {
|
pub fn provide_leaderboard() -> Arc<dyn Leaderboard> {
|
||||||
match env::var("REDIS_HOSTNAME") {
|
match env::var("REDIS_HOST") {
|
||||||
Ok(redis_host_name) => match redis::Client::open(format!("redis://{redis_host_name}/")) {
|
Ok(redis_host_name) => match redis::Client::open(format!("redis://{redis_host_name}/")) {
|
||||||
Ok(client) => Box::new(RedisLeaderboard::new(client)),
|
Ok(client) => Arc::new(RedisLeaderboard::new(client)),
|
||||||
Err(_) => Box::new(InMemoryLeaderboard()),
|
Err(_) => Arc::new(InMemoryLeaderboard::default()),
|
||||||
},
|
},
|
||||||
Err(_) => Box::new(InMemoryLeaderboard()),
|
Err(_) => Arc::new(InMemoryLeaderboard::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ mod leaderboard;
|
|||||||
mod player;
|
mod player;
|
||||||
mod room;
|
mod room;
|
||||||
|
|
||||||
|
use crate::leaderboard::{provide_leaderboard, Leaderboard};
|
||||||
use crate::room::{generate_room_name, Room, RoomHandle};
|
use crate::room::{generate_room_name, Room, RoomHandle};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_tungstenite::WebSocketStream;
|
use async_tungstenite::WebSocketStream;
|
||||||
@@ -64,6 +65,7 @@ async fn run_player(
|
|||||||
|
|
||||||
async fn handle_connection(
|
async fn handle_connection(
|
||||||
rooms: Rooms,
|
rooms: Rooms,
|
||||||
|
leaderboard: Arc<dyn Leaderboard>,
|
||||||
raw_stream: Async<TcpStream>,
|
raw_stream: Async<TcpStream>,
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
mut close_room: UnboundedSender<String>,
|
mut close_room: UnboundedSender<String>,
|
||||||
@@ -77,7 +79,7 @@ async fn handle_connection(
|
|||||||
match msg {
|
match msg {
|
||||||
ClientMessage::CreateRoom(player_name) => {
|
ClientMessage::CreateRoom(player_name) => {
|
||||||
let (write, read) = unbounded();
|
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 handle = RoomHandle { write, room };
|
||||||
let room_name = generate_room_name(&mut rooms.lock().unwrap(), handle.clone());
|
let room_name = generate_room_name(&mut rooms.lock().unwrap(), handle.clone());
|
||||||
println!("[{addr}] Creating room '{room_name}' for player '{player_name}'");
|
println!("[{addr}] Creating room '{room_name}' for player '{player_name}'");
|
||||||
@@ -127,6 +129,9 @@ fn main() -> Result<(), io::Error> {
|
|||||||
.parse::<SocketAddr>()
|
.parse::<SocketAddr>()
|
||||||
.expect("Invalid address");
|
.expect("Invalid address");
|
||||||
|
|
||||||
|
let leaderboard = provide_leaderboard();
|
||||||
|
println!("Using {} leaderboard.", leaderboard.driver());
|
||||||
|
|
||||||
let rooms = Rooms::new(Mutex::new(HashMap::new()));
|
let rooms = Rooms::new(Mutex::new(HashMap::new()));
|
||||||
|
|
||||||
let close_room = {
|
let close_room = {
|
||||||
@@ -149,8 +154,11 @@ fn main() -> Result<(), io::Error> {
|
|||||||
while let Ok((stream, addr)) = listener.accept().await {
|
while let Ok((stream, addr)) = listener.accept().await {
|
||||||
let close_room = close_room.clone();
|
let close_room = close_room.clone();
|
||||||
let rooms = rooms.clone();
|
let rooms = rooms.clone();
|
||||||
|
let leaderboard = leaderboard.clone();
|
||||||
smol::spawn(async move {
|
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}");
|
eprintln!("Failed to handle connection from {addr}: {e}");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::leaderboard::{InMemoryLeaderboard, Leaderboard};
|
||||||
use crate::player::Player;
|
use crate::player::Player;
|
||||||
use board_network::protocol::{ClientMessage, ServerMessage};
|
use board_network::protocol::{ClientMessage, ServerMessage};
|
||||||
use board_network::types::TileRef;
|
use board_network::types::TileRef;
|
||||||
@@ -9,16 +10,17 @@ use board_shared::position::Position2d;
|
|||||||
use board_shared::score::calc_score;
|
use board_shared::score::calc_score;
|
||||||
use board_shared::tile::Tile;
|
use board_shared::tile::Tile;
|
||||||
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||||
|
use futures::executor::block_on;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use rand::distributions::{Alphanumeric, DistString};
|
use rand::distributions::{Alphanumeric, DistString};
|
||||||
use std::collections::hash_map::Entry;
|
use std::collections::hash_map::Entry;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
type TaggedClientMessage = (SocketAddr, ClientMessage);
|
type TaggedClientMessage = (SocketAddr, ClientMessage);
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct Room {
|
pub struct Room {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub connections: HashMap<SocketAddr, usize>,
|
pub connections: HashMap<SocketAddr, usize>,
|
||||||
@@ -28,9 +30,18 @@ pub struct Room {
|
|||||||
pub board: Board,
|
pub board: Board,
|
||||||
pub validated_board: Board,
|
pub validated_board: Board,
|
||||||
pub deck: RngDeck,
|
pub deck: RngDeck,
|
||||||
|
pub leaderboard: Arc<dyn Leaderboard>,
|
||||||
|
pub successive_skipped_turns: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Room {
|
impl Room {
|
||||||
|
pub fn with_leaderboard(leaderboard: Arc<dyn Leaderboard>) -> Self {
|
||||||
|
Self {
|
||||||
|
leaderboard,
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_player(
|
pub fn add_player(
|
||||||
&mut self,
|
&mut self,
|
||||||
addr: SocketAddr,
|
addr: SocketAddr,
|
||||||
@@ -87,6 +98,11 @@ impl Room {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.successive_skipped_turns >= self.players.len() {
|
||||||
|
self.on_game_over();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
self.active_player = (self.active_player + 1) % self.players.len();
|
self.active_player = (self.active_player + 1) % self.players.len();
|
||||||
if self.players[self.active_player].ws.is_some() {
|
if self.players[self.active_player].ws.is_some() {
|
||||||
@@ -171,6 +187,10 @@ impl Room {
|
|||||||
|
|
||||||
fn on_validate(&mut self) {
|
fn on_validate(&mut self) {
|
||||||
let diff = self.board.difference(&self.validated_board);
|
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) {
|
if !Board::has_alignment(&diff) {
|
||||||
self.reset_player_moves();
|
self.reset_player_moves();
|
||||||
self.send(
|
self.send(
|
||||||
@@ -215,6 +235,12 @@ impl Room {
|
|||||||
self.players[player_id].score,
|
self.players[player_id].score,
|
||||||
));
|
));
|
||||||
self.validated_board = self.board.clone();
|
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) {
|
fn reset_player_moves(&mut self) {
|
||||||
@@ -226,6 +252,15 @@ impl Room {
|
|||||||
self.sync_hand(self.active_player);
|
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) {
|
fn on_client_disconnected(&mut self, addr: SocketAddr) {
|
||||||
if let Some(p) = self.connections.remove(&addr) {
|
if let Some(p) = self.connections.remove(&addr) {
|
||||||
self.players[p].ws = None;
|
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<Mutex<Room>>;
|
type RoomPtr = Arc<Mutex<Room>>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
Reference in New Issue
Block a user