wip encodage position

This commit is contained in:
Henri Bourcereau 2024-01-12 17:02:18 +01:00
parent 14ae91eae7
commit 8dbaf597c9
4 changed files with 203 additions and 85 deletions

View file

@ -40,55 +40,14 @@ cf. https://blessed.rs/crates
## Specs ## 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 13 14 .. 23 24
12 11 .. 2 1 12 11 .. 2 1
=> utilisation de la notation Backgammon : uniformisation de la notation quelque soit le jeu de table. Encodage efficace : https://www.gnu.org/software/gnubg/manual/html_node/A-technical-description-of-the-Position-ID.html
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
#### State data #### State data
* piece placement -> 77bits (24 + 23 + 30 max) * piece placement -> 77bits (24 + 23 + 30 max)

View file

@ -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

View file

@ -1,10 +1,11 @@
use crate::player::{Color, Player}; use crate::player::{Color, Player};
use crate::Error; use crate::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt;
/// Represents the Tric Trac board /// Represents the Tric Trac board
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct Board { pub struct Board {
board: [i8; 24], 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 { impl Board {
/// Create a new board /// Create a new board
pub fn new() -> Self { pub fn new() -> Self {
Board::default() Board::default()
} }
pub fn toGnupgPosId(&self) -> Vec<bool> {
// 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 /// 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 /// This method adds the amount of checkers for a player on a field. The field is numbered from

View file

@ -1,13 +1,13 @@
//! # Play a TricTrac Game //! # Play a TricTrac Game
use crate::player::{Color, Player, PlayerId};
use crate::board::{Board, Move}; use crate::board::{Board, Move};
use crate::dice::{Dices, Roll}; use crate::dice::{Dices, Roll};
use crate::player::{Color, Player, PlayerId};
use crate::Error; use crate::Error;
use log::{error, info, trace, warn}; use log::{error, info, trace, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt; use std::{fmt, vec};
type TGPN = [u8]; type TGPN = [u8];
@ -48,7 +48,7 @@ impl fmt::Display for GameState {
let mut s = String::new(); let mut s = String::new();
s.push_str(&format!("Dices: {:?}\n", self.dices)); 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!("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) write!(f, "{}", s)
} }
} }
@ -75,8 +75,48 @@ impl GameState {
} }
/// Format to TGPN notation (Tables games position notation) /// Format to TGPN notation (Tables games position notation)
fn toTGPN(&self, f: &mut fmt::Formatter) -> TGPN { // fn toTGPN(&self, f: &mut fmt::Formatter) -> TGPN {
b"ll" 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> { pub fn who_plays(&self) -> Option<&Player> {
@ -84,35 +124,39 @@ impl GameState {
} }
pub fn switch_active_player(&mut self) { pub fn switch_active_player(&mut self) {
let other_player_id = self.players.iter() let other_player_id = self
.filter(|(id, _player)| **id != self.active_player_id ) .players
.map(|(id, _player)| *id ) .iter()
.filter(|(id, _player)| **id != self.active_player_id)
.map(|(id, _player)| *id)
.next(); .next();
self.active_player_id = other_player_id.unwrap_or(0); self.active_player_id = other_player_id.unwrap_or(0);
} }
pub fn player_id_by_color(&self, color: Color) -> Option<&PlayerId> { pub fn player_id_by_color(&self, color: Color) -> Option<&PlayerId> {
self.players.iter() self.players
.iter()
.filter(|(_id, player)| player.color == color) .filter(|(_id, player)| player.color == color)
.map(|(id, _player)| id ) .map(|(id, _player)| id)
.next() .next()
} }
pub fn player_id(&self, player: &Player) -> Option<&PlayerId> { pub fn player_id(&self, player: &Player) -> Option<&PlayerId> {
self.players.iter() self.players
.iter()
.filter(|(_id, candidate)| player.color == candidate.color) .filter(|(_id, candidate)| player.color == candidate.color)
.map(|(id, _candidate)| id ) .map(|(id, _candidate)| id)
.next() .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 { pub fn validate(&self, event: &GameEvent) -> bool {
use GameEvent::*; use GameEvent::*;
match event { match event {
BeginGame { goes_first } => { BeginGame { goes_first } => {
// Check that the player supposed to go first exists // Check that the player supposed to go first exists
if !self.players.contains_key(goes_first) { 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) // 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 { if self.active_player_id != *player_id {
return false; return false;
} }
} }
Move { player_id, from, to } => { Move {
player_id,
from,
to,
} => {
// Check player exists // Check player exists
if !self.players.contains_key(player_id) { if !self.players.contains_key(player_id) {
error!("Player {} unknown", player_id); error!("Player {} unknown", player_id);
return false; return false;
} }
// Check player is currently the one making their move // Check player is currently the one making their move
if self.active_player_id != *player_id { 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; return false;
} }
@ -189,21 +236,28 @@ impl GameState {
} }
EndGame { reason: _ } => self.stage = Stage::Ended, EndGame { reason: _ } => self.stage = Stage::Ended,
PlayerJoined { player_id, name } => { 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( self.players.insert(
*player_id, *player_id,
Player { Player {
name: name.to_string(), name: name.to_string(),
color color,
}, },
); );
} }
PlayerDisconnected { player_id } => { PlayerDisconnected { player_id } => {
self.players.remove(player_id); self.players.remove(player_id);
} }
Roll { player_id } => { Roll { player_id } => {}
} Move {
Move { player_id, from, to } => { player_id,
from,
to,
} => {
let player = self.players.get(player_id).unwrap(); let player = self.players.get(player_id).unwrap();
self.board.set(player, *from, 0 as i8).unwrap(); self.board.set(player, *from, 0 as i8).unwrap();
self.board.set(player, *to, 1 as i8).unwrap(); self.board.set(player, *to, 1 as i8).unwrap();
@ -223,7 +277,6 @@ impl GameState {
pub fn determine_winner(&self) -> Option<PlayerId> { pub fn determine_winner(&self) -> Option<PlayerId> {
None None
} }
} }
/// The reasons why a game could end /// The reasons why a game could end
@ -238,28 +291,41 @@ pub enum EndGameReason {
/// An event that progresses the GameState forward /// An event that progresses the GameState forward
#[derive(Debug, Clone, Serialize, PartialEq, Deserialize)] #[derive(Debug, Clone, Serialize, PartialEq, Deserialize)]
pub enum GameEvent { pub enum GameEvent {
BeginGame { goes_first: PlayerId }, BeginGame {
EndGame { reason: EndGameReason }, goes_first: PlayerId,
PlayerJoined { player_id: PlayerId, name: String }, },
PlayerDisconnected { player_id: PlayerId }, EndGame {
Roll { player_id: PlayerId }, reason: EndGameReason,
Move { player_id: PlayerId, from: usize, to: usize }, },
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 { impl Roll for GameState {
fn roll(&mut self) -> Result<&mut Self, Error> { fn roll(&mut self) -> Result<&mut Self, Error> {
if !self.dices.consumed.0 if !self.dices.consumed.0 && !self.dices.consumed.1 {
&& !self.dices.consumed.1
{
return Err(Error::MoveFirst); return Err(Error::MoveFirst);
} }
self.dices = self.dices.roll(); self.dices = self.dices.roll();
if self.who_plays().is_none() { if self.who_plays().is_none() {
let diff = self.dices.values.0 - self.dices.values.1; 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); 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(); 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 // switch to other player if all dices have been consumed
if self.dices.consumed.0 if self.dices.consumed.0 && self.dices.consumed.1 {
&& self.dices.consumed.1
{
self.switch_active_player(); self.switch_active_player();
self.roll_first = true; self.roll_first = true;
} }