diff options
author | eug-vs <eugene@eug-vs.xyz> | 2023-02-21 17:49:51 +0300 |
---|---|---|
committer | eug-vs <eugene@eug-vs.xyz> | 2023-02-23 14:01:03 +0300 |
commit | f60c573ba71207c18a28413e3940a4e21b07c73f (patch) | |
tree | 3e50e9ea6cd0129414db92cd50805ebeb65a4676 /src/grossmeister/search.rs | |
parent | 69f3c48fb99d96f3fbc4ab49f5fb6d1d8e90e270 (diff) | |
download | chessnost-f60c573ba71207c18a28413e3940a4e21b07c73f.tar.gz |
refactor: create grossmeister module
Diffstat (limited to 'src/grossmeister/search.rs')
-rw-r--r-- | src/grossmeister/search.rs | 426 |
1 files changed, 426 insertions, 0 deletions
diff --git a/src/grossmeister/search.rs b/src/grossmeister/search.rs new file mode 100644 index 0000000..57ce672 --- /dev/null +++ b/src/grossmeister/search.rs @@ -0,0 +1,426 @@ +use std::{time::{Instant, Duration}, f32::INFINITY}; + +use crate::{moves::{Move, MoveKind}, board::io::IO}; + +use super::{Grossmeister, ttable::{NodeType, TTABLE_SIZE, TranspositionTableItem}}; + +const VALUE_WIN: f32 = 20_000.0; + +#[derive(Debug, Default, PartialEq)] +pub struct PerftResult { + leaf_nodes: u64, + captures: u64, + en_passants: u64, + castles: u64, + checks: u64, +} + +impl Grossmeister { + pub fn negamax_search(&mut self, mut alpha: f32, beta: f32, depth_left: u8, parent_killers: &mut Vec<Move>, deadline: Instant) -> (f32, Vec<Move>) { + let mut principal_variation = Vec::new(); + let mut killer_moves = Vec::new(); + let color = self.board.color(); + + match self.transposition() { + Some(transposition) => { + if transposition.depth == depth_left { + match transposition.node_type { + NodeType::PV => { // PV-nodes have exact score + principal_variation.push(transposition.mov); + return (transposition.score, principal_variation); + } + NodeType::Cut => { + if transposition.score >= beta { + principal_variation.push(transposition.mov); + return (beta, principal_variation); + } + } + NodeType::All => { + if transposition.score <= alpha { + principal_variation.push(transposition.mov); + return (alpha, principal_variation); + } + } + } + } + } + None => {}, + } + + if depth_left == 0 { + return (self.quiscence(alpha, beta), principal_variation); + } + + let mut moves = self.generate_pseudolegal_moves(); + moves = self.order_moves(moves, parent_killers.to_vec()); + + let mut should_pv_search = true; + let mut legal_move_found = false; + for mov in moves { + let ep_target_before = self.board.ep_target.clone(); + let castling_rights_before = self.board.castling_rights.clone(); + let hash_before = self.board.hash.clone(); + let captured_piece = self.board.make_move(mov); + + if !self.board.is_king_in_check(color) { + legal_move_found = true; + + let (mut score, mut subtree_pv) = if should_pv_search { + // Assume PV-node is high in the list (if move ordering is good) + self.negamax_search(-beta, -alpha, depth_left - 1, &mut killer_moves, deadline) + } else { + // After we have PV-node (that raised alpha) all other nodes will be searched + // with zero-window first to confirm this assumption + // TODO: changing 0.001 -> 0.0001 leads to a weird bug + let result = self.negamax_search(-(alpha + 0.001), -alpha, depth_left - 1, &mut killer_moves, deadline); + // In case some of the other nodes raises alpha, then it's true PV node now, + // let's research with full window to find its exact value + if -result.0 > alpha { + self.negamax_search(-beta, -alpha, depth_left - 1, &mut killer_moves, deadline) + } else { + result + } + }; + score *= -1.; + self.board.unmake_move(mov, captured_piece, ep_target_before, castling_rights_before, hash_before); + + if score >= beta { + self.transposition_table[(self.board.hash % TTABLE_SIZE) as usize] = Some(TranspositionTableItem { + hash: self.board.hash, + mov, + depth: depth_left, // TODO: should be actual depth searched + node_type: NodeType::Cut, + score, + }); + + if mov.kind == MoveKind::Quiet { + match parent_killers.iter().find(|m| **m == mov) { + None => parent_killers.push(mov), + Some(..) => {}, + } + } + + return (beta, principal_variation); + } + if score > alpha { + alpha = score; + should_pv_search = false; // Once we have PV-node we can start zero-window searching + principal_variation = Vec::with_capacity(depth_left as usize); + principal_variation.push(mov); + principal_variation.append(&mut subtree_pv); + + self.transposition_table[(self.board.hash % TTABLE_SIZE) as usize] = Some(TranspositionTableItem { + hash: self.board.hash, + mov, + depth: depth_left, // TODO: should be actual depth searched + node_type: NodeType::PV, + score, + }); + } else if self.transposition().is_none() { + self.transposition_table[(self.board.hash % TTABLE_SIZE) as usize] = Some(TranspositionTableItem { + hash: self.board.hash, + mov, + depth: depth_left, // TODO: should be actual depth searched + node_type: NodeType::All, + score, + }); + } + } else { + self.board.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 { + break; + } + } + + if !legal_move_found { + if self.board.is_king_in_check(color) { + return (-VALUE_WIN, principal_variation); + } + } + + (alpha, principal_variation) + } + + pub fn quiscence(&mut self, mut alpha: f32, beta: f32) -> f32 { + let color = self.board.color(); + let mut moves = self.generate_pseudolegal_moves(); + moves = self.order_moves(moves, Vec::new()); + + if !self.board.is_king_in_check(color) { + // If we are not in check, we can evaluate stand pat + let stand_pat = self.evaluate(); + + if stand_pat >= beta { + return beta; + } + if alpha < stand_pat { + alpha = stand_pat; + } + + // If we are not in check, we can only search tactical moves + moves = moves + .iter() + .filter(|m| m.is_tactical()) + .map(|m| *m) + .collect() + } + + let mut legal_move_found = false; + for mov in moves { + let ep_target_before = self.board.ep_target.clone(); + let castling_rights_before = self.board.castling_rights.clone(); + let hash_before = self.board.hash.clone(); + let captured_piece = self.board.make_move(mov); + + if !self.board.is_king_in_check(color) { + legal_move_found = true; + let evaluation = -self.quiscence(-beta, -alpha); + self.board.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.board.unmake_move(mov, captured_piece, ep_target_before, castling_rights_before, hash_before); + } + } + + if !legal_move_found { + if self.board.is_king_in_check(color) { + return -VALUE_WIN + } + } + + alpha + } + + pub fn iterative_deepening(&mut self, max_depth: u8, duration: Duration, print: bool) -> (f32, Vec<Move>) { + 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.25; + let mut gradual_widening_counter = 0; + let mut root_killers: Vec<Move> = Vec::new(); + + + while depth <= max_depth { + if print { + println!("\nSearching depth({}) in the window {:?}", depth, (alpha, beta)); + } + let search_result = self.negamax_search(alpha, beta, depth, &mut root_killers, deadline); + + if search_result.0.abs() >= VALUE_WIN { + return search_result + } + + if Instant::now() > deadline { + if print { + println!("Aborting..."); + } + break; + } + + if print { + println!("Finished depth({}) {:?} [{:?} left]", depth, search_result, deadline - Instant::now()); + } + + if search_result.0 <= alpha { // Alpha-cutoff + if print { + println!("Alpha cutoff {} <= {:?}", search_result.0, (alpha, beta)); + } + gradual_widening_counter += 1; + beta = alpha + window_size * 0.1; + alpha = search_result.0 - window_size * 2.0f32.powi(gradual_widening_counter); + continue; + } + if search_result.0 >= beta { // Beta-cutoff + if print { + println!("Beta cutoff {:?} <= {}", (alpha, beta), search_result.0); + } + gradual_widening_counter += 1; + alpha = beta - window_size * 0.1; + beta = search_result.0 + window_size * 2.0f32.powi(gradual_widening_counter); + continue; + } + + if search_result.1.len() > 0 { + depth += 1; + gradual_widening_counter = 0; + alpha = search_result.0 - window_size; + beta = search_result.0 + window_size; + } else { + panic!("Why the fuck no moves?"); + } + + result = Some(search_result); + } + + match result { + Some(r) => return r, + None => panic!("Could not find a move in time"), + } + } + + 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.board.color(); + + let moves = self.generate_pseudolegal_moves(); + + 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.board.ep_target.clone(); + let castling_rights_before = self.board.castling_rights.clone(); + let hash_before = self.board.hash.clone(); + let captured_piece = self.board.make_move(mov); + // King can not be in check after our own move + if !self.board.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.board.is_king_in_check(color.flip()) { + result.checks += 1; + } + } + + if print { + println!("{:?}", mov); + self.board.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.board.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 + } +} + + +#[cfg(test)] +mod tests { + use std::time::Duration; + use crate::{board::{Board, io::IO}, square::Square, moves::{Move, MoveKind}, grossmeister::{Grossmeister, search::PerftResult}}; + use super::VALUE_WIN; + + #[test] + fn perft() { + let board = Board::new(); + let mut gm = Grossmeister::new(board); + + assert_eq!(gm.perft(0, false), PerftResult { leaf_nodes: 1, captures: 0, en_passants: 0, castles: 0 , checks: 0 }); + assert_eq!(gm.perft(1, false), PerftResult { leaf_nodes: 20, captures: 0, en_passants: 0, castles: 0 , checks: 0 }); + assert_eq!(gm.perft(2, false), PerftResult { leaf_nodes: 400, captures: 0, en_passants: 0, castles: 0 , checks: 0 }); + assert_eq!(gm.perft(3, false), PerftResult { leaf_nodes: 8902, captures: 34, en_passants: 0, castles: 0 , checks: 12 }); + assert_eq!(gm.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 board = Board::from_FEN(fen); + let mut gm = Grossmeister::new(board); + + assert_eq!(gm.perft(0, false), PerftResult { leaf_nodes: 1, captures: 0, en_passants: 0, castles: 0 , checks: 0 }); + assert_eq!(gm.perft(1, false), PerftResult { leaf_nodes: 48, captures: 8, en_passants: 0, castles: 2 , checks: 0 }); + assert_eq!(gm.perft(2, false), PerftResult { leaf_nodes: 2039, captures: 351, en_passants: 1, castles: 91 , checks: 3 }); + assert_eq!(gm.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 endgame_perft() { + let fen = String::from("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - "); + let board = Board::from_FEN(fen); + let mut gm = Grossmeister::new(board); + + assert_eq!(gm.perft(1, false), PerftResult { leaf_nodes: 14, captures: 1, en_passants: 0, castles: 0 , checks: 2 }); + assert_eq!(gm.perft(2, false), PerftResult { leaf_nodes: 191, captures: 14, en_passants: 0, castles: 0 , checks: 10 }); + // assert_eq!(board.perft(3, false), PerftResult { leaf_nodes: 2812, captures: 209, en_passants: 2, castles: 0 , checks: 267 }); + } + + #[test] + fn checkmate() { + let fen = String::from("2kr1b1r/pp1npppp/2p1bn2/7q/5B2/2NB1Q1P/PPP1N1P1/2KR3R w - - 0 1"); + let board = Board::from_FEN(fen); + let mut gm = Grossmeister::new(board); + let (score, pv) = gm.iterative_deepening(8, Duration::from_secs(15), true); + + assert_eq!(score, VALUE_WIN); + assert_eq!(pv, vec![ + Move { source: Square::F3, target: Square::C6, kind: MoveKind::Capture }, + Move { source: Square::B7, target: Square::C6, kind: MoveKind::Capture }, + Move { source: Square::D3, target: Square::A6, kind: MoveKind::Quiet }, + ]); + } + + #[test] + fn stupid_knight_sac() { + let fen = String::from("r3k1r1/pp3ppp/1q6/2ppPn2/6P1/1PPP1P2/P1N3KP/R2QR3 b - - 0 18"); + let mut board = Board::from_FEN(fen); + board.ply += 1; // TODO: remove me when FEN parsing includes side to move + + let mut gm = Grossmeister::new(board); + let (_, pv) = gm.iterative_deepening(6, Duration::from_secs(60), true); + assert_eq!( + pv[0], + Move { source: Square::F5, target: Square::H4, kind: MoveKind::Quiet }, + "You should save this poor knight from danger!" + ); + } + + #[test] + fn weird_bishop_sac() { + let fen = String::from("r1b1k1nr/p4pp1/1pp1p3/4n2p/1b1qP3/1B1P3N/PPPBQPPP/RN2K2R w KQkq - 7 10"); + let board = Board::from_FEN(fen); + + let mut gm = Grossmeister::new(board); + let (_, pv) = gm.iterative_deepening(5, Duration::from_secs(60), true); + assert_eq!( + pv[0], + Move { source: Square::C2, target: Square::C3, kind: MoveKind::Quiet }, + "You should fork this bastard!" + ); + } +} |