use rustc_hash::FxHashMap;
use crate::{Action, Board, GameStatus, Team};
type PositionHash = u64;
const INSUFFICIENT_PROGRESS_THRESHOLD: u16 = 150;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Game {
board: Board,
position_counts: FxHashMap<PositionHash, u8>,
halfmove_clock: u16,
history: Vec<(Action, u16, bool)>,
}
const TYPICAL_GAME_LENGTH: usize = 100;
impl Game {
#[must_use]
pub fn new() -> Self {
let board = Board::new_default();
let mut game = Self {
board,
position_counts: FxHashMap::with_capacity_and_hasher(
TYPICAL_GAME_LENGTH,
Default::default(),
),
halfmove_clock: 0,
history: Vec::with_capacity(TYPICAL_GAME_LENGTH),
};
game.record_position();
game
}
#[must_use]
pub fn from_board(board: Board) -> Self {
let mut game = Self {
board,
position_counts: FxHashMap::with_capacity_and_hasher(
TYPICAL_GAME_LENGTH,
Default::default(),
),
halfmove_clock: 0,
history: Vec::with_capacity(TYPICAL_GAME_LENGTH),
};
game.record_position();
game
}
#[inline]
#[must_use]
pub const fn board(&self) -> &Board {
&self.board
}
#[inline]
#[must_use]
pub const fn turn(&self) -> Team {
self.board.turn
}
#[inline]
#[must_use]
pub const fn halfmove_clock(&self) -> u16 {
self.halfmove_clock
}
#[inline]
#[must_use]
pub fn move_count(&self) -> usize {
self.history.len()
}
#[inline]
#[must_use]
pub fn actions(&self) -> Vec<Action> {
self.board.actions()
}
#[must_use]
pub fn status(&self) -> GameStatus {
let basic_status = self.board.status();
if basic_status != GameStatus::InProgress {
return basic_status;
}
if self.is_threefold_repetition() {
return GameStatus::Draw;
}
if self.halfmove_clock >= INSUFFICIENT_PROGRESS_THRESHOLD {
return GameStatus::Draw;
}
GameStatus::InProgress
}
#[inline]
pub fn make_move(&mut self, action: &Action) {
let is_capture = self.is_capture_action(action);
let prev_halfmove = self.halfmove_clock;
self.board.apply_(action);
self.board.swap_turn_();
if is_capture {
self.halfmove_clock = 0;
} else {
self.halfmove_clock += 1;
}
self.record_position();
self.history.push((*action, prev_halfmove, is_capture));
}
#[inline]
pub fn undo_move(&mut self) -> bool {
if let Some((action, prev_halfmove, _)) = self.history.pop() {
self.decrement_position_count();
self.board.swap_turn_();
self.board.apply_(&action);
self.halfmove_clock = prev_halfmove;
true
} else {
false
}
}
#[inline]
#[must_use]
pub fn is_threefold_repetition(&self) -> bool {
let hash = self.position_hash();
self.position_counts.get(&hash).copied().unwrap_or(0) >= 3
}
#[inline]
#[must_use]
pub fn position_occurrence_count(&self) -> u8 {
let hash = self.position_hash();
self.position_counts.get(&hash).copied().unwrap_or(0)
}
pub fn clear_history(&mut self) {
self.position_counts.clear();
self.history.clear();
self.halfmove_clock = 0;
self.record_position();
}
pub fn perft(&mut self, depth: u64) -> u64 {
if depth == 0 {
return 1;
}
let actions = self.actions();
if actions.is_empty() {
return 1; }
let mut nodes = 0u64;
for action in &actions {
self.make_move(action);
nodes += self.perft(depth - 1);
self.undo_move();
}
nodes
}
#[inline]
fn position_hash(&self) -> PositionHash {
let mut hash = self.board.state.pieces[0];
hash ^= self.board.state.pieces[1].wrapping_mul(0x9e37_79b9_7f4a_7c15);
hash ^= self.board.state.kings.wrapping_mul(0x517c_c1b7_2722_0a95);
hash ^= (self.board.turn.to_usize() as u64).wrapping_mul(0x2545_f491_4f6c_dd1d);
hash
}
#[inline]
fn record_position(&mut self) {
let hash = self.position_hash();
*self.position_counts.entry(hash).or_insert(0) += 1;
}
#[inline]
fn decrement_position_count(&mut self) {
let hash = self.position_hash();
if let Some(count) = self.position_counts.get_mut(&hash) {
*count = count.saturating_sub(1);
if *count == 0 {
self.position_counts.remove(&hash);
}
}
}
#[inline]
fn is_capture_action(&self, action: &Action) -> bool {
let opponent_index = self.board.turn.opponent().to_usize();
action.delta.pieces[opponent_index] != 0
}
}
impl Default for Game {
fn default() -> Self {
Self::new()
}
}
impl From<Board> for Game {
fn from(board: Board) -> Self {
Self::from_board(board)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Square;
#[test]
fn new_game_starts_at_default_position() {
let game = Game::new();
assert_eq!(game.board, Board::new_default());
assert_eq!(game.turn(), Team::White);
assert_eq!(game.halfmove_clock(), 0);
assert_eq!(game.move_count(), 0);
}
#[test]
fn from_board_preserves_position() {
let board = Board::from_squares(
Team::Black,
&[Square::A2, Square::B2],
&[Square::G7, Square::H7],
&[],
);
let game = Game::from_board(board);
assert_eq!(game.board, board);
assert_eq!(game.turn(), Team::Black);
}
#[test]
fn make_move_updates_board() {
let mut game = Game::new();
let actions = game.actions();
assert!(!actions.is_empty());
let action = actions[0];
game.make_move(&action);
assert_eq!(game.turn(), Team::Black);
assert_eq!(game.move_count(), 1);
}
#[test]
fn undo_move_restores_board() {
let mut game = Game::new();
let original_board = game.board;
let actions = game.actions();
game.make_move(&actions[0]);
assert_ne!(game.board, original_board);
game.undo_move();
assert_eq!(game.board, original_board);
assert_eq!(game.move_count(), 0);
}
#[test]
fn halfmove_clock_increments_for_king_moves() {
let board = Board::from_squares(
Team::White,
&[Square::D4],
&[Square::E6],
&[Square::D4, Square::E6], );
let mut game = Game::from_board(board);
let actions = game.actions();
game.make_move(&actions[0]);
assert_eq!(game.halfmove_clock(), 1);
}
#[test]
fn halfmove_clock_resets_on_capture() {
let board = Board::from_squares(
Team::White,
&[Square::D4],
&[Square::D5, Square::H8], &[], );
let mut game = Game::from_board(board);
game.halfmove_clock = 10;
let actions = game.actions();
assert_eq!(actions.len(), 1); game.make_move(&actions[0]);
assert_eq!(game.halfmove_clock(), 0);
}
#[test]
fn threefold_repetition_detection() {
let board = Board::from_squares(
Team::White,
&[Square::A1, Square::B1],
&[Square::H7, Square::H8],
&[Square::A1, Square::B1, Square::H7, Square::H8],
);
let mut game = Game::from_board(board);
assert_eq!(game.position_occurrence_count(), 1);
assert!(!game.is_threefold_repetition());
let find_move = |g: &Game, from: Square, to: Square| -> Action {
g.actions()
.into_iter()
.find(|a| {
let from_mask = from.to_mask();
let to_mask = to.to_mask();
let friendly_idx = g.turn().to_usize();
let delta = a.delta.pieces[friendly_idx];
(delta & from_mask) != 0 && (delta & to_mask) != 0
})
.expect("Move not found")
};
game.make_move(&find_move(&game, Square::A1, Square::A2)); game.make_move(&find_move(&game, Square::H8, Square::G8)); game.make_move(&find_move(&game, Square::A2, Square::A1)); game.make_move(&find_move(&game, Square::G8, Square::H8)); assert_eq!(game.position_occurrence_count(), 2);
assert!(!game.is_threefold_repetition());
game.make_move(&find_move(&game, Square::A1, Square::A2));
game.make_move(&find_move(&game, Square::H8, Square::G8));
game.make_move(&find_move(&game, Square::A2, Square::A1));
game.make_move(&find_move(&game, Square::G8, Square::H8));
assert_eq!(game.position_occurrence_count(), 3);
assert!(game.is_threefold_repetition());
assert_eq!(game.status(), GameStatus::Draw);
}
#[test]
fn insufficient_progress_draw() {
let board = Board::from_squares(
Team::White,
&[Square::D4, Square::A1],
&[Square::E6, Square::H8],
&[Square::D4, Square::A1, Square::E6, Square::H8], );
let mut game = Game::from_board(board);
game.halfmove_clock = INSUFFICIENT_PROGRESS_THRESHOLD - 1;
assert_eq!(game.status(), GameStatus::InProgress);
game.halfmove_clock = INSUFFICIENT_PROGRESS_THRESHOLD;
assert_eq!(game.status(), GameStatus::Draw);
}
#[test]
fn status_checks_all_conditions() {
let board = Board::from_squares(Team::White, &[Square::A1], &[Square::H8], &[]);
let game = Game::from_board(board);
assert_eq!(game.status(), GameStatus::Draw);
}
#[test]
fn clear_history_resets_everything() {
let mut game = Game::new();
let actions = game.actions();
game.make_move(&actions[0]);
game.halfmove_clock = 50;
game.clear_history();
assert_eq!(game.halfmove_clock(), 0);
assert_eq!(game.move_count(), 0);
assert_eq!(game.position_occurrence_count(), 1);
}
#[test]
fn position_count_decrements_on_undo() {
let mut game = Game::new();
let initial_count = game.position_occurrence_count();
let actions = game.actions();
game.make_move(&actions[0]);
game.undo_move();
assert_eq!(game.position_occurrence_count(), initial_count);
}
#[test]
fn from_board_trait_impl() {
let board = Board::from_squares(Team::Black, &[Square::C3], &[Square::F6], &[Square::C3]);
let game: Game = board.into();
assert_eq!(game.turn(), Team::Black);
assert_eq!(game.halfmove_clock(), 0);
assert_eq!(game.position_occurrence_count(), 1);
}
#[test]
fn undo_move_returns_false_when_empty() {
let mut game = Game::new();
assert!(!game.undo_move());
}
#[test]
fn default_trait_impl() {
let game1 = Game::default();
let game2 = Game::new();
assert_eq!(game1, game2);
}
#[test]
fn halfmove_clock_does_not_reset_on_pawn_move() {
let board = Board::from_squares(
Team::White,
&[Square::D4, Square::A1], &[Square::H7, Square::H8],
&[Square::A1, Square::H7, Square::H8], );
let mut game = Game::from_board(board);
game.halfmove_clock = 10;
let pawn_move = game
.actions()
.into_iter()
.find(|a| {
let d4 = Square::D4.to_mask();
let d5 = Square::D5.to_mask();
(a.delta.pieces[0] & d4) != 0 && (a.delta.pieces[0] & d5) != 0
})
.expect("Pawn move not found");
game.make_move(&pawn_move);
assert_eq!(game.halfmove_clock(), 11);
}
#[test]
fn insufficient_progress_threshold_is_configurable() {
assert_eq!(INSUFFICIENT_PROGRESS_THRESHOLD, 150);
}
#[test]
fn perft_matches_board_perft() {
let board = Board::new_default();
let mut game = Game::new();
for depth in 0..=5 {
let board_result = board.perft(depth);
let game_result = game.perft(depth);
assert_eq!(
board_result, game_result,
"Game::perft({}) = {} != Board::perft({}) = {}",
depth, game_result, depth, board_result
);
}
}
#[test]
fn perft_state_unchanged_after_call() {
let mut game = Game::new();
let original_board = game.board;
let original_count = game.move_count();
game.perft(3);
assert_eq!(game.board, original_board);
assert_eq!(game.move_count(), original_count);
}
}