From 6478f5043dbdc1e297823ca157f9ce402bc22b79 Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Sun, 26 Jan 2025 17:52:57 +0100 Subject: [PATCH] fix: allowed moves infinite loop --- bot/src/lib.rs | 5 +- bot/src/strategy/default.rs | 2 +- client_cli/src/game_runner.rs | 8 ++ doc/refs/journal.md | 7 +- store/src/board.rs | 8 +- store/src/game.rs | 15 ++- store/src/game_rules_moves.rs | 177 ++++++++++++++++++++++----------- store/src/game_rules_points.rs | 2 +- 8 files changed, 160 insertions(+), 64 deletions(-) diff --git a/bot/src/lib.rs b/bot/src/lib.rs index 9318fea..927fbc6 100644 --- a/bot/src/lib.rs +++ b/bot/src/lib.rs @@ -1,6 +1,6 @@ mod strategy; -use store::{CheckerMove, Color, GameEvent, GameState, PlayerId, PointsRules, TurnStage}; +use store::{CheckerMove, Color, GameEvent, GameState, PlayerId, PointsRules, Stage, TurnStage}; pub use strategy::default::DefaultStrategy; pub trait BotStrategy: std::fmt::Debug { @@ -61,6 +61,9 @@ impl Bot { pub fn handle_event(&mut self, event: &GameEvent) -> Option { let game = self.strategy.get_mut_game(); game.consume(event); + if game.stage == Stage::Ended { + return None; + } if game.active_player_id == self.player_id { return match game.turn_stage { TurnStage::MarkAdvPoints => Some(GameEvent::Mark { diff --git a/bot/src/strategy/default.rs b/bot/src/strategy/default.rs index 22482eb..98e8322 100644 --- a/bot/src/strategy/default.rs +++ b/bot/src/strategy/default.rs @@ -56,7 +56,7 @@ impl BotStrategy for DefaultStrategy { fn choose_move(&self) -> (CheckerMove, CheckerMove) { let rules = MoveRules::new(&self.color, &self.game.board, self.game.dice); - let possible_moves = rules.get_possible_moves_sequences(true); + let possible_moves = rules.get_possible_moves_sequences(true, vec![]); let choosen_move = *possible_moves .first() .unwrap_or(&(CheckerMove::default(), CheckerMove::default())); diff --git a/client_cli/src/game_runner.rs b/client_cli/src/game_runner.rs index 2d9dbef..f68ea5e 100644 --- a/client_cli/src/game_runner.rs +++ b/client_cli/src/game_runner.rs @@ -99,6 +99,14 @@ impl GameRunner { }; } } + + if let Some(winner) = self.state.determine_winner() { + // panic!("WE HAVE A WINNER!"); + next_event = Some(store::GameEvent::EndGame { + reason: store::EndGameReason::PlayerWon { winner }, + }); + } + next_event } diff --git a/doc/refs/journal.md b/doc/refs/journal.md index 75b028a..dd6d99c 100644 --- a/doc/refs/journal.md +++ b/doc/refs/journal.md @@ -10,6 +10,10 @@ Organisation store / server / client selon cf. l.15 - check_exit_rules - - get_possible_moves_sequences -> cf l.15 + - get_possible_moves_sequences(without exedents) -> cf l.15 - get_quarter_filling_moves_sequences - get_possible_moves_sequences -> cf l.15 - state.consume (RollResult) (ok) diff --git a/store/src/board.rs b/store/src/board.rs index 630a3a5..ced30e4 100644 --- a/store/src/board.rs +++ b/store/src/board.rs @@ -441,7 +441,7 @@ impl Board { 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 + (has_checker && !blocked) || cmove == &EMPTY_MOVE } /// Return if there is a quarter filled by the color @@ -651,6 +651,12 @@ mod tests { assert!(board.set(&Color::White, 23, -3).is_err()); } + #[test] + fn move_possible() { + let board = Board::new(); + assert!(board.move_possible(&Color::White, &EMPTY_MOVE)); + } + #[test] fn get_color_fields() { let board = Board::new(); diff --git a/store/src/game.rs b/store/src/game.rs index b0dfd0f..43bda3e 100644 --- a/store/src/game.rs +++ b/store/src/game.rs @@ -4,7 +4,7 @@ use crate::dice::Dice; use crate::game_rules_moves::MoveRules; use crate::game_rules_points::{PointsRules, PossibleJans}; use crate::player::{Color, Player, PlayerId}; -use log::error; +use log::{error, info}; // use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -170,7 +170,7 @@ impl GameState { } pub fn who_plays(&self) -> Option<&Player> { - self.players.get(&self.active_player_id) + self.get_active_player() } pub fn get_white_player(&self) -> Option<&Player> { @@ -392,7 +392,9 @@ impl GameState { self.stage = Stage::InGame; self.turn_stage = TurnStage::RollDice; } - EndGame { reason: _ } => self.stage = Stage::Ended, + EndGame { reason: _ } => { + self.stage = Stage::Ended; + } PlayerJoined { player_id, name } => { let color = if !self.players.is_empty() { Color::White @@ -542,6 +544,13 @@ impl GameState { } p.points = sum_points % 12; p.holes += holes; + + if points > 0 && p.holes > 15 { + info!( + "player {:?} holes : {:?} added points : {:?}", + player_id, p.holes, points + ) + } p }); diff --git a/store/src/game_rules_moves.rs b/store/src/game_rules_moves.rs index bd6c0f7..1a67340 100644 --- a/store/src/game_rules_moves.rs +++ b/store/src/game_rules_moves.rs @@ -33,6 +33,13 @@ pub enum MoveError { MustPlayStrongerDie, } +#[derive(std::cmp::PartialEq, Debug, Clone)] +pub enum TricTracRule { + Exit, + MustFillQuarter, + Corner, +} + /// MoveRules always consider that the current player is White /// You must use 'mirror' functions on board & CheckerMoves if player is Black #[derive(Default)] @@ -62,12 +69,17 @@ impl MoveRules { } } - pub fn moves_follow_rules(&self, moves: &(CheckerMove, CheckerMove)) -> bool { + pub fn moves_follow_rules( + &self, + moves: &(CheckerMove, CheckerMove), + // ignored_rules: Vec, + ) -> bool { // Check moves possibles on the board // Check moves conforms to the dice // Check move is allowed by the rules (to desactivate when playing with schools) self.moves_possible(moves) && self.moves_follows_dices(moves) && { let is_allowed = self.moves_allowed(moves); + // let is_allowed = self.moves_allowed(moves, ignored_rules); if is_allowed.is_err() { info!("Move not allowed : {:?}", is_allowed.unwrap_err()); false @@ -165,7 +177,11 @@ impl MoveRules { } /// ---- moves_allowed : Third of three checks for moves - pub fn moves_allowed(&self, moves: &(CheckerMove, CheckerMove)) -> Result<(), MoveError> { + pub fn moves_allowed( + &self, + moves: &(CheckerMove, CheckerMove), + // ignored_rules: Vec, + ) -> Result<(), MoveError> { self.check_corner_rules(moves)?; if self.is_move_by_puissance(moves) { @@ -179,7 +195,7 @@ impl MoveRules { // Si possible, les deux dés doivent être joués if moves.0.get_from() == 0 || moves.1.get_from() == 0 { - let mut possible_moves_sequences = self.get_possible_moves_sequences(true); + let mut possible_moves_sequences = self.get_possible_moves_sequences(true, vec![]); possible_moves_sequences.retain(|moves| self.check_exit_rules(moves).is_ok()); // possible_moves_sequences.retain(|moves| self.check_corner_rules(moves).is_ok()); if !possible_moves_sequences.contains(moves) && !possible_moves_sequences.is_empty() { @@ -197,21 +213,42 @@ impl MoveRules { } // check exit rules + // if !ignored_rules.contains(&TricTracRule::Exit) { self.check_exit_rules(moves)?; + // } // --- interdit de jouer dans un cadran que l'adversaire peut encore remplir ---- + self.check_opponent_can_fill_quarter_rule(moves)?; + + // --- remplir cadran si possible & conserver cadran rempli si possible ---- + // if !ignored_rules.contains(&TricTracRule::MustFillQuarter) { + self.check_must_fill_quarter_rule(moves)?; + // } + // no rule was broken + Ok(()) + } + + // --- interdit de jouer dans un cadran que l'adversaire peut encore remplir ---- + fn check_opponent_can_fill_quarter_rule( + &self, + moves: &(CheckerMove, CheckerMove), + ) -> Result<(), MoveError> { let farthest = cmp::max(moves.0.get_to(), moves.1.get_to()); let in_opponent_side = farthest > 12; if in_opponent_side && self.board.is_quarter_fillable(Color::Black, farthest) { return Err(MoveError::OpponentCanFillQuarter); } + Ok(()) + } - // --- remplir cadran si possible & conserver cadran rempli si possible ---- + fn check_must_fill_quarter_rule( + &self, + moves: &(CheckerMove, CheckerMove), + ) -> Result<(), MoveError> { let filling_moves_sequences = self.get_quarter_filling_moves_sequences(); if !filling_moves_sequences.contains(moves) && !filling_moves_sequences.is_empty() { return Err(MoveError::MustFillQuarter); } - // no rule was broken Ok(()) } @@ -267,49 +304,52 @@ impl MoveRules { } // toutes les sorties directes sont autorisées, ainsi que les nombres défaillants - let possible_moves_sequences = self.get_possible_moves_sequences(false); - 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); - } + let ignored_rules = vec![TricTracRule::Exit]; + let possible_moves_sequences_without_excedent = + self.get_possible_moves_sequences(false, ignored_rules); + if possible_moves_sequences_without_excedent.contains(moves) { + return Ok(()); + } + // À 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 sont possibles, on + // refuse cette séquence + if !possible_moves_sequences_without_excedent.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::White); - checkers.sort_by(|a, b| b.0.cmp(&a.0)); - let mut farthest = 24; - let mut next_farthest = 24; - 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; + // - la dame choisie doit être la plus éloignée de la sortie + let mut checkers = self.board.get_color_fields(Color::White); + checkers.sort_by(|a, b| b.0.cmp(&a.0)); + let mut farthest = 24; + let mut next_farthest = 24; + 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 cmp::max(moves.0.get_from(), moves.1.get_from()) > next_farthest { + return Err(MoveError::ExitNotFasthest); } - } - - // 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 cmp::max(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 { - // 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); - } + moves.1.get_from() + }; + if exit_move_field != farthest { + return Err(MoveError::ExitNotFasthest); } } } @@ -319,6 +359,7 @@ impl MoveRules { pub fn get_possible_moves_sequences( &self, with_excedents: bool, + ignored_rules: Vec, ) -> Vec<(CheckerMove, CheckerMove)> { let (dice1, dice2) = self.dice.values; let (dice_max, dice_min) = if dice1 > dice2 { @@ -326,8 +367,13 @@ impl MoveRules { } else { (dice2, dice1) }; - let mut moves_seqs = - self.get_possible_moves_sequences_by_dices(dice_max, dice_min, with_excedents, false); + let mut moves_seqs = self.get_possible_moves_sequences_by_dices( + dice_max, + dice_min, + with_excedents, + false, + ignored_rules.clone(), + ); // if we got valid sequences with the highest die, we don't accept sequences using only the // lowest die let ignore_empty = !moves_seqs.is_empty(); @@ -336,6 +382,7 @@ impl MoveRules { dice_max, with_excedents, ignore_empty, + ignored_rules, ); moves_seqs.append(&mut moves_seqs_order2); let empty_removed = moves_seqs @@ -400,7 +447,8 @@ impl MoveRules { pub fn get_quarter_filling_moves_sequences(&self) -> Vec<(CheckerMove, CheckerMove)> { let mut moves_seqs = Vec::new(); let color = &Color::White; - for moves in self.get_possible_moves_sequences(true) { + let ignored_rules = vec![TricTracRule::Exit, TricTracRule::MustFillQuarter]; + for moves in self.get_possible_moves_sequences(true, ignored_rules) { let mut board = self.board.clone(); board.move_checker(color, moves.0).unwrap(); board.move_checker(color, moves.1).unwrap(); @@ -418,6 +466,7 @@ impl MoveRules { dice2: u8, with_excedents: bool, ignore_empty: bool, + ignored_rules: Vec, ) -> Vec<(CheckerMove, CheckerMove)> { let mut moves_seqs = Vec::new(); let color = &Color::White; @@ -439,24 +488,37 @@ impl MoveRules { board2.get_possible_moves(*color, dice2, with_excedents, true, forbid_exits) { if self.check_corner_rules(&(first_move, second_move)).is_ok() + && self + .check_opponent_can_fill_quarter_rule(&(first_move, second_move)) + .is_ok() && !(self.is_move_by_puissance(&(first_move, second_move)) && self.can_take_corner_by_effect()) + && (ignored_rules.contains(&TricTracRule::Exit) + || self.check_exit_rules(&(first_move, second_move)).is_ok()) + && (ignored_rules.contains(&TricTracRule::MustFillQuarter) + || self + .check_must_fill_quarter_rule(&(first_move, second_move)) + .is_ok()) { moves_seqs.push((first_move, second_move)); has_second_dice_move = true; } - // TODO : autres règles à vérifier (cf. moves_allowed) - // - check_exit_rules -> utilise get_possible_moves_sequences ! - // - get_quarter_filling_moves_sequences -> utilise get_possible_moves_sequences ! } if !has_second_dice_move && with_excedents && !ignore_empty && self.check_corner_rules(&(first_move, EMPTY_MOVE)).is_ok() - // TODO : autres règles à vérifier (cf. moves_allowed) - // - can_take_corner_by_effect - // - check_exit_rules - // - get_quarter_filling_moves_sequences + && self + .check_opponent_can_fill_quarter_rule(&(first_move, EMPTY_MOVE)) + .is_ok() + && !(self.is_move_by_puissance(&(first_move, EMPTY_MOVE)) + && self.can_take_corner_by_effect()) + && (ignored_rules.contains(&TricTracRule::Exit) + || self.check_exit_rules(&(first_move, EMPTY_MOVE)).is_ok()) + && (ignored_rules.contains(&TricTracRule::MustFillQuarter) + || self + .check_must_fill_quarter_rule(&(first_move, EMPTY_MOVE)) + .is_ok()) { // empty move moves_seqs.push((first_move, EMPTY_MOVE)); @@ -1078,6 +1140,9 @@ mod tests { CheckerMove::new(9, 11).unwrap(), CheckerMove::new(11, 14).unwrap(), ); - assert_eq!(vec![moves], state.get_possible_moves_sequences(true)); + assert_eq!( + vec![moves], + state.get_possible_moves_sequences(true, vec![]) + ); } } diff --git a/store/src/game_rules_points.rs b/store/src/game_rules_points.rs index 485e3b9..8656b54 100644 --- a/store/src/game_rules_points.rs +++ b/store/src/game_rules_points.rs @@ -331,7 +331,7 @@ impl PointsRules { } // Jan qui ne peut : dés non jouables - let poss = self.move_rules.get_possible_moves_sequences(true); + let poss = self.move_rules.get_possible_moves_sequences(true, vec![]); let moves = poss.iter().fold(vec![], |mut acc, (m1, m2)| { acc.push(*m1); acc.push(*m2);