Merge branch 'feature/performance' into develop

This commit is contained in:
Henri Bourcereau 2026-03-06 22:19:26 +01:00
commit 145ab7dcda
4 changed files with 124 additions and 82 deletions

View file

@ -598,12 +598,40 @@ impl Board {
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> {
self.remove_checker(color, cmove.from)?;
self.add_checker(color, cmove.to)?;
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> {
if field == 0 {
return Ok(());

View file

@ -156,13 +156,6 @@ impl GameState {
if let Some(p1) = self.players.get(&1) {
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;
GameState {
stage: self.stage,
@ -171,7 +164,7 @@ impl GameState {
active_player_id: mirrored_active_player,
// active_player_id: self.active_player_id,
players: mirrored_players,
history: mirrored_history,
history: Vec::new(),
dice: self.dice,
dice_points: self.dice_points,
dice_moves: (move1.mirror(), move2.mirror()),

View file

@ -220,7 +220,7 @@ impl MoveRules {
// Si possible, les deux dés doivent être joués
if moves.0.get_from() == 0 || moves.1.get_from() == 0 {
let mut possible_moves_sequences = self.get_possible_moves_sequences(true, 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());
if !possible_moves_sequences.contains(moves) && !possible_moves_sequences.is_empty() {
if *moves == (EMPTY_MOVE, EMPTY_MOVE) {
@ -238,7 +238,7 @@ impl MoveRules {
// check exit rules
// 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 ----
@ -321,7 +321,11 @@ impl MoveRules {
.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() {
return Ok(());
}
@ -331,16 +335,22 @@ impl MoveRules {
}
// toutes les sorties directes sont autorisées, ainsi que les nombres défaillants
let ignored_rules = vec![TricTracRule::Exit];
let possible_moves_sequences_without_excedent =
self.get_possible_moves_sequences(false, ignored_rules);
if possible_moves_sequences_without_excedent.contains(moves) {
let owned;
let seqs = match exit_seqs {
Some(s) => s,
None => {
owned = self
.get_possible_moves_sequences(false, vec![TricTracRule::Exit]);
&owned
}
};
if seqs.contains(moves) {
return Ok(());
}
// À ce stade au moins un des déplacements concerne un nombre en excédant
// - si d'autres séquences de mouvements sans nombre en excédant sont possibles, on
// refuse cette séquence
if !possible_moves_sequences_without_excedent.is_empty() {
if !seqs.is_empty() {
return Err(MoveError::ExitByEffectPossible);
}
@ -441,12 +451,18 @@ impl MoveRules {
} else {
(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(
dice_max,
dice_min,
with_excedents,
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
// lowest die
@ -456,7 +472,8 @@ impl MoveRules {
dice_max,
with_excedents,
ignore_empty,
ignored_rules,
&ignored_rules,
filling_seqs.as_deref(),
);
moves_seqs.append(&mut moves_seqs_order2);
let empty_removed = moves_seqs
@ -527,14 +544,16 @@ impl MoveRules {
let mut moves_seqs = Vec::new();
let color = &Color::White;
let ignored_rules = vec![TricTracRule::Exit, TricTracRule::MustFillQuarter];
let mut board = self.board.clone();
for moves in self.get_possible_moves_sequences(true, ignored_rules) {
let mut board = self.board.clone();
board.move_checker(color, moves.0).unwrap();
board.move_checker(color, moves.1).unwrap();
// println!("get_quarter_filling_moves_sequences board : {:?}", board);
if board.any_quarter_filled(*color) && !moves_seqs.contains(&moves) {
moves_seqs.push(moves);
}
board.unmove_checker(color, moves.1);
board.unmove_checker(color, moves.0);
}
moves_seqs
}
@ -545,18 +564,27 @@ impl MoveRules {
dice2: u8,
with_excedents: bool,
ignore_empty: bool,
ignored_rules: Vec<TricTracRule>,
ignored_rules: &[TricTracRule],
filling_seqs: Option<&[(CheckerMove, CheckerMove)]>,
) -> Vec<(CheckerMove, CheckerMove)> {
let mut moves_seqs = Vec::new();
let color = &Color::White;
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");
for first_move in
self.board
.get_possible_moves(*color, dice1, with_excedents, false, forbid_exits)
{
let mut board2 = self.board.clone();
if board2.move_checker(color, first_move).is_err() {
if board.move_checker(color, first_move).is_err() {
println!("err move");
continue;
}
@ -566,7 +594,7 @@ impl MoveRules {
let mut has_second_dice_move = false;
// println!(" ==== Second");
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
.check_corner_rules(&(first_move, second_move))
@ -590,24 +618,10 @@ impl MoveRules {
&& self.can_take_corner_by_effect())
&& (ignored_rules.contains(&TricTracRule::Exit)
|| self
.check_exit_rules(&(first_move, second_move))
// .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
// )
// })
.check_exit_rules(&(first_move, second_move), exit_seqs.as_deref())
.is_ok())
&& filling_seqs
.map_or(true, |seqs| seqs.is_empty() || seqs.contains(&(first_move, second_move)))
{
if second_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.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())
|| self.check_exit_rules(&(first_move, EMPTY_MOVE), exit_seqs.as_deref()).is_ok())
&& filling_seqs
.map_or(true, |seqs| seqs.is_empty() || seqs.contains(&(first_move, EMPTY_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
}
@ -1498,6 +1510,7 @@ mod tests {
CheckerMove::new(23, 0).unwrap(),
CheckerMove::new(24, 0).unwrap(),
);
let filling_seqs = Some(state.get_quarter_filling_moves_sequences());
assert_eq!(
vec![moves],
state.get_possible_moves_sequences_by_dices(
@ -1505,7 +1518,8 @@ mod tests {
state.dice.values.1,
true,
false,
vec![]
&[],
filling_seqs.as_deref(),
)
);
@ -1520,6 +1534,7 @@ mod tests {
CheckerMove::new(19, 23).unwrap(),
CheckerMove::new(22, 0).unwrap(),
)];
let filling_seqs = Some(state.get_quarter_filling_moves_sequences());
assert_eq!(
moves,
state.get_possible_moves_sequences_by_dices(
@ -1527,7 +1542,8 @@ mod tests {
state.dice.values.1,
true,
false,
vec![]
&[],
filling_seqs.as_deref(),
)
);
let moves = vec![(
@ -1541,7 +1557,8 @@ mod tests {
state.dice.values.0,
true,
false,
vec![]
&[],
filling_seqs.as_deref(),
)
);
@ -1557,6 +1574,7 @@ mod tests {
CheckerMove::new(19, 21).unwrap(),
CheckerMove::new(23, 0).unwrap(),
);
let filling_seqs = Some(state.get_quarter_filling_moves_sequences());
assert_eq!(
vec![moves],
state.get_possible_moves_sequences_by_dices(
@ -1564,7 +1582,8 @@ mod tests {
state.dice.values.1,
true,
false,
vec![]
&[],
filling_seqs.as_deref(),
)
);
}
@ -1583,13 +1602,13 @@ mod tests {
CheckerMove::new(19, 23).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 = (
CheckerMove::new(19, 24).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.board.set_positions(
@ -1602,7 +1621,7 @@ mod tests {
CheckerMove::new(20, 24).unwrap(),
CheckerMove::new(23, 0).unwrap(),
);
assert!(state.check_exit_rules(&moves).is_ok());
assert!(state.check_exit_rules(&moves, None).is_ok());
}
#[test]

View file

@ -3,7 +3,6 @@
use std::cmp::{max, min};
use std::fmt::{Debug, Display, Formatter};
use crate::board::Board;
use crate::{CheckerMove, Dice, GameEvent, GameState};
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
let rules = crate::MoveRules::new(&color, &game_state.board, game_state.dice);
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 {
valid_actions.push(checker_moves_to_trictrac_action(
&move1, &move2, &color, game_state,
valid_actions.push(white_checker_moves_to_trictrac_action(
&move1, &move2, &game_state.dice, &cum,
)?);
}
}
@ -235,10 +235,11 @@ pub fn get_valid_actions(game_state: &GameState) -> anyhow::Result<Vec<TrictracA
// Empty move
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 {
valid_actions.push(checker_moves_to_trictrac_action(
&move1, &move2, &color, game_state,
valid_actions.push(white_checker_moves_to_trictrac_action(
&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)
}
#[cfg(test)]
fn checker_moves_to_trictrac_action(
move1: &CheckerMove,
move2: &CheckerMove,
color: &crate::Color,
state: &GameState,
) -> anyhow::Result<TrictracAction> {
let dice = &state.dice;
let board = &state.board;
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())
// Moves are always in White's coordinate system. For Black, mirror the board first.
let cum = if color == &crate::Color::Black {
state.board.mirror().white_checker_cumulative()
} 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(
move1: &CheckerMove,
move2: &CheckerMove,
dice: &Dice,
board: &Board,
cum: &[u8; 25],
) -> anyhow::Result<TrictracAction> {
let to1 = move1.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 checker1 = board.get_field_checker(&crate::Color::White, from1) as usize;
let mut tmp_board = board.clone();
// should not raise an error for a valid action
tmp_board.move_checker(&crate::Color::White, *move1)?;
let checker2 = tmp_board.get_field_checker(&crate::Color::White, from2) as usize;
// cum[i] = # white checkers in fields 1..=i (precomputed by the caller).
// checker1 is the ordinal of the last checker at from1.
let checker1 = cum[from1] as usize;
// checker2 is the ordinal on the board after move1 (removed from from1, added to to1).
// 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 {
dice_order,
checker1,