wip server & reducer
This commit is contained in:
parent
92fcea330c
commit
c8e7420396
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
467
store/src/board.rs
Normal file
467
store/src/board.rs
Normal file
|
|
@ -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<bool, Error> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
79
store/src/dice.rs
Normal file
79
store/src/dice.rs
Normal file
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
100
store/src/error.rs
Normal file
100
store/src/error.rs
Normal file
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
333
store/src/game.rs
Normal file
333
store/src/game.rs
Normal file
|
|
@ -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<PlayerId, Player>,
|
||||
pub history: Vec<GameEvent>,
|
||||
/// 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"
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
mod game;
|
||||
pub use game::Game;
|
||||
|
||||
mod player;
|
||||
pub use player::Player;
|
||||
|
||||
73
store/src/player.rs
Normal file
73
store/src/player.rs
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue