From c8e742039614c12ea9a376b7569ad423a74635a1 Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Sat, 7 Oct 2023 20:46:24 +0200 Subject: [PATCH] wip server & reducer --- server/src/main.rs | 51 ++++- store/src/board.rs | 467 ++++++++++++++++++++++++++++++++++++++++++++ store/src/dice.rs | 79 ++++++++ store/src/error.rs | 100 ++++++++++ store/src/game.rs | 333 +++++++++++++++++++++++++++++++ store/src/lib.rs | 6 + store/src/player.rs | 73 +++++++ 7 files changed, 1108 insertions(+), 1 deletion(-) create mode 100644 store/src/board.rs create mode 100644 store/src/dice.rs create mode 100644 store/src/error.rs create mode 100644 store/src/game.rs create mode 100644 store/src/player.rs diff --git a/server/src/main.rs b/server/src/main.rs index e7a11a9..0f89ced 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,3 +1,52 @@ +use log::{info, trace}; +use renet::{RenetConnectionConfig, RenetServer, ServerAuthentication, ServerConfig, ServerEvent}; +use std::net::{SocketAddr, UdpSocket}; +use std::time::{Duration, Instant, SystemTime}; + +// Only clients that can provide the same PROTOCOL_ID that the server is using will be able to connect. +// This can be used to make sure players use the most recent version of the client for instance. +pub const PROTOCOL_ID: u64 = 2878; + fn main() { - println!("Hello, world!"); + env_logger::init(); + + let server_addr: SocketAddr = "127.0.0.1:5000".parse().unwrap(); + let mut server: RenetServer = RenetServer::new( + // Pass the current time to renet, so it can use it to order messages + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(), + // Pass a server configuration specifying that we want to allow only 2 clients to connect + // and that we don't want to authenticate them. Everybody is welcome! + ServerConfig::new(2, PROTOCOL_ID, server_addr, ServerAuthentication::Unsecure), + // Pass the default connection configuration. + // This will create a reliable, unreliable and blocking channel. + // We only actually need the reliable one, but we can just not use the other two. + RenetConnectionConfig::default(), + UdpSocket::bind(server_addr).unwrap(), + ) + .unwrap(); + + trace!("❂ TricTrac server listening on {}", server_addr); + + let mut last_updated = Instant::now(); + loop { + // Update server time + let now = Instant::now(); + server.update(now - last_updated).unwrap(); + last_updated = now; + + // Receive connection events from clients + while let Some(event) = server.get_event() { + match event { + ServerEvent::ClientConnected(id, _user_data) => { + info!("🎉 Client {} connected.", id); + } + ServerEvent::ClientDisconnected(id) => { + info!("👋 Client {} disconnected", id); + } + } + } + std::thread::sleep(Duration::from_millis(50)); + } } diff --git a/store/src/board.rs b/store/src/board.rs new file mode 100644 index 0000000..17703b5 --- /dev/null +++ b/store/src/board.rs @@ -0,0 +1,467 @@ +use crate::Player; +use crate::Error; +use serde::{Deserialize, Serialize}; + +/// Represents the Backgammon board +/// +/// A Backgammon board consists of 24 fields, each of which can hold 0 or more checkers. In +/// addition there is a bar to hold checkers that have been hit and an off area to hold checkers +/// that have been removed from the board. +/// +/// ``` +/// # fn foo() {} +/// // +12-11-10--9--8--7-------6--5--4--3--2--1-+ +/// // | X O | | O X | +-------+ +/// // | X O | | O X | | OFF O | +/// // | X O | | O | +-------+ +/// // | X | | O | +/// // | X | | O | +/// // | |BAR| | +/// // | O | | X | +/// // | O | | X | +/// // | O X | | X | +-------+ +/// // | O X | | X O | | OFF X | +/// // | O X | | X O | +-------+ +/// // +13-14-15-16-17-18------19-20-21-22-23-24-+ +/// ``` + +#[derive(Debug, Clone, Serialize, PartialEq, Deserialize, Default)] +pub struct Board { + raw_board: (PlayerBoard, PlayerBoard), +} + +/// Represents the Backgammon board for both players (to be used for graphical representation). +#[derive(Debug, Serialize, PartialEq, Deserialize)] +pub struct BoardDisplay { + /// The board represented as an array of 24 fields, each of which can hold 0 or more checkers. + /// Positive amounts represent checkers of player 0, negative amounts represent checkers of + /// player 1. + pub board: [i8; 24], + /// The off for both players + pub off: (u8, u8), +} + +impl Board { + /// Create a new board + pub fn new() -> Self { + Board::default() + } + + /// Get the board for both players. Use for graphical representation of the board. + /// + /// This method outputs a tuple with three values: + /// + /// 1. the board represented as an array of 24 fields, each of which can hold 0 or more + /// checkers. Positive amounts represent checkers of player 0, negative amounts represent + /// checkers of player 1. + /// 3. the off for both players + pub fn get(&self) -> BoardDisplay { + let mut board: [i8; 24] = [0; 24]; + + for (i, val) in board.iter_mut().enumerate() { + *val = self.raw_board.0.board[i] as i8 - self.raw_board.1.board[23 - i] as i8; + } + + BoardDisplay { + board, + bar: self.get_bar(), + off: self.get_off(), + } + } + + /// Get the off for both players + fn get_off(&self) -> (u8, u8) { + (self.raw_board.0.off, self.raw_board.1.off) + } + + /// Set checkers for a player on a field + /// + /// This method adds the amount of checkers for a player on a field. The field is numbered from + /// 0 to 23, starting from the last field of each player in the home board, the most far away + /// field for each player (where there are 2 checkers to start with) is number 23. + /// + /// If the field is blocked for the player, an error is returned. If the field is not blocked, + /// but there is already one checker from the other player on the field, that checker is hit and + /// moved to the bar. + pub fn set(&mut self, player: Player, field: usize, amount: i8) -> Result<(), Error> { + if field > 23 { + return Err(Error::FieldInvalid); + } + + if self.blocked(player, field)? { + return Err(Error::FieldBlocked); + } + + match player { + Player::Player0 => { + let new = self.raw_board.0.board[field] as i8 + amount; + if new < 0 { + return Err(Error::MoveInvalid); + } + self.raw_board.0.board[field] = new as u8; + + // in case one opponent's checker is hit, move it to the bar + self.raw_board.1.bar += self.raw_board.1.board[23 - field]; + self.raw_board.1.board[23 - field] = 0; + Ok(()) + } + Player::Player1 => { + let new = self.raw_board.1.board[field] as i8 + amount; + if new < 0 { + return Err(Error::MoveInvalid); + } + self.raw_board.1.board[field] = new as u8; + + // in case one opponent's checker is hit, move it to the bar + self.raw_board.0.bar += self.raw_board.0.board[23 - field]; + self.raw_board.0.board[23 - field] = 0; + Ok(()) + } + Player::Nobody => Err(Error::PlayerInvalid), + } + } + + /// Check if a field is blocked for a player + pub fn blocked(&self, player: Player, field: usize) -> Result { + if field > 23 { + return Err(Error::FieldInvalid); + } + + match player { + Player::Player0 => { + if self.raw_board.1.board[23 - field] > 1 { + Ok(true) + } else { + Ok(false) + } + } + Player::Player1 => { + if self.raw_board.0.board[23 - field] > 1 { + Ok(true) + } else { + Ok(false) + } + } + Player::Nobody => Err(Error::PlayerInvalid), + } + } + + /// Set checkers for a player on the bar. This method adds amount to the already existing + /// checkers there. + pub fn set_bar(&mut self, player: Player, amount: i8) -> Result<(), Error> { + match player { + Player::Player0 => { + let new = self.raw_board.0.bar as i8 + amount; + if new < 0 { + return Err(Error::MoveInvalid); + } + self.raw_board.0.bar = new as u8; + Ok(()) + } + Player::Player1 => { + let new = self.raw_board.1.bar as i8 + amount; + if new < 0 { + return Err(Error::MoveInvalid); + } + self.raw_board.1.bar = new as u8; + Ok(()) + } + Player::Nobody => Err(Error::PlayerInvalid), + } + } + + /// Set checkers for a player off the board. This method adds amount to the already existing + /// checkers there. + pub fn set_off(&mut self, player: Player, amount: u8) -> Result<(), Error> { + match player { + Player::Player0 => { + let new = self.raw_board.0.off + amount; + self.raw_board.0.off = new; + Ok(()) + } + Player::Player1 => { + let new = self.raw_board.1.off + amount; + self.raw_board.1.off = new; + Ok(()) + } + Player::Nobody => Err(Error::PlayerInvalid), + } + } +} + +/// Represents the Backgammon board for one player +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +struct PlayerBoard { + board: [u8; 24], + bar: u8, + off: u8, +} + +impl Default for PlayerBoard { + fn default() -> Self { + PlayerBoard { + board: [ + 0, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, + ], + bar: 0, + off: 0, + } + } +} + +/// Trait to move checkers +pub trait Move { + /// Move a checker + fn move_checker(&mut self, player: Player, dice: u8, from: usize) -> Result<&mut Self, Error> + where + Self: Sized; + + /// Move a checker from bar + fn move_checker_from_bar(&mut self, player: Player, dice: u8) -> Result<&mut Self, Error> + where + Self: Sized; + + /// Move permitted + fn move_permitted(&mut self, player: Player, dice: u8) -> Result<&mut Self, Error> + where + Self: Sized; +} + +// Unit Tests +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_board() { + assert_eq!(Board::new(), Board::default()); + } + + #[test] + fn default_player_board() { + assert_eq!( + PlayerBoard::default(), + PlayerBoard { + board: [0, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2,], + bar: 0, + off: 0 + } + ); + } + + #[test] + fn get_board() { + let board = Board::new(); + assert_eq!( + board.get(), + BoardDisplay { + board: [ + -2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, -5, 5, 0, 0, 0, -3, 0, -5, 0, 0, 0, 0, 2, + ], + bar: (0, 0), + off: (0, 0) + } + ); + } + + #[test] + fn get_bar() { + let board = Board::new(); + assert_eq!(board.get_bar(), (0, 0)); + } + + #[test] + fn get_off() { + let board = Board::new(); + assert_eq!(board.get_off(), (0, 0)); + } + + #[test] + fn set_player0() -> Result<(), Error> { + let mut board = Board::new(); + board.set(Player::Player0, 1, 1)?; + assert_eq!(board.get().board[1], 1); + Ok(()) + } + + #[test] + fn set_player1() -> Result<(), Error> { + let mut board = Board::new(); + board.set(Player::Player1, 2, 1)?; + assert_eq!(board.get().board[21], -1); + Ok(()) + } + + #[test] + fn set_player0_bar() -> Result<(), Error> { + let mut board = Board::new(); + board.set_bar(Player::Player0, 1)?; + assert_eq!(board.get().bar.0, 1); + Ok(()) + } + + #[test] + fn set_player1_bar() -> Result<(), Error> { + let mut board = Board::new(); + board.set_bar(Player::Player1, 1)?; + assert_eq!(board.get().bar.1, 1); + Ok(()) + } + + #[test] + fn set_player0_off() -> Result<(), Error> { + let mut board = Board::new(); + board.set_off(Player::Player0, 1)?; + assert_eq!(board.get().off.0, 1); + Ok(()) + } + + #[test] + fn set_player1_off() -> Result<(), Error> { + let mut board = Board::new(); + board.set_off(Player::Player1, 1)?; + assert_eq!(board.get().off.1, 1); + Ok(()) + } + + #[test] + fn set_player1_off1() -> Result<(), Error> { + let mut board = Board::new(); + board.set_off(Player::Player1, 1)?; + board.set_off(Player::Player1, 1)?; + assert_eq!(board.get().off.1, 2); + Ok(()) + } + + #[test] + fn set_invalid_player() { + let mut board = Board::new(); + assert!(board.set(Player::Nobody, 0, 1).is_err()); + assert!(board.set_bar(Player::Nobody, 1).is_err()); + assert!(board.set_off(Player::Nobody, 1).is_err()); + } + + #[test] + fn blocked_player0() -> Result<(), Error> { + let board = Board::new(); + assert!(board.blocked(Player::Player0, 0)?); + Ok(()) + } + + #[test] + fn blocked_player1() -> Result<(), Error> { + let board = Board::new(); + assert!(board.blocked(Player::Player1, 0)?); + Ok(()) + } + + #[test] + fn blocked_player0_a() -> Result<(), Error> { + let mut board = Board::new(); + board.set(Player::Player1, 1, 2)?; + assert!(board.blocked(Player::Player0, 22)?); + Ok(()) + } + + #[test] + fn blocked_player1_a() -> Result<(), Error> { + let mut board = Board::new(); + board.set(Player::Player0, 1, 2)?; + assert!(board.blocked(Player::Player1, 22)?); + Ok(()) + } + + #[test] + fn blocked_invalid_player() { + let board = Board::new(); + assert!(board.blocked(Player::Nobody, 0).is_err()); + } + + #[test] + fn blocked_invalid_field() { + let board = Board::new(); + assert!(board.blocked(Player::Player0, 24).is_err()); + } + + #[test] + fn set_field_with_1_checker_player0_a() -> Result<(), Error> { + let mut board = Board::new(); + board.set(Player::Player0, 1, 1)?; + board.set(Player::Player1, 22, 1)?; + assert_eq!(board.get().board[1], -1); + assert_eq!(board.get().bar.0, 1); + Ok(()) + } + + #[test] + fn set_field_with_1_checker_player0_b() -> Result<(), Error> { + let mut board = Board::new(); + board.set(Player::Player0, 1, 1)?; + board.set_bar(Player::Player0, 5)?; + board.set(Player::Player1, 22, 1)?; + assert_eq!(board.get().board[1], -1); + assert_eq!(board.get().bar.0, 6); + Ok(()) + } + + #[test] + fn set_field_with_1_checker_player1_a() -> Result<(), Error> { + let mut board = Board::new(); + board.set(Player::Player1, 1, 1)?; + board.set(Player::Player0, 22, 1)?; + assert_eq!(board.get().board[22], 1); + assert_eq!(board.get().bar.1, 1); + Ok(()) + } + + #[test] + fn set_field_with_1_checker_player1_b() -> Result<(), Error> { + let mut board = Board::new(); + board.set(Player::Player1, 1, 1)?; + board.set_bar(Player::Player1, 5)?; + board.set(Player::Player0, 22, 1)?; + assert_eq!(board.get().board[22], 1); + assert_eq!(board.get().bar.1, 6); + Ok(()) + } + + #[test] + fn set_field_with_2_checkers_player0_a() -> Result<(), Error> { + let mut board = Board::new(); + board.set(Player::Player0, 23, 2)?; + assert_eq!(board.get().board[23], 4); + Ok(()) + } + + #[test] + fn set_field_with_2_checkers_player0_b() -> Result<(), Error> { + let mut board = Board::new(); + board.set(Player::Player0, 23, -1)?; + assert_eq!(board.get().board[23], 1); + Ok(()) + } + + #[test] + fn set_field_blocked() { + let mut board = Board::new(); + assert!(board.set(Player::Player0, 0, 2).is_err()); + } + + #[test] + fn set_wrong_field1() { + let mut board = Board::new(); + assert!(board.set(Player::Player0, 50, 2).is_err()); + } + + #[test] + fn set_wrong_amount0() { + let mut board = Board::new(); + assert!(board.set(Player::Player0, 23, -3).is_err()); + } + + #[test] + fn set_wrong_amount1() { + let mut board = Board::new(); + assert!(board.set(Player::Player1, 23, -3).is_err()); + } +} diff --git a/store/src/dice.rs b/store/src/dice.rs new file mode 100644 index 0000000..3c3a006 --- /dev/null +++ b/store/src/dice.rs @@ -0,0 +1,79 @@ +use crate::Error; +use rand::distributions::{Distribution, Uniform}; +use serde::{Deserialize, Serialize}; + +/// Represents the two dices +/// +/// Backgammon is always played with two dices. +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Deserialize, Default)] +pub struct Dices { + /// The two dice values + pub values: (u8, u8), + /// Boolean indicating whether the dices have been consumed already. We use a tuple + /// of four booleans in case the dices are equal, in which case we have four dices + /// to play. + pub consumed: (bool, bool, bool, bool), +} +impl Dices { + /// Roll the dices which generates two random numbers between 1 and 6, replicating a perfect + /// dice. We use the operating system's random number generator. + pub fn roll(self) -> Self { + let between = Uniform::new_inclusive(1, 6); + let mut rng = rand::thread_rng(); + + let v = (between.sample(&mut rng), between.sample(&mut rng)); + + // if both dices are equal, we have four dices to play + if v.0 == v.1 { + Dices { + values: (v.0, v.1), + consumed: (false, false, false, false), + } + } else { + Dices { + values: (v.0, v.1), + consumed: (false, false, true, true), + } + } + } +} + +/// Trait to roll the dices +pub trait Roll { + /// Roll the dices + fn roll(&mut self) -> Result<&mut Self, Error>; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_roll() { + let dices = Dices::default().roll(); + assert!(dices.values.0 >= 1 && dices.values.0 <= 6); + assert!(dices.values.1 >= 1 && dices.values.1 <= 6); + } + + #[test] + fn test_roll_consumed() { + let dices = Dices::default().roll(); + if dices.values.0 == dices.values.1 { + assert_eq!(dices.consumed, (false, false, false, false)); + } else { + assert_eq!(dices.consumed, (false, false, true, true)); + } + } + + #[test] + fn test_roll_consumed1() { + for _i in 0..100 { + let dices = Dices::default().roll(); + if dices.values.0 == dices.values.1 { + assert_eq!(dices.consumed, (false, false, false, false)); + } else { + assert_eq!(dices.consumed, (false, false, true, true)); + } + } + } +} diff --git a/store/src/error.rs b/store/src/error.rs new file mode 100644 index 0000000..694009a --- /dev/null +++ b/store/src/error.rs @@ -0,0 +1,100 @@ +/// This module contains the error definition for the Backgammon game. +use std::fmt; + +/// Holds all possible errors that can occur during a Backgammon game. +#[derive(Debug)] +pub enum Error { + /// Game has already started + GameStarted, + /// Game has already ended + GameEnded, + /// Opponent offered doubling cube. Need to react on this event first. + CubeReceived, + /// Doubling not permitted + DoublingNotPermitted, + /// Invalid cube value + CubeValueInvalid, + /// Invalid player + PlayerInvalid, + /// Field blocked + FieldBlocked, + /// Invalid field + FieldInvalid, + /// Not your turn + NotYourTurn, + /// Invalid move + MoveInvalid, + /// Invalid move, checker on bar + MoveInvalidBar, + /// Move first + MoveFirst, + /// Roll first + RollFirst, + /// Dice Invalid + DiceInvalid, +} + +// implement Error trait +impl std::error::Error for Error {} + +// implement Display trait +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::GameStarted => write!(f, "Game has already started"), + Error::GameEnded => write!(f, "Game has already ended"), + Error::PlayerInvalid => write!(f, "Invalid player"), + Error::CubeReceived => { + write!( + f, + "Opponent offered dice. Need to first accept or decline the doubling dice." + ) + } + Error::CubeValueInvalid => write!(f, "Invalid cube value"), + Error::DoublingNotPermitted => write!(f, "Doubling not permitted"), + Error::FieldBlocked => write!(f, "Field blocked"), + Error::FieldInvalid => write!(f, "Invalid field"), + Error::NotYourTurn => write!(f, "Not your turn"), + Error::MoveInvalid => write!(f, "Invalid move"), + Error::MoveFirst => write!(f, "Move first"), + Error::RollFirst => write!(f, "Roll first"), + Error::DiceInvalid => write!(f, "Invalid dice"), + Error::MoveInvalidBar => write!(f, "Invalid move, checker on bar"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_display() { + assert_eq!( + format!("{}", Error::GameStarted), + "Game has already started" + ); + assert_eq!(format!("{}", Error::GameEnded), "Game has already ended"); + assert_eq!(format!("{}", Error::PlayerInvalid), "Invalid player"); + assert_eq!( + format!("{}", Error::CubeReceived), + "Opponent offered dice. Need to first accept or decline the doubling dice." + ); + assert_eq!(format!("{}", Error::CubeValueInvalid), "Invalid cube value"); + assert_eq!( + format!("{}", Error::DoublingNotPermitted), + "Doubling not permitted" + ); + assert_eq!(format!("{}", Error::FieldBlocked), "Field blocked"); + assert_eq!(format!("{}", Error::FieldInvalid), "Invalid field"); + assert_eq!(format!("{}", Error::NotYourTurn), "Not your turn"); + assert_eq!(format!("{}", Error::MoveInvalid), "Invalid move"); + assert_eq!(format!("{}", Error::MoveFirst), "Move first"); + assert_eq!(format!("{}", Error::RollFirst), "Roll first"); + assert_eq!(format!("{}", Error::DiceInvalid), "Invalid dice"); + assert_eq!( + format!("{}", Error::MoveInvalidBar), + "Invalid move, checker on bar" + ); + } +} diff --git a/store/src/game.rs b/store/src/game.rs new file mode 100644 index 0000000..9b4bca5 --- /dev/null +++ b/store/src/game.rs @@ -0,0 +1,333 @@ +//! # Play a TricTrac Game +use crate::CurrentPlayer; +use crate::{Player, PlayerId}; +use crate::{Board, Move}; +use crate::{Dices, Roll}; +use crate::Error; + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fmt; + +/// The different states a game can be in. (not to be confused with the entire "GameState") +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum Stage { + PreGame, + InGame, + Ended, +} + +/// Represents a TricTrac game +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct GameState { + pub stage: Stage, + pub board: Board, + pub active_player_id: PlayerId, + pub players: HashMap, + pub history: Vec, + /// last dice pair rolled + pub dices: Dices, + /// true if player needs to roll first + roll_first: bool, +} + +// implement Display trait +impl fmt::Display for GameState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut s = String::new(); + s.push_str(&format!("Dices: {:?}\n", self.dices)); + s.push_str(&format!("Who plays: {}\n", self.who_plays)); + s.push_str(&format!("Board: {:?}\n", self.board.get())); + write!(f, "{}", s) + } +} + +impl Default for GameState { + fn default() -> Self { + Self { + stage: Stage::PreGame, + board: Board::default(), + active_player_id: 0, + players: HashMap::new(), + history: Vec::new(), + dices: Dices::default(), + roll_first: true, + } + } +} + +impl GameState { + /// Create a new default game + pub fn new() -> Self { + GameState::default() + } + + /// Determines whether an event is valid considering the current GameState + pub fn validate(&self, event: &GameEvent) -> bool { + use GameEvent::*; + match event { + BeginGame { goes_first } => { + // Check that the player supposed to go first exists + if !self.players.contains_key(goes_first) { + return false; + } + + // Check that the game hasn't started yet. (we don't want to double start a game) + if self.stage != Stage::PreGame { + return false; + } + } + EndGame { reason } => match reason { + EndGameReason::PlayerWon { winner: _ } => { + // Check that the game has started before someone wins it + if self.stage != Stage::InGame { + return false; + } + } + _ => {} + }, + PlayerJoined { player_id, name: _ } => { + // Check that there isn't another player with the same id + if self.players.contains_key(player_id) { + return false; + } + } + PlayerDisconnected { player_id } => { + // Check player exists + if !self.players.contains_key(player_id) { + return false; + } + } + Roll { player_id } => { + // Check player exists + if !self.players.contains_key(player_id) { + return false; + } + // Check player is currently the one making their move + if self.active_player_id != *player_id { + return false; + } + + } + Move { player_id, at } => { + // Check player exists + if !self.players.contains_key(player_id) { + return false; + } + // Check player is currently the one making their move + if self.active_player_id != *player_id { + return false; + } + + // Check that the tile index is inside the board + if *at > 23 { + return false; + } + } + } + + // We couldn't find anything wrong with the event so it must be good + true + } + + /// Consumes an event, modifying the GameState and adding the event to its history + /// NOTE: consume assumes the event to have already been validated and will accept *any* event passed to it + pub fn consume(&mut self, valid_event: &GameEvent) { + use GameEvent::*; + match valid_event { + BeginGame { goes_first } => { + self.active_player_id = *goes_first; + self.stage = Stage::InGame; + } + EndGame { reason: _ } => self.stage = Stage::Ended, + PlayerJoined { player_id, name } => { + self.players.insert( + *player_id, + Player { + name: name.to_string(), + }, + ); + } + PlayerDisconnected { player_id } => { + self.players.remove(player_id); + } + Roll { player_id } => { + } + Move { player_id, from, to } => { + let player = self.players.get(player_id).unwrap(); + self.board.set(player, at, 1); + self.board.set(player, at, 1); + self.active_player_id = self + .players + .keys() + .find(|id| *id != player_id) + .unwrap() + .clone(); + } + } + + self.history.push(valid_event.clone()); + } +} + + +/// The reasons why a game could end +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Deserialize)] +pub enum EndGameReason { + // In tic tac toe it doesn't make sense to keep playing when one of the players disconnect. + // Note that it might make sense to keep playing in some other game (like Team Fight Tactics for instance). + PlayerLeft { player_id: PlayerId }, + PlayerWon { winner: PlayerId }, +} + +/// An event that progresses the GameState forward +#[derive(Debug, Clone, Serialize, PartialEq, Deserialize)] +pub enum GameEvent { + BeginGame { goes_first: PlayerId }, + EndGame { reason: EndGameReason }, + PlayerJoined { player_id: PlayerId, name: String }, + PlayerDisconnected { player_id: PlayerId }, + Roll { player_id: PlayerId }, + Move { player_id: PlayerId, at: usize }, +} + + + +impl Roll for GameState { + fn roll(&mut self) -> Result<&mut Self, Error> { + if !self.dices.consumed.0 + && !self.dices.consumed.1 + { + return Err(Error::MoveFirst); + } + + self.dices = self.dices.roll(); + if self.who_plays == Player::Nobody { + let diff = self.dices.values.0 - self.dices.values.1; + match diff { + 0 => { + self.who_plays = Player::Nobody; + } + _ if diff > 0 => { + self.who_plays = Player::Player0; + } + _ => { + self.who_plays = Player::Player1; + } + } + } + Ok(self) + } +} + +impl Move for GameState { + fn move_checker(&mut self, player: Player, dice: u8, from: usize) -> Result<&mut Self, Error> { + // check if move is permitted + let _ = self.move_permitted(player, dice)?; + + // check if the dice value has been consumed + if (dice == self.dices.values.0 && self.dices.consumed.0) + || (dice == self.dices.values.1 && self.dices.consumed.1) + { + return Err(Error::MoveInvalid); + } + + // remove checker from old position + self.board.set(player, from, -1)?; + + // move checker to new position, in case it is reaching the off position, set it off + let new_position = from as i8 - dice as i8; + if new_position < 0 { + self.board.set_off(player, 1)?; + } else { + self.board.set(player, new_position as usize, 1)?; + } + + // set dice value to consumed + if dice == self.dices.values.0 && !self.dices.consumed.0 { + self.dices.consumed.0 = true; + } else if dice == self.dices.values.1 && !self.dices.consumed.1 { + self.dices.consumed.1 = true; + } + + // switch to other player if all dices have been consumed + if self.dices.consumed.0 + && self.dices.consumed.1 + { + self.who_plays = self.who_plays.other(); + self.roll_first = true; + } + + Ok(self) + } + + fn move_checker_from_bar(&mut self, player: Player, dice: u8) -> Result<&mut Self, Error> { + // check if move is permitted + let _ = self.move_permitted(player, dice)?; + + // check if the dice value has been consumed + if (dice == self.dices.values.0 && self.dices.consumed.0) + || (dice == self.dices.values.1 && self.dices.consumed.1) + { + return Err(Error::MoveInvalid); + } + + // set dice value to consumed + if dice == self.dices.values.0 && !self.dices.consumed.0 { + self.dices.consumed.0 = true; + } else if dice == self.dices.values.1 && !self.dices.consumed.1 { + self.dices.consumed.1 = true; + } + + // switch to other player if all dices have been consumed + if self.dices.consumed.0 + && self.dices.consumed.1 + { + self.who_plays = self.who_plays.other(); + self.roll_first = true; + } + + Ok(self) + } + + /// Implements checks to validate if the player is allowed to move + fn move_permitted(&mut self, player: Player, dice: u8) -> Result<&mut Self, Error> { + // check if player is allowed to move + if player != self.who_plays { + return Err(Error::NotYourTurn); + } + + // if player is nobody, you can not play and have to roll first + if self.who_plays == Player::Nobody { + return Err(Error::RollFirst); + } + + // check if player has to roll first + if self.roll_first { + return Err(Error::RollFirst); + } + + // check if dice value has actually been rolled + if dice != self.dices.values.0 && dice != self.dices.values.1 { + return Err(Error::DiceInvalid); + } + + Ok(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Test Display trait for Game + #[test] + fn test_display() { + let g = Game::new(); + assert_eq!( + format!("{}", g), + "Rules: Points: 7\nDices: Dices { values: (0, 0), consumed: (false, false, false, false) }\nWho plays: Nobody\nBoard: BoardDisplay { board: [-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, -5, 5, 0, 0, 0, -3, 0, -5, 0, 0, 0, 0, 2], bar: (0, 0), off: (0, 0) }\n" + ); + } + +} diff --git a/store/src/lib.rs b/store/src/lib.rs index e69de29..bc0bac1 100644 --- a/store/src/lib.rs +++ b/store/src/lib.rs @@ -0,0 +1,6 @@ +mod game; +pub use game::Game; + +mod player; +pub use player::Player; + diff --git a/store/src/player.rs b/store/src/player.rs new file mode 100644 index 0000000..efa5fa6 --- /dev/null +++ b/store/src/player.rs @@ -0,0 +1,73 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; + + +// This just makes it easier to dissern between a player id and any ol' u64 +type PlayerId = u64; + +pub enum Color { + White, + Black, +} + +/// Struct for storing player related data. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Player { + pub name: String, + pub color: Color, +} + +/// Represents a player in the game. +/// +/// Part of the rules of the game is that this game is for only two players, we call them Player 0 +/// and Player 1. The labels are chosen arbitrarily and do not affect the game at all, however, it +/// is convenient here to use 0 and 1 as labels because we sometimes use Rust tuples which we can +/// then address the same way. There is a special case where nobody is allowed to move or act, for +/// example when a game begins or ends, thus we define this as the default. +#[derive( + Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd, Hash, Serialize, Deserialize, Default, +)] +pub enum CurrentPlayer { + /// None of the two players, e.g. at start or end of game. + #[default] + Nobody, + /// Player 0 + Player0, + /// Player 1 + Player1, +} + +impl CurrentPlayer { + /// Returns the other player, i.e. the player who is not the current player. + pub fn other(&self) -> Self { + match *self { + CurrentPlayer::Nobody => CurrentPlayer::Nobody, + CurrentPlayer::Player0 => CurrentPlayer::Player1, + CurrentPlayer::Player1 => CurrentPlayer::Player0, + } + } +} + +// Implement Display trait for Player +impl fmt::Display for CurrentPlayer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + CurrentPlayer::Nobody => write!(f, "Nobody"), + CurrentPlayer::Player0 => write!(f, "Player 0"), + CurrentPlayer::Player1 => write!(f, "Player 1"), + } + } +} + +// Test Display trait for Player +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_other() { + assert_eq!(CurrentPlayer::Nobody.other(), CurrentPlayer::Nobody); + assert_eq!(CurrentPlayer::Player0.other(), CurrentPlayer::Player1); + assert_eq!(CurrentPlayer::Player1.other(), CurrentPlayer::Player0); + } +}