Skip to main content

arimaa_engine/
game_state.rs

1use super::bits::for_each_bit_idx;
2use super::constants::MAX_NUM_STEPS;
3use super::path_permutation::{PathPermutations, get_path_permutations, get_path_permutations_with_exact_steps};
4use super::piece_board::ExcludedSquaresWithDirections;
5use super::piece_board::get_exclusion_for_direction;
6use super::piece_board::{BOTTOM_ROW_MASK, PieceBoard, TOP_ROW_MASK, TRAP_MASK};
7use super::piece_board::{can_move_in_direction, get_excluded_direction, get_supported_square_exclusions, get_threatened_pieces_for_dir, get_threats_for_piece_type, shift_pieces_in_direction};
8use super::repetition_counts::RepetitionCounts;
9use super::value::Value;
10use super::zobrist::Zobrist;
11use super::{Action, Direction, Path, Piece, PushPullDirection, Square};
12
13use itertools::Itertools;
14use std::cell::OnceCell;
15use std::hash::{Hash, Hasher};
16use std::sync::Arc;
17use std::sync::atomic::{AtomicU64, Ordering};
18use tinyvec::ArrayVec;
19
20const P1_OBJECTIVE_MASK: u64 = TOP_ROW_MASK;
21const P2_OBJECTIVE_MASK: u64 = BOTTOM_ROW_MASK;
22
23#[derive(Clone, Debug)]
24pub enum GameState {
25    PlaceState(PlaceState),
26    PlayState(PlayState),
27}
28
29#[derive(Clone, Debug)]
30pub struct PlaceState {
31    p1_turn_to_move: bool,
32    piece_board: Box<PieceBoard>,
33    hash: Zobrist,
34}
35
36#[derive(Clone, Debug)]
37pub struct PlayState {
38    turn_state: TurnState,
39    step_state: StepState,
40}
41
42#[derive(Clone, Debug)]
43pub struct TurnState {
44    p1_turn_to_move: bool,
45    move_number: u16,
46    piece_board: PieceBoard,
47    hash: Zobrist,
48    hash_history: RepetitionCounts,
49}
50
51#[derive(Clone, Debug)]
52pub struct StepState {
53    steps: ArrayVec<[(Square, Direction); 3]>,
54    actions: ArrayVec<[Action; 3]>,
55    piece_board: PieceBoard,
56    banned_piece_mask: Arc<AtomicU64>,
57}
58
59impl StepState {
60    fn new(piece_board: PieceBoard) -> Self {
61        Self {
62            steps: ArrayVec::new(),
63            actions: ArrayVec::new(),
64            piece_board,
65            banned_piece_mask: Arc::new(AtomicU64::new(u64::MAX)),
66        }
67    }
68}
69
70impl Hash for GameState {
71    fn hash<H: Hasher>(&self, state: &mut H) {
72        state.write_u64(self.hash().board_state_hash());
73    }
74}
75
76impl PartialEq for GameState {
77    fn eq(&self, other: &GameState) -> bool {
78        self.hash().board_state_hash() == other.hash().board_state_hash()
79    }
80}
81
82impl Eq for GameState {}
83
84impl GameState {
85    pub fn new(p1_turn_to_move: bool, move_number: usize, piece_board: PieceBoard) -> Self {
86        let hash = Zobrist::from_piece_board(&piece_board, p1_turn_to_move, Some(0));
87
88        let turn_state = TurnState {
89            p1_turn_to_move,
90            move_number: move_number as u16,
91            piece_board: piece_board.clone(),
92            hash,
93            hash_history: RepetitionCounts::new().append(hash),
94        };
95
96        let step_state = StepState::new(piece_board);
97
98        Self::PlayState(PlayState { turn_state, step_state })
99    }
100
101    pub fn initial() -> Self {
102        Self::PlaceState(PlaceState {
103            p1_turn_to_move: true,
104            piece_board: Box::new(PieceBoard::initial()),
105            hash: Zobrist::initial(),
106        })
107    }
108
109    #[must_use]
110    pub fn take_action(&self, action: &Action) -> Self {
111        match self {
112            Self::PlaceState(place_state) => place_state.take_action(action),
113            Self::PlayState(play_state) => GameState::PlayState(play_state.take_action(action)),
114        }
115    }
116
117    pub fn is_terminal(&self) -> Option<Value> {
118        match self {
119            Self::PlaceState(_) => None,
120            Self::PlayState(play_state) => play_state.is_terminal(),
121        }
122    }
123
124    pub fn valid_actions(&self) -> Vec<Action> {
125        self.valid_actions_(true)
126    }
127
128    pub fn valid_actions_no_exclusions(&self) -> Vec<Action> {
129        self.valid_actions_(false)
130    }
131
132    pub fn get_piece_board(&self) -> &PieceBoard {
133        match self {
134            Self::PlaceState(place_state) => place_state.get_piece_board(),
135            Self::PlayState(play_state) => play_state.get_piece_board(),
136        }
137    }
138
139    pub fn is_p1_turn_to_move(&self) -> bool {
140        match self {
141            Self::PlaceState(place_state) => place_state.p1_turn_to_move,
142            Self::PlayState(play_state) => play_state.turn_state.p1_turn_to_move,
143        }
144    }
145
146    pub fn get_move_number(&self) -> usize {
147        match self {
148            Self::PlaceState(_) => 1,
149            Self::PlayState(play_state) => play_state.turn_state.move_number,
150        }
151        .into()
152    }
153
154    pub fn valid_actions_(&self, check_repetitions: bool) -> Vec<Action> {
155        match self {
156            Self::PlaceState(place_state) => place_state.valid_actions(),
157            Self::PlayState(play_state) => play_state.valid_actions(check_repetitions),
158        }
159    }
160
161    pub fn hash(&self) -> Zobrist {
162        match self {
163            Self::PlaceState(place_state) => place_state.hash,
164            Self::PlayState(play_state) => play_state.hash(),
165        }
166    }
167
168    /// Gets the current step number during the play phase. Note that the step number is 0 based. So values are always in the range 0..=3
169    pub fn step(&self) -> usize {
170        match self {
171            Self::PlaceState(_) => 0,
172            Self::PlayState(play_state) => play_state.get_step(),
173        }
174    }
175
176    pub fn is_play_phase(&self) -> bool {
177        matches!(self, Self::PlayState(_))
178    }
179
180    pub fn get_banned_piece_mask(&self) -> u64 {
181        match self {
182            Self::PlaceState(_) => 0,
183            Self::PlayState(play_state) => play_state.get_banned_piece_mask(),
184        }
185    }
186
187    pub fn get_vertical_symmetry(&self) -> Self {
188        match self {
189            Self::PlaceState(place_state) => Self::PlaceState(place_state.get_vertical_symmetry()),
190            Self::PlayState(play_state) => Self::PlayState(play_state.get_vertical_symmetry()),
191        }
192    }
193
194    pub fn get_trapped_animal_for_move(&self, square: Square, direction: Direction) -> Option<(Square, Piece, bool)> {
195        match self {
196            Self::PlaceState(_) => panic!("Get trapped animal is not valid in the place state"),
197            Self::PlayState(play_state) => play_state.get_trapped_animal_for_move(square, direction),
198        }
199    }
200
201    pub fn piece_to_place(&self) -> Piece {
202        match self {
203            Self::PlaceState(place_state) => place_state.piece_to_place(),
204            Self::PlayState(_) => panic!("Piece to place is not valid in the play state"),
205        }
206    }
207}
208
209impl PlayState {
210    fn initial(hash: Zobrist, piece_board: PieceBoard) -> Self {
211        Self {
212            turn_state: TurnState {
213                piece_board: piece_board.clone(),
214                p1_turn_to_move: true,
215                move_number: 2,
216                hash,
217                hash_history: RepetitionCounts::new().append(hash),
218            },
219            step_state: StepState::new(piece_board),
220        }
221    }
222
223    pub fn get_step(&self) -> usize {
224        self.step_state.steps.len()
225    }
226
227    pub fn get_trapped_animal_for_move(&self, square: Square, direction: Direction) -> Option<(Square, Piece, bool)> {
228        let mut piece_board = self.get_piece_board().clone();
229
230        piece_board.move_piece_update_board(square, direction);
231
232        let trapped_animal_bits = piece_board.get_trapped_piece_bits();
233        if trapped_animal_bits != 0 {
234            let square = Square::from_bit_board(trapped_animal_bits);
235            let piece = piece_board.get_piece_type_at_square(&square).unwrap();
236            let is_p1_piece = piece_board.get_bits_for_piece(piece, true) & square.as_bit_board() != 0;
237
238            return Some((square, piece, is_p1_piece));
239        }
240
241        None
242    }
243
244    pub fn get_vertical_symmetry(&self) -> Self {
245        Self {
246            turn_state: self.turn_state.get_vertical_symmetry(),
247            step_state: self.step_state.get_vertical_symmetry(),
248        }
249    }
250
251    fn take_action(&self, action: &Action) -> Self {
252        match action {
253            Action::Pass => self.pass(),
254            Action::Place(_) => panic!("Place action is not valid in the play state"),
255            Action::Move(square, path) => self.move_piece(*square, *path),
256            Action::PushPull(square, direction) => self.push_pull_piece(*square, *direction),
257        }
258    }
259
260    fn is_terminal(&self) -> Option<Value> {
261        let piece_board = &self.get_piece_board();
262
263        // The order of checking for win/lose conditions is as follows assuming player A just made the move and player B now needs to move:
264        if self.get_step() > 0 {
265            return self.has_move(piece_board);
266        }
267
268        // Check if the position has occurred 3 times; the player who just moved loses.
269        if self.turn_state.hash_history.num_repetitions(self.get_initial_hash_of_move()) >= 3 {
270            return Some(if self.turn_state.p1_turn_to_move { Value([1.0, 0.0]) } else { Value([0.0, 1.0]) });
271        }
272
273        // Check if a rabbit of player A reached goal. If so player A wins.
274        // Check if a rabbit of player B reached goal. If so player B wins.
275        self.rabbit_at_goal(piece_board)
276            // Check if player B lost all rabbits. If so player A wins.
277            // Check if player A lost all rabbits. If so player B wins.
278            .or_else(|| self.lost_all_rabbits(piece_board))
279            // Check if player B has no possible move (all pieces are frozen or have no place to move). If so player A wins.
280            // Check if the only moves player B has are 3rd time repetitions. If so player A wins.
281            .or_else(|| self.has_move(piece_board))
282    }
283
284    fn valid_actions(&self, check_repetitions: bool) -> Vec<Action> {
285        let piece_board = &self.get_piece_board();
286        let hash = self.hash();
287        let current_step = self.get_step();
288        let p1_turn_to_move = self.turn_state.p1_turn_to_move;
289        let banned_piece_mask = self.get_banned_piece_mask();
290
291        let mut valid_actions = Vec::with_capacity(100);
292        Self::each_with_push_pull_piece_actions(piece_board, p1_turn_to_move, current_step, |a| {
293            if !self.is_action_passing(&a, piece_board, hash, check_repetitions) {
294                valid_actions.push(a);
295            }
296            false
297        });
298        Self::each_valid_curr_player_piece_moves(piece_board, banned_piece_mask, p1_turn_to_move, current_step, |a| {
299            if !self.is_action_passing(&a, piece_board, hash, check_repetitions) {
300                valid_actions.push(a);
301            }
302            false
303        });
304
305        if !self.is_pass_passing_like_action(hash, check_repetitions) {
306            valid_actions.push(Action::Pass);
307        }
308
309        valid_actions
310    }
311
312    fn has_move(&self, piece_board: &PieceBoard) -> Option<Value> {
313        if self.can_move(piece_board) {
314            None
315        } else if self.turn_state.p1_turn_to_move {
316            Some(Value([0.0, 1.0]))
317        } else {
318            Some(Value([1.0, 0.0]))
319        }
320    }
321
322    fn can_move(&self, piece_board: &PieceBoard) -> bool {
323        let piece_trapped = OnceCell::new();
324        let piece_trapped = || piece_trapped.get_or_init(|| self.piece_trapped_this_turn());
325        let banned_piece_mask = OnceCell::new();
326        let banned_piece_mask = || banned_piece_mask.get_or_init(|| self.get_banned_piece_mask());
327        let check_repetitions = true;
328        let hash = self.hash();
329        let current_step = self.get_step();
330        let p1_turn_to_move = self.turn_state.p1_turn_to_move;
331        let is_action_not_passing = |action| *piece_trapped() || !self.is_action_passing(&action, piece_board, hash, check_repetitions);
332        let can_pass = || self.can_pass(hash, check_repetitions);
333        let can_move_piece = || Self::each_valid_curr_player_piece_moves(piece_board, *banned_piece_mask(), p1_turn_to_move, current_step, is_action_not_passing);
334        let can_push_pull_piece = || Self::each_with_push_pull_piece_actions(piece_board, p1_turn_to_move, current_step, is_action_not_passing);
335
336        can_pass() || can_move_piece() || can_push_pull_piece()
337    }
338
339    fn can_pass(&self, hash: Zobrist, check_repetitions: bool) -> bool {
340        self.get_step() > 0 && !self.is_pass_passing_like_action(hash, check_repetitions)
341    }
342
343    fn is_action_valid(piece_board: &PieceBoard, p1_turn_to_move: bool, action: &Action) -> bool {
344        let is_matching_action = |a: Action| a == *action;
345
346        match action {
347            Action::Move(square, path) => {
348                let piece_type = piece_board.get_piece_type_at_square(square);
349
350                if piece_type.is_none() {
351                    return false;
352                }
353
354                let curr_player_piece_mask = piece_board.get_curr_player_piece_mask(p1_turn_to_move);
355                let opp_piece_mask = piece_board.get_opponent_piece_mask(p1_turn_to_move);
356                let supported_square_exclusions = &get_supported_square_exclusions(curr_player_piece_mask);
357                let path_permuations = get_path_permutations_with_exact_steps(path.len());
358                let dangling_pieces_mask = OnceCell::new();
359                let dangling_pieces_mask = || *dangling_pieces_mask.get_or_init(|| Self::get_dangling_pieces(curr_player_piece_mask));
360                let banned_piece_mask = !square.as_bit_board();
361
362                Self::each_valid_move_for_piece(
363                    piece_board,
364                    banned_piece_mask,
365                    piece_type.unwrap(),
366                    curr_player_piece_mask,
367                    dangling_pieces_mask,
368                    opp_piece_mask,
369                    path_permuations,
370                    supported_square_exclusions,
371                    p1_turn_to_move,
372                    is_matching_action,
373                )
374            }
375            Action::PushPull(_, _) => Self::each_with_push_pull_piece_actions(piece_board, p1_turn_to_move, 2, is_matching_action),
376            _ => panic!("Invalid sequential action type {}", action),
377        }
378    }
379
380    fn get_piece_board(&self) -> &PieceBoard {
381        &self.step_state.piece_board
382    }
383
384    fn get_initial_hash_of_move(&self) -> Zobrist {
385        self.turn_state.hash
386    }
387
388    fn rabbit_at_goal(&self, piece_board: &PieceBoard) -> Option<Value> {
389        let p1_objective_met = piece_board.p1_pieces & piece_board.rabbits & P1_OBJECTIVE_MASK != 0;
390        let p2_objective_met = !piece_board.p1_pieces & piece_board.rabbits & P2_OBJECTIVE_MASK != 0;
391
392        if p1_objective_met || p2_objective_met {
393            // Objective is opposite of the player to move since we are checking if there is a winner after the turn is complete.
394            // Logic should include the condition of if both players have a rabbit at the goal. In that case the player who was last to move wins.
395            let last_to_move_is_p1 = !self.turn_state.p1_turn_to_move;
396            let last_to_move_objective_met = if last_to_move_is_p1 { p1_objective_met } else { p2_objective_met };
397            let p1_won = !(last_to_move_is_p1 ^ last_to_move_objective_met);
398            Some(if p1_won { Value([1.0, 0.0]) } else { Value([0.0, 1.0]) })
399        } else {
400            None
401        }
402    }
403
404    fn lost_all_rabbits(&self, piece_board: &PieceBoard) -> Option<Value> {
405        let p1_lost_rabbits = piece_board.p1_pieces & piece_board.rabbits == 0;
406        let p2_lost_rabbits = !piece_board.p1_pieces & piece_board.rabbits == 0;
407
408        // Check if player B lost all rabbits. If so player A wins.
409        // Check if player A lost all rabbits. If so player B wins.
410
411        if p1_lost_rabbits || p2_lost_rabbits {
412            // Objective is opposite of the player to move since we are checking if there is a winner after the turn is complete.
413            // Logic should include the condition of if both players lost their rabbits. In that case the player who was last to move wins.
414            let last_to_move_is_p1 = !self.turn_state.p1_turn_to_move;
415            let last_to_move_objective_met = if last_to_move_is_p1 { p2_lost_rabbits } else { p1_lost_rabbits };
416            let p1_won = !(last_to_move_is_p1 ^ last_to_move_objective_met);
417            Some(if p1_won { Value([1.0, 0.0]) } else { Value([0.0, 1.0]) })
418        } else {
419            None
420        }
421    }
422
423    fn is_action_passing(&self, action: &Action, piece_board: &PieceBoard, hash: Zobrist, check_repetitions: bool) -> bool {
424        match action {
425            Action::Move(square, path) => self.is_move_passing_like_action(*square, *path, piece_board, hash, check_repetitions),
426            Action::PushPull(square, direction) => self.is_push_pull_passing_like_action(*square, *direction, piece_board, hash, check_repetitions),
427            Action::Pass => self.is_pass_passing_like_action(hash, check_repetitions),
428            Action::Place(_) => false,
429        }
430    }
431
432    #[allow(clippy::let_and_return)]
433    fn is_move_passing_like_action(&self, square: Square, path: Path, piece_board: &PieceBoard, hash: Zobrist, check_repetitions: bool) -> bool {
434        let updated_piece_board_hash = || {
435            let mut new_piece_board = piece_board.clone();
436            new_piece_board.move_piece_on_path(square, path);
437
438            let updated_piece_board_hash = hash.update_piece_board(piece_board, &new_piece_board);
439
440            updated_piece_board_hash
441        };
442
443        self.is_passing_like_action(path.len(), updated_piece_board_hash, check_repetitions)
444    }
445
446    #[allow(clippy::let_and_return)]
447    fn is_push_pull_passing_like_action(&self, square: Square, direction: PushPullDirection, piece_board: &PieceBoard, hash: Zobrist, check_repetitions: bool) -> bool {
448        let update_piece_board_hash = || {
449            let mut new_piece_board = piece_board.clone();
450            new_piece_board.push_pull_piece(square, direction);
451
452            let updated_piece_board_hash = hash.update_piece_board(piece_board, &new_piece_board);
453
454            updated_piece_board_hash
455        };
456
457        self.is_passing_like_action(2, update_piece_board_hash, check_repetitions)
458    }
459
460    fn is_pass_passing_like_action(&self, hash: Zobrist, check_repetitions: bool) -> bool {
461        self.is_passing_like_action(4, || hash, check_repetitions)
462    }
463
464    fn is_passing_like_action<F: FnMut() -> Zobrist>(&self, steps_used: usize, mut update_piece_board_hash: F, check_repetitions: bool) -> bool {
465        let curr_step_num = self.get_step();
466
467        if curr_step_num + steps_used < MAX_NUM_STEPS {
468            return false;
469        }
470
471        let initial_hash_of_move = self.get_initial_hash_of_move();
472        let updated_piece_board_hash = update_piece_board_hash();
473
474        let curr_is_p1_turn_to_move = self.turn_state.p1_turn_to_move;
475        let new_hash_no_player_switch = updated_piece_board_hash.update_step(curr_step_num, 0);
476        let new_hash_switch_players = new_hash_no_player_switch.update_player_to_move(curr_is_p1_turn_to_move, !curr_is_p1_turn_to_move);
477
478        new_hash_no_player_switch == initial_hash_of_move || (check_repetitions && self.turn_state.hash_history.num_repetitions(new_hash_switch_players) >= 2)
479    }
480
481    fn each_valid_curr_player_piece_moves<F: FnMut(Action) -> bool>(piece_board: &PieceBoard, banned_piece_mask: u64, p1_turn_to_move: bool, current_step: usize, mut cb: F) -> bool {
482        let curr_player_piece_mask = piece_board.get_curr_player_piece_mask(p1_turn_to_move);
483        let opp_piece_mask = piece_board.get_opponent_piece_mask(p1_turn_to_move);
484        let num_steps_remaining = MAX_NUM_STEPS - current_step;
485        let supported_square_exclusions = get_supported_square_exclusions(curr_player_piece_mask);
486        let path_permuations = get_path_permutations(num_steps_remaining);
487        let dangling_pieces_mask = OnceCell::new();
488        let dangling_pieces_mask = || *dangling_pieces_mask.get_or_init(|| Self::get_dangling_pieces(curr_player_piece_mask));
489
490        for piece_type in [Piece::Rabbit, Piece::Cat, Piece::Dog, Piece::Horse, Piece::Camel, Piece::Elephant] {
491            let complete = Self::each_valid_move_for_piece(
492                piece_board,
493                banned_piece_mask,
494                piece_type,
495                curr_player_piece_mask,
496                dangling_pieces_mask,
497                opp_piece_mask,
498                path_permuations,
499                &supported_square_exclusions,
500                p1_turn_to_move,
501                &mut cb,
502            );
503
504            if complete {
505                return true;
506            }
507        }
508
509        false
510    }
511
512    #[allow(clippy::too_many_arguments)]
513    fn each_valid_move_for_piece<F: FnMut(Action) -> bool, F2: Fn() -> u64>(
514        piece_board: &PieceBoard,
515        banned_piece_mask: u64,
516        piece_type: Piece,
517        curr_player_piece_mask: u64,
518        dangling_pieces_mask: F2,
519        opp_piece_mask: u64,
520        path_permuations: &[PathPermutations],
521        supported_square_exclusions: &ExcludedSquaresWithDirections,
522        p1_turn_to_move: bool,
523        mut cb: F,
524    ) -> bool {
525        let pieces_to_move_board = piece_board.get_bits_by_piece_type(piece_type) & curr_player_piece_mask & !banned_piece_mask;
526
527        // If there are no pieces of this type, move to the next piece type.
528        if pieces_to_move_board == 0 {
529            return false;
530        }
531
532        // Exclude frozen pieces to allow for short circuiting since a frozen piece is unable to be moved.
533        let threatened_squares = get_threats_for_piece_type(opp_piece_mask, piece_type, piece_board);
534        let pieces_to_move_board = pieces_to_move_board & (supported_square_exclusions.none | !threatened_squares);
535
536        // If there are pieces of this type but they are all frozen, move to the next piece type.
537        if pieces_to_move_board == 0 {
538            return false;
539        }
540
541        let empty_squares = !piece_board.all_pieces;
542        let invalid_rabbit_direction = if p1_turn_to_move { Direction::Down } else { Direction::Up };
543        let has_potential_dangling_piece = curr_player_piece_mask & TRAP_MASK != 0;
544
545        'moves: for paths in path_permuations.iter() {
546            let mut successful_moves = 0u64;
547
548            'paths: for (path, packed_path) in paths {
549                // Do not include moves that have been proven when calculating other paths.
550                let mut moving_pieces_board = pieces_to_move_board & !successful_moves;
551
552                for (i, direction) in path.iter().enumerate() {
553                    if piece_type == Piece::Rabbit && *direction == invalid_rabbit_direction {
554                        continue 'paths;
555                    }
556                    let mut supported_squares = get_excluded_direction(i, path, supported_square_exclusions);
557
558                    // Check if the current piece was supporting a dangling piece on the trap.
559                    // When the current piece moves along the outside of the trap, the trapped piece will no longer provide support.
560                    if has_potential_dangling_piece && i == 2 && path.len() >= 3 && path[0] != path[1] {
561                        let dangling_pieces_mask: u64 = dangling_pieces_mask();
562                        if dangling_pieces_mask != 0 {
563                            let no_longer_supported_squares = Self::get_no_longer_supported_pieces(moving_pieces_board, dangling_pieces_mask, supported_square_exclusions, path);
564                            supported_squares &= !no_longer_supported_squares
565                        }
566                    }
567
568                    let supported_unendangered_squares = supported_squares | !(TRAP_MASK | threatened_squares);
569                    let unoccupied_directions = can_move_in_direction(*direction, empty_squares);
570                    let valid_curr_piece_moves = moving_pieces_board & supported_unendangered_squares & unoccupied_directions;
571
572                    // Move the pieces that are valid to be moved.
573                    moving_pieces_board = shift_pieces_in_direction(valid_curr_piece_moves, *direction);
574
575                    // If there are no more movable pieces. Continue from the other paths loop since we are done attempting this path.
576                    if moving_pieces_board == 0 {
577                        continue 'paths;
578                    }
579                }
580
581                let successfully_moved_pieces = Self::unwind_piece_path(moving_pieces_board, path);
582
583                let complete = Self::for_each_square(successfully_moved_pieces, |s| cb(Action::Move(s, *packed_path)));
584
585                if complete {
586                    return true;
587                }
588
589                // Set any of the moves that were completed to the list of successful moves.
590                successful_moves |= successfully_moved_pieces;
591
592                // If all pieces have found a path for this move, continue from the moves loop since we are done attempting this move for the current piece type.
593                if pieces_to_move_board == successful_moves {
594                    continue 'moves;
595                }
596            }
597        }
598
599        false
600    }
601
602    // Iterate over the directions in reverse to move the pieces back to their starting locations.
603    fn unwind_piece_path(piece_board: u64, path: &[Direction]) -> u64 {
604        path.iter().rev().fold(piece_board, |piece_board, dir| shift_pieces_in_direction(piece_board, dir.invert()))
605    }
606
607    fn for_each_square<F: FnMut(Square) -> bool>(piece_board: u64, mut cb: F) -> bool {
608        let mut complete = false;
609        for_each_bit_idx(piece_board, |idx| {
610            if !complete {
611                let square = Square::from_index(idx as u8);
612                complete |= cb(square);
613            }
614        });
615        complete
616    }
617
618    fn each_with_push_pull_piece_actions<F: FnMut(Action) -> bool>(piece_board: &PieceBoard, p1_turn_to_move: bool, current_step: usize, mut cb: F) -> bool {
619        if current_step <= 2 {
620            let predator_piece_mask = piece_board.get_curr_player_non_frozen_pieces(p1_turn_to_move);
621            let opp_piece_mask = piece_board.get_opponent_piece_mask(p1_turn_to_move);
622            let empty_squares = !piece_board.all_pieces;
623
624            for (occupying_dir, vacating_dirs) in [
625                (
626                    Direction::Up,
627                    [
628                        (Direction::Up, PushPullDirection::NN),
629                        (Direction::Right, PushPullDirection::EN),
630                        (Direction::Left, PushPullDirection::WN),
631                    ],
632                ),
633                (
634                    Direction::Right,
635                    [
636                        (Direction::Up, PushPullDirection::NE),
637                        (Direction::Right, PushPullDirection::EE),
638                        (Direction::Down, PushPullDirection::SE),
639                    ],
640                ),
641                (
642                    Direction::Down,
643                    [
644                        (Direction::Right, PushPullDirection::ES),
645                        (Direction::Down, PushPullDirection::SS),
646                        (Direction::Left, PushPullDirection::WS),
647                    ],
648                ),
649                (
650                    Direction::Left,
651                    [
652                        (Direction::Up, PushPullDirection::NW),
653                        (Direction::Down, PushPullDirection::SW),
654                        (Direction::Left, PushPullDirection::WW),
655                    ],
656                ),
657            ]
658            .iter()
659            {
660                let opp_threatened_pieces_push = get_threatened_pieces_for_dir(predator_piece_mask, opp_piece_mask, *occupying_dir, piece_board);
661                let opp_threatened_pieces_pull = get_threatened_pieces_for_dir(predator_piece_mask, opp_piece_mask, occupying_dir.invert(), piece_board);
662                let opp_threatened_pieces_pull = shift_pieces_in_direction(opp_threatened_pieces_pull, *occupying_dir);
663
664                for (vacating_dir, move_direction) in vacating_dirs {
665                    let unoccupied_directions = can_move_in_direction(*vacating_dir, empty_squares);
666
667                    let valid_push_moves = unoccupied_directions & opp_threatened_pieces_push;
668                    let valid_pull_moves = unoccupied_directions & opp_threatened_pieces_pull;
669
670                    debug_assert_eq!(valid_push_moves & valid_pull_moves, 0);
671
672                    let valid_push_pull_moves = valid_push_moves | valid_pull_moves;
673
674                    if valid_push_pull_moves != 0 {
675                        let complete = Self::for_each_square(valid_push_pull_moves, |s| cb(Action::PushPull(s, *move_direction)));
676
677                        if complete {
678                            return true;
679                        }
680                    }
681                }
682            }
683        }
684
685        false
686    }
687
688    fn pass(&self) -> PlayState {
689        let hash = self.hash().pass(self.get_step());
690        let new_hash_history = if self.piece_trapped_this_turn() {
691            RepetitionCounts::new()
692        } else {
693            self.turn_state.hash_history.clone()
694        };
695        let new_hash_history = new_hash_history.append(hash);
696
697        let piece_board = self.get_piece_board();
698        let turn_state = TurnState {
699            p1_turn_to_move: !self.turn_state.p1_turn_to_move,
700            move_number: self.turn_state.move_number + if self.turn_state.p1_turn_to_move { 0 } else { 1 },
701            hash,
702            hash_history: new_hash_history,
703            piece_board: piece_board.clone(),
704        };
705
706        let step_state = StepState::new(piece_board.clone());
707
708        PlayState { turn_state, step_state }
709    }
710
711    fn move_piece(&self, square: Square, path: Path) -> Self {
712        let mut piece_board = self.get_piece_board().clone();
713        piece_board.move_piece_on_path(square, path);
714        let steps = path
715            .iter()
716            .scan(square, |square, direction| {
717                let original = *square;
718                *square = square.shift_in_direction(direction);
719                Some((original, direction))
720            })
721            .collect::<ArrayVec<[_; 4]>>();
722
723        self.get_state_after_move(piece_board, &steps, Action::Move(square, path))
724    }
725
726    fn push_pull_piece(&self, square: Square, direction: PushPullDirection) -> Self {
727        let mut piece_board = self.get_piece_board().clone();
728        piece_board.push_pull_piece(square, direction);
729        let steps = direction.get_steps(square);
730
731        self.get_state_after_move(piece_board, &steps, Action::PushPull(square, direction))
732    }
733
734    fn get_state_after_move(&self, new_piece_board: PieceBoard, steps: &[(Square, Direction)], action: Action) -> Self {
735        let num_steps_taken = steps.len();
736        let curr_step = self.get_step();
737        let num_steps_this_turn = curr_step + num_steps_taken;
738        debug_assert!(num_steps_this_turn <= MAX_NUM_STEPS, "Number of steps should not be greater than MAX_NUM_STEPS");
739        let is_last_step = num_steps_this_turn >= MAX_NUM_STEPS;
740        let new_p1_turn_to_move = if is_last_step { !self.turn_state.p1_turn_to_move } else { self.turn_state.p1_turn_to_move };
741        let new_step = if is_last_step { 0 } else { num_steps_this_turn };
742
743        let new_hash = self
744            .get_initial_hash_of_move()
745            .update_piece_board(&self.turn_state.piece_board, &new_piece_board)
746            .update_step(0, new_step)
747            .update_player_to_move(self.turn_state.p1_turn_to_move, new_p1_turn_to_move);
748
749        let new_move_number = self.turn_state.move_number + if is_last_step && new_p1_turn_to_move { 1 } else { 0 };
750
751        let new_turn_state = if is_last_step {
752            let new_hash_history = if self.piece_trapped_this_turn() {
753                RepetitionCounts::new()
754            } else {
755                self.turn_state.hash_history.clone()
756            };
757
758            TurnState {
759                p1_turn_to_move: new_p1_turn_to_move,
760                move_number: new_move_number,
761                piece_board: new_piece_board.clone(),
762                hash: new_hash,
763                hash_history: new_hash_history.append(new_hash),
764            }
765        } else {
766            self.turn_state.clone()
767        };
768
769        let new_step_state = if is_last_step {
770            StepState::new(new_piece_board)
771        } else {
772            let mut new_steps = self.step_state.steps;
773            new_steps.extend_from_slice(steps);
774
775            let mut new_actions = self.step_state.actions.clone();
776            new_actions.push(action);
777
778            StepState {
779                steps: new_steps,
780                actions: new_actions,
781                piece_board: new_piece_board,
782                banned_piece_mask: Arc::new(AtomicU64::new(u64::MAX)),
783            }
784        };
785
786        PlayState {
787            turn_state: new_turn_state,
788            step_state: new_step_state,
789        }
790    }
791
792    fn piece_trapped_this_turn(&self) -> bool {
793        let mut piece_board = self.turn_state.piece_board.clone();
794        for (square, direction) in self.step_state.steps.iter() {
795            if piece_board.move_piece(*square, *direction) {
796                return true;
797            }
798        }
799
800        false
801    }
802
803    fn own_piece_trapped(&self, piece_board: &PieceBoard, action: &Action) -> bool {
804        let mut piece_board = piece_board.clone();
805        for (square, direction) in action.get_steps() {
806            piece_board.move_piece_update_board(square, direction);
807            let trapped_piece_bit = piece_board.get_trapped_piece_bits();
808
809            if trapped_piece_bit != 0 {
810                let curr_player_piece_mask = piece_board.get_curr_player_piece_mask(self.turn_state.p1_turn_to_move);
811
812                if trapped_piece_bit & curr_player_piece_mask != 0 {
813                    return true;
814                }
815            }
816
817            piece_board.remove_trapped_pieces();
818        }
819
820        false
821    }
822
823    fn is_valid_transposition(initial_turn_piece_board: &PieceBoard, p1_turn_to_move: bool, actions: &[&Action], candidate_action: Action, expected_piece_board: &PieceBoard) -> bool {
824        let mut rearranged_order_piece_board = initial_turn_piece_board.clone();
825        let candidate_action_steps = candidate_action.get_steps();
826        let (candidate_action_departing_square, candidate_action_departing_direction) = candidate_action_steps.first().unwrap();
827        let mut has_interposed = false;
828
829        for action in actions {
830            let is_valid = Self::is_action_valid(&rearranged_order_piece_board, p1_turn_to_move, action);
831
832            if !is_valid {
833                return false;
834            }
835
836            let steps = action.get_steps();
837            for (square, direction) in steps {
838                rearranged_order_piece_board.move_piece(square, direction);
839            }
840
841            if !has_interposed && matches!(action, Action::Move(_, _)) {
842                let landing_square = action.landing_square();
843                if landing_square == *candidate_action_departing_square {
844                    let is_inverse_direction = candidate_action_departing_direction.invert() == action.get_steps().last().unwrap().1;
845
846                    if is_inverse_direction {
847                        return false;
848                    }
849
850                    let is_valid = Self::is_action_valid(&rearranged_order_piece_board, p1_turn_to_move, &candidate_action);
851
852                    if !is_valid {
853                        return false;
854                    }
855
856                    for (square, direction) in candidate_action_steps {
857                        rearranged_order_piece_board.move_piece(square, direction);
858                    }
859
860                    has_interposed = true;
861                }
862            }
863        }
864
865        rearranged_order_piece_board == *expected_piece_board
866    }
867
868    fn update_and_verify_piece_board_with_actions(piece_board: &mut PieceBoard, p1_turn_to_move: bool, actions: &[Action]) -> bool {
869        for action in actions {
870            let is_valid = Self::is_action_valid(piece_board, p1_turn_to_move, action);
871
872            if !is_valid {
873                return false;
874            }
875
876            for (square, direction) in action.get_steps() {
877                piece_board.move_piece(square, direction);
878            }
879        }
880
881        true
882    }
883
884    fn get_banned_piece_mask(&self) -> u64 {
885        let mut banned_piece_mask = self.step_state.banned_piece_mask.load(Ordering::Relaxed);
886
887        if banned_piece_mask == u64::MAX {
888            banned_piece_mask = self.get_banned_piece_mask_inner();
889            self.step_state.banned_piece_mask.store(banned_piece_mask, Ordering::SeqCst);
890        }
891
892        banned_piece_mask
893    }
894
895    fn get_banned_piece_mask_inner(&self) -> u64 {
896        if self.get_step() == 0 {
897            return 0;
898        }
899
900        let p1_turn_to_move = self.turn_state.p1_turn_to_move;
901        let initial_turn_piece_board = &self.turn_state.piece_board;
902
903        let unban_pieces = |banned_piece_mask: u64, actions: &[Action], piece_board: &PieceBoard| -> u64 {
904            if banned_piece_mask == 0 {
905                return banned_piece_mask;
906            }
907
908            let mut banned_piece_mask = banned_piece_mask;
909
910            // Force current_step to 3 since we only need to test for actions of 1 step.
911            let current_step = 3;
912
913            Self::each_valid_curr_player_piece_moves(piece_board, !banned_piece_mask, p1_turn_to_move, current_step, |candidate_action| {
914                debug_assert!(candidate_action.get_steps().len() == 1, "Candidate actions should always be of 1 step.");
915
916                let candidate_action_steps = candidate_action.get_steps();
917                let candidate_action_departing_square = candidate_action_steps.first().unwrap().0;
918
919                if candidate_action_departing_square.as_bit_board() & banned_piece_mask == 0 {
920                    return false;
921                }
922
923                let expected_piece_board = {
924                    let mut expected_piece_board = initial_turn_piece_board.clone();
925                    let mut actions = Vec::from(actions);
926                    actions.push(candidate_action.clone());
927                    let is_valid = Self::update_and_verify_piece_board_with_actions(&mut expected_piece_board, p1_turn_to_move, &actions);
928
929                    if !is_valid {
930                        return false;
931                    }
932
933                    expected_piece_board
934                };
935
936                let min_actions = 1.max(actions.len() - 1);
937                let has_valid_transpostion = (min_actions..=actions.len())
938                    .flat_map(|c| actions.iter().permutations(c))
939                    .any(|actions_inner| Self::is_valid_transposition(initial_turn_piece_board, p1_turn_to_move, &actions_inner, candidate_action.clone(), &expected_piece_board));
940
941                if !has_valid_transpostion {
942                    banned_piece_mask &= !candidate_action_departing_square.as_bit_board();
943                }
944
945                banned_piece_mask == 0
946            });
947
948            banned_piece_mask
949        };
950
951        let mut piece_board = initial_turn_piece_board.clone();
952        let actions = &self.step_state.actions;
953        let mut banned_piece_mask = 0;
954
955        for (i, action) in actions.iter().enumerate() {
956            let latest_action_steps = action.get_steps();
957            let own_piece_trapped = self.own_piece_trapped(&piece_board, action);
958
959            for (square, direction) in latest_action_steps {
960                piece_board.move_piece(square, direction);
961            }
962
963            banned_piece_mask = unban_pieces(banned_piece_mask, &actions[..=i], &piece_board);
964            let is_move_action = matches!(action, Action::Move(_, _));
965
966            // Don't bother banning if action results in trapping of own piece. This allows for a shuffle of a piece as well as moving onto the trap square of the piece that was just trapped.
967            if !is_move_action {
968                continue;
969            }
970
971            // If a piece is trapped, clear it from the list of banned pieces since it no longer exists.
972            if own_piece_trapped {
973                let curr_player_piece_mask = piece_board.get_curr_player_piece_mask(p1_turn_to_move);
974                banned_piece_mask &= curr_player_piece_mask;
975            }
976
977            // Shortcut since banning a piece here always results in an unban since the piece can be moved back to its previous square prior to the drop.
978            if own_piece_trapped && latest_action_steps.len() <= 2 {
979                continue;
980            }
981
982            // We can use steps here since we can only get to this point if the action is of type move.
983            let landing_square_bit = action.landing_square().as_bit_board();
984
985            // Skip banning the piece if it was frozen.
986            let piece_is_frozen_after_move = piece_board.get_curr_player_frozen_pieces(p1_turn_to_move) & landing_square_bit != 0;
987
988            if piece_is_frozen_after_move {
989                continue;
990            }
991
992            banned_piece_mask |= landing_square_bit;
993        }
994
995        banned_piece_mask
996    }
997
998    fn get_dangling_pieces(curr_player_piece_mask: u64) -> u64 {
999        let up = shift_up!(curr_player_piece_mask);
1000        let right = shift_right!(curr_player_piece_mask);
1001        let down = shift_down!(curr_player_piece_mask);
1002        let left = shift_left!(curr_player_piece_mask);
1003        let mut set_once = 0;
1004        let mut set_mult = 0;
1005        for board in [up, right, down, left] {
1006            set_mult |= set_once & board;
1007            set_once ^= board;
1008        }
1009
1010        let dangling_piece_mask = curr_player_piece_mask & TRAP_MASK & !set_mult;
1011        shift_up!(dangling_piece_mask) | shift_right!(dangling_piece_mask) | shift_down!(dangling_piece_mask) | shift_left!(dangling_piece_mask)
1012    }
1013
1014    fn get_no_longer_supported_pieces(moving_pieces_board: u64, dangling_pieces: u64, supported_square_exclusions: &ExcludedSquaresWithDirections, path: &ArrayVec<[Direction; 4]>) -> u64 {
1015        let started_from_dangling_pos = shift_pieces_in_direction(shift_pieces_in_direction(dangling_pieces, path[0]), path[1]);
1016        let dropped_dangling_piece = moving_pieces_board & started_from_dangling_pos & dangling_pieces;
1017        dropped_dangling_piece & !get_exclusion_for_direction(path[0], supported_square_exclusions)
1018    }
1019
1020    fn hash(&self) -> Zobrist {
1021        let step_num = self.get_step();
1022        let hash = self.get_initial_hash_of_move();
1023
1024        if step_num == 0 {
1025            return hash;
1026        }
1027
1028        let turn_piece_board = &self.turn_state.piece_board;
1029        let curr_piece_board = self.get_piece_board();
1030        hash.update_piece_board(turn_piece_board, curr_piece_board).update_step(0, step_num)
1031    }
1032}
1033
1034impl PlaceState {
1035    fn take_action(&self, action: &Action) -> GameState {
1036        match action {
1037            Action::Place(square) => self.place(*square),
1038            _ => panic!("Provided action is not valid in the place state: {:?}", action),
1039        }
1040    }
1041
1042    fn valid_actions(&self) -> Vec<Action> {
1043        let empty_placement_bits = self.get_piece_board().empty_placements(self.p1_turn_to_move);
1044
1045        Square::from_multiple_bits(empty_placement_bits).into_iter().map(Action::Place).collect()
1046    }
1047
1048    const PIECES_TO_PLACE: [(Piece, u32); 5] = [(Piece::Elephant, 1u32), (Piece::Camel, 1), (Piece::Horse, 2), (Piece::Dog, 2), (Piece::Cat, 2)];
1049
1050    fn place(&self, square: Square) -> GameState {
1051        let mut piece_board = self.get_piece_board().clone();
1052
1053        let p1_turn_to_move = self.p1_turn_to_move;
1054        let piece_to_place = self.piece_to_place();
1055        piece_board.place(piece_to_place, square, p1_turn_to_move);
1056
1057        let last_piece_placed = piece_board.get_bits_for_piece(Piece::Cat, p1_turn_to_move).count_ones() == 2;
1058
1059        let mut new_hash = self.hash;
1060        if last_piece_placed {
1061            Self::fill_remaining_rabbits(&mut piece_board, p1_turn_to_move);
1062            new_hash = new_hash.fill_rabbits(&piece_board, p1_turn_to_move);
1063        }
1064
1065        let switch_players = last_piece_placed && p1_turn_to_move;
1066        let switch_phases = last_piece_placed && !p1_turn_to_move;
1067        let new_p1_turn_to_move = if switch_players {
1068            false
1069        } else if switch_phases {
1070            true
1071        } else {
1072            p1_turn_to_move
1073        };
1074
1075        let new_hash = new_hash.place_piece(piece_to_place, square, p1_turn_to_move, switch_players, switch_phases);
1076
1077        if switch_phases {
1078            GameState::PlayState(PlayState::initial(new_hash, piece_board))
1079        } else {
1080            GameState::PlaceState(PlaceState {
1081                p1_turn_to_move: new_p1_turn_to_move,
1082                piece_board: Box::new(piece_board),
1083                hash: new_hash,
1084            })
1085        }
1086    }
1087
1088    pub fn piece_to_place(&self) -> Piece {
1089        let piece_board = self.get_piece_board();
1090
1091        Self::PIECES_TO_PLACE
1092            .iter()
1093            .find(|(piece, count)| piece_board.get_bits_for_piece(*piece, self.p1_turn_to_move).count_ones() < *count)
1094            .expect("Expected available piece to place.")
1095            .0
1096    }
1097
1098    pub fn fill_remaining_rabbits(piece_board: &mut PieceBoard, p1_turn_to_move: bool) {
1099        let empty_squares = piece_board.empty_placements(p1_turn_to_move);
1100
1101        assert_eq!(empty_squares.count_ones(), 8, "Expected 8 squares for rabbits to be placed.");
1102
1103        piece_board.fill(Piece::Rabbit, empty_squares, p1_turn_to_move);
1104    }
1105
1106    pub fn get_piece_board(&self) -> &PieceBoard {
1107        &self.piece_board
1108    }
1109
1110    pub fn get_vertical_symmetry(&self) -> Self {
1111        let piece_board = Box::new(self.piece_board.get_vertical_symmetry());
1112        let hash = Zobrist::from_piece_board(&piece_board, self.p1_turn_to_move, Some(0));
1113
1114        Self {
1115            p1_turn_to_move: self.p1_turn_to_move,
1116            piece_board,
1117            hash,
1118        }
1119    }
1120}
1121
1122impl TurnState {
1123    fn get_vertical_symmetry(&self) -> Self {
1124        let piece_board = self.piece_board.get_vertical_symmetry();
1125        let hash = Zobrist::from_piece_board(&piece_board, self.p1_turn_to_move, Some(0));
1126
1127        Self {
1128            p1_turn_to_move: self.p1_turn_to_move,
1129            move_number: self.move_number,
1130            piece_board,
1131            hash,
1132            hash_history: RepetitionCounts::new().append(hash),
1133        }
1134    }
1135}
1136
1137impl StepState {
1138    fn get_vertical_symmetry(&self) -> Self {
1139        Self {
1140            steps: self.steps.iter().map(|(s, d)| (s.vertical_symmetry(), d.vertical_symmetry())).collect(),
1141            actions: self.actions.iter().map(|a| a.vertical_symmetry()).collect(),
1142            piece_board: self.piece_board.get_vertical_symmetry(),
1143            banned_piece_mask: Arc::new(AtomicU64::new(u64::MAX)),
1144        }
1145    }
1146}