From b7b3c6c5b8620e513e837a99c0c501637339ce89 Mon Sep 17 00:00:00 2001 From: dogeystamp Date: Sat, 2 Nov 2024 16:05:04 -0400 Subject: [PATCH] fix: not going for checkmate engine will now prioritize closer mates rather than continuously going "ah i'll checkmate next move" then never checkkmating --- src/eval.rs | 28 ++++++------ src/search.rs | 124 ++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 103 insertions(+), 49 deletions(-) diff --git a/src/eval.rs b/src/eval.rs index 6854a9b..e5978a9 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -46,7 +46,7 @@ pub(crate) mod eval_score { /// Score from a given perspective (e.g. midgame, endgame). #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default, Debug)] pub struct EvalScore { - pub(crate) score: EvalInt, + pub score: EvalInt, } impl EvalScore { @@ -157,12 +157,12 @@ const fn make_pst(val: PstSide, base_val: EvalInt) -> PstPiece { pub const PST_MIDGAME: Pst = Pst([ // rook make_pst([ - 0, 0, 0, 0, 0, 0, 0, 0, // 8 + 1, 3, 2, 1, 4, 3, 2, 1, // 8 20, 20, 20, 20, 20, 20, 20, 20, // 7 - 0, 0, 0, 0, 0, 0, 0, 0, // 6 - 0, 0, 0, 0, 0, 0, 0, 0, // 5 - 0, 0, 0, 0, 0, 0, 0, 0, // 4 - 0, 0, 0, 0, 0, 0, 0, 0, // 3 + 1, 2, 3, 1, 2, 1, 2, 1, // 6 + -1, -2, 1, 2, 1, -1, 1, -1, // 5 + -1, -1, 2, -1, 1, -1, 2, 1, // 4 + 2, 1, 1, 0, 0, 0, 0, 0, // 3 0, 0, 0, 0, 0, 0, 0, 0, // 2 0, 0, 0, 10, 10, 5, 0, 0, // 1 // a b c d e f g h @@ -183,14 +183,14 @@ pub const PST_MIDGAME: Pst = Pst([ // knight make_pst([ - 0, 0, 0, 0, 0, 0, 0, 0, // 8 - 0, 0, 0, 0, 0, 0, 0, 0, // 7 - 0, 0, 0, 0, 0, 0, 0, 0, // 6 - 0, 0, 0, 10, 10, 0, 0, 0, // 5 - 0, 0, 0, 10, 10, 0, 0, 0, // 4 - 0, 0, 10, 0, 0, 10, 0, 0, // 3 - 0, 0, 0, 0, 0, 0, 0, 0, // 2 - 0, 0, 0, 0, 0, 0, 0, 0, // 1 + -5, -5, -5, -5, -5, -5, -5, -5, // 8 + -5, 0, 0, 0, 0, 0, 0, -5, // 7 + -5, 1, 0, 0, 0, 0, 0, -5, // 6 + -5, 2, 0, 10, 10, 0, 0, -5, // 5 + -5, 0, 1, 10, 10, 0, 0, -5, // 4 + -5, 2, 10, 0, 0, 10, 0, -5, // 3 + -5, 1, 0, 0, 0, 0, 0, -5, // 2 + -5, -5, -5, -5, -5, -5, -5, -5, // 1 // a b c d e f g h ], 300), diff --git a/src/search.rs b/src/search.rs index 0168f57..e54d0c8 100644 --- a/src/search.rs +++ b/src/search.rs @@ -34,7 +34,64 @@ mod test_eval_int { } } -/// Search the game tree to find the absolute (positive good) eval for the current player. +/// Eval in the context of search. +#[derive(PartialEq, Eq, Clone, Copy)] +enum SearchEval { + /// Mate in |n| - 1 half moves, negative for own mate. + Checkmate(i8), + /// Centipawn score. + Centipawns(EvalInt), +} + +impl SearchEval { + /// Flip side, and increment the "mate in n" counter. + fn increment(self) -> Self { + match self { + SearchEval::Checkmate(n) => { + debug_assert_ne!(n, 0); + if n < 0 { + Self::Checkmate(-(n - 1)) + } else { + Self::Checkmate(-(n + 1)) + } + } + SearchEval::Centipawns(eval) => Self::Centipawns(-eval), + } + } +} + +impl From for EvalInt { + fn from(value: SearchEval) -> Self { + match value { + SearchEval::Checkmate(n) => { + debug_assert_ne!(n, 0); + if n < 0 { + EVAL_WORST + EvalInt::from(n) + } else { + EVAL_BEST - EvalInt::from(n) + } + } + SearchEval::Centipawns(eval) => eval, + } + } +} + +impl Ord for SearchEval { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let e1 = EvalInt::from(*self); + let e2 = EvalInt::from(*other); + e1.cmp(&e2) + } +} + +impl PartialOrd for SearchEval { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// Search the game tree to find the absolute (positive good) move and corresponding eval for the +/// current player. /// /// # Arguments /// @@ -42,7 +99,16 @@ mod test_eval_int { /// * depth: how deep to analyze the game tree. /// * alpha: best score (absolute, from current player perspective) guaranteed for current player. /// * beta: best score (absolute, from current player perspective) guaranteed for other player. -fn minmax(board: &mut Board, depth: usize, alpha: Option, beta: Option) -> EvalInt { +/// +/// # Returns +/// +/// The best move, and its corresponding absolute eval for the current player. +fn minmax( + board: &mut Board, + depth: usize, + alpha: Option, + beta: Option, +) -> (Option, SearchEval) { // default to worst, then gradually improve let mut alpha = alpha.unwrap_or(EVAL_WORST); // our best is their worst @@ -51,31 +117,36 @@ fn minmax(board: &mut Board, depth: usize, alpha: Option, beta: Option< if depth == 0 { let eval = board.eval(); match board.turn { - crate::Color::White => return eval, - crate::Color::Black => return -eval, + crate::Color::White => return (None, SearchEval::Centipawns(eval)), + crate::Color::Black => return (None, SearchEval::Centipawns(-eval)), } } let mvs: Vec<_> = board.gen_moves().into_iter().collect(); - let mut abs_best = EVAL_WORST; + let mut abs_best = SearchEval::Centipawns(EVAL_WORST); + let mut best_move: Option = None; if mvs.is_empty() { if board.is_check(board.turn) { - return EVAL_WORST; + return (None, SearchEval::Checkmate(-1)); } else { // stalemate - return 0; + return (None, SearchEval::Centipawns(0)); } } for mv in mvs { let anti_mv = mv.make(board); - let abs_score = -minmax(board, depth - 1, Some(-beta), Some(-alpha)); - abs_best = max(abs_best, abs_score); - alpha = max(alpha, abs_best); + let (_best_continuation, score) = minmax(board, depth - 1, Some(-beta), Some(-alpha)); + let abs_score = score.increment(); + if abs_score >= abs_best { + abs_best = abs_score; + best_move = Some(mv); + } + alpha = max(alpha, abs_best.into()); anti_mv.unmake(board); - if alpha >= beta { + if alpha >= beta { // alpha-beta prune. // // Beta represents the best eval that the other player can get in sibling branches @@ -86,32 +157,15 @@ fn minmax(board: &mut Board, depth: usize, alpha: Option, beta: Option< } } - abs_best -} - -/// Find the best move for a position (internal interface). -fn search(board: &mut Board) -> Option { - const DEPTH: usize = 4; - let mvs: Vec<_> = board.gen_moves().into_iter().collect(); - - // absolute eval value - let mut best_eval = EVAL_WORST; - let mut best_mv: Option = None; - - for mv in mvs { - let anti_mv = mv.make(board); - let abs_eval = -minmax(board, DEPTH, None, None); - if abs_eval >= best_eval { - best_eval = abs_eval; - best_mv = Some(mv); - } - anti_mv.unmake(board); - } - - best_mv + (best_move, abs_best) } /// Find the best move. pub fn best_move(board: &mut Board) -> Option { - search(board) + let (mv, eval) = minmax(board, 5, None, None); + match eval { + SearchEval::Checkmate(n) => println!("info score mate {}", n / 2), + SearchEval::Centipawns(eval) => println!("info score cp {eval}"), + } + mv }