Allow joining an existing room and validating tile placement
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user