diff --git a/store/src/game.rs b/store/src/game.rs index b3542f1..9fe0c35 100644 --- a/store/src/game.rs +++ b/store/src/game.rs @@ -1,9 +1,9 @@ //! # Play a TricTrac Game use crate::board::{Board, CheckerMove, Field, EMPTY_MOVE}; use crate::dice::Dice; +use crate::game_rules_moves::{MoveError, MoveRules}; use crate::player::{Color, Player, PlayerId}; use log::error; -use std::cmp; // use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -12,27 +12,6 @@ use std::{fmt, str}; use base64::{engine::general_purpose, Engine as _}; -#[derive(std::cmp::PartialEq, Debug)] -pub enum MoveError { - // 2 checkers must go at the same time on an empty corner - // & the last 2 checkers of a corner must leave at the same time - CornerNeedsTwoCheckers, - // Prise de coin de repos par puissance alors qu'il est possible - // de le prendre directement (par "effet") - CornerByEffectPossible, - // toutes les dames doivent être dans le jan de retour - ExitNeedsAllCheckersOnLastQuarter, - // mouvement avec nombre en exédant alors qu'une séquence de mouvements - // sans nombre en excédant est possible - ExitByEffectPossible, - // Sortie avec nombre en excédant d'une dame qui n'est pas la plus éloignée - ExitNotFasthest, - // Jeu dans un cadran que l'adversaire peut encore remplir - OpponentCanFillQuarter, - // remplir cadran si possible & conserver cadran rempli si possible ---- - MustFillQuarter, -} - /// The different stages a game can be in. (not to be confused with the entire "GameState") #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Stage { @@ -80,6 +59,15 @@ impl fmt::Display for GameState { } } +impl MoveRules for GameState { + fn board(&self) -> &Board { + &self.board + } + fn dice(&self) -> &Dice { + &self.dice + } +} + impl Default for GameState { fn default() -> Self { Self { @@ -295,361 +283,6 @@ impl GameState { true } - fn moves_possible(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { - // Check move is physically possible - if !self.board.move_possible(color, &moves.0) { - return false; - } - - // Chained_move : "Tout d'une" - if let Ok(chained_move) = moves.0.chain(moves.1) { - if !self.board.move_possible(color, &chained_move) { - return false; - } - } else if !self.board.move_possible(color, &moves.1) { - return false; - } - true - } - - fn get_move_compatible_dices(&self, color: &Color, cmove: &CheckerMove) -> Vec { - let (dice1, dice2) = self.dice.values; - - let mut move_dices = Vec::new(); - if cmove.get_to() == 0 { - // handle empty move (0, 0) only one checker left, exiting with the first die. - if cmove.get_from() == 0 { - move_dices.push(dice1); - move_dices.push(dice2); - return move_dices; - } - - // Exits - let min_dist = match color { - Color::White => 25 - cmove.get_from(), - Color::Black => cmove.get_from(), - }; - if dice1 as usize >= min_dist { - move_dices.push(dice1); - } - if dice2 as usize >= min_dist { - move_dices.push(dice2); - } - } else { - let dist = (cmove.get_to() as i8 - cmove.get_from() as i8).unsigned_abs(); - if dice1 == dist { - move_dices.push(dice1); - } - if dice2 == dist { - move_dices.push(dice2); - } - } - move_dices - } - - fn moves_follows_dices(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { - // Prise de coin par puissance - if self.is_move_by_puissance(color, moves) { - return true; - } - - let (dice1, dice2) = self.dice.values; - let (move1, move2): &(CheckerMove, CheckerMove) = moves; - - let move1_dices = self.get_move_compatible_dices(color, move1); - if move1_dices.is_empty() { - return false; - } - let move2_dices = self.get_move_compatible_dices(color, move2); - if move2_dices.is_empty() { - return false; - } - if move1_dices.len() == 1 - && move2_dices.len() == 1 - && move1_dices[0] == move2_dices[0] - && dice1 != dice2 - { - return false; - } - - // no rule was broken - true - } - - fn moves_allowed( - &self, - color: &Color, - moves: &(CheckerMove, CheckerMove), - ) -> Result<(), MoveError> { - // ------- corner rules ---------- - let corner_field: Field = self.board.get_color_corner(color); - let (corner_count, _color) = self.board.get_field_checkers(corner_field).unwrap(); - let (from0, to0, from1, to1) = ( - moves.0.get_from(), - moves.0.get_to(), - moves.1.get_from(), - moves.1.get_to(), - ); - // 2 checkers must go at the same time on an empty corner - if (to0 == corner_field || to1 == corner_field) && (to0 != to1) && corner_count == 0 { - return Err(MoveError::CornerNeedsTwoCheckers); - } - - // the last 2 checkers of a corner must leave at the same time - if (from0 == corner_field || from1 == corner_field) && (from0 != from1) && corner_count == 2 - { - return Err(MoveError::CornerNeedsTwoCheckers); - } - - if self.is_move_by_puissance(color, moves) && self.can_take_corner_by_effect(color) { - return Err(MoveError::CornerByEffectPossible); - } - - // check exit rules - if moves.0.get_to() == 0 || moves.1.get_to() == 0 { - // toutes les dames doivent être dans le jan de retour - let has_outsiders = !self - .board - .get_color_fields(*color) - .iter() - .filter(|(field, _count)| { - (*color == Color::White && *field < 19) - || (*color == Color::Black && *field > 6) - }) - .collect::>() - .is_empty(); - if has_outsiders { - return Err(MoveError::ExitNeedsAllCheckersOnLastQuarter); - } - - // toutes les sorties directes sont autorisées, ainsi que les nombre défaillants - let possible_moves_sequences = self.get_possible_moves_sequences(color); - if !possible_moves_sequences.contains(moves) { - // À ce stade au moins un des déplacements concerne un nombre en excédant - // - si d'autres séquences de mouvements sans nombre en excédant étaient possibles, on - // refuse cette séquence - if !possible_moves_sequences.is_empty() { - return Err(MoveError::ExitByEffectPossible); - } - - // - la dame choisie doit être la plus éloignée de la sortie - let mut checkers = self.board.get_color_fields(*color); - checkers.sort_by(|a, b| { - if *color == Color::White { - b.0.cmp(&a.0) - } else { - a.0.cmp(&b.0) - } - }); - let mut farthest = if *color == Color::White { 24 } else { 1 }; - let mut next_farthest = if *color == Color::White { 24 } else { 1 }; - let mut has_two_checkers = false; - if let Some((field, count)) = checkers.first() { - farthest = *field; - if *count > 1 { - next_farthest = *field; - has_two_checkers = true; - } else if let Some((field, _count)) = checkers.get(1) { - next_farthest = *field; - has_two_checkers = true; - } - } - - // s'il reste au moins deux dames, on vérifie que les plus éloignées soint choisies - if has_two_checkers { - if moves.0.get_to() == 0 && moves.1.get_to() == 0 { - // Deux coups sortants en excédant - if *color == Color::White { - if cmp::max(moves.0.get_from(), moves.1.get_from()) > next_farthest { - return Err(MoveError::ExitNotFasthest); - } - } else if cmp::min(moves.0.get_from(), moves.1.get_from()) < next_farthest { - return Err(MoveError::ExitNotFasthest); - } - } else { - // Un seul coup sortant en excédant le coup sortant doit concerner la plus éloignée du bord - let exit_move_field = if moves.0.get_to() == 0 { - moves.0.get_from() - } else { - moves.1.get_from() - }; - if exit_move_field != farthest { - return Err(MoveError::ExitNotFasthest); - } - } - } - } - } - - // --- interdit de jouer dans cadran que l'adversaire peut encore remplir ---- - let farthest = if *color == Color::White { - cmp::max(moves.0.get_to(), moves.1.get_to()) - } else { - cmp::min(moves.0.get_to(), moves.1.get_to()) - }; - let in_opponent_side = if *color == Color::White { - farthest > 12 - } else { - farthest < 13 - }; - - if in_opponent_side - && self - .board - .is_quarter_fillable(color.opponent_color(), farthest) - { - return Err(MoveError::OpponentCanFillQuarter); - } - - // --- remplir cadran si possible & conserver cadran rempli si possible ---- - let filling_moves_sequences = self.get_quarter_filling_moves_sequences(color); - if !filling_moves_sequences.contains(moves) && !filling_moves_sequences.is_empty() { - return Err(MoveError::MustFillQuarter); - } - // no rule was broken - Ok(()) - } - - fn get_possible_moves_sequences(&self, color: &Color) -> Vec<(CheckerMove, CheckerMove)> { - let (dice1, dice2) = self.dice.values; - let mut moves_seqs = self.get_possible_moves_sequences_by_dices(color, dice1, dice2); - let mut moves_seqs_order2 = self.get_possible_moves_sequences_by_dices(color, dice1, dice2); - moves_seqs.append(&mut moves_seqs_order2); - moves_seqs - } - - fn get_quarter_filling_moves_sequences( - &self, - color: &Color, - ) -> Vec<(CheckerMove, CheckerMove)> { - let mut moves_seqs = Vec::new(); - for moves in self.get_possible_moves_sequences(color) { - let mut board = self.board.clone(); - board.move_checker(color, moves.0).unwrap(); - board.move_checker(color, moves.1).unwrap(); - if board.any_quarter_filled(*color) { - moves_seqs.push(moves); - } - } - moves_seqs - } - - fn get_possible_moves_sequences_by_dices( - &self, - color: &Color, - dice1: u8, - dice2: u8, - ) -> Vec<(CheckerMove, CheckerMove)> { - let mut moves_seqs = Vec::new(); - for first_move in self.board.get_possible_moves(*color, dice1, false) { - let mut board2 = self.board.clone(); - if board2.move_checker(color, first_move).is_err() { - println!("err move"); - continue; - } - if board2.get_color_fields(*color).is_empty() { - // no checkers left : empty move - println!("empty move"); - moves_seqs.push((first_move, EMPTY_MOVE)); - } else { - for second_move in board2.get_possible_moves(*color, dice2, false) { - moves_seqs.push((first_move, second_move)); - } - } - } - moves_seqs - } - - fn get_direct_exit_moves(&self, color: &Color) -> Vec { - let mut moves = Vec::new(); - let (dice1, dice2) = self.dice.values; - - // sorties directes simples - let (field1_candidate, field2_candidate) = if color == &Color::White { - (25 - dice1 as usize, 25 - dice2 as usize) - } else { - (dice1 as usize, dice2 as usize) - }; - let (count1, col1) = self.board.get_field_checkers(field1_candidate).unwrap(); - let (count2, col2) = self.board.get_field_checkers(field2_candidate).unwrap(); - if count1 > 0 { - moves.push(CheckerMove::new(field1_candidate, 0).unwrap()); - } - if dice2 != dice1 { - if count2 > 0 { - moves.push(CheckerMove::new(field2_candidate, 0).unwrap()); - } - } else if count1 > 1 { - // doublet et deux dames disponibles - moves.push(CheckerMove::new(field1_candidate, 0).unwrap()); - } - - // sortie directe tout d'une - let fieldall_candidate = if color == &Color::White { - 25 - dice1 - dice2 - } else { - dice1 + dice2 - } as usize; - let (countall, _col) = self.board.get_field_checkers(fieldall_candidate).unwrap(); - if countall > 0 { - if col1.is_none() || col1 == Some(color) { - moves.push(CheckerMove::new(fieldall_candidate, field1_candidate).unwrap()); - moves.push(CheckerMove::new(field1_candidate, 0).unwrap()); - } - if col2.is_none() || col2 == Some(color) { - moves.push(CheckerMove::new(fieldall_candidate, field2_candidate).unwrap()); - moves.push(CheckerMove::new(field2_candidate, 0).unwrap()); - } - } - moves - } - - fn is_move_by_puissance(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { - let (dice1, dice2) = self.dice.values; - let (move1, move2): &(CheckerMove, CheckerMove) = moves; - let dist1 = (move1.get_to() as i8 - move1.get_from() as i8).unsigned_abs(); - let dist2 = (move2.get_to() as i8 - move2.get_from() as i8).unsigned_abs(); - - // Both corners must be empty - let (count1, _color) = self.board.get_field_checkers(12).unwrap(); - let (count2, _color2) = self.board.get_field_checkers(13).unwrap(); - if count1 > 0 || count2 > 0 { - return false; - } - - move1.get_to() == move2.get_to() - && move1.get_to() == self.board.get_color_corner(color) - && ((*color == Color::White - && cmp::min(dist1, dist2) == cmp::min(dice1, dice2) - 1 - && cmp::max(dist1, dist2) == cmp::max(dice1, dice2) - 1) - || (*color == Color::Black - && cmp::min(dist1, dist2) == cmp::min(dice1, dice2) + 1 - && cmp::max(dist1, dist2) == cmp::max(dice1, dice2) + 1)) - } - - fn can_take_corner_by_effect(&self, color: &Color) -> bool { - // return false if corner already taken - let corner_field: Field = self.board.get_color_corner(color); - let (count, _col) = self.board.get_field_checkers(corner_field).unwrap(); - if count > 0 { - return false; - } - - let (dice1, dice2) = self.dice.values; - let (field1, field2) = match color { - Color::White => (12 - dice1, 12 - dice2), - Color::Black => (13 + dice1, 13 + dice2), - }; - let res1 = self.board.get_field_checkers(field1.into()); - let res2 = self.board.get_field_checkers(field2.into()); - if res1.is_err() || res2.is_err() { - return false; - } - let (count1, opt_color1) = res1.unwrap(); - let (count2, opt_color2) = res2.unwrap(); - count1 > 0 && count2 > 0 && opt_color1 == Some(color) && opt_color2 == Some(color) - } - // ---------------------------------------------------------------------------------- // State updates // ---------------------------------------------------------------------------------- @@ -820,298 +453,4 @@ mod tests { // println!("string_id : {}", string_id); assert!(string_id == "Dz8+AAAAAT8/MAAAAAQAADAD"); } - - #[test] - fn moves_possible() { - let mut state = GameState::default(); - let player1 = Player::new("player1".into(), Color::White); - let player_id = 1; - state.add_player(player_id, player1); - state.add_player(2, Player::new("player2".into(), Color::Black)); - state.consume(&GameEvent::BeginGame { - goes_first: player_id, - }); - - // Chained moves - let moves = ( - CheckerMove::new(1, 5).unwrap(), - CheckerMove::new(5, 9).unwrap(), - ); - assert!(state.moves_possible(&Color::White, &moves)); - - // not chained moves - let moves = ( - CheckerMove::new(1, 5).unwrap(), - CheckerMove::new(6, 9).unwrap(), - ); - assert!(!state.moves_possible(&Color::White, &moves)); - - // black moves - let moves = ( - CheckerMove::new(24, 20).unwrap(), - CheckerMove::new(20, 19).unwrap(), - ); - assert!(state.moves_possible(&Color::Black, &moves)); - } - - #[test] - fn moves_follow_dices() { - let mut state = GameState::default(); - let player1 = Player::new("player1".into(), Color::White); - let player_id = 1; - state.add_player(player_id, player1); - state.add_player(2, Player::new("player2".into(), Color::Black)); - state.consume(&GameEvent::BeginGame { - goes_first: player_id, - }); - state.consume(&GameEvent::Roll { player_id }); - let dice = state.dice.values; - let moves = ( - CheckerMove::new(1, (1 + dice.0).into()).unwrap(), - CheckerMove::new((1 + dice.0).into(), (1 + dice.0 + dice.1).into()).unwrap(), - ); - assert!(state.moves_follows_dices(&Color::White, &moves)); - - let badmoves = ( - CheckerMove::new(1, (2 + dice.0).into()).unwrap(), - CheckerMove::new((1 + dice.0).into(), (1 + dice.0 + dice.1).into()).unwrap(), - ); - assert!(!state.moves_follows_dices(&Color::White, &badmoves)); - } - - #[test] - fn can_take_corner_by_effect() { - let mut state = GameState::default(); - let player1 = Player::new("player1".into(), Color::White); - let player_id = 1; - state.add_player(player_id, player1); - state.add_player(2, Player::new("player2".into(), Color::Black)); - state.consume(&GameEvent::BeginGame { - goes_first: player_id, - }); - state.consume(&GameEvent::Roll { player_id }); - - state.board.set_positions([ - 10, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, - ]); - state.dice.values = (4, 4); - assert!(state.can_take_corner_by_effect(&Color::White)); - - state.dice.values = (5, 5); - assert!(!state.can_take_corner_by_effect(&Color::White)); - - state.board.set_positions([ - 10, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, - ]); - state.dice.values = (4, 4); - assert!(!state.can_take_corner_by_effect(&Color::White)); - - state.board.set_positions([ - 10, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, - ]); - state.dice.values = (1, 1); - assert!(state.can_take_corner_by_effect(&Color::Black)); - } - - #[test] - fn prise_en_puissance() { - let mut state = GameState::default(); - let player1 = Player::new("player1".into(), Color::White); - let player_id = 1; - state.add_player(player_id, player1); - state.add_player(2, Player::new("player2".into(), Color::Black)); - state.consume(&GameEvent::BeginGame { - goes_first: player_id, - }); - state.consume(&GameEvent::Roll { player_id }); - - // prise par puissance ok - state.board.set_positions([ - 10, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, - ]); - state.dice.values = (5, 5); - let moves = ( - CheckerMove::new(8, 12).unwrap(), - CheckerMove::new(8, 12).unwrap(), - ); - assert!(state.is_move_by_puissance(&Color::White, &moves)); - assert!(state.moves_follows_dices(&Color::White, &moves)); - assert!(state.moves_allowed(&Color::White, &moves).is_ok()); - - // opponent corner must be empty - state.board.set_positions([ - 10, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, - ]); - assert!(!state.is_move_by_puissance(&Color::White, &moves)); - assert!(!state.moves_follows_dices(&Color::White, &moves)); - - // Si on a la possibilité de prendre son coin à la fois par effet, c'est à dire naturellement, et aussi par puissance, on doit le prendre par effet - state.board.set_positions([ - 5, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, - ]); - assert_eq!( - Err(MoveError::CornerByEffectPossible), - state.moves_allowed(&Color::White, &moves) - ); - - // on a déjà pris son coin : on ne peux plus y deplacer des dames par puissance - state.board.set_positions([ - 8, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, - ]); - assert!(!state.is_move_by_puissance(&Color::White, &moves)); - assert!(!state.moves_follows_dices(&Color::White, &moves)); - } - - #[test] - fn exit() { - let mut state = GameState::default(); - let player1 = Player::new("player1".into(), Color::White); - let player_id = 1; - state.add_player(player_id, player1); - state.add_player(2, Player::new("player2".into(), Color::Black)); - state.consume(&GameEvent::BeginGame { - goes_first: player_id, - }); - state.consume(&GameEvent::Roll { player_id }); - - // exit ok - state.board.set_positions([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, - ]); - state.dice.values = (5, 5); - let moves = ( - CheckerMove::new(20, 0).unwrap(), - CheckerMove::new(20, 0).unwrap(), - ); - assert!(state.moves_possible(&Color::White, &moves)); - assert!(state.moves_follows_dices(&Color::White, &moves)); - assert!(state.moves_allowed(&Color::White, &moves).is_ok()); - - // toutes les dames doivent être dans le jan de retour - state.board.set_positions([ - 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, - ]); - state.dice.values = (5, 5); - let moves = ( - CheckerMove::new(20, 0).unwrap(), - CheckerMove::new(20, 0).unwrap(), - ); - assert_eq!( - Err(MoveError::ExitNeedsAllCheckersOnLastQuarter), - state.moves_allowed(&Color::White, &moves) - ); - - // on ne peut pas sortir une dame avec un nombre excédant si on peut en jouer une avec un nombre défaillant - state.board.set_positions([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 0, 2, 0, - ]); - state.dice.values = (5, 5); - let moves = ( - CheckerMove::new(20, 0).unwrap(), - CheckerMove::new(23, 0).unwrap(), - ); - assert_eq!( - Err(MoveError::ExitByEffectPossible), - state.moves_allowed(&Color::White, &moves) - ); - - // on doit jouer le nombre excédant le plus éloigné - state.board.set_positions([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, - ]); - state.dice.values = (5, 5); - let moves = ( - CheckerMove::new(20, 0).unwrap(), - CheckerMove::new(23, 0).unwrap(), - ); - assert_eq!( - Err(MoveError::ExitNotFasthest), - state.moves_allowed(&Color::White, &moves) - ); - let moves = ( - CheckerMove::new(20, 0).unwrap(), - CheckerMove::new(20, 0).unwrap(), - ); - assert!(state.moves_allowed(&Color::White, &moves).is_ok()); - - // Cas de la dernière dame - state.board.set_positions([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, - ]); - state.dice.values = (5, 5); - let moves = ( - CheckerMove::new(23, 0).unwrap(), - CheckerMove::new(0, 0).unwrap(), - ); - assert!(state.moves_possible(&Color::White, &moves)); - assert!(state.moves_follows_dices(&Color::White, &moves)); - assert!(state.moves_allowed(&Color::White, &moves).is_ok()); - } - - #[test] - fn move_check_opponent_fillable_quarter() { - let mut state = GameState::default(); - state.board.set_positions([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, - ]); - state.dice.values = (5, 5); - let moves = ( - CheckerMove::new(11, 16).unwrap(), - CheckerMove::new(11, 16).unwrap(), - ); - assert!(state.moves_allowed(&Color::White, &moves).is_ok()); - - state.board.set_positions([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 1, 0, - ]); - state.dice.values = (5, 5); - let moves = ( - CheckerMove::new(11, 16).unwrap(), - CheckerMove::new(11, 16).unwrap(), - ); - assert_eq!( - Err(MoveError::OpponentCanFillQuarter), - state.moves_allowed(&Color::White, &moves) - ); - } - - #[test] - fn move_check_fillable_quarter() { - let mut state = GameState::default(); - state.board.set_positions([ - 3, 3, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, - ]); - state.dice.values = (5, 4); - let moves = ( - CheckerMove::new(1, 6).unwrap(), - CheckerMove::new(2, 6).unwrap(), - ); - assert!(state.moves_allowed(&Color::White, &moves).is_ok()); - let moves = ( - CheckerMove::new(1, 5).unwrap(), - CheckerMove::new(2, 7).unwrap(), - ); - assert_eq!( - Err(MoveError::MustFillQuarter), - state.moves_allowed(&Color::White, &moves) - ); - - state.board.set_positions([ - 2, 3, 2, 2, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]); - state.dice.values = (2, 3); - let moves = ( - CheckerMove::new(6, 8).unwrap(), - CheckerMove::new(6, 9).unwrap(), - ); - assert_eq!( - Err(MoveError::MustFillQuarter), - state.moves_allowed(&Color::White, &moves) - ); - let moves = ( - CheckerMove::new(2, 4).unwrap(), - CheckerMove::new(5, 8).unwrap(), - ); - assert!(state.moves_allowed(&Color::White, &moves).is_ok()); - } } diff --git a/store/src/game_rules_moves.rs b/store/src/game_rules_moves.rs new file mode 100644 index 0000000..c2c455b --- /dev/null +++ b/store/src/game_rules_moves.rs @@ -0,0 +1,625 @@ +//! # Play a TricTrac Game +use crate::board::{Board, CheckerMove, Field, EMPTY_MOVE}; +use crate::dice::Dice; +use crate::game::GameState; +use crate::player::Color; +use std::cmp; + +#[derive(std::cmp::PartialEq, Debug)] +pub enum MoveError { + // 2 checkers must go at the same time on an empty corner + // & the last 2 checkers of a corner must leave at the same time + CornerNeedsTwoCheckers, + // Prise de coin de repos par puissance alors qu'il est possible + // de le prendre directement (par "effet") + CornerByEffectPossible, + // toutes les dames doivent être dans le jan de retour + ExitNeedsAllCheckersOnLastQuarter, + // mouvement avec nombre en exédant alors qu'une séquence de mouvements + // sans nombre en excédant est possible + ExitByEffectPossible, + // Sortie avec nombre en excédant d'une dame qui n'est pas la plus éloignée + ExitNotFasthest, + // Jeu dans un cadran que l'adversaire peut encore remplir + OpponentCanFillQuarter, + // remplir cadran si possible & conserver cadran rempli si possible ---- + MustFillQuarter, +} + +pub trait MoveRules { + fn board(&self) -> &Board; + fn dice(&self) -> &Dice; + + fn moves_possible(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { + // Check move is physically possible + if !self.board().move_possible(color, &moves.0) { + return false; + } + + // Chained_move : "Tout d'une" + if let Ok(chained_move) = moves.0.chain(moves.1) { + if !self.board().move_possible(color, &chained_move) { + return false; + } + } else if !self.board().move_possible(color, &moves.1) { + return false; + } + true + } + + fn get_move_compatible_dices(&self, color: &Color, cmove: &CheckerMove) -> Vec { + let (dice1, dice2) = self.dice().values; + + let mut move_dices = Vec::new(); + if cmove.get_to() == 0 { + // handle empty move (0, 0) only one checker left, exiting with the first die. + if cmove.get_from() == 0 { + move_dices.push(dice1); + move_dices.push(dice2); + return move_dices; + } + + // Exits + let min_dist = match color { + Color::White => 25 - cmove.get_from(), + Color::Black => cmove.get_from(), + }; + if dice1 as usize >= min_dist { + move_dices.push(dice1); + } + if dice2 as usize >= min_dist { + move_dices.push(dice2); + } + } else { + let dist = (cmove.get_to() as i8 - cmove.get_from() as i8).unsigned_abs(); + if dice1 == dist { + move_dices.push(dice1); + } + if dice2 == dist { + move_dices.push(dice2); + } + } + move_dices + } + + fn moves_follows_dices(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { + // Prise de coin par puissance + if self.is_move_by_puissance(color, moves) { + return true; + } + + let (dice1, dice2) = self.dice().values; + let (move1, move2): &(CheckerMove, CheckerMove) = moves; + + let move1_dices = self.get_move_compatible_dices(color, move1); + if move1_dices.is_empty() { + return false; + } + let move2_dices = self.get_move_compatible_dices(color, move2); + if move2_dices.is_empty() { + return false; + } + if move1_dices.len() == 1 + && move2_dices.len() == 1 + && move1_dices[0] == move2_dices[0] + && dice1 != dice2 + { + return false; + } + + // no rule was broken + true + } + + fn moves_allowed( + &self, + color: &Color, + moves: &(CheckerMove, CheckerMove), + ) -> Result<(), MoveError> { + // ------- corner rules ---------- + let corner_field: Field = self.board().get_color_corner(color); + let (corner_count, _color) = self.board().get_field_checkers(corner_field).unwrap(); + let (from0, to0, from1, to1) = ( + moves.0.get_from(), + moves.0.get_to(), + moves.1.get_from(), + moves.1.get_to(), + ); + // 2 checkers must go at the same time on an empty corner + if (to0 == corner_field || to1 == corner_field) && (to0 != to1) && corner_count == 0 { + return Err(MoveError::CornerNeedsTwoCheckers); + } + + // the last 2 checkers of a corner must leave at the same time + if (from0 == corner_field || from1 == corner_field) && (from0 != from1) && corner_count == 2 + { + return Err(MoveError::CornerNeedsTwoCheckers); + } + + if self.is_move_by_puissance(color, moves) && self.can_take_corner_by_effect(color) { + return Err(MoveError::CornerByEffectPossible); + } + + // check exit rules + if moves.0.get_to() == 0 || moves.1.get_to() == 0 { + // toutes les dames doivent être dans le jan de retour + let has_outsiders = !self + .board() + .get_color_fields(*color) + .iter() + .filter(|(field, _count)| { + (*color == Color::White && *field < 19) + || (*color == Color::Black && *field > 6) + }) + .collect::>() + .is_empty(); + if has_outsiders { + return Err(MoveError::ExitNeedsAllCheckersOnLastQuarter); + } + + // toutes les sorties directes sont autorisées, ainsi que les nombre défaillants + let possible_moves_sequences = self.get_possible_moves_sequences(color); + if !possible_moves_sequences.contains(moves) { + // À ce stade au moins un des déplacements concerne un nombre en excédant + // - si d'autres séquences de mouvements sans nombre en excédant étaient possibles, on + // refuse cette séquence + if !possible_moves_sequences.is_empty() { + return Err(MoveError::ExitByEffectPossible); + } + + // - la dame choisie doit être la plus éloignée de la sortie + let mut checkers = self.board().get_color_fields(*color); + checkers.sort_by(|a, b| { + if *color == Color::White { + b.0.cmp(&a.0) + } else { + a.0.cmp(&b.0) + } + }); + let mut farthest = if *color == Color::White { 24 } else { 1 }; + let mut next_farthest = if *color == Color::White { 24 } else { 1 }; + let mut has_two_checkers = false; + if let Some((field, count)) = checkers.first() { + farthest = *field; + if *count > 1 { + next_farthest = *field; + has_two_checkers = true; + } else if let Some((field, _count)) = checkers.get(1) { + next_farthest = *field; + has_two_checkers = true; + } + } + + // s'il reste au moins deux dames, on vérifie que les plus éloignées soint choisies + if has_two_checkers { + if moves.0.get_to() == 0 && moves.1.get_to() == 0 { + // Deux coups sortants en excédant + if *color == Color::White { + if cmp::max(moves.0.get_from(), moves.1.get_from()) > next_farthest { + return Err(MoveError::ExitNotFasthest); + } + } else if cmp::min(moves.0.get_from(), moves.1.get_from()) < next_farthest { + return Err(MoveError::ExitNotFasthest); + } + } else { + // Un seul coup sortant en excédant le coup sortant doit concerner la plus éloignée du bord + let exit_move_field = if moves.0.get_to() == 0 { + moves.0.get_from() + } else { + moves.1.get_from() + }; + if exit_move_field != farthest { + return Err(MoveError::ExitNotFasthest); + } + } + } + } + } + + // --- interdit de jouer dans cadran que l'adversaire peut encore remplir ---- + let farthest = if *color == Color::White { + cmp::max(moves.0.get_to(), moves.1.get_to()) + } else { + cmp::min(moves.0.get_to(), moves.1.get_to()) + }; + let in_opponent_side = if *color == Color::White { + farthest > 12 + } else { + farthest < 13 + }; + + if in_opponent_side + && self + .board() + .is_quarter_fillable(color.opponent_color(), farthest) + { + return Err(MoveError::OpponentCanFillQuarter); + } + + // --- remplir cadran si possible & conserver cadran rempli si possible ---- + let filling_moves_sequences = self.get_quarter_filling_moves_sequences(color); + if !filling_moves_sequences.contains(moves) && !filling_moves_sequences.is_empty() { + return Err(MoveError::MustFillQuarter); + } + // no rule was broken + Ok(()) + } + + fn get_possible_moves_sequences(&self, color: &Color) -> Vec<(CheckerMove, CheckerMove)> { + let (dice1, dice2) = self.dice().values; + let mut moves_seqs = self.get_possible_moves_sequences_by_dices(color, dice1, dice2); + let mut moves_seqs_order2 = self.get_possible_moves_sequences_by_dices(color, dice1, dice2); + moves_seqs.append(&mut moves_seqs_order2); + moves_seqs + } + + fn get_quarter_filling_moves_sequences( + &self, + color: &Color, + ) -> Vec<(CheckerMove, CheckerMove)> { + let mut moves_seqs = Vec::new(); + for moves in self.get_possible_moves_sequences(color) { + let mut board = self.board().clone(); + board.move_checker(color, moves.0).unwrap(); + board.move_checker(color, moves.1).unwrap(); + if board.any_quarter_filled(*color) { + moves_seqs.push(moves); + } + } + moves_seqs + } + + fn get_possible_moves_sequences_by_dices( + &self, + color: &Color, + dice1: u8, + dice2: u8, + ) -> Vec<(CheckerMove, CheckerMove)> { + let mut moves_seqs = Vec::new(); + for first_move in self.board().get_possible_moves(*color, dice1, false) { + let mut board2 = self.board().clone(); + if board2.move_checker(color, first_move).is_err() { + println!("err move"); + continue; + } + if board2.get_color_fields(*color).is_empty() { + // no checkers left : empty move + println!("empty move"); + moves_seqs.push((first_move, EMPTY_MOVE)); + } else { + for second_move in board2.get_possible_moves(*color, dice2, false) { + moves_seqs.push((first_move, second_move)); + } + } + } + moves_seqs + } + + fn get_direct_exit_moves(&self, state: &GameState, color: &Color) -> Vec { + let mut moves = Vec::new(); + let (dice1, dice2) = state.dice.values; + + // sorties directes simples + let (field1_candidate, field2_candidate) = if color == &Color::White { + (25 - dice1 as usize, 25 - dice2 as usize) + } else { + (dice1 as usize, dice2 as usize) + }; + let (count1, col1) = state.board.get_field_checkers(field1_candidate).unwrap(); + let (count2, col2) = state.board.get_field_checkers(field2_candidate).unwrap(); + if count1 > 0 { + moves.push(CheckerMove::new(field1_candidate, 0).unwrap()); + } + if dice2 != dice1 { + if count2 > 0 { + moves.push(CheckerMove::new(field2_candidate, 0).unwrap()); + } + } else if count1 > 1 { + // doublet et deux dames disponibles + moves.push(CheckerMove::new(field1_candidate, 0).unwrap()); + } + + // sortie directe tout d'une + let fieldall_candidate = if color == &Color::White { + 25 - dice1 - dice2 + } else { + dice1 + dice2 + } as usize; + let (countall, _col) = state.board.get_field_checkers(fieldall_candidate).unwrap(); + if countall > 0 { + if col1.is_none() || col1 == Some(color) { + moves.push(CheckerMove::new(fieldall_candidate, field1_candidate).unwrap()); + moves.push(CheckerMove::new(field1_candidate, 0).unwrap()); + } + if col2.is_none() || col2 == Some(color) { + moves.push(CheckerMove::new(fieldall_candidate, field2_candidate).unwrap()); + moves.push(CheckerMove::new(field2_candidate, 0).unwrap()); + } + } + moves + } + + fn is_move_by_puissance(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { + let (dice1, dice2) = self.dice().values; + let (move1, move2): &(CheckerMove, CheckerMove) = moves; + let dist1 = (move1.get_to() as i8 - move1.get_from() as i8).unsigned_abs(); + let dist2 = (move2.get_to() as i8 - move2.get_from() as i8).unsigned_abs(); + + // Both corners must be empty + let (count1, _color) = self.board().get_field_checkers(12).unwrap(); + let (count2, _color2) = self.board().get_field_checkers(13).unwrap(); + if count1 > 0 || count2 > 0 { + return false; + } + + move1.get_to() == move2.get_to() + && move1.get_to() == self.board().get_color_corner(color) + && ((*color == Color::White + && cmp::min(dist1, dist2) == cmp::min(dice1, dice2) - 1 + && cmp::max(dist1, dist2) == cmp::max(dice1, dice2) - 1) + || (*color == Color::Black + && cmp::min(dist1, dist2) == cmp::min(dice1, dice2) + 1 + && cmp::max(dist1, dist2) == cmp::max(dice1, dice2) + 1)) + } + + fn can_take_corner_by_effect(&self, color: &Color) -> bool { + // return false if corner already taken + let corner_field: Field = self.board().get_color_corner(color); + let (count, _col) = self.board().get_field_checkers(corner_field).unwrap(); + if count > 0 { + return false; + } + + let (dice1, dice2) = self.dice().values; + let (field1, field2) = match color { + Color::White => (12 - dice1, 12 - dice2), + Color::Black => (13 + dice1, 13 + dice2), + }; + let res1 = self.board().get_field_checkers(field1.into()); + let res2 = self.board().get_field_checkers(field2.into()); + if res1.is_err() || res2.is_err() { + return false; + } + let (count1, opt_color1) = res1.unwrap(); + let (count2, opt_color2) = res2.unwrap(); + count1 > 0 && count2 > 0 && opt_color1 == Some(color) && opt_color2 == Some(color) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_take_corner_by_effect() { + let mut state = GameState::default(); + state.board.set_positions([ + 10, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, + ]); + state.dice.values = (4, 4); + assert!(state.can_take_corner_by_effect(&Color::White)); + + state.dice.values = (5, 5); + assert!(!state.can_take_corner_by_effect(&Color::White)); + + state.board.set_positions([ + 10, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, + ]); + state.dice.values = (4, 4); + assert!(!state.can_take_corner_by_effect(&Color::White)); + + state.board.set_positions([ + 10, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, + ]); + state.dice.values = (1, 1); + assert!(state.can_take_corner_by_effect(&Color::Black)); + } + + #[test] + fn prise_en_puissance() { + let mut state = GameState::default(); + // prise par puissance ok + state.board.set_positions([ + 10, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, + ]); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(8, 12).unwrap(), + CheckerMove::new(8, 12).unwrap(), + ); + assert!(state.is_move_by_puissance(&Color::White, &moves)); + assert!(state.moves_follows_dices(&Color::White, &moves)); + assert!(state.moves_allowed(&Color::White, &moves).is_ok()); + + // opponent corner must be empty + state.board.set_positions([ + 10, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, + ]); + assert!(!state.is_move_by_puissance(&Color::White, &moves)); + assert!(!state.moves_follows_dices(&Color::White, &moves)); + + // Si on a la possibilité de prendre son coin à la fois par effet, c'est à dire naturellement, et aussi par puissance, on doit le prendre par effet + state.board.set_positions([ + 5, 0, 0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, + ]); + assert_eq!( + Err(MoveError::CornerByEffectPossible), + state.moves_allowed(&Color::White, &moves) + ); + + // on a déjà pris son coin : on ne peux plus y deplacer des dames par puissance + state.board.set_positions([ + 8, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, + ]); + assert!(!state.is_move_by_puissance(&Color::White, &moves)); + assert!(!state.moves_follows_dices(&Color::White, &moves)); + } + + #[test] + fn exit() { + let mut state = GameState::default(); + // exit ok + state.board.set_positions([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, + ]); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(20, 0).unwrap(), + CheckerMove::new(20, 0).unwrap(), + ); + assert!(state.moves_follows_dices(&Color::White, &moves)); + assert!(state.moves_allowed(&Color::White, &moves).is_ok()); + + // toutes les dames doivent être dans le jan de retour + state.board.set_positions([ + 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, + ]); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(20, 0).unwrap(), + CheckerMove::new(20, 0).unwrap(), + ); + assert_eq!( + Err(MoveError::ExitNeedsAllCheckersOnLastQuarter), + state.moves_allowed(&Color::White, &moves) + ); + + // on ne peut pas sortir une dame avec un nombre excédant si on peut en jouer une avec un nombre défaillant + state.board.set_positions([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 0, 0, 2, 0, + ]); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(20, 0).unwrap(), + CheckerMove::new(23, 0).unwrap(), + ); + assert_eq!( + Err(MoveError::ExitByEffectPossible), + state.moves_allowed(&Color::White, &moves) + ); + + // on doit jouer le nombre excédant le plus éloigné + state.board.set_positions([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, + ]); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(20, 0).unwrap(), + CheckerMove::new(23, 0).unwrap(), + ); + assert_eq!( + Err(MoveError::ExitNotFasthest), + state.moves_allowed(&Color::White, &moves) + ); + let moves = ( + CheckerMove::new(20, 0).unwrap(), + CheckerMove::new(20, 0).unwrap(), + ); + assert!(state.moves_allowed(&Color::White, &moves).is_ok()); + + // Cas de la dernière dame + state.board.set_positions([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, + ]); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(23, 0).unwrap(), + CheckerMove::new(0, 0).unwrap(), + ); + assert!(state.moves_follows_dices(&Color::White, &moves)); + assert!(state.moves_allowed(&Color::White, &moves).is_ok()); + } + + #[test] + fn move_check_opponent_fillable_quarter() { + let mut state = GameState::default(); + state.board.set_positions([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, + ]); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(11, 16).unwrap(), + CheckerMove::new(11, 16).unwrap(), + ); + assert!(state.moves_allowed(&Color::White, &moves).is_ok()); + + state.board.set_positions([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 1, 0, + ]); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(11, 16).unwrap(), + CheckerMove::new(11, 16).unwrap(), + ); + assert_eq!( + Err(MoveError::OpponentCanFillQuarter), + state.moves_allowed(&Color::White, &moves) + ); + } + + #[test] + fn move_check_fillable_quarter() { + let mut state = GameState::default(); + state.board.set_positions([ + 3, 3, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, + ]); + state.dice.values = (5, 4); + let moves = ( + CheckerMove::new(1, 6).unwrap(), + CheckerMove::new(2, 6).unwrap(), + ); + assert!(state.moves_allowed(&Color::White, &moves).is_ok()); + let moves = ( + CheckerMove::new(1, 5).unwrap(), + CheckerMove::new(2, 7).unwrap(), + ); + assert_eq!( + Err(MoveError::MustFillQuarter), + state.moves_allowed(&Color::White, &moves) + ); + + state.board.set_positions([ + 2, 3, 2, 2, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + state.dice.values = (2, 3); + let moves = ( + CheckerMove::new(6, 8).unwrap(), + CheckerMove::new(6, 9).unwrap(), + ); + assert_eq!( + Err(MoveError::MustFillQuarter), + state.moves_allowed(&Color::White, &moves) + ); + let moves = ( + CheckerMove::new(2, 4).unwrap(), + CheckerMove::new(5, 8).unwrap(), + ); + assert!(state.moves_allowed(&Color::White, &moves).is_ok()); + } + + #[test] + fn moves_possible() { + let state = GameState::default(); + + // Chained moves + let moves = ( + CheckerMove::new(1, 5).unwrap(), + CheckerMove::new(5, 9).unwrap(), + ); + assert!(state.moves_possible(&Color::White, &moves)); + + // not chained moves + let moves = ( + CheckerMove::new(1, 5).unwrap(), + CheckerMove::new(6, 9).unwrap(), + ); + assert!(!state.moves_possible(&Color::White, &moves)); + + // black moves + let moves = ( + CheckerMove::new(24, 20).unwrap(), + CheckerMove::new(20, 19).unwrap(), + ); + assert!(state.moves_possible(&Color::Black, &moves)); + } +} diff --git a/store/src/lib.rs b/store/src/lib.rs index cbdcf64..435f562 100644 --- a/store/src/lib.rs +++ b/store/src/lib.rs @@ -1,4 +1,5 @@ mod game; +mod game_rules_moves; pub use game::{EndGameReason, GameEvent, GameState, Stage, TurnStage}; mod player;