trictrac/store/src/game.rs

606 lines
20 KiB
Rust
Raw Normal View History

2023-10-07 20:46:24 +02:00
//! # Play a TricTrac Game
2024-01-30 21:59:47 +01:00
use crate::board::{Board, CheckerMove, Field, Move};
2023-10-28 15:12:04 +02:00
use crate::dice::{Dices, Roll};
2024-01-12 17:02:18 +01:00
use crate::player::{Color, Player, PlayerId};
2023-10-07 20:46:24 +02:00
use crate::Error;
2024-01-31 15:39:02 +01:00
use log::{error, info};
2024-02-05 22:15:13 +01:00
use std::cmp;
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,
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("")));
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(),
dices: Dices::default(),
roll_first: true,
}
}
}
impl GameState {
/// Create a new default game
pub fn new() -> Self {
GameState::default()
}
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 -> 2 bits
2024-01-20 21:40:06 +01:00
let step_bits = match self.turn_stage {
TurnStage::RollDice => "01",
TurnStage::MarkPoints => "01",
TurnStage::Move => "10",
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
let dice_bits = self.dices.to_bits_string();
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-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;
}
}
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;
}
}
2024-02-03 22:16:14 +01:00
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;
}
}
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-02-03 22:16:14 +01:00
let color = &self.players[player_id].color;
2024-02-05 22:15:13 +01:00
// Check moves possibles on the board
if !self.moves_possible(color, moves) {
2024-02-03 22:16:14 +01:00
return false;
}
2023-10-07 20:46:24 +02:00
2024-02-05 22:15:13 +01:00
// Check moves conforms to the dices
if !self.moves_follows_dices(color, moves) {
2024-01-30 21:59:47 +01:00
return false;
}
2024-01-29 21:42:40 +01:00
// Check move is allowed by the rules (to desactivate when playing with schools)
if !self.moves_allowed(color, 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-05 22:15:13 +01:00
fn moves_possible(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool {
// Check move is physically possible
if !self.board.move_possible(color, &moves.0) {
return false;
}
// Chained_move : "Tout d'une"
let chained_move = moves.0.chain(moves.1);
if chained_move.is_ok() {
if !self.board.move_possible(color, &chained_move.unwrap()) {
return false;
}
} else if !self.board.move_possible(color, &moves.1) {
return false;
}
true
}
2024-02-03 22:16:14 +01:00
fn moves_follows_dices(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool {
2024-02-05 22:15:13 +01:00
let (dice1, dice2) = self.dices.values;
let (move1, move2): &(CheckerMove, CheckerMove) = moves.into();
let dist1 = (move1.get_to() - move1.get_from()) as u8;
let dist2 = (move2.get_to() - move2.get_from()) as u8;
print!("{}, {}, {}, {}", dist1, dist2, dice1, dice2);
2024-02-03 22:16:14 +01:00
// basic : same number
2024-02-05 22:15:13 +01:00
if cmp::min(dist1, dist2) != cmp::min(dice1, dice2)
|| cmp::max(dist1, dist2) != cmp::max(dice1, dice2)
{
return false;
}
2024-02-03 22:16:14 +01:00
// prise de coin par puissance
// sorties
// no rule was broken
2024-02-05 22:15:13 +01:00
true
2024-02-03 22:16:14 +01:00
}
2024-01-29 21:42:40 +01:00
fn moves_allowed(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool {
// ------- corner rules ----------
let corner_field: Field = self.board.get_color_corner(color);
let (corner_count, _color) = self.board.get_field_checkers(corner_field).unwrap();
2024-01-30 21:59:47 +01:00
let (from0, to0, from1, to1) = (
moves.0.get_from(),
moves.0.get_to(),
moves.1.get_from(),
moves.1.get_to(),
);
2024-01-29 21:42:40 +01:00
// 2 checkers must go at the same time on an empty corner
2024-01-30 21:59:47 +01:00
if (to0 == corner_field || to1 == corner_field) && (to0 != to1) && corner_count == 0 {
return false;
2024-01-29 21:42:40 +01:00
}
2024-01-30 21:59:47 +01:00
2024-01-29 21:42:40 +01:00
// the lat 2 checkers of a corner must leave at the same time
2024-01-30 21:59:47 +01:00
if (from0 == corner_field || from1 == corner_field) && (from0 != from1) && corner_count == 2
{
return false;
2024-01-29 21:42:40 +01:00
}
2024-01-30 21:59:47 +01:00
// ------- exit rules ----------
// -- toutes les dames doivent être dans le jan de retour
// -- si on peut sortir, on doit sortir
// -- priorité :
// - dame se trouvant sur la flêche correspondant au dé
// - dame se trouvant plus loin de la sortie que la flêche (point défaillant)
// - dame se trouvant plus près que la flêche (point exédant)
// --- cadran rempli si possible ----
// --- interdit de jouer dans cadran que l'adversaire peut encore remplir ----
2024-01-29 21:42:40 +01:00
// no rule was broken
true
}
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;
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-01-12 17:02:18 +01: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(),
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,
2023-10-07 20:46:24 +02:00
},
);
}
PlayerDisconnected { player_id } => {
self.players.remove(player_id);
}
2024-02-03 22:16:14 +01:00
Roll { player_id: _ } => {
self.roll();
self.turn_stage = TurnStage::MarkPoints;
}
Mark { player_id, points } => {
self.mark_points(*player_id, *points);
if self.stage != Stage::Ended {
self.turn_stage = TurnStage::Move;
}
}
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();
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
}
2024-02-03 22:16:14 +01:00
fn mark_points(&mut self, player_id: PlayerId, points: u8) {
todo!()
}
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-02-03 22:16:14 +01:00
Mark {
player_id: PlayerId,
points: u8,
},
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
}
impl Roll for GameState {
2024-02-03 22:16:14 +01:00
fn roll(&mut self) -> &mut Self {
2023-10-07 20:46:24 +02:00
self.dices = self.dices.roll();
2023-10-28 15:12:04 +02:00
if self.who_plays().is_none() {
2024-01-30 21:59:47 +01:00
let active_color = match self.dices.coin() {
2024-01-27 19:11:23 +01:00
false => Color::Black,
2024-01-30 21:59:47 +01:00
true => Color::White,
2024-01-27 19:11:23 +01:00
};
2023-10-28 15:12:04 +02:00
let color_player_id = self.player_id_by_color(active_color);
2024-01-12 17:02:18 +01:00
if color_player_id.is_some() {
2023-10-28 15:12:04 +02:00
self.active_player_id = *color_player_id.unwrap();
2023-10-07 20:46:24 +02:00
}
}
2024-02-03 22:16:14 +01:00
self
2023-10-07 20:46:24 +02:00
}
}
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)?;
// remove checker from old position
2024-01-27 20:22:20 +01:00
self.board.set(&player.color, from, -1)?;
2023-10-07 20:46:24 +02:00
// 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 {
2024-01-20 21:40:06 +01:00
// self.board.set_off(player, 1)?;
2023-10-07 20:46:24 +02:00
} else {
2024-01-20 21:40:06 +01:00
// self.board.set(player, new_position as usize, 1)?;
2023-10-07 20:46:24 +02:00
}
// switch to other player if all dices have been consumed
2024-01-20 21:40:06 +01:00
self.switch_active_player();
self.roll_first = true;
2023-10-07 20:46:24 +02:00
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)
}
2024-01-20 21:40:06 +01:00
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_string_id() {
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 == "Dz8+AAAAAT8/MAAAAAQAADAD");
}
2024-01-31 15:39:02 +01:00
#[test]
2024-02-05 22:15:13 +01:00
fn test_moves_possible() {
2024-01-31 15:39:02 +01:00
let mut state = GameState::default();
let player1 = Player::new("player1".into(), Color::White);
let player_id = 1;
state.add_player(player_id, player1);
state.add_player(2, Player::new("player2".into(), Color::Black));
state.consume(&GameEvent::BeginGame {
goes_first: player_id,
});
// Chained moves
let moves = (
CheckerMove::new(1, 5).unwrap(),
CheckerMove::new(5, 9).unwrap(),
);
2024-02-05 22:15:13 +01:00
assert!(state.moves_possible(&Color::White, &moves));
2024-01-31 15:39:02 +01:00
// not chained moves
let moves = (
CheckerMove::new(1, 5).unwrap(),
CheckerMove::new(6, 9).unwrap(),
);
2024-02-05 22:15:13 +01:00
assert!(!state.moves_possible(&Color::White, &moves));
2024-01-31 15:39:02 +01:00
}
2024-02-03 22:16:14 +01:00
#[test]
fn test_moves_follow_dices() {
let mut state = GameState::default();
let player1 = Player::new("player1".into(), Color::White);
let player_id = 1;
state.add_player(player_id, player1);
state.add_player(2, Player::new("player2".into(), Color::Black));
state.consume(&GameEvent::BeginGame {
goes_first: player_id,
});
state.consume(&GameEvent::Roll { player_id });
2024-02-05 22:15:13 +01:00
let dices = state.dices.values;
2024-02-03 22:16:14 +01:00
let moves = (
2024-02-05 22:15:13 +01:00
CheckerMove::new(1, (1 + dices.0).into()).unwrap(),
CheckerMove::new((1 + dices.0).into(), (1 + dices.0 + dices.1).into()).unwrap(),
2024-02-03 22:16:14 +01:00
);
assert!(state.moves_follows_dices(&Color::White, &moves));
2024-02-05 22:15:13 +01:00
let badmoves = (
CheckerMove::new(1, (2 + dices.0).into()).unwrap(),
CheckerMove::new((1 + dices.0).into(), (1 + dices.0 + dices.1).into()).unwrap(),
);
assert!(!state.moves_follows_dices(&Color::White, &badmoves));
2024-02-03 22:16:14 +01:00
}
2023-10-07 20:46:24 +02:00
}