check physical move

This commit is contained in:
Henri Bourcereau 2024-01-27 20:22:20 +01:00
parent 6f504acf12
commit 06aeed95a5
8 changed files with 131 additions and 83 deletions

View file

@ -1,9 +1,9 @@
use std::{net::UdpSocket, time::SystemTime}; use std::{net::UdpSocket, time::SystemTime};
use renet::transport::{NetcodeClientTransport, NetcodeTransportError, NETCODE_USER_DATA_BYTES}; use renet::transport::{NetcodeClientTransport, NetcodeTransportError, NETCODE_USER_DATA_BYTES};
use store::{GameEvent, GameState}; use store::{GameEvent, GameState, CheckerMove};
use bevy::{prelude::*}; use bevy::prelude::*;
use bevy::window::PrimaryWindow; use bevy::window::PrimaryWindow;
use bevy_renet::{ use bevy_renet::{
renet::{transport::ClientAuthentication, ConnectionConfig, RenetClient}, renet::{transport::ClientAuthentication, ConnectionConfig, RenetClient},
@ -123,9 +123,9 @@ fn update_board(
) { ) {
for event in game_events.iter() { for event in game_events.iter() {
match event.0 { match event.0 {
GameEvent::Move { player_id, from: _, to } => { GameEvent::Move { player_id, moves } => {
// backgammon postions, TODO : dépend de player_id // trictrac positions, TODO : dépend de player_id
let (x, y) = if to < 13 { (13 - to, 1) } else { (to - 13, 0)}; let (x, y) = if moves.0.get_to() < 13 { (13 - moves.0.get_to(), 1) } else { (moves.0.get_to() - 13, 0)};
let texture = let texture =
asset_server.load(match game_state.0.players[&player_id].color { asset_server.load(match game_state.0.players[&player_id].color {
store::Color::Black => "tac.png", store::Color::Black => "tac.png",
@ -211,8 +211,10 @@ fn input(
info!("sending movement from: {:?} to: {:?} ", from_tile, tile); info!("sending movement from: {:?} to: {:?} ", from_tile, tile);
let event = GameEvent::Move { let event = GameEvent::Move {
player_id: client_id.0, player_id: client_id.0,
from: from_tile, moves: (
to: tile, CheckerMove::new(from_tile, tile).unwrap(),
CheckerMove::new(from_tile, tile).unwrap()
)
}; };
client.send_message(0, bincode::serialize(&event).unwrap()); client.send_message(0, bincode::serialize(&event).unwrap());
} }

View file

@ -66,7 +66,7 @@ Encodage efficace : https://www.gnu.org/software/gnubg/manual/html_node/A-techni
Total : 77 + 1 + 2 + 6 + 20 = 105 bits = 17.666 * 6 -> 18 u32 (108 possible) Total : 77 + 1 + 2 + 6 + 20 = 105 bits = 17.666 * 6 -> 18 u32 (108 possible)
## TODO ## DONE
### Epic : jeu simple ### Epic : jeu simple
@ -75,6 +75,11 @@ Store
- déplacement de dames - déplacement de dames
- jet des dés - jet des dés
- déplacements physiques possibles - déplacements physiques possibles
## TODO
### Epic : jeu simple
- déplacements autorisés par les règles (pourront être validés physiquement si jeu avec écoles) - déplacements autorisés par les règles (pourront être validés physiquement si jeu avec écoles)
- calcul des points automatique (pas d'écoles) - calcul des points automatique (pas d'écoles)

View file

@ -8,7 +8,7 @@ cargo add pico-args
Organisation store / server / client selon https://herluf-ba.github.io/making-a-turn-based-multiplayer-game-in-rust-01-whats-a-turn-based-game-anyway Organisation store / server / client selon https://herluf-ba.github.io/making-a-turn-based-multiplayer-game-in-rust-01-whats-a-turn-based-game-anyway
_store_ est la bibliothèque contenant le _reducer_ qui transforme l'état du jeu en fonction les évènements. Elle est utilisée par le _server_ et le _client_. Seuls les évènements sont transmis entre clients et serveur. _store_ est la bibliothèque contenant le _reducer_ qui transforme l'état du jeu en fonction des évènements. Elle est utilisée par le _server_ et le _client_. Seuls les évènements sont transmis entre clients et serveur.
## Organisation du store ## Organisation du store

4
doc/vocabulary.md Normal file
View file

@ -0,0 +1,4 @@
# Vocabulary
Dames : checkers / men
cases : points

View file

@ -3,16 +3,38 @@ use crate::Error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
/// field (aka 'point') position on the board (from 1 to 24)
pub type Field = usize;
#[derive(Debug, Copy, Clone, Serialize, PartialEq, Deserialize)]
pub struct CheckerMove {
from: Field,
to: Field,
}
impl CheckerMove {
pub fn new(from: Field, to: Field) -> Result<Self, Error> {
if from < 1 || 24 < from || to < 1 || 24 < to {
return Err(Error::FieldInvalid);
}
Ok(CheckerMove { from, to })
}
pub fn get_to(&self) -> Field {
self.to
}
}
/// Represents the Tric Trac board /// Represents the Tric Trac board
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Board { pub struct Board {
board: [i8; 24], positions: [i8; 24],
} }
impl Default for Board { impl Default for Board {
fn default() -> Self { fn default() -> Self {
Board { Board {
board: [ positions: [
15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15,
], ],
} }
@ -23,7 +45,7 @@ impl Default for Board {
impl fmt::Display for Board { impl fmt::Display for Board {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut s = String::new(); let mut s = String::new();
s.push_str(&format!("{:?}", self.board)); s.push_str(&format!("{:?}", self.positions));
write!(f, "{}", s) write!(f, "{}", s)
} }
} }
@ -39,7 +61,7 @@ impl Board {
// Pieces placement -> 77bits (24 + 23 + 30 max) // 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 // inspired by https://www.gnu.org/software/gnubg/manual/html_node/A-technical-description-of-the-Position-ID.html
// - white positions // - white positions
let white_board = self.board.clone(); let white_board = self.positions.clone();
let mut pos_bits = white_board.iter().fold(vec![], |acc, nb| { let mut pos_bits = white_board.iter().fold(vec![], |acc, nb| {
let mut new_acc = acc.clone(); let mut new_acc = acc.clone();
if *nb > 0 { if *nb > 0 {
@ -51,7 +73,7 @@ impl Board {
}); });
// - black positions // - black positions
let mut black_board = self.board.clone(); let mut black_board = self.positions.clone();
black_board.reverse(); black_board.reverse();
let mut pos_black_bits = black_board.iter().fold(vec![], |acc, nb| { let mut pos_black_bits = black_board.iter().fold(vec![], |acc, nb| {
let mut new_acc = acc.clone(); let mut new_acc = acc.clone();
@ -79,31 +101,31 @@ impl Board {
/// If the field is blocked for the player, an error is returned. If the field is not blocked, /// If the field is blocked for the player, an error is returned. If the field is not blocked,
/// but there is already one checker from the other player on the field, that checker is hit and /// but there is already one checker from the other player on the field, that checker is hit and
/// moved to the bar. /// moved to the bar.
pub fn set(&mut self, player: &Player, field: usize, amount: i8) -> Result<(), Error> { pub fn set(&mut self, color: &Color, field: Field, amount: i8) -> Result<(), Error> {
if field > 24 { if field > 24 {
return Err(Error::FieldInvalid); return Err(Error::FieldInvalid);
} }
if self.blocked(player, field)? { if self.blocked(color, field)? {
return Err(Error::FieldBlocked); return Err(Error::FieldBlocked);
} }
match player.color { match color {
Color::White => { Color::White => {
let new = self.board[field - 1] + amount; let new = self.positions[field - 1] + amount;
if new < 0 { if new < 0 {
return Err(Error::MoveInvalid); return Err(Error::MoveInvalid);
} }
self.board[field - 1] = new; self.positions[field - 1] = new;
Ok(()) Ok(())
} }
Color::Black => { Color::Black => {
let new = self.board[24 - field] - amount; let new = self.positions[24 - field] - amount;
if new > 0 { if new > 0 {
return Err(Error::MoveInvalid); return Err(Error::MoveInvalid);
} }
self.board[24 - field] = new; self.positions[24 - field] = new;
Ok(()) Ok(())
} }
@ -111,21 +133,22 @@ impl Board {
} }
/// Check if a field is blocked for a player /// Check if a field is blocked for a player
pub fn blocked(&self, player: &Player, field: usize) -> Result<bool, Error> { pub fn blocked(&self, color: &Color, field: Field) -> Result<bool, Error> {
if field < 1 || 24 < field { if field < 1 || 24 < field {
return Err(Error::FieldInvalid); return Err(Error::FieldInvalid);
} }
match player.color { // the square is blocked on the opponent rest corner or if there are opponent's men on the square
match color {
Color::White => { Color::White => {
if self.board[field - 1] < 0 { if field == 13 || self.positions[field - 1] < 0 {
Ok(true) Ok(true)
} else { } else {
Ok(false) Ok(false)
} }
} }
Color::Black => { Color::Black => {
if self.board[23 - field] > 1 { if field == 12 || self.positions[23 - field] > 1 {
Ok(true) Ok(true)
} else { } else {
Ok(false) Ok(false)
@ -133,12 +156,59 @@ impl Board {
} }
} }
} }
pub fn get_checkers_color(&self, field: Field) -> Result<Option<&Color>, Error> {
if field < 1 || field > 24 {
return Err(Error::FieldInvalid);
}
let checkers_count = self.positions[field - 1];
let color = if checkers_count < 0 {
Some(&Color::Black)
} else if checkers_count > 0 {
Some(&Color::White)
} else {
None
};
Ok(color)
}
pub fn move_possible(&self, color: &Color, cmove: CheckerMove) -> bool {
let blocked = self.blocked(color, cmove.to).unwrap_or(true);
// Check if there is a player's checker on the 'from' square
let has_checker = self.get_checkers_color(cmove.from).unwrap_or(None) == Some(color);
has_checker && !blocked
}
pub fn move_checker(&mut self, color: &Color, cmove: CheckerMove) -> Result<(), Error> {
self.remove_checker(color, cmove.from)?;
self.add_checker(color, cmove.to)?;
Ok(())
}
pub fn remove_checker(&mut self, color: &Color, field: Field) -> Result<(), Error> {
let checker_color = self.get_checkers_color(field)?;
if Some(color) != checker_color {
return Err(Error::FieldInvalid);
}
self.positions[field] -= 1;
Ok(())
}
pub fn add_checker(&mut self, color: &Color, field: Field) -> Result<(), Error> {
let checker_color = self.get_checkers_color(field)?;
// error if the case contains the other color
if None != checker_color && Some(color) != checker_color {
return Err(Error::FieldInvalid);
}
self.positions[field] += 1;
Ok(())
}
} }
/// Trait to move checkers /// Trait to move checkers
pub trait Move { pub trait Move {
/// Move a checker /// Move a checker
fn move_checker(&mut self, player: &Player, dice: u8, from: usize) -> Result<&mut Self, Error> fn move_checker(&mut self, player: &Player, dice: u8, from: Field) -> Result<&mut Self, Error>
where where
Self: Sized; Self: Sized;
@ -161,25 +231,22 @@ mod tests {
#[test] #[test]
fn blocked_outofrange() -> Result<(), Error> { fn blocked_outofrange() -> Result<(), Error> {
let board = Board::new(); let board = Board::new();
let player = Player::new("".into(), Color::White); assert!(board.blocked( &Color::White, 0).is_err());
assert!(board.blocked( &player, 0).is_err()); assert!(board.blocked( &Color::White, 28).is_err());
assert!(board.blocked( &player, 28).is_err());
Ok(()) Ok(())
} }
#[test] #[test]
fn blocked_otherplayer() -> Result<(), Error> { fn blocked_otherplayer() -> Result<(), Error> {
let board = Board::new(); let board = Board::new();
let player = Player::new("".into(), Color::White); assert!(board.blocked( &Color::White, 24)?);
assert!(board.blocked( &player, 24)?);
Ok(()) Ok(())
} }
#[test] #[test]
fn blocked_notblocked() -> Result<(), Error> { fn blocked_notblocked() -> Result<(), Error> {
let board = Board::new(); let board = Board::new();
let player = Player::new("".into(), Color::White); assert!(!board.blocked( &Color::White, 6)?);
assert!(!board.blocked( &player, 6)?);
Ok(()) Ok(())
} }
@ -187,9 +254,8 @@ mod tests {
#[test] #[test]
fn set_field_blocked() { fn set_field_blocked() {
let mut board = Board::new(); let mut board = Board::new();
let player = Player::new("".into(), Color::White);
assert!( assert!(
board.set( &player, 0, 24) board.set( &Color::White, 0, 24)
.is_err() .is_err()
); );
} }
@ -197,18 +263,16 @@ mod tests {
#[test] #[test]
fn set_wrong_field1() { fn set_wrong_field1() {
let mut board = Board::new(); let mut board = Board::new();
let player = Player::new("".into(), Color::White);
assert!(board assert!(board
.set( &player, 50, 2) .set( &Color::White, 50, 2)
.is_err()); .is_err());
} }
#[test] #[test]
fn set_wrong_amount0() { fn set_wrong_amount0() {
let mut board = Board::new(); let mut board = Board::new();
let player = Player::new("".into(), Color::White);
assert!(board assert!(board
.set(&player , 23, -3) .set(&Color::White , 23, -3)
.is_err()); .is_err());
} }
@ -217,7 +281,7 @@ mod tests {
let mut board = Board::new(); let mut board = Board::new();
let player = Player::new("".into(), Color::White); let player = Player::new("".into(), Color::White);
assert!(board assert!(board
.set( &player, 23, -3) .set( &Color::White, 23, -3)
.is_err()); .is_err());
} }
} }

View file

@ -1,19 +1,15 @@
/// This module contains the error definition for the Backgammon game. /// This module contains the error definition for the Trictrac game.
use std::fmt; use std::fmt;
/// Holds all possible errors that can occur during a Backgammon game. /// Holds all possible errors that can occur during a Trictrac game.
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
/// Game has already started /// Game has already started
GameStarted, GameStarted,
/// Game has already ended /// Game has already ended
GameEnded, GameEnded,
/// Opponent offered doubling cube. Need to react on this event first.
CubeReceived,
/// Doubling not permitted /// Doubling not permitted
DoublingNotPermitted, DoublingNotPermitted,
/// Invalid cube value
CubeValueInvalid,
/// Invalid player /// Invalid player
PlayerInvalid, PlayerInvalid,
/// Field blocked /// Field blocked
@ -24,8 +20,6 @@ pub enum Error {
NotYourTurn, NotYourTurn,
/// Invalid move /// Invalid move
MoveInvalid, MoveInvalid,
/// Invalid move, checker on bar
MoveInvalidBar,
/// Move first /// Move first
MoveFirst, MoveFirst,
/// Roll first /// Roll first
@ -44,13 +38,6 @@ impl fmt::Display for Error {
Error::GameStarted => write!(f, "Game has already started"), Error::GameStarted => write!(f, "Game has already started"),
Error::GameEnded => write!(f, "Game has already ended"), Error::GameEnded => write!(f, "Game has already ended"),
Error::PlayerInvalid => write!(f, "Invalid player"), Error::PlayerInvalid => write!(f, "Invalid player"),
Error::CubeReceived => {
write!(
f,
"Opponent offered dice. Need to first accept or decline the doubling dice."
)
}
Error::CubeValueInvalid => write!(f, "Invalid cube value"),
Error::DoublingNotPermitted => write!(f, "Doubling not permitted"), Error::DoublingNotPermitted => write!(f, "Doubling not permitted"),
Error::FieldBlocked => write!(f, "Field blocked"), Error::FieldBlocked => write!(f, "Field blocked"),
Error::FieldInvalid => write!(f, "Invalid field"), Error::FieldInvalid => write!(f, "Invalid field"),
@ -59,7 +46,6 @@ impl fmt::Display for Error {
Error::MoveFirst => write!(f, "Move first"), Error::MoveFirst => write!(f, "Move first"),
Error::RollFirst => write!(f, "Roll first"), Error::RollFirst => write!(f, "Roll first"),
Error::DiceInvalid => write!(f, "Invalid dice"), Error::DiceInvalid => write!(f, "Invalid dice"),
Error::MoveInvalidBar => write!(f, "Invalid move, checker on bar"),
} }
} }
} }
@ -76,15 +62,6 @@ mod tests {
); );
assert_eq!(format!("{}", Error::GameEnded), "Game has already ended"); assert_eq!(format!("{}", Error::GameEnded), "Game has already ended");
assert_eq!(format!("{}", Error::PlayerInvalid), "Invalid player"); assert_eq!(format!("{}", Error::PlayerInvalid), "Invalid player");
assert_eq!(
format!("{}", Error::CubeReceived),
"Opponent offered dice. Need to first accept or decline the doubling dice."
);
assert_eq!(format!("{}", Error::CubeValueInvalid), "Invalid cube value");
assert_eq!(
format!("{}", Error::DoublingNotPermitted),
"Doubling not permitted"
);
assert_eq!(format!("{}", Error::FieldBlocked), "Field blocked"); assert_eq!(format!("{}", Error::FieldBlocked), "Field blocked");
assert_eq!(format!("{}", Error::FieldInvalid), "Invalid field"); assert_eq!(format!("{}", Error::FieldInvalid), "Invalid field");
assert_eq!(format!("{}", Error::NotYourTurn), "Not your turn"); assert_eq!(format!("{}", Error::NotYourTurn), "Not your turn");
@ -92,9 +69,5 @@ mod tests {
assert_eq!(format!("{}", Error::MoveFirst), "Move first"); assert_eq!(format!("{}", Error::MoveFirst), "Move first");
assert_eq!(format!("{}", Error::RollFirst), "Roll first"); assert_eq!(format!("{}", Error::RollFirst), "Roll first");
assert_eq!(format!("{}", Error::DiceInvalid), "Invalid dice"); assert_eq!(format!("{}", Error::DiceInvalid), "Invalid dice");
assert_eq!(
format!("{}", Error::MoveInvalidBar),
"Invalid move, checker on bar"
);
} }
} }

View file

@ -1,9 +1,9 @@
//! # Play a TricTrac Game //! # Play a TricTrac Game
use crate::board::{Board, Move}; use crate::board::{Board, Field, CheckerMove, Move};
use crate::dice::{Dices, Roll}; use crate::dice::{Dices, Roll};
use crate::player::{Color, Player, PlayerId}; use crate::player::{Color, Player, PlayerId};
use crate::Error; use crate::Error;
use log::{error}; use log::error;
// use itertools::Itertools; // use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -215,8 +215,7 @@ impl GameState {
} }
Move { Move {
player_id, player_id,
from, moves,
to,
} => { } => {
// Check player exists // Check player exists
if !self.players.contains_key(player_id) { if !self.players.contains_key(player_id) {
@ -229,13 +228,14 @@ impl GameState {
return false; return false;
} }
// Check that the tile index is inside the board // Check move is physically possible
if *to > 23 { if !self.board.move_possible(&self.players[player_id].color, moves.0){
return false; return false;
} }
if *from > 23 { if !self.board.move_possible(&self.players[player_id].color, moves.1){
return false; return false;
} }
// Check move is allowed by the rules (to desactivate when playing with schools)
} }
} }
@ -277,12 +277,11 @@ impl GameState {
Roll { player_id: _ } => {} Roll { player_id: _ } => {}
Move { Move {
player_id, player_id,
from, moves
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.move_checker(&player.color, moves.0).unwrap();
self.board.set(player, *to, 1 as i8).unwrap(); self.board.move_checker(&player.color, moves.1).unwrap();
self.active_player_id = self self.active_player_id = self
.players .players
.keys() .keys()
@ -331,8 +330,7 @@ pub enum GameEvent {
}, },
Move { Move {
player_id: PlayerId, player_id: PlayerId,
from: usize, moves: (CheckerMove, CheckerMove),
to: usize,
}, },
} }
@ -359,7 +357,7 @@ impl Move for GameState {
let _ = self.move_permitted(player, dice)?; let _ = self.move_permitted(player, dice)?;
// remove checker from old position // remove checker from old position
self.board.set(player, from, -1)?; self.board.set(&player.color, from, -1)?;
// move checker to new position, in case it is reaching the off position, set it off // 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; let new_position = from as i8 - dice as i8;

View file

@ -8,4 +8,6 @@ mod error;
pub use error::Error; pub use error::Error;
mod board; mod board;
pub use board::CheckerMove;
mod dice; mod dice;