refact : extract game_rules_moves
This commit is contained in:
parent
e43a742c1e
commit
7790c07dcc
|
|
@ -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<u8> {
|
||||
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::<Vec<&(usize, i8)>>()
|
||||
.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<CheckerMove> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
625
store/src/game_rules_moves.rs
Normal file
625
store/src/game_rules_moves.rs
Normal file
|
|
@ -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<u8> {
|
||||
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::<Vec<&(usize, i8)>>()
|
||||
.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<CheckerMove> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
mod game;
|
||||
mod game_rules_moves;
|
||||
pub use game::{EndGameReason, GameEvent, GameState, Stage, TurnStage};
|
||||
|
||||
mod player;
|
||||
|
|
|
|||
Loading…
Reference in a new issue