//! # Play a TricTrac Game use crate::board::{Board, CheckerMove}; use crate::dice::Dice; use crate::game_rules_moves::MoveRules; use crate::game_rules_points::{PointsRules, PossibleJans}; use crate::player::{Color, Player, PlayerId}; use log::{error, info}; // use itertools::Itertools; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::{fmt, str}; use base64::{engine::general_purpose, Engine as _}; /// The different stages 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, } /// The different stages a game turn can be in. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum TurnStage { RollDice, RollWaiting, MarkPoints, HoldOrGoChoice, Move, MarkAdvPoints, } impl From for TurnStage { fn from(item: u8) -> Self { match item { 0 => TurnStage::RollWaiting, 1 => TurnStage::RollDice, 2 => TurnStage::MarkPoints, 3 => TurnStage::HoldOrGoChoice, 4 => TurnStage::Move, 5 => TurnStage::MarkAdvPoints, _ => TurnStage::RollWaiting, } } } impl From for u8 { fn from(stage: TurnStage) -> u8 { match stage { TurnStage::RollWaiting => 0, TurnStage::RollDice => 1, TurnStage::MarkPoints => 2, TurnStage::HoldOrGoChoice => 3, TurnStage::Move => 4, TurnStage::MarkAdvPoints => 5, } } } /// Represents a TricTrac game #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct GameState { pub stage: Stage, pub turn_stage: TurnStage, pub board: Board, pub active_player_id: PlayerId, pub players: HashMap, pub history: Vec, /// last dice pair rolled pub dice: Dice, /// players points computed for the last dice pair rolled pub dice_points: (u8, u8), pub dice_moves: (CheckerMove, CheckerMove), pub dice_jans: PossibleJans, /// true if player needs to roll first roll_first: bool, // NOTE: add to a Setting struct if other fields needed pub schools_enabled: 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!( "Stage: {:?} / {:?}\n", self.stage, self.turn_stage )); s.push_str(&format!("Dice: {:?}\n", self.dice)); // s.push_str(&format!("Who plays: {}\n", self.who_plays().map(|player| &player.name ).unwrap_or(""))); s.push_str(&format!("Board: {:?}\n", self.board)); write!(f, "{}", s) } } impl Default for GameState { fn default() -> Self { Self { stage: Stage::PreGame, turn_stage: TurnStage::RollDice, board: Board::default(), active_player_id: 0, players: HashMap::new(), history: Vec::new(), dice: Dice::default(), dice_points: (0, 0), dice_moves: (CheckerMove::default(), CheckerMove::default()), dice_jans: PossibleJans::default(), roll_first: true, schools_enabled: false, } } } impl GameState { /// Create a new default game pub fn new(schools_enabled: bool) -> Self { let mut gs = GameState::default(); gs.set_schools_enabled(schools_enabled); gs } fn set_schools_enabled(&mut self, schools_enabled: bool) { self.schools_enabled = schools_enabled; } fn get_active_player(&self) -> Option<&Player> { self.players.get(&self.active_player_id) } fn get_opponent_id(&self) -> Option { self.players .keys() .copied() .filter(|k| k != &self.active_player_id) .collect::>() .first() .copied() } // ------------------------------------------------------------------------- // accessors // ------------------------------------------------------------------------- pub fn to_vec_float(&self) -> Vec { self.to_vec().iter().map(|&x| x as f32).collect() } /// Get state as a vector (to be used for bot training input) : /// length = 36 pub fn to_vec(&self) -> Vec { let state_len = 36; let mut state = Vec::with_capacity(state_len); // length = 24 state.extend(self.board.to_vec()); // active player -> length = 1 // white : 0 (false) // black : 1 (true) state.push( self.who_plays() .map(|player| if player.color == Color::Black { 1 } else { 0 }) .unwrap_or(0), // White by default ); // step -> length = 1 let turn_stage: u8 = self.turn_stage.into(); state.push(turn_stage as i8); // dice roll -> length = 2 state.push(self.dice.values.0 as i8); state.push(self.dice.values.1 as i8); // points, trous, bredouille, grande bredouille length=4 x2 joueurs = 8 let white_player: Vec = self .get_white_player() .unwrap() .to_vec() .iter() .map(|&x| x as i8) .collect(); state.extend(white_player); let black_player: Vec = self .get_black_player() .unwrap() .to_vec() .iter() .map(|&x| x as i8) .collect(); // .iter().map(|&x| x as i8) .collect() state.extend(black_player); // ensure state has length state_len state.truncate(state_len); while state.len() < state_len { state.push(0); } state } /// Calculate game state id : pub fn to_string_id(&self) -> String { // Pieces placement -> 77 bits (24 + 23 + 30 max) let mut pos_bits = self.board.to_gnupg_pos_id(); // active player -> 1 bit // white : 0 (false) // black : 1 (true) pos_bits.push( self.who_plays() .map(|player| { if player.color == Color::Black { '1' } else { '0' } }) .unwrap_or('0'), // White by default ); // step -> 3 bits let step_bits = match self.turn_stage { TurnStage::RollWaiting => "000", TurnStage::RollDice => "001", TurnStage::MarkPoints => "010", TurnStage::HoldOrGoChoice => "011", TurnStage::Move => "100", TurnStage::MarkAdvPoints => "101", }; pos_bits.push_str(step_bits); // dice roll -> 6 bits let dice_bits = self.dice.to_bits_string(); pos_bits.push_str(&dice_bits); // points 10bits x2 joueurs = 20bits let white_bits = self.get_white_player().unwrap().to_bits_string(); let black_bits = self.get_black_player().unwrap().to_bits_string(); pos_bits.push_str(&white_bits); pos_bits.push_str(&black_bits); pos_bits = format!("{:0>108}", pos_bits); // println!("{}", pos_bits); let pos_u8 = pos_bits .as_bytes() .chunks(6) .map(|chunk| str::from_utf8(chunk).unwrap()) .map(|chunk| u8::from_str_radix(chunk, 2).unwrap()) .collect::>(); general_purpose::STANDARD.encode(pos_u8) } pub fn who_plays(&self) -> Option<&Player> { self.get_active_player() } pub fn get_white_player(&self) -> Option<&Player> { self.players .iter() .filter(|(_id, player)| player.color == Color::White) .map(|(_id, player)| player) .next() } pub fn get_black_player(&self) -> Option<&Player> { self.players .iter() .filter(|(_id, player)| player.color == Color::Black) .map(|(_id, player)| player) .next() } pub fn player_id_by_color(&self, color: Color) -> Option<&PlayerId> { self.players .iter() .filter(|(_id, player)| player.color == color) .map(|(id, _player)| id) .next() } pub fn player_id(&self, player: &Player) -> Option<&PlayerId> { self.players .iter() .filter(|(_id, candidate)| player.color == candidate.color) .map(|(id, _candidate)| id) .next() } pub fn player_color_by_id(&self, player_id: &PlayerId) -> Option { self.players .iter() .filter(|(id, _)| *id == player_id) .map(|(_, player)| player.color) .next() } // ---------------------------------------------------------------------------------- // Rules checks // ---------------------------------------------------------------------------------- /// 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 } => { if let EndGameReason::PlayerWon { winner: _ } = reason { // 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 } | RollResult { player_id, dice: _ } => { // 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; } } Mark { player_id, points: _, } => { // 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 points are correct // let (board, moves) = if *color == Color::Black { // (board.mirror(), (moves.0.mirror(), moves.1.mirror())) // } else { // (board.clone(), *moves) // }; // let rules_points: u8 = self.get_points().iter().map(|r| r.0).sum(); // if rules_points != *points { // return false; // } } Go { player_id } => { if !self.players.contains_key(player_id) { error!("Player {} unknown", player_id); return false; } // Check player is currently the one making their move if self.active_player_id != *player_id { return false; } // Check the player can leave (ie the game is in the KeepOrLeaveChoice stage) if self.turn_stage != TurnStage::HoldOrGoChoice { return false; } } Move { player_id, moves } => { // Check player exists if !self.players.contains_key(player_id) { error!("Player {} unknown", player_id); return false; } // Check player is currently the one making their move if self.active_player_id != *player_id { error!("Player not active : {}", self.active_player_id); return false; } // Check the turn stage if self.turn_stage != TurnStage::Move && self.turn_stage != TurnStage::HoldOrGoChoice { error!("bad stage {:?}", self.turn_stage); return false; } let color = &self.players[player_id].color; let rules = MoveRules::new(color, &self.board, self.dice); let moves = if *color == Color::Black { (moves.0.mirror(), moves.1.mirror()) } else { *moves }; if !rules.moves_follow_rules(&moves) { error!("rules not followed "); return false; } } PlayError => { return true; } } // We couldn't find anything wrong with the event so it must be good true } // ---------------------------------------------------------------------------------- // State updates // ---------------------------------------------------------------------------------- pub fn init_player(&mut self, player_name: &str) -> Option { if self.players.len() > 2 { println!("more than two players"); return None; } let player_id = self.players.len() + 1; let color = if player_id == 1 { Color::White } else { Color::Black }; let player = Player::new(player_name.into(), color); self.players.insert(player_id as PlayerId, player); Some(player_id as PlayerId) } #[cfg(test)] fn add_player(&mut self, player_id: PlayerId, player: Player) { self.players.insert(player_id, player); } pub fn switch_active_player(&mut self) { let other_player_id = self .players .iter() .filter(|(id, _player)| **id != self.active_player_id) .map(|(id, _player)| *id) .next(); self.active_player_id = other_player_id.unwrap_or(0); } /// 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; // if self.who_plays().is_none() { // let active_color = match self.dice.coin() { // false => Color::Black, // true => 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(); // } // } self.stage = Stage::InGame; self.turn_stage = TurnStage::RollDice; } EndGame { reason: _ } => { self.stage = Stage::Ended; } PlayerJoined { player_id, name } => { let color = if !self.players.is_empty() { Color::White } else { Color::Black }; self.players.insert( *player_id, Player { name: name.to_string(), color, holes: 0, points: 0, can_bredouille: true, can_big_bredouille: true, dice_roll_count: 0, }, ); } PlayerDisconnected { player_id } => { self.players.remove(player_id); } Roll { player_id: _ } => { self.turn_stage = TurnStage::RollWaiting; } RollResult { player_id: _, dice } => { self.dice = *dice; self.inc_roll_count(self.active_player_id); self.turn_stage = TurnStage::MarkPoints; (self.dice_jans, self.dice_points) = self.get_rollresult_jans(dice); if !self.schools_enabled { // Schools are not enabled. We mark points automatically // the points earned by the opponent will be marked on its turn let new_hole = self.mark_points(self.active_player_id, self.dice_points.0); if new_hole { if self.get_active_player().unwrap().holes > 12 { self.stage = Stage::Ended; } else { self.turn_stage = TurnStage::HoldOrGoChoice; } } else { self.turn_stage = TurnStage::Move; } } } Mark { player_id, points } => { if self.schools_enabled { let new_hole = self.mark_points(*player_id, *points); if new_hole { if self.get_active_player().unwrap().holes > 12 { self.stage = Stage::Ended; } else { self.turn_stage = if self.turn_stage == TurnStage::MarkAdvPoints { TurnStage::RollDice } else { TurnStage::HoldOrGoChoice }; } } else { self.turn_stage = if self.turn_stage == TurnStage::MarkAdvPoints { TurnStage::RollDice } else { TurnStage::Move }; } } } Go { player_id: _ } => self.new_pick_up(), Move { player_id, moves } => { let player = self.players.get(player_id).unwrap(); self.board.move_checker(&player.color, moves.0).unwrap(); self.board.move_checker(&player.color, moves.1).unwrap(); self.dice_moves = *moves; self.active_player_id = *self.players.keys().find(|id| *id != player_id).unwrap(); self.turn_stage = if self.schools_enabled { TurnStage::MarkAdvPoints } else { // The player has moved, we can mark its opponent's points (which is now the current player) let new_hole = self.mark_points(self.active_player_id, self.dice_points.1); if new_hole && self.get_active_player().unwrap().holes > 12 { self.stage = Stage::Ended; } TurnStage::RollDice }; } PlayError => {} } self.history.push(valid_event.clone()); } /// Set a new pick up ('relevé') after a player won a hole and choose to 'go', /// or after a player has bore off (took of his men off the board) fn new_pick_up(&mut self) { self.players.iter_mut().for_each(|(_id, p)| { // reset points p.points = 0; // reset dice_roll_count p.dice_roll_count = 0; // reset bredouille p.can_bredouille = true; // XXX : switch colors // désactivé pour le moment car la vérification des mouvements échoue, cf. https://code.rhumbs.fr/henri/trictrac/issues/31 // p.color = p.color.opponent_color(); }); // joueur actif = joueur ayant sorti ses dames ou est parti (donc deux jeux successifs) self.turn_stage = TurnStage::RollDice; // reset board self.board = Board::new(); } fn get_rollresult_jans(&self, dice: &Dice) -> (PossibleJans, (u8, u8)) { let player = &self.players.get(&self.active_player_id).unwrap(); let points_rules = PointsRules::new(&player.color, &self.board, *dice); points_rules.get_result_jans(player.dice_roll_count) } /// Determines if someone has won the game pub fn determine_winner(&self) -> Option { // A player has won if he has got 12 holes self.players .iter() .filter(|(_, p)| p.holes > 11) .map(|(id, _)| *id) .next() } fn inc_roll_count(&mut self, player_id: PlayerId) { self.players.get_mut(&player_id).map(|p| { if p.dice_roll_count < u8::MAX { p.dice_roll_count += 1; } p }); } fn mark_points(&mut self, player_id: PlayerId, points: u8) -> bool { // Update player points and holes let mut new_hole = false; self.players.get_mut(&player_id).map(|p| { let sum_points = p.points + points; let jeux = sum_points / 12; let holes = match (jeux, p.can_bredouille) { (0, _) => 0, (_, false) => 2 * jeux - 1, (_, true) => 2 * jeux, }; new_hole = holes > 0; if new_hole { p.can_bredouille = true; } p.points = sum_points % 12; p.holes += holes; if points > 0 && p.holes > 15 { info!( "player {:?} holes : {:?} added points : {:?}", player_id, p.holes, points ) } p }); // Opponent updates let maybe_op = if player_id == self.active_player_id { self.get_opponent_id() } else { Some(player_id) }; if let Some(opp_id) = maybe_op { if points > 0 { self.players.get_mut(&opp_id).map(|opponent| { // Cancel opponent bredouille opponent.can_bredouille = false; // Reset opponent points if the player finished a hole if new_hole { opponent.points = 0; opponent.can_bredouille = true; } opponent }); } } new_hole } } /// The reasons why a game could end #[derive(Debug, Clone, Copy, Serialize, PartialEq, Deserialize)] pub enum EndGameReason { 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, }, RollResult { player_id: PlayerId, dice: Dice, }, Mark { player_id: PlayerId, points: u8, }, Go { player_id: PlayerId, }, Move { player_id: PlayerId, moves: (CheckerMove, CheckerMove), }, PlayError, } impl GameEvent { pub fn player_id(&self) -> Option { match self { Self::PlayerJoined { player_id, name: _ } => Some(*player_id), Self::PlayerDisconnected { player_id } => Some(*player_id), Self::Roll { player_id } => Some(*player_id), Self::RollResult { player_id, dice: _ } => Some(*player_id), Self::Mark { player_id, points: _, } => Some(*player_id), Self::Go { player_id } => Some(*player_id), Self::Move { player_id, moves: _, } => Some(*player_id), _ => None, } } } #[cfg(test)] mod tests { use super::*; fn init_test_gamestate(turn: TurnStage) -> GameState { let mut state = GameState::default(); state.add_player(1, Player::new("player1".into(), Color::White)); state.add_player(2, Player::new("player2".into(), Color::Black)); state.active_player_id = 1; state.turn_stage = turn; state } #[test] fn to_string_id() { let state = init_test_gamestate(TurnStage::RollDice); let string_id = state.to_string_id(); // println!("string_id : {}", string_id); assert_eq!(string_id, "Hz88AAAAAz8/IAAAAAQAADAD"); } #[test] fn hold_or_go() { let mut game_state = init_test_gamestate(TurnStage::MarkPoints); game_state.schools_enabled = true; let pid = game_state.active_player_id; game_state.consume( &(GameEvent::Mark { player_id: pid, points: 13, }), ); let player = game_state.get_active_player().unwrap(); assert_eq!(player.points, 1); assert_eq!(player.holes, 2); // because can bredouille assert_eq!(game_state.turn_stage, TurnStage::HoldOrGoChoice); // Go game_state.consume( &(GameEvent::Go { player_id: game_state.active_player_id, }), ); assert_eq!(game_state.active_player_id, pid); let player = game_state.get_active_player().unwrap(); assert_eq!(player.points, 0); assert_eq!(game_state.turn_stage, TurnStage::RollDice); // Hold let mut game_state = init_test_gamestate(TurnStage::MarkPoints); game_state.schools_enabled = true; let pid = game_state.active_player_id; game_state.consume( &(GameEvent::Mark { player_id: pid, points: 13, }), ); let moves = ( CheckerMove::new(1, 3).unwrap(), CheckerMove::new(1, 3).unwrap(), ); game_state.consume( &(GameEvent::Move { player_id: game_state.active_player_id, moves, }), ); assert_ne!(game_state.active_player_id, pid); assert_eq!(game_state.players.get(&pid).unwrap().points, 1); assert_eq!(game_state.get_active_player().unwrap().points, 0); assert_eq!(game_state.turn_stage, TurnStage::MarkAdvPoints); } }