trictrac/store/src/game.rs

561 lines
19 KiB
Rust
Raw Normal View History

2023-10-07 20:46:24 +02:00
//! # Play a TricTrac Game
2024-05-20 19:04:46 +02:00
use crate::board::{Board, CheckerMove};
2024-05-09 21:49:56 +02:00
use crate::dice::Dice;
2024-05-20 19:04:46 +02:00
use crate::game_rules_moves::MoveRules;
use crate::game_rules_points::PointsRules;
2024-01-12 17:02:18 +01:00
use crate::player::{Color, Player, PlayerId};
2024-05-09 21:49:56 +02:00
use log::error;
2023-10-07 20:46:24 +02:00
2024-01-20 21:40:06 +01:00
// use itertools::Itertools;
2023-10-07 20:46:24 +02:00
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
2024-01-27 19:11:23 +01:00
use std::{fmt, str};
2023-10-07 20:46:24 +02:00
2024-01-20 21:40:06 +01:00
use base64::{engine::general_purpose, Engine as _};
2024-01-09 20:50:52 +01:00
/// The different stages a game can be in. (not to be confused with the entire "GameState")
2023-10-07 20:46:24 +02:00
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Stage {
PreGame,
InGame,
Ended,
}
2024-01-09 20:50:52 +01:00
/// The different stages a game turn can be in.
2023-12-30 21:53:17 +01:00
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TurnStage {
RollDice,
2024-03-27 21:10:15 +01:00
RollWaiting,
2023-12-30 21:53:17 +01:00
MarkPoints,
2024-09-26 17:41:03 +02:00
HoldOrGoChoice,
2023-12-30 21:53:17 +01:00
Move,
MarkAdvPoints,
2023-12-30 21:53:17 +01:00
}
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
2024-03-11 20:45:36 +01:00
pub dice: Dice,
2024-09-22 16:11:42 +02:00
/// players points computed for the last dice pair rolled
dice_points: (u8, u8),
2023-10-07 20:46:24 +02:00
/// true if player needs to roll first
roll_first: bool,
2024-09-22 16:11:42 +02:00
// NOTE: add to a Setting struct if other fields needed
pub schools_enabled: bool,
2023-10-07 20:46:24 +02:00
}
// implement Display trait
impl fmt::Display for GameState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut s = String::new();
2024-03-31 15:23:18 +02:00
s.push_str(&format!(
"Stage: {:?} / {:?}\n",
self.stage, self.turn_stage
));
2024-03-11 20:45:36 +01:00
s.push_str(&format!("Dice: {:?}\n", self.dice));
2023-10-29 20:48:53 +01:00
// s.push_str(&format!("Who plays: {}\n", self.who_plays().map(|player| &player.name ).unwrap_or("")));
2024-01-12 17:02:18 +01:00
s.push_str(&format!("Board: {:?}\n", self.board));
2023-10-07 20:46:24 +02:00
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(),
2024-03-11 20:45:36 +01:00
dice: Dice::default(),
2024-09-22 16:11:42 +02:00
dice_points: (0, 0),
2023-10-07 20:46:24 +02:00
roll_first: true,
2024-09-22 16:11:42 +02:00
schools_enabled: false,
2023-10-07 20:46:24 +02:00
}
}
}
impl GameState {
/// Create a new default game
2024-09-22 16:11:42 +02:00
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_opponent_id(&self) -> Option<PlayerId> {
self.players
.keys()
.map(|k| *k)
.filter(|k| k != &self.active_player_id)
.collect::<Vec<PlayerId>>()
.first()
.copied()
2023-10-07 20:46:24 +02:00
}
2024-02-03 22:16:14 +01:00
// -------------------------------------------------------------------------
// accessors
// -------------------------------------------------------------------------
2024-01-20 21:40:06 +01:00
2024-01-12 17:02:18 +01:00
/// Calculate game state id :
2024-01-20 21:40:06 +01:00
pub fn to_string_id(&self) -> String {
2024-01-12 17:02:18 +01:00
// Pieces placement -> 77 bits (24 + 23 + 30 max)
2024-01-20 21:40:06 +01:00
let mut pos_bits = self.board.to_gnupg_pos_id();
2024-01-12 17:02:18 +01:00
// active player -> 1 bit
// white : 0 (false)
// black : 1 (true)
pos_bits.push(
self.who_plays()
2024-01-30 21:59:47 +01:00
.map(|player| {
if player.color == Color::Black {
'1'
} else {
'0'
}
})
2024-01-20 21:40:06 +01:00
.unwrap_or('0'), // White by default
2024-01-12 17:02:18 +01:00
);
// step -> 3 bits
2024-01-20 21:40:06 +01:00
let step_bits = match self.turn_stage {
TurnStage::RollWaiting => "000",
TurnStage::RollDice => "001",
TurnStage::MarkPoints => "010",
2024-09-26 17:41:03 +02:00
TurnStage::HoldOrGoChoice => "011",
TurnStage::Move => "100",
TurnStage::MarkAdvPoints => "101",
2024-01-12 17:02:18 +01:00
};
2024-01-20 21:40:06 +01:00
pos_bits.push_str(step_bits);
2024-01-12 17:02:18 +01:00
2024-01-20 21:40:06 +01:00
// dice roll -> 6 bits
2024-03-11 20:45:36 +01:00
let dice_bits = self.dice.to_bits_string();
2024-01-20 21:40:06 +01:00
pos_bits.push_str(&dice_bits);
2024-01-12 17:02:18 +01:00
2024-01-20 21:40:06 +01:00
// 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
2024-01-30 21:59:47 +01:00
.as_bytes()
.chunks(6)
2024-01-20 21:40:06 +01:00
.map(|chunk| str::from_utf8(chunk).unwrap())
.map(|chunk| u8::from_str_radix(chunk, 2).unwrap())
.collect::<Vec<u8>>();
general_purpose::STANDARD.encode(pos_u8)
2024-01-09 20:50:52 +01:00
}
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
}
2024-01-20 21:40:06 +01:00
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()
}
2023-10-28 15:12:04 +02:00
pub fn player_id_by_color(&self, color: Color) -> Option<&PlayerId> {
2024-01-12 17:02:18 +01:00
self.players
.iter()
2023-11-01 14:20:34 +01:00
.filter(|(_id, player)| player.color == color)
2024-01-12 17:02:18 +01:00
.map(|(id, _player)| id)
2023-10-28 15:12:04 +02:00
.next()
}
pub fn player_id(&self, player: &Player) -> Option<&PlayerId> {
2024-01-12 17:02:18 +01:00
self.players
.iter()
2023-11-01 14:20:34 +01:00
.filter(|(_id, candidate)| player.color == candidate.color)
2024-01-12 17:02:18 +01:00
.map(|(id, _candidate)| id)
2023-10-28 15:12:04 +02:00
.next()
}
2024-03-24 18:37:35 +01:00
pub fn player_color_by_id(&self, player_id: &PlayerId) -> Option<Color> {
self.players
.iter()
.filter(|(id, _)| *id == player_id)
.map(|(_, player)| player.color)
.next()
}
2024-02-03 22:16:14 +01:00
// ----------------------------------------------------------------------------------
// Rules checks
// ----------------------------------------------------------------------------------
2024-01-12 17:02:18 +01:00
/// Determines whether an event is valid considering the current GameState
2023-10-07 20:46:24 +02:00
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) {
2024-01-12 17:02:18 +01:00
return false;
2023-10-07 20:46:24 +02:00
}
// Check that the game hasn't started yet. (we don't want to double start a game)
if self.stage != Stage::PreGame {
return false;
}
}
2024-05-09 21:49:56 +02:00
EndGame { reason } => {
if let EndGameReason::PlayerWon { winner: _ } = reason {
2023-10-07 20:46:24 +02:00
// Check that the game has started before someone wins it
if self.stage != Stage::InGame {
return false;
}
}
2024-05-09 21:49:56 +02:00
}
2023-10-07 20:46:24 +02:00
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;
}
}
2024-03-11 20:45:36 +01:00
Roll { player_id } | RollResult { player_id, dice: _ } => {
2023-10-07 20:46:24 +02:00
// 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;
}
}
2024-05-20 19:04:46 +02:00
Mark { player_id, points } => {
2024-02-03 22:16:14 +01:00
// 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;
}
2024-05-20 19:04:46 +02:00
// Check points are correct
2024-05-25 19:56:38 +02:00
// 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;
// }
2024-02-03 22:16:14 +01:00
}
2024-09-26 17:41:03 +02:00
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;
}
}
2024-01-30 21:59:47 +01:00
Move { player_id, moves } => {
2023-10-07 20:46:24 +02:00
// Check player exists
if !self.players.contains_key(player_id) {
2024-01-12 17:02:18 +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 {
2024-01-12 17:02:18 +01:00
error!("Player not active : {}", self.active_player_id);
2023-10-07 20:46:24 +02:00
return false;
}
2024-09-26 17:41:03 +02:00
// Check the turn stage
if self.turn_stage != TurnStage::HoldOrGoChoice
|| self.turn_stage != TurnStage::Move
{
return false;
}
2024-02-03 22:16:14 +01:00
let color = &self.players[player_id].color;
2024-05-25 19:56:38 +02:00
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) {
2023-11-01 14:20:34 +01:00
return false;
}
2023-10-07 20:46:24 +02:00
}
}
2024-01-12 17:02:18 +01:00
// We couldn't find anything wrong with the event so it must be good
2023-10-07 20:46:24 +02:00
true
}
2024-01-12 17:02:18 +01:00
2024-02-03 22:16:14 +01:00
// ----------------------------------------------------------------------------------
// State updates
// ----------------------------------------------------------------------------------
2024-03-09 22:20:11 +01:00
pub fn init_player(&mut self, player_name: &str) -> Option<PlayerId> {
if self.players.len() > 2 {
println!("more than two players");
return None;
}
let player_id = self.players.len() + 1;
println!("player_id {}", player_id);
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)
}
2024-02-03 22:16:14 +01:00
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);
}
2023-10-07 20:46:24 +02:00
/// 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;
2024-03-11 20:45:36 +01:00
// 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();
// }
// }
2023-10-07 20:46:24 +02:00
self.stage = Stage::InGame;
2024-02-03 22:16:14 +01:00
self.turn_stage = TurnStage::RollDice;
2023-10-07 20:46:24 +02:00
}
EndGame { reason: _ } => self.stage = Stage::Ended,
PlayerJoined { player_id, name } => {
2024-05-09 21:49:56 +02:00
let color = if !self.players.is_empty() {
2024-01-12 17:02:18 +01:00
Color::White
} else {
Color::Black
};
2023-10-07 20:46:24 +02:00
self.players.insert(
*player_id,
Player {
name: name.to_string(),
2024-01-12 17:02:18 +01:00
color,
2024-01-20 21:40:06 +01:00
holes: 0,
points: 0,
can_bredouille: true,
2024-01-30 21:59:47 +01:00
can_big_bredouille: true,
dice_roll_count: 0,
2023-10-07 20:46:24 +02:00
},
);
}
PlayerDisconnected { player_id } => {
self.players.remove(player_id);
}
2024-03-27 21:10:15 +01:00
Roll { player_id: _ } => {
2024-09-22 16:11:42 +02:00
// Opponent has moved, we can mark pending points earned during opponent's turn
self.mark_points(self.active_player_id, self.dice_points.1);
if self.stage != Stage::Ended {
self.turn_stage = TurnStage::RollWaiting;
}
2024-03-27 21:10:15 +01:00
}
2024-09-22 16:11:42 +02:00
RollResult { player_id, dice } => {
2024-03-11 20:45:36 +01:00
self.dice = *dice;
self.inc_roll_count(self.active_player_id);
2024-02-03 22:16:14 +01:00
self.turn_stage = TurnStage::MarkPoints;
self.dice_points = self.get_rollresult_points(dice);
2024-09-22 16:11:42 +02:00
if !self.schools_enabled {
// Schools are not enabled. We mark points automatically
// the points earned by the opponent will be marked on its turn
self.mark_points(self.active_player_id, self.dice_points.0);
if self.stage != Stage::Ended {
self.turn_stage = TurnStage::Move;
}
}
2024-02-03 22:16:14 +01:00
}
Mark { player_id, points } => {
self.mark_points(*player_id, *points);
if self.stage != Stage::Ended {
self.turn_stage = if self.turn_stage == TurnStage::MarkAdvPoints {
TurnStage::RollDice
} else {
TurnStage::Move
};
2024-02-03 22:16:14 +01:00
}
}
2024-09-26 17:41:03 +02:00
Go { player_id } => self.new_pick_up()
2024-01-30 21:59:47 +01:00
Move { player_id, moves } => {
2023-10-07 20:46:24 +02:00
let player = self.players.get(player_id).unwrap();
2024-01-27 20:22:20 +01:00
self.board.move_checker(&player.color, moves.0).unwrap();
self.board.move_checker(&player.color, moves.1).unwrap();
2024-05-09 21:49:56 +02:00
self.active_player_id = *self.players.keys().find(|id| *id != player_id).unwrap();
2024-09-22 16:11:42 +02:00
self.turn_stage = if self.schools_enabled {
TurnStage::MarkAdvPoints
} else {
TurnStage::RollDice
};
2023-10-07 20:46:24 +02:00
}
}
self.history.push(valid_event.clone());
}
2024-09-26 17:41:03 +02:00
/// 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) {
// réinitialisation dice_roll_count
self.players.iter_mut().map(|(id, p)| p.dice_roll_count = 0);
// joueur actif = joueur ayant sorti ses dames (donc deux jeux successifs)
self.turn_stage = TurnStage::RollDice;
// TODO:
// - échanger les couleurs
// - remettre les dames des deux joueurs aux talons
// - jeton bredouille replaçé sur joueur actif (?)
}
fn get_rollresult_points(&self, dice: &Dice) -> (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_points(player.dice_roll_count)
}
2023-10-28 15:12:04 +02:00
/// Determines if someone has won the game
pub fn determine_winner(&self) -> Option<PlayerId> {
None
}
2024-02-03 22:16:14 +01:00
fn inc_roll_count(&mut self, player_id: PlayerId) {
self.players.get_mut(&player_id).map(|p| {
p.dice_roll_count += 1;
p
});
}
2024-02-03 22:16:14 +01:00
fn mark_points(&mut self, player_id: PlayerId, points: u8) {
2024-03-26 21:07:47 +01:00
self.players.get_mut(&player_id).map(|p| {
2024-09-22 16:11:42 +02:00
let sum_points = p.points + points;
2024-09-24 17:44:45 +02:00
let jeux = sum_points / 12;
2024-09-22 16:11:42 +02:00
p.points = sum_points % 12;
2024-09-24 17:44:45 +02:00
p.holes += match (jeux, p.can_bredouille) {
(0, _) => 0,
(_, false) => 2 * jeux - 1,
(_, true) => 2 * jeux,
};
2024-03-26 21:07:47 +01:00
p
});
2024-02-03 22:16:14 +01:00
}
2023-10-28 15:12:04 +02:00
}
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 {
2024-01-12 17:02:18 +01:00
BeginGame {
goes_first: PlayerId,
},
EndGame {
reason: EndGameReason,
},
PlayerJoined {
player_id: PlayerId,
name: String,
},
PlayerDisconnected {
player_id: PlayerId,
},
Roll {
player_id: PlayerId,
},
2024-03-11 20:45:36 +01:00
RollResult {
player_id: PlayerId,
dice: Dice,
},
2024-02-03 22:16:14 +01:00
Mark {
player_id: PlayerId,
points: u8,
},
2024-09-26 17:41:03 +02:00
Go {
player_id: PlayerId,
},
2024-01-12 17:02:18 +01:00
Move {
player_id: PlayerId,
2024-01-27 20:22:20 +01:00
moves: (CheckerMove, CheckerMove),
2024-01-12 17:02:18 +01:00
},
2023-10-07 20:46:24 +02:00
}
2024-01-20 21:40:06 +01:00
#[cfg(test)]
mod tests {
use super::*;
#[test]
2024-05-09 21:49:56 +02:00
fn to_string_id() {
2024-01-20 21:40:06 +01:00
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));
let string_id = state.to_string_id();
// println!("string_id : {}", string_id);
assert!(string_id == "Hz88AAAAAz8/IAAAAAQAADAD");
2024-01-20 21:40:06 +01:00
}
2023-10-07 20:46:24 +02:00
}