Allow joining an existing room and validating tile placement
continuous-integration/drone/push Build is passing Details

main
Clément FRÉVILLE 2 years ago
parent f1564ca6e3
commit ec6542aa52

@ -2,7 +2,7 @@ use crate::types::{Position2dRef, TileRef};
use board_shared::{position::Position2d, tile::Tile}; use board_shared::{position::Position2d, tile::Tile};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub enum ClientMessage { pub enum ClientMessage {
/// Creates a new room and join it with the given player name. /// Creates a new room and join it with the given player name.
/// ///
@ -12,9 +12,10 @@ pub enum ClientMessage {
Disconnected, Disconnected,
TileUse(#[serde(with = "Position2dRef")] Position2d, usize), TileUse(#[serde(with = "Position2dRef")] Position2d, usize),
TileTake(usize), TileTake(usize),
Validate,
} }
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub enum ServerMessage { pub enum ServerMessage {
/// Informs that a room has been joined. /// Informs that a room has been joined.
JoinedRoom { JoinedRoom {
@ -22,6 +23,7 @@ pub enum ServerMessage {
players: Vec<(String, u32, bool)>, players: Vec<(String, u32, bool)>,
active_player: usize, active_player: usize,
}, },
JoinFailed(String),
/// Notify that new player has joined the game. /// Notify that new player has joined the game.
PlayerConnected(String), PlayerConnected(String),
/// Notify that new player has rejoined the game. /// Notify that new player has rejoined the game.
@ -30,6 +32,8 @@ pub enum ServerMessage {
PlayerDisconnected(usize), PlayerDisconnected(usize),
/// Change the current player /// Change the current player
PlayerTurn(usize), PlayerTurn(usize),
/// Update the current hand of the player
SyncHand(#[serde(with = "TileRef")] Tile), // TODO: Vec<Tile>
/// Informs that a tile has been placed /// Informs that a tile has been placed
TilePlaced( TilePlaced(
#[serde(with = "Position2dRef")] Position2d, #[serde(with = "Position2dRef")] Position2d,
@ -37,4 +41,5 @@ pub enum ServerMessage {
), ),
/// Informs that a tile has been removed /// Informs that a tile has been removed
TileRemoved(#[serde(with = "Position2dRef")] Position2d), TileRemoved(#[serde(with = "Position2dRef")] Position2d),
TurnRejected(String),
} }

@ -2,7 +2,7 @@ use board_shared::position::Position2d;
use board_shared::tile::{Digit, Operator, Tile}; use board_shared::tile::{Digit, Operator, Tile};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(remote = "Tile")] #[serde(remote = "Tile")]
pub enum TileRef { pub enum TileRef {
Digit(#[serde(with = "DigitRef")] Digit), Digit(#[serde(with = "DigitRef")] Digit),
@ -10,7 +10,7 @@ pub enum TileRef {
Equals, Equals,
} }
#[derive(Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(remote = "Digit")] #[serde(remote = "Digit")]
pub struct DigitRef { pub struct DigitRef {
pub value: i8, pub value: i8,
@ -18,7 +18,7 @@ pub struct DigitRef {
pub has_right_parenthesis: bool, pub has_right_parenthesis: bool,
} }
#[derive(Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(remote = "Operator")] #[serde(remote = "Operator")]
pub enum OperatorRef { pub enum OperatorRef {
Add, Add,
@ -27,7 +27,7 @@ pub enum OperatorRef {
Divide, Divide,
} }
#[derive(Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(remote = "Position2d")] #[serde(remote = "Position2d")]
pub struct Position2dRef { pub struct Position2dRef {
pub x: usize, pub x: usize,

@ -4,7 +4,7 @@ mod room;
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;
use board_network::protocol::ClientMessage; use board_network::protocol::{ClientMessage, ServerMessage};
use futures::channel::mpsc::{unbounded, UnboundedSender}; use futures::channel::mpsc::{unbounded, UnboundedSender};
use futures::future::join; use futures::future::join;
use futures::{future, SinkExt, StreamExt}; use futures::{future, SinkExt, StreamExt};
@ -94,6 +94,23 @@ async fn handle_connection(
return Ok(()); 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:?}"), msg => eprintln!("[{addr}] Received illegal message {msg:?}"),
} }
} }

@ -1,9 +1,11 @@
use board_network::protocol::ServerMessage; use board_network::protocol::ServerMessage;
use board_shared::game::Hand;
use futures::channel::mpsc::UnboundedSender; use futures::channel::mpsc::UnboundedSender;
#[derive(Debug)] #[derive(Debug)]
pub struct Player { pub struct Player {
pub name: String, pub name: String,
pub score: u32, pub score: u32,
pub hand: Hand,
pub ws: Option<UnboundedSender<ServerMessage>>, pub ws: Option<UnboundedSender<ServerMessage>>,
} }

@ -1,5 +1,10 @@
use crate::player::Player; use crate::player::Player;
use board_network::protocol::{ClientMessage, ServerMessage}; 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::channel::mpsc::{UnboundedReceiver, UnboundedSender};
use futures::StreamExt; use futures::StreamExt;
use rand::distributions::{Alphanumeric, DistString}; use rand::distributions::{Alphanumeric, DistString};
@ -16,6 +21,9 @@ pub struct Room {
pub connections: HashMap<SocketAddr, usize>, pub connections: HashMap<SocketAddr, usize>,
pub players: Vec<Player>, pub players: Vec<Player>,
pub active_player: usize, pub active_player: usize,
pub board: Board,
pub validated_board: Board,
pub deck: RngDeck,
} }
impl Room { impl Room {
@ -43,9 +51,13 @@ impl Room {
self.broadcast(ServerMessage::PlayerConnected(player_name.clone())); self.broadcast(ServerMessage::PlayerConnected(player_name.clone()));
player_index = Some(self.players.len()); player_index = Some(self.players.len());
let mut hand = Hand::default();
hand.complete(&mut self.deck)?;
self.players.push(Player { self.players.push(Player {
name: player_name, name: player_name,
score: 0, score: 0,
hand,
ws: Some(tx.clone()), ws: Some(tx.clone()),
}); });
} }
@ -67,17 +79,91 @@ impl Room {
Ok(()) 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 { pub fn on_message(&mut self, addr: SocketAddr, msg: ClientMessage) -> bool {
match msg { match msg {
ClientMessage::Disconnected => self.on_client_disconnected(addr), ClientMessage::Disconnected => self.on_client_disconnected(addr),
ClientMessage::CreateRoom(_) | ClientMessage::JoinRoom(_, _) => { ClientMessage::CreateRoom(_) | ClientMessage::JoinRoom(_, _) => {
eprintln!("[{}] Illegal client message {:?}", self.name, msg); 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!(), _ => todo!(),
} }
!self.connections.is_empty() !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) { 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;
@ -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<Mutex<Room>>; type RoomPtr = Arc<Mutex<Room>>;

@ -1,3 +1,4 @@
use std::error::Error;
use crate::tile::Operator; use crate::tile::Operator;
use enum_map::EnumMap; use enum_map::EnumMap;
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
@ -6,7 +7,14 @@ type DeckSize = u16;
type DigitDeck = [DeckSize; 19]; type DigitDeck = [DeckSize; 19];
/// When a deck is empty, new tiles cannot be retrieved. /// 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. /// A entire deck of tiles.
#[derive(Debug, Clone, Default, PartialEq)] #[derive(Debug, Clone, Default, PartialEq)]

@ -35,12 +35,20 @@ impl Hand {
pub fn complete(&mut self, deck: &mut RngDeck) -> Result<(), EmptyDeckError> { pub fn complete(&mut self, deck: &mut RngDeck) -> Result<(), EmptyDeckError> {
for _ in 0..self.count_missing_operators() { for _ in 0..self.count_missing_operators() {
self.tiles 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() { for _ in 0..self.count_missing_numbers() {
self.tiles 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(()) Ok(())
} }
pub fn remove(&mut self, idx: usize) -> Option<Tile> {
if idx < self.tiles.len() {
Some(self.tiles.remove(idx))
} else {
None
}
}
} }

Loading…
Cancel
Save