From 8dbaf597c9e522d3b4f091e50c9a9630f6662a4b Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Fri, 12 Jan 2024 17:02:18 +0100 Subject: [PATCH] wip encodage position --- doc/backlog.md | 47 +---------- doc/blog/game-state-notation.md | 50 +++++++++++ store/src/board.rs | 47 ++++++++++- store/src/game.rs | 144 +++++++++++++++++++++++--------- 4 files changed, 203 insertions(+), 85 deletions(-) create mode 100644 doc/blog/game-state-notation.md diff --git a/doc/backlog.md b/doc/backlog.md index 93a1570..16d3ada 100644 --- a/doc/backlog.md +++ b/doc/backlog.md @@ -40,55 +40,14 @@ cf. https://blessed.rs/crates ## Specs -### Game state notation +## Représentation des cases : -#### History +cf. ./blog/game-state-notation.md -Jollyvet : rien - -1698 Le jeu de trictrac... -Noirs T 1 2 .. 11 -Blancs T 1 2 .. 11 - -1738 Le Grand Trictrac, Bernard Laurent Soumille -A B C D E F G H I K L M -& Z Y X V T S R Q P O N - -1816 Guiton -Noirs T 1 2 .. 11 -Blancs T 1 2 .. 11 - -1818 Cours complet de Trictrac, Pierre Marie Michel Lepeintre -m n o p q r s t u v x y -l k j i h g f e d c b a - -1852 Le jeu de trictrac rendu facile -Noirs T 1 2 .. 11 -Blancs T 1 2 .. 11 - -#### Références actuelles - -https://salondesjeux.fr/trictrac.htm : Guiton -Noirs T 1 2 .. 11 -Blancs T 1 2 .. 11 - -http://trictrac.org/content/index2.html -N1 N2 .. N12 -B1 B2 .. B12 - -Backgammon 13 14 .. 23 24 12 11 .. 2 1 -=> utilisation de la notation Backgammon : uniformisation de la notation quelque soit le jeu de table. -Non dénuée d'avantages : -- on se débarrasse de la notation spéciale du talon -- on évite confusion entre côté noir et blanc. -- bien que l'orientation change par rapport à la plupart des traité, on suit celle du Lepeintre, et celle des vidéos de Philippe Lalanne - -Backgammon notation : https://nymann.dev/2023/05/16/Introducing-the-Backgammon-Position-Notation-BPN-A-Standard-for-Representing-Game-State/ - -GnuBg : https://www.gnu.org/software/gnubg/manual/html_node/A-technical-description-of-the-Position-ID.html +Encodage efficace : https://www.gnu.org/software/gnubg/manual/html_node/A-technical-description-of-the-Position-ID.html #### State data * piece placement -> 77bits (24 + 23 + 30 max) diff --git a/doc/blog/game-state-notation.md b/doc/blog/game-state-notation.md new file mode 100644 index 0000000..e96ffee --- /dev/null +++ b/doc/blog/game-state-notation.md @@ -0,0 +1,50 @@ + +# Game state notation + +## History + +Jollyvet : rien + +1698 Le jeu de trictrac... +Noirs T 1 2 .. 11 +Blancs T 1 2 .. 11 + +1738 Le Grand Trictrac, Bernard Laurent Soumille +A B C D E F G H I K L M +& Z Y X V T S R Q P O N + +1816 Guiton +Noirs T 1 2 .. 11 +Blancs T 1 2 .. 11 + +1818 Cours complet de Trictrac, Pierre Marie Michel Lepeintre +m n o p q r s t u v x y +l k j i h g f e d c b a + +1852 Le jeu de trictrac rendu facile +Noirs T 1 2 .. 11 +Blancs T 1 2 .. 11 + +## Références actuelles + +https://salondesjeux.fr/trictrac.htm : Guiton +Noirs T 1 2 .. 11 +Blancs T 1 2 .. 11 + +http://trictrac.org/content/index2.html +N1 N2 .. N12 +B1 B2 .. B12 + +Backgammon +13 14 .. 23 24 +12 11 .. 2 1 + +=> utilisation de la notation Backgammon : uniformisation de la notation quelque soit le jeu de table. +Non dénuée d'avantages : +- on se débarrasse de la notation spéciale du talon +- on évite confusion entre côté noir et blanc. +- bien que l'orientation change par rapport à la plupart des traité, on suit celle du Lepeintre, et celle des vidéos de Philippe Lalanne + +Backgammon notation : https://nymann.dev/2023/05/16/Introducing-the-Backgammon-Position-Notation-BPN-A-Standard-for-Representing-Game-State/ + +GnuBg : https://www.gnu.org/software/gnubg/manual/html_node/A-technical-description-of-the-Position-ID.html diff --git a/store/src/board.rs b/store/src/board.rs index dc57cb3..13d9e2c 100644 --- a/store/src/board.rs +++ b/store/src/board.rs @@ -1,10 +1,11 @@ use crate::player::{Color, Player}; use crate::Error; use serde::{Deserialize, Serialize}; +use std::fmt; /// Represents the Tric Trac board #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -struct Board { +pub struct Board { board: [i8; 24], } @@ -18,12 +19,56 @@ impl Default for Board { } } +// implement Display trait +impl fmt::Display for Board { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut s = String::new(); + s.push_str(&format!("{:?}", self.board)); + write!(f, "{}", s) + } +} + impl Board { /// Create a new board pub fn new() -> Self { Board::default() } + pub fn toGnupgPosId(&self) -> Vec { + // Pieces placement -> 77bits (24 + 23 + 30 max) + // inspired by https://www.gnu.org/software/gnubg/manual/html_node/A-technical-description-of-the-Position-ID.html + // - white positions + let white_board = self.board.clone(); + let mut posBits = white_board.iter().fold(vec![], |acc, nb| { + let mut newAcc = acc.clone(); + if *nb as usize > 0 { + // add as many `true` as there are pieces on the arrow + newAcc.append(&mut vec![true; *nb as usize]); + } + newAcc.push(false); // arrow separator + newAcc + }); + + // - black positions + let mut black_board = self.board.clone(); + black_board.reverse(); + let mut posBlackBits = black_board.iter().fold(vec![], |acc, nb| { + let mut newAcc = acc.clone(); + if (*nb as usize) < 0 { + // add as many `true` as there are pieces on the arrow + newAcc.append(&mut vec![true; 0 - *nb as usize]); + } + newAcc.push(false); // arrow separator + newAcc + }); + + posBits.append(&mut posBlackBits); + + // fill with 0 bits until 77 + posBits.resize(77, false); + posBits + } + /// 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 diff --git a/store/src/game.rs b/store/src/game.rs index c9c2500..fb53382 100644 --- a/store/src/game.rs +++ b/store/src/game.rs @@ -1,13 +1,13 @@ //! # Play a TricTrac Game -use crate::player::{Color, Player, PlayerId}; use crate::board::{Board, Move}; use crate::dice::{Dices, Roll}; +use crate::player::{Color, Player, PlayerId}; use crate::Error; use log::{error, info, trace, warn}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::fmt; +use std::{fmt, vec}; type TGPN = [u8]; @@ -48,7 +48,7 @@ impl fmt::Display for GameState { let mut s = String::new(); s.push_str(&format!("Dices: {:?}\n", self.dices)); // s.push_str(&format!("Who plays: {}\n", self.who_plays().map(|player| &player.name ).unwrap_or(""))); - s.push_str(&format!("Board: {:?}\n", self.board.get())); + s.push_str(&format!("Board: {:?}\n", self.board)); write!(f, "{}", s) } } @@ -75,8 +75,48 @@ impl GameState { } /// Format to TGPN notation (Tables games position notation) - fn toTGPN(&self, f: &mut fmt::Formatter) -> TGPN { - b"ll" + // fn toTGPN(&self, f: &mut fmt::Formatter) -> TGPN { + pub fn toTGPN(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut s = String::new(); + // s.push_str(&format!("Dices: {:?}\n", self.dices)); + write!(f, "{}", s) + } + + /// Calculate game state id : + pub fn to_string_id(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Pieces placement -> 77 bits (24 + 23 + 30 max) + let mut pos_bits = self.board.toGnupgPosId(); + + // active player -> 1 bit + // white : 0 (false) + // black : 1 (true) + pos_bits.push( + self.who_plays() + .map(|player| player.color == Color::Black) + .unwrap_or(false), // White by default + ); + + // step -> 2 bits + // * roll dice + // * mark points (jeton & fichet) & set bredouille markers (3rd jeton & pavillon) + // * move pieces + let mut step_bits = match self.turn_stage { + TurnStage::RollDice => [false, false], + TurnStage::MarkPoints => [false, true], + TurnStage::Move => [true, false], + }; + pos_bits.append(&mut step_bits.into()); + + // dice roll -> 4 bits + // points 10bits x2 joueurs = 20bits + // * points -> 4bits + // * trous -> 4bits + // * bredouille possible 1bit + // * grande bredouille possible 1bit + + let mut s = String::new(); + // s.push_str(&format!("Dices: {:?}\n", self.dices)); + write!(f, "{}", s) } pub fn who_plays(&self) -> Option<&Player> { @@ -84,35 +124,39 @@ impl GameState { } 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 ) + 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); } pub fn player_id_by_color(&self, color: Color) -> Option<&PlayerId> { - self.players.iter() + self.players + .iter() .filter(|(_id, player)| player.color == color) - .map(|(id, _player)| id ) + .map(|(id, _player)| id) .next() } pub fn player_id(&self, player: &Player) -> Option<&PlayerId> { - self.players.iter() + self.players + .iter() .filter(|(_id, candidate)| player.color == candidate.color) - .map(|(id, _candidate)| id ) + .map(|(id, _candidate)| id) .next() } - /// Determines whether an event is valid considering the current GameState + /// 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; + return false; } // Check that the game hasn't started yet. (we don't want to double start a game) @@ -150,17 +194,20 @@ impl GameState { if self.active_player_id != *player_id { return false; } - } - Move { player_id, from, to } => { + Move { + player_id, + from, + to, + } => { // Check player exists if !self.players.contains_key(player_id) { - error!("Player {} unknown", 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); + error!("Player not active : {}", self.active_player_id); return false; } @@ -174,10 +221,10 @@ impl GameState { } } - // We couldn't find anything wrong with the event so it must be good + // 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) { @@ -189,21 +236,28 @@ impl GameState { } EndGame { reason: _ } => self.stage = Stage::Ended, PlayerJoined { player_id, name } => { - let color = if self.players.len() > 0 { Color::White } else { Color::Black }; + let color = if self.players.len() > 0 { + Color::White + } else { + Color::Black + }; self.players.insert( *player_id, Player { name: name.to_string(), - color + color, }, ); } PlayerDisconnected { player_id } => { self.players.remove(player_id); } - Roll { player_id } => { - } - Move { player_id, from, to } => { + Roll { player_id } => {} + Move { + player_id, + from, + to, + } => { let player = self.players.get(player_id).unwrap(); self.board.set(player, *from, 0 as i8).unwrap(); self.board.set(player, *to, 1 as i8).unwrap(); @@ -223,7 +277,6 @@ impl GameState { pub fn determine_winner(&self) -> Option { None } - } /// The reasons why a game could end @@ -238,28 +291,41 @@ pub enum EndGameReason { /// 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, from: usize, to: usize }, + 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, + from: usize, + to: usize, + }, } impl Roll for GameState { fn roll(&mut self) -> Result<&mut Self, Error> { - if !self.dices.consumed.0 - && !self.dices.consumed.1 - { + if !self.dices.consumed.0 && !self.dices.consumed.1 { return Err(Error::MoveFirst); } self.dices = self.dices.roll(); if self.who_plays().is_none() { let diff = self.dices.values.0 - self.dices.values.1; - let active_color = if diff < 0 { Color::Black } else { Color::White }; + 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(){ + if color_player_id.is_some() { self.active_player_id = *color_player_id.unwrap(); } } @@ -298,9 +364,7 @@ impl Move for GameState { } // switch to other player if all dices have been consumed - if self.dices.consumed.0 - && self.dices.consumed.1 - { + if self.dices.consumed.0 && self.dices.consumed.1 { self.switch_active_player(); self.roll_first = true; }