From 0ce155d4ca7630474df581101d443509b48b507c Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Thu, 9 May 2024 21:49:56 +0200 Subject: [PATCH] =?UTF-8?q?wip=20r=C3=A8gles=20de=20sortie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- store/src/board.rs | 54 ++++-- store/src/game.rs | 408 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 369 insertions(+), 93 deletions(-) diff --git a/store/src/board.rs b/store/src/board.rs index 3b03281..da6ec49 100644 --- a/store/src/board.rs +++ b/store/src/board.rs @@ -266,20 +266,8 @@ impl Board { // the square is blocked on the opponent rest corner or if there are opponent's men on the square match color { - Color::White => { - if field == 13 || self.positions[field - 1] < 0 { - Ok(true) - } else { - Ok(false) - } - } - Color::Black => { - if field == 12 || self.positions[23 - field] > 1 { - Ok(true) - } else { - Ok(false) - } - } + Color::White => Ok(field == 13 || self.positions[field - 1] < 0), + Color::Black => Ok(field == 12 || self.positions[23 - field] > 1), } } @@ -330,6 +318,44 @@ impl Board { } } + pub fn get_possible_moves( + &self, + color: Color, + dice: u8, + with_excedants: bool, + ) -> Vec { + let mut moves = Vec::new(); + + let get_dest = |from| { + if color == Color::White { + if from + dice as i32 == 25 { + 0 + } else { + from + dice as i32 + } + } else { + from - dice as i32 + } + }; + + for (field, _count) in self.get_color_fields(color) { + let mut dest = get_dest(field as i32); + if !(0..25).contains(&dest) { + if with_excedants { + dest = 0; + } else { + continue; + } + } + if let Ok(cmove) = CheckerMove::new(field, dest.unsigned_abs() as usize) { + if let Ok(false) = self.blocked(&color, dest.unsigned_abs() as usize) { + moves.push(cmove); + } + } + } + moves + } + 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 diff --git a/store/src/game.rs b/store/src/game.rs index 2a76745..576ffa0 100644 --- a/store/src/game.rs +++ b/store/src/game.rs @@ -1,11 +1,9 @@ //! # Play a TricTrac Game use crate::board::{Board, CheckerMove, Field}; -use crate::dice::{Dice, DiceRoller, Roll}; +use crate::dice::Dice; use crate::player::{Color, Player, PlayerId}; -use crate::Error; -use log::{error, info}; +use log::error; use std::cmp; -use std::fmt::Display; // use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -199,15 +197,14 @@ impl GameState { return false; } } - EndGame { reason } => match reason { - EndGameReason::PlayerWon { winner: _ } => { + EndGame { reason } => { + if let EndGameReason::PlayerWon { winner: _ } = reason { // Check that the game has started before someone wins it if self.stage != Stage::InGame { return false; } } - _ => {} - }, + } PlayerJoined { player_id, name: _ } => { // Check that there isn't another player with the same id if self.players.contains_key(player_id) { @@ -230,7 +227,10 @@ impl GameState { return false; } } - Mark { player_id, points } => { + Mark { + player_id, + points: _, + } => { // Check player exists if !self.players.contains_key(player_id) { return false; @@ -281,9 +281,8 @@ impl GameState { } // Chained_move : "Tout d'une" - let chained_move = moves.0.chain(moves.1); - if chained_move.is_ok() { - if !self.board.move_possible(color, &chained_move.unwrap()) { + 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) { @@ -292,33 +291,254 @@ impl GameState { true } - fn moves_follows_dices(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { + fn get_move_compatible_dices(&self, color: &Color, cmove: &CheckerMove) -> Vec { let (dice1, dice2) = self.dice.values; - let (move1, move2): &(CheckerMove, CheckerMove) = moves; - let dist1 = (move1.get_to() as i8 - move1.get_from() as i8).abs() as u8; - let dist2 = (move2.get_to() as i8 - move2.get_from() as i8).abs() as u8; - // print!("{}, {}, {}, {}", dist1, dist2, dice1, dice2); - // exceptions - // - prise de coin par puissance + + let mut move_dices = Vec::new(); + if cmove.get_to() == 0 { + // 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; } - // - sorties - // default : must be same number - if cmp::min(dist1, dist2) != cmp::min(dice1, dice2) - || cmp::max(dist1, dist2) != cmp::max(dice1, dice2) + + 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)) -> bool { + // ------- 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 false; + } + + // 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 false; + } + + if self.is_move_by_puissance(color, moves) && self.can_take_corner_by_effect(color) { + return false; + } + + // 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 false; + } + + // 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 false; + } + + // - 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; + } + } + + if has_two_checkers && 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 false; + } + } else if cmp::min(moves.0.get_from(), moves.1.get_from()) < next_farthest { + return false; + } + } else { + // Un seul coup sortant en excédant : soit il ne reste qu'une dame et on fait + // tout d'une, soit il y en a au moins deux et le coup sortant doit concerner + // la plus éloignée du bord + if has_two_checkers { + 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 false; + } + } else if *color == Color::White { + if cmp::min(moves.0.get_from(), moves.1.get_from()) != farthest { + return false; + } + } else if cmp::max(moves.0.get_from(), moves.1.get_from()) != farthest { + return false; + } + } + } + } + + // --- remplir cadran si possible ---- + // --- conserver cadran rempli si possible ---- + // --- interdit de jouer dans cadran que l'adversaire peut encore remplir ---- + // no rule was broken + true + } + + 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_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() { + continue; + } + 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.into(); - let dist1 = (move1.get_to() as i8 - move1.get_from() as i8).abs() as u8; - let dist2 = (move2.get_to() as i8 - move2.get_from() as i8).abs() as u8; + 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(); @@ -360,46 +580,6 @@ impl GameState { count1 > 0 && count2 > 0 && opt_color1 == Some(color) && opt_color2 == Some(color) } - fn moves_allowed(&self, color: &Color, moves: &(CheckerMove, CheckerMove)) -> bool { - // ------- 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 false; - } - - // 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 false; - } - - if self.is_move_by_puissance(color, moves) && self.can_take_corner_by_effect(color) { - return false; - } - - // ------- exit rules ---------- - // -- toutes les dames doivent être dans le jan de retour - // -- si on peut sortir, on doit sortir - // -- priorité : - // - dame se trouvant sur la flêche correspondant au dé - // - dame se trouvant plus loin de la sortie que la flêche (point défaillant) - // - dame se trouvant plus près que la flêche (point exédant) - - // --- remplir cadran si possible ---- - // --- conserver cadran rempli si possible ---- - // --- interdit de jouer dans cadran que l'adversaire peut encore remplir ---- - // no rule was broken - true - } - // ---------------------------------------------------------------------------------- // State updates // ---------------------------------------------------------------------------------- @@ -457,7 +637,7 @@ impl GameState { } EndGame { reason: _ } => self.stage = Stage::Ended, PlayerJoined { player_id, name } => { - let color = if self.players.len() > 0 { + let color = if !self.players.is_empty() { Color::White } else { Color::Black @@ -494,12 +674,7 @@ impl GameState { let player = self.players.get(player_id).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() - .find(|id| *id != player_id) - .unwrap() - .clone(); + self.active_player_id = *self.players.keys().find(|id| *id != player_id).unwrap(); self.turn_stage = TurnStage::RollDice; } } @@ -514,7 +689,7 @@ impl GameState { fn mark_points(&mut self, player_id: PlayerId, points: u8) { self.players.get_mut(&player_id).map(|p| { - p.points = p.points + points; + p.points += points; p }); } @@ -567,7 +742,7 @@ mod tests { use super::*; #[test] - fn test_to_string_id() { + fn to_string_id() { let mut state = GameState::default(); state.add_player(1, Player::new("player1".into(), Color::White)); state.add_player(2, Player::new("player2".into(), Color::Black)); @@ -577,7 +752,7 @@ mod tests { } #[test] - fn test_moves_possible() { + fn moves_possible() { let mut state = GameState::default(); let player1 = Player::new("player1".into(), Color::White); let player_id = 1; @@ -610,7 +785,7 @@ mod tests { } #[test] - fn test_moves_follow_dices() { + fn moves_follow_dices() { let mut state = GameState::default(); let player1 = Player::new("player1".into(), Color::White); let player_id = 1; @@ -635,7 +810,7 @@ mod tests { } #[test] - fn test_can_take_corner_by_effect() { + fn can_take_corner_by_effect() { let mut state = GameState::default(); let player1 = Player::new("player1".into(), Color::White); let player_id = 1; @@ -669,7 +844,7 @@ mod tests { } #[test] - fn test_prise_en_puissance() { + fn prise_en_puissance() { let mut state = GameState::default(); let player1 = Player::new("player1".into(), Color::White); let player_id = 1; @@ -713,4 +888,79 @@ mod tests { 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)); + + // 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!(!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!(!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, 2, 0, 0, 1, 0, + ]); + state.dice.values = (5, 5); + let moves = ( + CheckerMove::new(20, 0).unwrap(), + CheckerMove::new(23, 0).unwrap(), + ); + assert!(!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)); + + // 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_allowed(&Color::White, &moves)); + } }