Compare commits

...

8 commits

4 changed files with 124 additions and 82 deletions

View file

@ -598,12 +598,40 @@ impl Board {
core::array::from_fn(|i| i + min) core::array::from_fn(|i| i + min)
} }
/// Returns cumulative white-checker counts: result[i] = # white checkers in fields 1..=i.
/// result[0] = 0.
pub fn white_checker_cumulative(&self) -> [u8; 25] {
let mut cum = [0u8; 25];
let mut total = 0u8;
for (i, &count) in self.positions.iter().enumerate() {
if count > 0 {
total += count as u8;
}
cum[i + 1] = total;
}
cum
}
pub fn move_checker(&mut self, color: &Color, cmove: CheckerMove) -> Result<(), Error> { pub fn move_checker(&mut self, color: &Color, cmove: CheckerMove) -> Result<(), Error> {
self.remove_checker(color, cmove.from)?; self.remove_checker(color, cmove.from)?;
self.add_checker(color, cmove.to)?; self.add_checker(color, cmove.to)?;
Ok(()) Ok(())
} }
/// Reverse a previously applied `move_checker`. No validation: assumes the move was valid.
pub fn unmove_checker(&mut self, color: &Color, cmove: CheckerMove) {
let unit = match color {
Color::White => 1,
Color::Black => -1,
};
if cmove.from != 0 {
self.positions[cmove.from - 1] += unit;
}
if cmove.to != 0 {
self.positions[cmove.to - 1] -= unit;
}
}
pub fn remove_checker(&mut self, color: &Color, field: Field) -> Result<(), Error> { pub fn remove_checker(&mut self, color: &Color, field: Field) -> Result<(), Error> {
if field == 0 { if field == 0 {
return Ok(()); return Ok(());

View file

@ -156,13 +156,6 @@ impl GameState {
if let Some(p1) = self.players.get(&1) { if let Some(p1) = self.players.get(&1) {
mirrored_players.insert(2, p1.mirror()); mirrored_players.insert(2, p1.mirror());
} }
let mirrored_history = self
.history
.clone()
.iter()
.map(|evt| evt.get_mirror(false))
.collect();
let (move1, move2) = self.dice_moves; let (move1, move2) = self.dice_moves;
GameState { GameState {
stage: self.stage, stage: self.stage,
@ -171,7 +164,7 @@ impl GameState {
active_player_id: mirrored_active_player, active_player_id: mirrored_active_player,
// active_player_id: self.active_player_id, // active_player_id: self.active_player_id,
players: mirrored_players, players: mirrored_players,
history: mirrored_history, history: Vec::new(),
dice: self.dice, dice: self.dice,
dice_points: self.dice_points, dice_points: self.dice_points,
dice_moves: (move1.mirror(), move2.mirror()), dice_moves: (move1.mirror(), move2.mirror()),

View file

@ -220,7 +220,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, vec![]); 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, None).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() {
if *moves == (EMPTY_MOVE, EMPTY_MOVE) { if *moves == (EMPTY_MOVE, EMPTY_MOVE) {
@ -238,7 +238,7 @@ impl MoveRules {
// check exit rules // check exit rules
// if !ignored_rules.contains(&TricTracRule::Exit) { // if !ignored_rules.contains(&TricTracRule::Exit) {
self.check_exit_rules(moves)?; self.check_exit_rules(moves, None)?;
// } // }
// --- interdit de jouer dans un cadran que l'adversaire peut encore remplir ---- // --- interdit de jouer dans un cadran que l'adversaire peut encore remplir ----
@ -321,7 +321,11 @@ impl MoveRules {
.is_empty() .is_empty()
} }
fn check_exit_rules(&self, moves: &(CheckerMove, CheckerMove)) -> Result<(), MoveError> { fn check_exit_rules(
&self,
moves: &(CheckerMove, CheckerMove),
exit_seqs: Option<&[(CheckerMove, CheckerMove)]>,
) -> Result<(), MoveError> {
if !moves.0.is_exit() && !moves.1.is_exit() { if !moves.0.is_exit() && !moves.1.is_exit() {
return Ok(()); return Ok(());
} }
@ -331,16 +335,22 @@ 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 ignored_rules = vec![TricTracRule::Exit]; let owned;
let possible_moves_sequences_without_excedent = let seqs = match exit_seqs {
self.get_possible_moves_sequences(false, ignored_rules); Some(s) => s,
if possible_moves_sequences_without_excedent.contains(moves) { None => {
owned = self
.get_possible_moves_sequences(false, vec![TricTracRule::Exit]);
&owned
}
};
if seqs.contains(moves) {
return Ok(()); 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 sont 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_without_excedent.is_empty() { if !seqs.is_empty() {
return Err(MoveError::ExitByEffectPossible); return Err(MoveError::ExitByEffectPossible);
} }
@ -441,12 +451,18 @@ impl MoveRules {
} else { } else {
(dice2, dice1) (dice2, dice1)
}; };
let filling_seqs = if !ignored_rules.contains(&TricTracRule::MustFillQuarter) {
Some(self.get_quarter_filling_moves_sequences())
} else {
None
};
let mut moves_seqs = self.get_possible_moves_sequences_by_dices( let mut moves_seqs = self.get_possible_moves_sequences_by_dices(
dice_max, dice_max,
dice_min, dice_min,
with_excedents, with_excedents,
false, false,
ignored_rules.clone(), &ignored_rules,
filling_seqs.as_deref(),
); );
// 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
@ -456,7 +472,8 @@ impl MoveRules {
dice_max, dice_max,
with_excedents, with_excedents,
ignore_empty, ignore_empty,
ignored_rules, &ignored_rules,
filling_seqs.as_deref(),
); );
moves_seqs.append(&mut moves_seqs_order2); moves_seqs.append(&mut moves_seqs_order2);
let empty_removed = moves_seqs let empty_removed = moves_seqs
@ -527,14 +544,16 @@ impl MoveRules {
let mut moves_seqs = Vec::new(); let mut moves_seqs = Vec::new();
let color = &Color::White; let color = &Color::White;
let ignored_rules = vec![TricTracRule::Exit, TricTracRule::MustFillQuarter]; 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();
for moves in self.get_possible_moves_sequences(true, ignored_rules) {
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();
// println!("get_quarter_filling_moves_sequences board : {:?}", board); // println!("get_quarter_filling_moves_sequences board : {:?}", board);
if board.any_quarter_filled(*color) && !moves_seqs.contains(&moves) { if board.any_quarter_filled(*color) && !moves_seqs.contains(&moves) {
moves_seqs.push(moves); moves_seqs.push(moves);
} }
board.unmove_checker(color, moves.1);
board.unmove_checker(color, moves.0);
} }
moves_seqs moves_seqs
} }
@ -545,18 +564,27 @@ impl MoveRules {
dice2: u8, dice2: u8,
with_excedents: bool, with_excedents: bool,
ignore_empty: bool, ignore_empty: bool,
ignored_rules: Vec<TricTracRule>, ignored_rules: &[TricTracRule],
filling_seqs: Option<&[(CheckerMove, CheckerMove)]>,
) -> 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;
let forbid_exits = self.has_checkers_outside_last_quarter(); let forbid_exits = self.has_checkers_outside_last_quarter();
// Precompute non-excedant sequences once so check_exit_rules need not repeat
// the full move generation for every exit-move candidate.
// Only needed when Exit is not already ignored and exits are actually reachable.
let exit_seqs = if !ignored_rules.contains(&TricTracRule::Exit) && !forbid_exits {
Some(self.get_possible_moves_sequences(false, vec![TricTracRule::Exit]))
} else {
None
};
let mut board = self.board.clone();
// println!("==== First"); // println!("==== First");
for first_move in for first_move in
self.board self.board
.get_possible_moves(*color, dice1, with_excedents, false, forbid_exits) .get_possible_moves(*color, dice1, with_excedents, false, forbid_exits)
{ {
let mut board2 = self.board.clone(); if board.move_checker(color, first_move).is_err() {
if board2.move_checker(color, first_move).is_err() {
println!("err move"); println!("err move");
continue; continue;
} }
@ -566,7 +594,7 @@ impl MoveRules {
let mut has_second_dice_move = false; let mut has_second_dice_move = false;
// println!(" ==== Second"); // println!(" ==== Second");
for second_move in for second_move in
board2.get_possible_moves(*color, dice2, with_excedents, true, forbid_exits) board.get_possible_moves(*color, dice2, with_excedents, true, forbid_exits)
{ {
if self if self
.check_corner_rules(&(first_move, second_move)) .check_corner_rules(&(first_move, second_move))
@ -590,24 +618,10 @@ impl MoveRules {
&& self.can_take_corner_by_effect()) && self.can_take_corner_by_effect())
&& (ignored_rules.contains(&TricTracRule::Exit) && (ignored_rules.contains(&TricTracRule::Exit)
|| self || self
.check_exit_rules(&(first_move, second_move)) .check_exit_rules(&(first_move, second_move), exit_seqs.as_deref())
// .inspect_err(|e| {
// println!(
// " 2nd (exit rule): {:?} - {:?}, {:?}",
// e, first_move, second_move
// )
// })
.is_ok())
&& (ignored_rules.contains(&TricTracRule::MustFillQuarter)
|| self
.check_must_fill_quarter_rule(&(first_move, second_move))
// .inspect_err(|e| {
// println!(
// " 2nd: {:?} - {:?}, {:?} for {:?}",
// e, first_move, second_move, self.board
// )
// })
.is_ok()) .is_ok())
&& filling_seqs
.map_or(true, |seqs| seqs.is_empty() || seqs.contains(&(first_move, second_move)))
{ {
if second_move.get_to() == 0 if second_move.get_to() == 0
&& first_move.get_to() == 0 && first_move.get_to() == 0
@ -630,16 +644,14 @@ impl MoveRules {
&& !(self.is_move_by_puissance(&(first_move, EMPTY_MOVE)) && !(self.is_move_by_puissance(&(first_move, EMPTY_MOVE))
&& self.can_take_corner_by_effect()) && self.can_take_corner_by_effect())
&& (ignored_rules.contains(&TricTracRule::Exit) && (ignored_rules.contains(&TricTracRule::Exit)
|| self.check_exit_rules(&(first_move, EMPTY_MOVE)).is_ok()) || self.check_exit_rules(&(first_move, EMPTY_MOVE), exit_seqs.as_deref()).is_ok())
&& (ignored_rules.contains(&TricTracRule::MustFillQuarter) && filling_seqs
|| self .map_or(true, |seqs| seqs.is_empty() || seqs.contains(&(first_move, EMPTY_MOVE)))
.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));
} }
//if board2.get_color_fields(*color).is_empty() { board.unmove_checker(color, first_move);
} }
moves_seqs moves_seqs
} }
@ -1498,6 +1510,7 @@ mod tests {
CheckerMove::new(23, 0).unwrap(), CheckerMove::new(23, 0).unwrap(),
CheckerMove::new(24, 0).unwrap(), CheckerMove::new(24, 0).unwrap(),
); );
let filling_seqs = Some(state.get_quarter_filling_moves_sequences());
assert_eq!( assert_eq!(
vec![moves], vec![moves],
state.get_possible_moves_sequences_by_dices( state.get_possible_moves_sequences_by_dices(
@ -1505,7 +1518,8 @@ mod tests {
state.dice.values.1, state.dice.values.1,
true, true,
false, false,
vec![] &[],
filling_seqs.as_deref(),
) )
); );
@ -1520,6 +1534,7 @@ mod tests {
CheckerMove::new(19, 23).unwrap(), CheckerMove::new(19, 23).unwrap(),
CheckerMove::new(22, 0).unwrap(), CheckerMove::new(22, 0).unwrap(),
)]; )];
let filling_seqs = Some(state.get_quarter_filling_moves_sequences());
assert_eq!( assert_eq!(
moves, moves,
state.get_possible_moves_sequences_by_dices( state.get_possible_moves_sequences_by_dices(
@ -1527,7 +1542,8 @@ mod tests {
state.dice.values.1, state.dice.values.1,
true, true,
false, false,
vec![] &[],
filling_seqs.as_deref(),
) )
); );
let moves = vec![( let moves = vec![(
@ -1541,7 +1557,8 @@ mod tests {
state.dice.values.0, state.dice.values.0,
true, true,
false, false,
vec![] &[],
filling_seqs.as_deref(),
) )
); );
@ -1557,6 +1574,7 @@ mod tests {
CheckerMove::new(19, 21).unwrap(), CheckerMove::new(19, 21).unwrap(),
CheckerMove::new(23, 0).unwrap(), CheckerMove::new(23, 0).unwrap(),
); );
let filling_seqs = Some(state.get_quarter_filling_moves_sequences());
assert_eq!( assert_eq!(
vec![moves], vec![moves],
state.get_possible_moves_sequences_by_dices( state.get_possible_moves_sequences_by_dices(
@ -1564,7 +1582,8 @@ mod tests {
state.dice.values.1, state.dice.values.1,
true, true,
false, false,
vec![] &[],
filling_seqs.as_deref(),
) )
); );
} }
@ -1583,13 +1602,13 @@ mod tests {
CheckerMove::new(19, 23).unwrap(), CheckerMove::new(19, 23).unwrap(),
CheckerMove::new(22, 0).unwrap(), CheckerMove::new(22, 0).unwrap(),
); );
assert!(state.check_exit_rules(&moves).is_ok()); assert!(state.check_exit_rules(&moves, None).is_ok());
let moves = ( let moves = (
CheckerMove::new(19, 24).unwrap(), CheckerMove::new(19, 24).unwrap(),
CheckerMove::new(22, 0).unwrap(), CheckerMove::new(22, 0).unwrap(),
); );
assert!(state.check_exit_rules(&moves).is_ok()); assert!(state.check_exit_rules(&moves, None).is_ok());
state.dice.values = (6, 4); state.dice.values = (6, 4);
state.board.set_positions( state.board.set_positions(
@ -1602,7 +1621,7 @@ mod tests {
CheckerMove::new(20, 24).unwrap(), CheckerMove::new(20, 24).unwrap(),
CheckerMove::new(23, 0).unwrap(), CheckerMove::new(23, 0).unwrap(),
); );
assert!(state.check_exit_rules(&moves).is_ok()); assert!(state.check_exit_rules(&moves, None).is_ok());
} }
#[test] #[test]

View file

@ -3,7 +3,6 @@
use std::cmp::{max, min}; use std::cmp::{max, min};
use std::fmt::{Debug, Display, Formatter}; use std::fmt::{Debug, Display, Formatter};
use crate::board::Board;
use crate::{CheckerMove, Dice, GameEvent, GameState}; use crate::{CheckerMove, Dice, GameEvent, GameState};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -221,10 +220,11 @@ pub fn get_valid_actions(game_state: &GameState) -> anyhow::Result<Vec<TrictracA
// Ajoute aussi les mouvements possibles // Ajoute aussi les mouvements possibles
let rules = crate::MoveRules::new(&color, &game_state.board, game_state.dice); let rules = crate::MoveRules::new(&color, &game_state.board, game_state.dice);
let possible_moves = rules.get_possible_moves_sequences(true, vec![]); let possible_moves = rules.get_possible_moves_sequences(true, vec![]);
// rules.board is already White-perspective (mirrored if Black): compute cum once.
let cum = rules.board.white_checker_cumulative();
for (move1, move2) in possible_moves { for (move1, move2) in possible_moves {
valid_actions.push(checker_moves_to_trictrac_action( valid_actions.push(white_checker_moves_to_trictrac_action(
&move1, &move2, &color, game_state, &move1, &move2, &game_state.dice, &cum,
)?); )?);
} }
} }
@ -235,10 +235,11 @@ pub fn get_valid_actions(game_state: &GameState) -> anyhow::Result<Vec<TrictracA
// Empty move // Empty move
possible_moves.push((CheckerMove::default(), CheckerMove::default())); possible_moves.push((CheckerMove::default(), CheckerMove::default()));
} }
// rules.board is already White-perspective (mirrored if Black): compute cum once.
let cum = rules.board.white_checker_cumulative();
for (move1, move2) in possible_moves { for (move1, move2) in possible_moves {
valid_actions.push(checker_moves_to_trictrac_action( valid_actions.push(white_checker_moves_to_trictrac_action(
&move1, &move2, &color, game_state, &move1, &move2, &game_state.dice, &cum,
)?); )?);
} }
} }
@ -251,36 +252,27 @@ pub fn get_valid_actions(game_state: &GameState) -> anyhow::Result<Vec<TrictracA
Ok(valid_actions) Ok(valid_actions)
} }
#[cfg(test)]
fn checker_moves_to_trictrac_action( fn checker_moves_to_trictrac_action(
move1: &CheckerMove, move1: &CheckerMove,
move2: &CheckerMove, move2: &CheckerMove,
color: &crate::Color, color: &crate::Color,
state: &GameState, state: &GameState,
) -> anyhow::Result<TrictracAction> { ) -> anyhow::Result<TrictracAction> {
let dice = &state.dice; // Moves are always in White's coordinate system. For Black, mirror the board first.
let board = &state.board; let cum = if color == &crate::Color::Black {
state.board.mirror().white_checker_cumulative()
if color == &crate::Color::Black {
// Moves are already 'white', so we don't mirror them
white_checker_moves_to_trictrac_action(
move1,
move2,
// &move1.clone().mirror(),
// &move2.clone().mirror(),
dice,
&board.clone().mirror(),
)
// .map(|a| a.mirror())
} else { } else {
white_checker_moves_to_trictrac_action(move1, move2, dice, board) state.board.white_checker_cumulative()
} };
white_checker_moves_to_trictrac_action(move1, move2, &state.dice, &cum)
} }
fn white_checker_moves_to_trictrac_action( fn white_checker_moves_to_trictrac_action(
move1: &CheckerMove, move1: &CheckerMove,
move2: &CheckerMove, move2: &CheckerMove,
dice: &Dice, dice: &Dice,
board: &Board, cum: &[u8; 25],
) -> anyhow::Result<TrictracAction> { ) -> anyhow::Result<TrictracAction> {
let to1 = move1.get_to(); let to1 = move1.get_to();
let to2 = move2.get_to(); let to2 = move2.get_to();
@ -321,11 +313,21 @@ fn white_checker_moves_to_trictrac_action(
} }
let dice_order = diff_move1 == dice.values.0 as usize; let dice_order = diff_move1 == dice.values.0 as usize;
let checker1 = board.get_field_checker(&crate::Color::White, from1) as usize; // cum[i] = # white checkers in fields 1..=i (precomputed by the caller).
let mut tmp_board = board.clone(); // checker1 is the ordinal of the last checker at from1.
// should not raise an error for a valid action let checker1 = cum[from1] as usize;
tmp_board.move_checker(&crate::Color::White, *move1)?; // checker2 is the ordinal on the board after move1 (removed from from1, added to to1).
let checker2 = tmp_board.get_field_checker(&crate::Color::White, from2) as usize; // Adjust the cumulative in O(1) without cloning the board.
let checker2 = {
let mut c = cum[from2];
if from1 > 0 && from2 >= from1 {
c -= 1; // one checker was removed from from1, shifting later ordinals down
}
if from1 > 0 && to1 > 0 && from2 >= to1 {
c += 1; // one checker was added at to1, shifting later ordinals up
}
c as usize
};
Ok(TrictracAction::Move { Ok(TrictracAction::Move {
dice_order, dice_order,
checker1, checker1,