use std::{time::{Instant, Duration}, f32::INFINITY}; use crate::{bitboard::pop_count, board::*}; use super::ttable::{NodeType, TranspositionTableItem}; #[derive(Debug, Default, PartialEq)] pub struct PerftResult { leaf_nodes: u64, captures: u64, en_passants: u64, castles: u64, checks: u64, } impl Board { pub fn perft(&mut self, depth: u8, print: bool) -> PerftResult { let mut result = PerftResult::default(); if depth == 0 { result.leaf_nodes = 1; return result; } let color = self.color(); let moves = self.generate_pseudolegal_moves(color); if print { println!("Running perft for depth {}. Color to move is {:?}\n{} moves available", depth, color, moves.len()); println!("{} moves available", moves.len()); } for mov in moves { let ep_target_before = self.ep_target.clone(); let castling_rights_before = self.castling_rights.clone(); let hash_before = self.hash.clone(); let captured_piece = self.make_move(mov); // King can not be in check after our own move if !self.is_king_in_check(color) { if depth == 1 { match mov.kind { MoveKind::Capture => { result.captures += 1; } MoveKind::EnPassant => { result.en_passants += 1; result.captures += 1; } MoveKind::Castle => { result.castles += 1; } _ => {} } if self.is_king_in_check(color.flip()) { result.checks += 1; } } if print { println!("{:?}", mov); self.print(); } let subtree_result = self.perft(depth - 1, print); result.leaf_nodes += subtree_result.leaf_nodes; result.captures += subtree_result.captures; result.checks += subtree_result.checks; result.castles += subtree_result.castles; result.en_passants += subtree_result.en_passants; } self.unmake_move(mov, captured_piece, ep_target_before, castling_rights_before, hash_before); } if print { println!("Found {} leaf nodes in this subtree (depth {})", result.leaf_nodes, depth); } result } /// Compute material advantage relative to the current player pub fn material_advantage(&self) -> f32 { let mut eval = 0f32; for (piece_index, bitboard) in self.pieces.iter().enumerate() { let piece_type = PieceType::from(piece_index); let sign = if Color::from_piece(piece_type) == self.color() { 1. } else { -1. }; eval += sign * match piece_type { PieceType::Pawn => { serialize_bitboard(*bitboard).iter().fold(0., |acc, square| { acc + match (*square).rank() { 6 => 3., 5 => 2., _ => 1., } }) } PieceType::PawnBlack => { serialize_bitboard(*bitboard).iter().fold(0., |acc, square| { acc + match (*square).rank() { 1 => 3., 2 => 2., _ => 1., } }) } _ => { piece_type.static_eval() * pop_count(*bitboard) as f32 } }; } eval } /// Evaluate a position relative to the current player pub fn evaluate(&self, precomputed_mobility: Option) -> f32 { let opponent_mobility = self.mobility(self.color().flip()); let player_mobility = match precomputed_mobility { Some(m) => m, None => self.mobility(self.color()), }; let mobility_advantage = player_mobility - opponent_mobility as f32; let material_advantage = self.material_advantage(); material_advantage + 0.1 * mobility_advantage } /// Evaluate move for move ordering, prioritizing efficient captures /// where low-value pieces capture high-value pieces fn eval_move(&self, m: Move) -> f32 { let [source_eval, target_eval] = [m.source, m.target] .map(|sq| self.piece_by_square(sq)) .map(|p| { match p { Some(p) => p.static_eval(), None => 0., } }); 2. * target_eval - source_eval } pub fn negamax_search(&mut self, mut alpha: f32, beta: f32, depth_left: u8, deadline: Instant) -> (f32, Vec) { let mut principal_variation = Vec::new(); let color = self.color(); let mut moves = self.generate_pseudolegal_moves(color); moves.sort_unstable_by(|a, b| { let a_eval = self.eval_move(*a); let b_eval = self.eval_move(*b); a_eval.total_cmp(&b_eval).reverse() }); match self.transposition_table[(self.hash % TTABLE_SIZE) as usize] { Some(item) => { if item.hash == self.hash { moves.insert(0, item.best_move); } } None => {}, } if depth_left == 0 { return (self.quiscence(alpha, beta), principal_variation); } for mov in moves { let ep_target_before = self.ep_target.clone(); let castling_rights_before = self.castling_rights.clone(); let hash_before = self.hash.clone(); let captured_piece = self.make_move(mov); if !self.is_king_in_check(color) { let (mut score, mut subtree_pv) = self.negamax_search(-beta, -alpha, depth_left - 1, deadline); score *= -1.; self.unmake_move(mov, captured_piece, ep_target_before, castling_rights_before, hash_before); if score >= beta { self.transposition_table[(self.hash % TTABLE_SIZE) as usize] = Some(TranspositionTableItem { hash: self.hash, best_move: mov, depth: depth_left, // TODO: should be actual depth searched node_type: NodeType::Cut, score, }); return (beta, principal_variation); } if score > alpha { alpha = score; principal_variation = Vec::with_capacity(depth_left as usize); principal_variation.push(mov); principal_variation.append(&mut subtree_pv); self.transposition_table[(self.hash % TTABLE_SIZE) as usize] = Some(TranspositionTableItem { hash: self.hash, best_move: mov, depth: depth_left, // TODO: should be actual depth searched node_type: NodeType::PV, score, }); } else { self.transposition_table[(self.hash % TTABLE_SIZE) as usize] = Some(TranspositionTableItem { hash: self.hash, best_move: mov, depth: depth_left, // TODO: should be actual depth searched node_type: NodeType::All, score, }); } } else { self.unmake_move(mov, captured_piece, ep_target_before, castling_rights_before, hash_before); } // Could not finish in time, return what we have so far if Instant::now() > deadline { println!("Returning early!"); return (alpha, principal_variation) } } (alpha, principal_variation) } pub fn quiscence(&mut self, mut alpha: f32, beta: f32) -> f32 { let color = self.color(); let mut moves = self.generate_pseudolegal_moves(color); moves.sort_unstable_by(|a, b| { let a_eval = self.eval_move(*a); let b_eval = self.eval_move(*b); a_eval.total_cmp(&b_eval).reverse() }); match self.transposition_table[(self.hash % TTABLE_SIZE) as usize] { Some(item) => { if item.hash == self.hash { moves.insert(0, item.best_move); } } None => {}, } let stand_pat = self.evaluate(Some(moves.len() as f32)); if stand_pat >= beta { return beta; } if alpha < stand_pat { alpha = stand_pat; } let tactical_moves = moves.iter().filter(|m| m.kind == MoveKind::Capture || m.kind == MoveKind::EnPassant); for mov in tactical_moves { let ep_target_before = self.ep_target.clone(); let castling_rights_before = self.castling_rights.clone(); let hash_before = self.hash.clone(); let captured_piece = self.make_move(*mov); if !self.is_king_in_check(color) { let evaluation = -self.quiscence(-beta, -alpha); self.unmake_move(*mov, captured_piece, ep_target_before, castling_rights_before, hash_before); if evaluation >= beta { return beta; // Fail-hard beta-cutoff } if evaluation > alpha { alpha = evaluation; } } else { self.unmake_move(*mov, captured_piece, ep_target_before, castling_rights_before, hash_before); } } alpha } pub fn iterative_deepening(&mut self, duration: Duration) -> (f32, Vec) { let start = Instant::now(); let deadline = start + duration; let mut result = None; let mut depth = 1; let mut alpha = -INFINITY; let mut beta = INFINITY; let window_size = 0.5; loop { let search_result = self.negamax_search(alpha, beta, depth, deadline); println!("Finished depth({}) {:?} [{:?} left]", depth, search_result, deadline - Instant::now()); if Instant::now() > deadline { match result { Some(r) => return r, None => panic!("Could not find a move in time"), } } if search_result.1.len() > 0 { depth += 1; alpha = search_result.0 - window_size; beta = search_result.0 + window_size; } else if search_result.0 <= alpha { // Alpha-cutoff println!("Alpha cutoff {} <= {:?}", search_result.0, (alpha, beta)); alpha = search_result.0 - window_size; continue; } else if search_result.0 >= beta { // Beta-cutoff println!("Beta cutoff {:?} <= {}", (alpha, beta), search_result.0); beta = search_result.0 + window_size; continue; } else { panic!("Can this ever be possible? (probably not)"); } result = Some(search_result); } } } #[cfg(test)] mod tests { use crate::board::{Board, engine::PerftResult}; #[test] fn perft() { let mut board = Board::new(); assert_eq!(board.perft(0, false), PerftResult { leaf_nodes: 1, captures: 0, en_passants: 0, castles: 0 , checks: 0 }); assert_eq!(board.perft(1, false), PerftResult { leaf_nodes: 20, captures: 0, en_passants: 0, castles: 0 , checks: 0 }); assert_eq!(board.perft(2, false), PerftResult { leaf_nodes: 400, captures: 0, en_passants: 0, castles: 0 , checks: 0 }); assert_eq!(board.perft(3, false), PerftResult { leaf_nodes: 8902, captures: 34, en_passants: 0, castles: 0 , checks: 12 }); assert_eq!(board.perft(4, false), PerftResult { leaf_nodes: 197281, captures: 1576, en_passants: 0, castles: 0 , checks: 469 }); // assert_eq!(board.perft(5, false), PerftResult { leaf_nodes: 4865609, captures: 82719, en_passants: 258, castles: 0, checks: 27351 }); } #[test] fn position_perft() { let fen = String::from("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - "); let mut board = Board::from_FEN(fen); assert_eq!(board.perft(0, false), PerftResult { leaf_nodes: 1, captures: 0, en_passants: 0, castles: 0 , checks: 0 }); assert_eq!(board.perft(1, false), PerftResult { leaf_nodes: 48, captures: 8, en_passants: 0, castles: 2 , checks: 0 }); assert_eq!(board.perft(2, false), PerftResult { leaf_nodes: 2039, captures: 351, en_passants: 1, castles: 91 , checks: 3 }); assert_eq!(board.perft(3, false), PerftResult { leaf_nodes: 97862, captures: 17102, en_passants: 45, castles: 3162, checks: 993 }); // assert_eq!(board.perft(4, false), PerftResult { leaf_nodes: 4085603, captures: 757163, en_passants: 1929, castles: 128013, checks: 25523 }); } #[test] fn material_advantage() { let board = Board::new(); assert_eq!(board.material_advantage(), 0.0, "Material advantage should be 0 at starting position"); } }