2023-10-07 20:46:24 +02:00
|
|
|
//! # Play a TricTrac Game
|
2023-10-28 15:12:04 +02:00
|
|
|
use crate::player::{Color, Player, PlayerId};
|
|
|
|
|
use crate::board::{Board, Move};
|
|
|
|
|
use crate::dice::{Dices, Roll};
|
2023-10-07 20:46:24 +02:00
|
|
|
use crate::Error;
|
2023-11-05 17:14:58 +01:00
|
|
|
use log::{error, info, trace, warn};
|
2023-10-07 20:46:24 +02:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-30 21:53:17 +01:00
|
|
|
/// The different states a game turn can be in.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
|
|
|
pub enum TurnStage {
|
|
|
|
|
RollDice,
|
|
|
|
|
MarkPoints,
|
|
|
|
|
Move,
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-07 20:46:24 +02:00
|
|
|
/// Represents a TricTrac game
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
|
|
|
pub struct GameState {
|
|
|
|
|
pub stage: Stage,
|
2023-12-30 21:53:17 +01:00
|
|
|
pub turn_stage: TurnStage,
|
2023-10-07 20:46:24 +02:00
|
|
|
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));
|
2023-10-29 20:48:53 +01:00
|
|
|
// s.push_str(&format!("Who plays: {}\n", self.who_plays().map(|player| &player.name ).unwrap_or("")));
|
2023-10-07 20:46:24 +02:00
|
|
|
s.push_str(&format!("Board: {:?}\n", self.board.get()));
|
|
|
|
|
write!(f, "{}", s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for GameState {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
stage: Stage::PreGame,
|
2023-12-30 21:53:17 +01:00
|
|
|
turn_stage: TurnStage::RollDice,
|
2023-10-07 20:46:24 +02:00
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-29 20:48:53 +01:00
|
|
|
pub fn who_plays(&self) -> Option<&Player> {
|
|
|
|
|
self.players.get(&self.active_player_id)
|
2023-10-28 15:12:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn switch_active_player(&mut self) {
|
|
|
|
|
let other_player_id = self.players.iter()
|
2023-11-01 14:20:34 +01:00
|
|
|
.filter(|(id, _player)| **id != self.active_player_id )
|
|
|
|
|
.map(|(id, _player)| *id )
|
2023-10-28 15:12:04 +02:00
|
|
|
.next();
|
|
|
|
|
self.active_player_id = other_player_id.unwrap_or(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn player_id_by_color(&self, color: Color) -> Option<&PlayerId> {
|
|
|
|
|
self.players.iter()
|
2023-11-01 14:20:34 +01:00
|
|
|
.filter(|(_id, player)| player.color == color)
|
|
|
|
|
.map(|(id, _player)| id )
|
2023-10-28 15:12:04 +02:00
|
|
|
.next()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn player_id(&self, player: &Player) -> Option<&PlayerId> {
|
|
|
|
|
self.players.iter()
|
2023-11-01 14:20:34 +01:00
|
|
|
.filter(|(_id, candidate)| player.color == candidate.color)
|
|
|
|
|
.map(|(id, _candidate)| id )
|
2023-10-28 15:12:04 +02:00
|
|
|
.next()
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-07 20:46:24 +02:00
|
|
|
/// 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
2023-10-28 15:12:04 +02:00
|
|
|
Move { player_id, from, to } => {
|
2023-10-07 20:46:24 +02:00
|
|
|
// Check player exists
|
|
|
|
|
if !self.players.contains_key(player_id) {
|
2023-11-05 17:14:58 +01:00
|
|
|
error!("Player {} unknown", player_id);
|
2023-10-07 20:46:24 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
// Check player is currently the one making their move
|
|
|
|
|
if self.active_player_id != *player_id {
|
2023-11-05 17:14:58 +01:00
|
|
|
error!("Player not active : {}", self.active_player_id);
|
2023-10-07 20:46:24 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check that the tile index is inside the board
|
2023-10-28 15:12:04 +02:00
|
|
|
if *to > 23 {
|
2023-10-07 20:46:24 +02:00
|
|
|
return false;
|
|
|
|
|
}
|
2023-11-01 14:20:34 +01:00
|
|
|
if *from > 23 {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2023-10-07 20:46:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 } => {
|
2023-10-28 15:12:04 +02:00
|
|
|
let color = if self.players.len() > 0 { Color::White } else { Color::Black };
|
2023-10-07 20:46:24 +02:00
|
|
|
self.players.insert(
|
|
|
|
|
*player_id,
|
|
|
|
|
Player {
|
|
|
|
|
name: name.to_string(),
|
2023-10-28 15:12:04 +02:00
|
|
|
color
|
2023-10-07 20:46:24 +02:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
PlayerDisconnected { player_id } => {
|
|
|
|
|
self.players.remove(player_id);
|
|
|
|
|
}
|
|
|
|
|
Roll { player_id } => {
|
|
|
|
|
}
|
|
|
|
|
Move { player_id, from, to } => {
|
|
|
|
|
let player = self.players.get(player_id).unwrap();
|
2023-11-01 14:20:34 +01:00
|
|
|
self.board.set(player, *from, 0 as i8).unwrap();
|
|
|
|
|
self.board.set(player, *to, 1 as i8).unwrap();
|
2023-10-07 20:46:24 +02:00
|
|
|
self.active_player_id = self
|
|
|
|
|
.players
|
|
|
|
|
.keys()
|
|
|
|
|
.find(|id| *id != player_id)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.clone();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.history.push(valid_event.clone());
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-28 15:12:04 +02:00
|
|
|
/// Determines if someone has won the game
|
|
|
|
|
pub fn determine_winner(&self) -> Option<PlayerId> {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
2023-10-07 20:46:24 +02:00
|
|
|
|
|
|
|
|
/// 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 },
|
2023-10-28 15:12:04 +02:00
|
|
|
Move { player_id: PlayerId, from: usize, to: usize },
|
2023-10-07 20:46:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
2023-10-28 15:12:04 +02:00
|
|
|
if self.who_plays().is_none() {
|
2023-10-07 20:46:24 +02:00
|
|
|
let diff = self.dices.values.0 - self.dices.values.1;
|
2023-10-28 15:12:04 +02:00
|
|
|
let active_color = if diff < 0 { Color::Black } else { Color::White };
|
|
|
|
|
let color_player_id = self.player_id_by_color(active_color);
|
|
|
|
|
if color_player_id.is_some(){
|
|
|
|
|
self.active_player_id = *color_player_id.unwrap();
|
2023-10-07 20:46:24 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(self)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Move for GameState {
|
2023-10-29 20:48:53 +01:00
|
|
|
fn move_checker(&mut self, player: &Player, dice: u8, from: usize) -> Result<&mut Self, Error> {
|
2023-10-07 20:46:24 +02:00
|
|
|
// 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
|
|
|
|
|
{
|
2023-10-28 15:12:04 +02:00
|
|
|
self.switch_active_player();
|
2023-10-07 20:46:24 +02:00
|
|
|
self.roll_first = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(self)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Implements checks to validate if the player is allowed to move
|
2023-10-29 20:48:53 +01:00
|
|
|
fn move_permitted(&mut self, player: &Player, dice: u8) -> Result<&mut Self, Error> {
|
2023-10-28 15:12:04 +02:00
|
|
|
let maybe_player_id = self.player_id(&player);
|
2023-10-07 20:46:24 +02:00
|
|
|
// check if player is allowed to move
|
2023-10-28 15:12:04 +02:00
|
|
|
if maybe_player_id != Some(&self.active_player_id) {
|
2023-10-07 20:46:24 +02:00
|
|
|
return Err(Error::NotYourTurn);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if player is nobody, you can not play and have to roll first
|
2023-10-28 15:12:04 +02:00
|
|
|
if maybe_player_id.is_none() {
|
2023-10-07 20:46:24 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|