wip encodage position
This commit is contained in:
parent
14ae91eae7
commit
8dbaf597c9
|
|
@ -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)
|
||||||
|
|
|
||||||
50
doc/blog/game-state-notation.md
Normal file
50
doc/blog/game-state-notation.md
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue