fix: allowed moves infinite loop

This commit is contained in:
Henri Bourcereau 2025-01-26 17:52:57 +01:00
parent 38100a61b2
commit 6478f5043d
8 changed files with 160 additions and 64 deletions

View file

@ -1,6 +1,6 @@
mod strategy; 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 use strategy::default::DefaultStrategy;
pub trait BotStrategy: std::fmt::Debug { pub trait BotStrategy: std::fmt::Debug {
@ -61,6 +61,9 @@ impl Bot {
pub fn handle_event(&mut self, event: &GameEvent) -> Option<GameEvent> { pub fn handle_event(&mut self, event: &GameEvent) -> Option<GameEvent> {
let game = self.strategy.get_mut_game(); let game = self.strategy.get_mut_game();
game.consume(event); game.consume(event);
if game.stage == Stage::Ended {
return None;
}
if game.active_player_id == self.player_id { if game.active_player_id == self.player_id {
return match game.turn_stage { return match game.turn_stage {
TurnStage::MarkAdvPoints => Some(GameEvent::Mark { TurnStage::MarkAdvPoints => Some(GameEvent::Mark {

View file

@ -56,7 +56,7 @@ impl BotStrategy for DefaultStrategy {
fn choose_move(&self) -> (CheckerMove, CheckerMove) { fn choose_move(&self) -> (CheckerMove, CheckerMove) {
let rules = MoveRules::new(&self.color, &self.game.board, self.game.dice); 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 let choosen_move = *possible_moves
.first() .first()
.unwrap_or(&(CheckerMove::default(), CheckerMove::default())); .unwrap_or(&(CheckerMove::default(), CheckerMove::default()));

View file

@ -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 next_event
} }

View file

@ -10,6 +10,10 @@ Organisation store / server / client selon <https://herluf-ba.github.io/making-a
_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. _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.
## Config neovim debugger launchers
Cela se passe dans la config neovim (lua/plugins/overrides.lua)
## Organisation du store ## Organisation du store
lib lib
@ -24,6 +28,7 @@ lib
## Algorithme de détermination des coups ## Algorithme de détermination des coups
- strategy::choose_move - strategy::choose_move
- GameRules.get_possible_moves_sequences(with_excedents: bool) - GameRules.get_possible_moves_sequences(with_excedents: bool)
- get_possible_moves_sequences_by_dices(dice_max, dice_min, with_excedents, false); - get_possible_moves_sequences_by_dices(dice_max, dice_min, with_excedents, false);
- get_possible_moves_sequences_by_dices(dice_min, dice_max, with_excedents, true); - get_possible_moves_sequences_by_dices(dice_min, dice_max, with_excedents, true);
@ -41,7 +46,7 @@ lib
- can_take_corner_by_effect ok - can_take_corner_by_effect ok
- get_possible_moves_sequences -> cf. l.15 - get_possible_moves_sequences -> cf. l.15
- check_exit_rules - 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_quarter_filling_moves_sequences
- get_possible_moves_sequences -> cf l.15 - get_possible_moves_sequences -> cf l.15
- state.consume (RollResult) (ok) - state.consume (RollResult) (ok)

View file

@ -441,7 +441,7 @@ impl Board {
let blocked = self.blocked(color, cmove.to).unwrap_or(true); let blocked = self.blocked(color, cmove.to).unwrap_or(true);
// Check if there is a player's checker on the 'from' square // 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); 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 /// 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()); 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] #[test]
fn get_color_fields() { fn get_color_fields() {
let board = Board::new(); let board = Board::new();

View file

@ -4,7 +4,7 @@ use crate::dice::Dice;
use crate::game_rules_moves::MoveRules; use crate::game_rules_moves::MoveRules;
use crate::game_rules_points::{PointsRules, PossibleJans}; use crate::game_rules_points::{PointsRules, PossibleJans};
use crate::player::{Color, Player, PlayerId}; use crate::player::{Color, Player, PlayerId};
use log::error; use log::{error, info};
// use itertools::Itertools; // use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -170,7 +170,7 @@ impl GameState {
} }
pub fn who_plays(&self) -> Option<&Player> { 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> { pub fn get_white_player(&self) -> Option<&Player> {
@ -392,7 +392,9 @@ impl GameState {
self.stage = Stage::InGame; self.stage = Stage::InGame;
self.turn_stage = TurnStage::RollDice; self.turn_stage = TurnStage::RollDice;
} }
EndGame { reason: _ } => self.stage = Stage::Ended, EndGame { reason: _ } => {
self.stage = Stage::Ended;
}
PlayerJoined { player_id, name } => { PlayerJoined { player_id, name } => {
let color = if !self.players.is_empty() { let color = if !self.players.is_empty() {
Color::White Color::White
@ -542,6 +544,13 @@ impl GameState {
} }
p.points = sum_points % 12; p.points = sum_points % 12;
p.holes += holes; p.holes += holes;
if points > 0 && p.holes > 15 {
info!(
"player {:?} holes : {:?} added points : {:?}",
player_id, p.holes, points
)
}
p p
}); });

View file

@ -33,6 +33,13 @@ pub enum MoveError {
MustPlayStrongerDie, MustPlayStrongerDie,
} }
#[derive(std::cmp::PartialEq, Debug, Clone)]
pub enum TricTracRule {
Exit,
MustFillQuarter,
Corner,
}
/// MoveRules always consider that the current player is White /// MoveRules always consider that the current player is White
/// You must use 'mirror' functions on board & CheckerMoves if player is Black /// You must use 'mirror' functions on board & CheckerMoves if player is Black
#[derive(Default)] #[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<TricTracRule>,
) -> bool {
// Check moves possibles on the board // Check moves possibles on the board
// Check moves conforms to the dice // Check moves conforms to the dice
// Check move is allowed by the rules (to desactivate when playing with schools) // Check move is allowed by the rules (to desactivate when playing with schools)
self.moves_possible(moves) && self.moves_follows_dices(moves) && { self.moves_possible(moves) && self.moves_follows_dices(moves) && {
let is_allowed = self.moves_allowed(moves); let is_allowed = self.moves_allowed(moves);
// let is_allowed = self.moves_allowed(moves, ignored_rules);
if is_allowed.is_err() { if is_allowed.is_err() {
info!("Move not allowed : {:?}", is_allowed.unwrap_err()); info!("Move not allowed : {:?}", is_allowed.unwrap_err());
false false
@ -165,7 +177,11 @@ impl MoveRules {
} }
/// ---- moves_allowed : Third of three checks for moves /// ---- 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<TricTracRule>,
) -> Result<(), MoveError> {
self.check_corner_rules(moves)?; self.check_corner_rules(moves)?;
if self.is_move_by_puissance(moves) { if self.is_move_by_puissance(moves) {
@ -179,7 +195,7 @@ impl MoveRules {
// Si possible, les deux dés doivent être joués // Si possible, les deux dés doivent être joués
if moves.0.get_from() == 0 || moves.1.get_from() == 0 { 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_exit_rules(moves).is_ok());
// possible_moves_sequences.retain(|moves| self.check_corner_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() { if !possible_moves_sequences.contains(moves) && !possible_moves_sequences.is_empty() {
@ -197,21 +213,42 @@ impl MoveRules {
} }
// check exit rules // check exit rules
// if !ignored_rules.contains(&TricTracRule::Exit) {
self.check_exit_rules(moves)?; self.check_exit_rules(moves)?;
// }
// --- interdit de jouer dans un cadran que l'adversaire peut encore remplir ---- // --- 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 farthest = cmp::max(moves.0.get_to(), moves.1.get_to());
let in_opponent_side = farthest > 12; let in_opponent_side = farthest > 12;
if in_opponent_side && self.board.is_quarter_fillable(Color::Black, farthest) { if in_opponent_side && self.board.is_quarter_fillable(Color::Black, farthest) {
return Err(MoveError::OpponentCanFillQuarter); 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(); let filling_moves_sequences = self.get_quarter_filling_moves_sequences();
if !filling_moves_sequences.contains(moves) && !filling_moves_sequences.is_empty() { if !filling_moves_sequences.contains(moves) && !filling_moves_sequences.is_empty() {
return Err(MoveError::MustFillQuarter); return Err(MoveError::MustFillQuarter);
} }
// no rule was broken
Ok(()) Ok(())
} }
@ -267,12 +304,16 @@ impl MoveRules {
} }
// toutes les sorties directes sont autorisées, ainsi que les nombres défaillants // toutes les sorties directes sont autorisées, ainsi que les nombres défaillants
let possible_moves_sequences = self.get_possible_moves_sequences(false); let ignored_rules = vec![TricTracRule::Exit];
if !possible_moves_sequences.contains(moves) { 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 // À 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 // - si d'autres séquences de mouvements sans nombre en excédant sont possibles, on
// refuse cette séquence // refuse cette séquence
if !possible_moves_sequences.is_empty() { if !possible_moves_sequences_without_excedent.is_empty() {
return Err(MoveError::ExitByEffectPossible); return Err(MoveError::ExitByEffectPossible);
} }
@ -312,13 +353,13 @@ impl MoveRules {
} }
} }
} }
}
Ok(()) Ok(())
} }
pub fn get_possible_moves_sequences( pub fn get_possible_moves_sequences(
&self, &self,
with_excedents: bool, with_excedents: bool,
ignored_rules: Vec<TricTracRule>,
) -> Vec<(CheckerMove, CheckerMove)> { ) -> Vec<(CheckerMove, CheckerMove)> {
let (dice1, dice2) = self.dice.values; let (dice1, dice2) = self.dice.values;
let (dice_max, dice_min) = if dice1 > dice2 { let (dice_max, dice_min) = if dice1 > dice2 {
@ -326,8 +367,13 @@ impl MoveRules {
} else { } else {
(dice2, dice1) (dice2, dice1)
}; };
let mut moves_seqs = let mut moves_seqs = self.get_possible_moves_sequences_by_dices(
self.get_possible_moves_sequences_by_dices(dice_max, dice_min, with_excedents, false); 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 // if we got valid sequences with the highest die, we don't accept sequences using only the
// lowest die // lowest die
let ignore_empty = !moves_seqs.is_empty(); let ignore_empty = !moves_seqs.is_empty();
@ -336,6 +382,7 @@ impl MoveRules {
dice_max, dice_max,
with_excedents, with_excedents,
ignore_empty, ignore_empty,
ignored_rules,
); );
moves_seqs.append(&mut moves_seqs_order2); moves_seqs.append(&mut moves_seqs_order2);
let empty_removed = moves_seqs let empty_removed = moves_seqs
@ -400,7 +447,8 @@ impl MoveRules {
pub fn get_quarter_filling_moves_sequences(&self) -> Vec<(CheckerMove, CheckerMove)> { pub fn get_quarter_filling_moves_sequences(&self) -> Vec<(CheckerMove, CheckerMove)> {
let mut moves_seqs = Vec::new(); let mut moves_seqs = Vec::new();
let color = &Color::White; 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(); let mut board = self.board.clone();
board.move_checker(color, moves.0).unwrap(); board.move_checker(color, moves.0).unwrap();
board.move_checker(color, moves.1).unwrap(); board.move_checker(color, moves.1).unwrap();
@ -418,6 +466,7 @@ impl MoveRules {
dice2: u8, dice2: u8,
with_excedents: bool, with_excedents: bool,
ignore_empty: bool, ignore_empty: bool,
ignored_rules: Vec<TricTracRule>,
) -> Vec<(CheckerMove, CheckerMove)> { ) -> Vec<(CheckerMove, CheckerMove)> {
let mut moves_seqs = Vec::new(); let mut moves_seqs = Vec::new();
let color = &Color::White; let color = &Color::White;
@ -439,24 +488,37 @@ impl MoveRules {
board2.get_possible_moves(*color, dice2, with_excedents, true, forbid_exits) board2.get_possible_moves(*color, dice2, with_excedents, true, forbid_exits)
{ {
if self.check_corner_rules(&(first_move, second_move)).is_ok() 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.is_move_by_puissance(&(first_move, second_move))
&& self.can_take_corner_by_effect()) && 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)); moves_seqs.push((first_move, second_move));
has_second_dice_move = true; 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 if !has_second_dice_move
&& with_excedents && with_excedents
&& !ignore_empty && !ignore_empty
&& self.check_corner_rules(&(first_move, EMPTY_MOVE)).is_ok() && self.check_corner_rules(&(first_move, EMPTY_MOVE)).is_ok()
// TODO : autres règles à vérifier (cf. moves_allowed) && self
// - can_take_corner_by_effect .check_opponent_can_fill_quarter_rule(&(first_move, EMPTY_MOVE))
// - check_exit_rules .is_ok()
// - get_quarter_filling_moves_sequences && !(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 // empty move
moves_seqs.push((first_move, EMPTY_MOVE)); moves_seqs.push((first_move, EMPTY_MOVE));
@ -1078,6 +1140,9 @@ mod tests {
CheckerMove::new(9, 11).unwrap(), CheckerMove::new(9, 11).unwrap(),
CheckerMove::new(11, 14).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![])
);
} }
} }

View file

@ -331,7 +331,7 @@ impl PointsRules {
} }
// Jan qui ne peut : dés non jouables // 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)| { let moves = poss.iter().fold(vec![], |mut acc, (m1, m2)| {
acc.push(*m1); acc.push(*m1);
acc.push(*m2); acc.push(*m2);