From 06aeed95a5a5ef9685c4f3e4fbe0dfe2573fc6bc Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Sat, 27 Jan 2024 20:22:20 +0100 Subject: [PATCH] check physical move --- client/src/main.rs | 16 +++--- doc/backlog.md | 7 ++- doc/journal.md | 2 +- doc/vocabulary.md | 4 ++ store/src/board.rs | 126 ++++++++++++++++++++++++++++++++++----------- store/src/error.rs | 31 +---------- store/src/game.rs | 26 +++++----- store/src/lib.rs | 2 + 8 files changed, 131 insertions(+), 83 deletions(-) create mode 100644 doc/vocabulary.md diff --git a/client/src/main.rs b/client/src/main.rs index b5c1962..504602e 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,9 +1,9 @@ use std::{net::UdpSocket, time::SystemTime}; 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_renet::{ renet::{transport::ClientAuthentication, ConnectionConfig, RenetClient}, @@ -123,9 +123,9 @@ fn update_board( ) { for event in game_events.iter() { match event.0 { - GameEvent::Move { player_id, from: _, to } => { - // backgammon postions, TODO : dépend de player_id - let (x, y) = if to < 13 { (13 - to, 1) } else { (to - 13, 0)}; + GameEvent::Move { player_id, moves } => { + // trictrac positions, TODO : dépend de player_id + let (x, y) = if moves.0.get_to() < 13 { (13 - moves.0.get_to(), 1) } else { (moves.0.get_to() - 13, 0)}; let texture = asset_server.load(match game_state.0.players[&player_id].color { store::Color::Black => "tac.png", @@ -211,8 +211,10 @@ fn input( info!("sending movement from: {:?} to: {:?} ", from_tile, tile); let event = GameEvent::Move { player_id: client_id.0, - from: from_tile, - to: tile, + moves: ( + CheckerMove::new(from_tile, tile).unwrap(), + CheckerMove::new(from_tile, tile).unwrap() + ) }; client.send_message(0, bincode::serialize(&event).unwrap()); } diff --git a/doc/backlog.md b/doc/backlog.md index 5c5bb51..923eeb3 100644 --- a/doc/backlog.md +++ b/doc/backlog.md @@ -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) -## TODO +## DONE ### Epic : jeu simple @@ -75,6 +75,11 @@ Store - déplacement de dames - jet des dés - 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) - calcul des points automatique (pas d'écoles) diff --git a/doc/journal.md b/doc/journal.md index 4585824..1435822 100644 --- a/doc/journal.md +++ b/doc/journal.md @@ -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 -_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 diff --git a/doc/vocabulary.md b/doc/vocabulary.md new file mode 100644 index 0000000..d14bcfe --- /dev/null +++ b/doc/vocabulary.md @@ -0,0 +1,4 @@ +# Vocabulary + +Dames : checkers / men +cases : points diff --git a/store/src/board.rs b/store/src/board.rs index e2adf21..220121a 100644 --- a/store/src/board.rs +++ b/store/src/board.rs @@ -3,16 +3,38 @@ use crate::Error; use serde::{Deserialize, Serialize}; 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 { + 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 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Board { - board: [i8; 24], + positions: [i8; 24], } impl Default for Board { fn default() -> Self { 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, ], } @@ -23,7 +45,7 @@ impl Default for Board { 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)); + s.push_str(&format!("{:?}", self.positions)); write!(f, "{}", s) } } @@ -39,7 +61,7 @@ impl Board { // 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 white_board = self.positions.clone(); let mut pos_bits = white_board.iter().fold(vec![], |acc, nb| { let mut new_acc = acc.clone(); if *nb > 0 { @@ -51,7 +73,7 @@ impl Board { }); // - black positions - let mut black_board = self.board.clone(); + let mut black_board = self.positions.clone(); black_board.reverse(); let mut pos_black_bits = black_board.iter().fold(vec![], |acc, nb| { 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, /// but there is already one checker from the other player on the field, that checker is hit and /// 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 { return Err(Error::FieldInvalid); } - if self.blocked(player, field)? { + if self.blocked(color, field)? { return Err(Error::FieldBlocked); } - match player.color { + match color { Color::White => { - let new = self.board[field - 1] + amount; + let new = self.positions[field - 1] + amount; if new < 0 { return Err(Error::MoveInvalid); } - self.board[field - 1] = new; + self.positions[field - 1] = new; Ok(()) } Color::Black => { - let new = self.board[24 - field] - amount; + let new = self.positions[24 - field] - amount; if new > 0 { return Err(Error::MoveInvalid); } - self.board[24 - field] = new; + self.positions[24 - field] = new; Ok(()) } @@ -111,21 +133,22 @@ impl Board { } /// Check if a field is blocked for a player - pub fn blocked(&self, player: &Player, field: usize) -> Result { + pub fn blocked(&self, color: &Color, field: Field) -> Result { if field < 1 || 24 < field { 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 => { - if self.board[field - 1] < 0 { + if field == 13 || self.positions[field - 1] < 0 { Ok(true) } else { Ok(false) } } Color::Black => { - if self.board[23 - field] > 1 { + if field == 12 || self.positions[23 - field] > 1 { Ok(true) } else { Ok(false) @@ -133,12 +156,59 @@ impl Board { } } } + + pub fn get_checkers_color(&self, field: Field) -> Result, 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 pub trait Move { /// 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 Self: Sized; @@ -161,25 +231,22 @@ mod tests { #[test] fn blocked_outofrange() -> Result<(), Error> { let board = Board::new(); - let player = Player::new("".into(), Color::White); - assert!(board.blocked( &player, 0).is_err()); - assert!(board.blocked( &player, 28).is_err()); + assert!(board.blocked( &Color::White, 0).is_err()); + assert!(board.blocked( &Color::White, 28).is_err()); Ok(()) } #[test] fn blocked_otherplayer() -> Result<(), Error> { let board = Board::new(); - let player = Player::new("".into(), Color::White); - assert!(board.blocked( &player, 24)?); + assert!(board.blocked( &Color::White, 24)?); Ok(()) } #[test] fn blocked_notblocked() -> Result<(), Error> { let board = Board::new(); - let player = Player::new("".into(), Color::White); - assert!(!board.blocked( &player, 6)?); + assert!(!board.blocked( &Color::White, 6)?); Ok(()) } @@ -187,9 +254,8 @@ mod tests { #[test] fn set_field_blocked() { let mut board = Board::new(); - let player = Player::new("".into(), Color::White); assert!( - board.set( &player, 0, 24) + board.set( &Color::White, 0, 24) .is_err() ); } @@ -197,18 +263,16 @@ mod tests { #[test] fn set_wrong_field1() { let mut board = Board::new(); - let player = Player::new("".into(), Color::White); assert!(board - .set( &player, 50, 2) + .set( &Color::White, 50, 2) .is_err()); } #[test] fn set_wrong_amount0() { let mut board = Board::new(); - let player = Player::new("".into(), Color::White); assert!(board - .set(&player , 23, -3) + .set(&Color::White , 23, -3) .is_err()); } @@ -217,7 +281,7 @@ mod tests { let mut board = Board::new(); let player = Player::new("".into(), Color::White); assert!(board - .set( &player, 23, -3) + .set( &Color::White, 23, -3) .is_err()); } } diff --git a/store/src/error.rs b/store/src/error.rs index 694009a..b6839f5 100644 --- a/store/src/error.rs +++ b/store/src/error.rs @@ -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; -/// Holds all possible errors that can occur during a Backgammon game. +/// Holds all possible errors that can occur during a Trictrac game. #[derive(Debug)] pub enum Error { /// Game has already started GameStarted, /// Game has already ended GameEnded, - /// Opponent offered doubling cube. Need to react on this event first. - CubeReceived, /// Doubling not permitted DoublingNotPermitted, - /// Invalid cube value - CubeValueInvalid, /// Invalid player PlayerInvalid, /// Field blocked @@ -24,8 +20,6 @@ pub enum Error { NotYourTurn, /// Invalid move MoveInvalid, - /// Invalid move, checker on bar - MoveInvalidBar, /// Move first MoveFirst, /// Roll first @@ -44,13 +38,6 @@ impl fmt::Display for Error { Error::GameStarted => write!(f, "Game has already started"), Error::GameEnded => write!(f, "Game has already ended"), 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::FieldBlocked => write!(f, "Field blocked"), Error::FieldInvalid => write!(f, "Invalid field"), @@ -59,7 +46,6 @@ impl fmt::Display for Error { Error::MoveFirst => write!(f, "Move first"), Error::RollFirst => write!(f, "Roll first"), 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::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::FieldInvalid), "Invalid field"); 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::RollFirst), "Roll first"); assert_eq!(format!("{}", Error::DiceInvalid), "Invalid dice"); - assert_eq!( - format!("{}", Error::MoveInvalidBar), - "Invalid move, checker on bar" - ); } } diff --git a/store/src/game.rs b/store/src/game.rs index dfa30f7..cfc5164 100644 --- a/store/src/game.rs +++ b/store/src/game.rs @@ -1,9 +1,9 @@ //! # Play a TricTrac Game -use crate::board::{Board, Move}; +use crate::board::{Board, Field, CheckerMove, Move}; use crate::dice::{Dices, Roll}; use crate::player::{Color, Player, PlayerId}; use crate::Error; -use log::{error}; +use log::error; // use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -215,8 +215,7 @@ impl GameState { } Move { player_id, - from, - to, + moves, } => { // Check player exists if !self.players.contains_key(player_id) { @@ -229,13 +228,14 @@ impl GameState { return false; } - // Check that the tile index is inside the board - if *to > 23 { + // Check move is physically possible + if !self.board.move_possible(&self.players[player_id].color, moves.0){ return false; } - if *from > 23 { + if !self.board.move_possible(&self.players[player_id].color, moves.1){ return false; } + // Check move is allowed by the rules (to desactivate when playing with schools) } } @@ -277,12 +277,11 @@ impl GameState { Roll { player_id: _ } => {} Move { player_id, - from, - to, + moves } => { 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(); + self.board.move_checker(&player.color, moves.0).unwrap(); + self.board.move_checker(&player.color, moves.1).unwrap(); self.active_player_id = self .players .keys() @@ -331,8 +330,7 @@ pub enum GameEvent { }, Move { player_id: PlayerId, - from: usize, - to: usize, + moves: (CheckerMove, CheckerMove), }, } @@ -359,7 +357,7 @@ impl Move for GameState { let _ = self.move_permitted(player, dice)?; // 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 let new_position = from as i8 - dice as i8; diff --git a/store/src/lib.rs b/store/src/lib.rs index 73a2101..564fcd8 100644 --- a/store/src/lib.rs +++ b/store/src/lib.rs @@ -8,4 +8,6 @@ mod error; pub use error::Error; mod board; +pub use board::CheckerMove; + mod dice;