From 0591f29c66ca7be4bfb8a3b190ccb229fb272716 Mon Sep 17 00:00:00 2001 From: dogeystamp Date: Mon, 4 Nov 2024 11:41:30 -0500 Subject: [PATCH] feat: better endgame heuristics it may or may not be able to deliver checkmate --- src/bin/engine.rs | 11 +++++++ src/eval.rs | 75 ++++++++++++++++++++++++++++++++++++----------- src/lib.rs | 39 ++++++++++++++++++++---- 3 files changed, 103 insertions(+), 22 deletions(-) diff --git a/src/bin/engine.rs b/src/bin/engine.rs index fb33417..288811c 100644 --- a/src/bin/engine.rs +++ b/src/bin/engine.rs @@ -15,6 +15,7 @@ Copyright © 2024 dogeystamp use chess_inator::fen::FromFen; use chess_inator::movegen::{FromUCIAlgebraic, Move, ToUCIAlgebraic}; use chess_inator::search::{best_line, SearchEval}; +use chess_inator::eval::{eval_metrics}; use chess_inator::Board; use std::io; @@ -108,6 +109,12 @@ fn cmd_go(mut _tokens: std::str::SplitWhitespace<'_>, board: &mut Board) { } } +/// Print static evaluation of the position. +fn cmd_eval(mut _tokens: std::str::SplitWhitespace<'_>, board: &mut Board) { + let res = eval_metrics(board); + println!("STATIC EVAL (negative black, positive white):\n- pst: {}\n- king distance: {} ({} distance)\n- phase: {}\n- total: {}", res.pst_eval, res.king_distance_eval, res.king_distance, res.phase, res.total_eval); +} + fn main() { let stdin = io::stdin(); @@ -137,6 +144,10 @@ fn main() { "go" => { cmd_go(tokens, &mut board); } + // non-standard command. + "eval" => { + cmd_eval(tokens, &mut board); + } _ => ignore!(), } diff --git a/src/eval.rs b/src/eval.rs index 8936407..b7f115b 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -14,7 +14,7 @@ Copyright © 2024 dogeystamp //! Position evaluation. use crate::{Board, Color, Piece, Square, N_COLORS, N_PIECES, N_SQUARES}; -use core::cmp::max; +use core::cmp::{max, min}; use core::ops::Index; /// Signed centipawn type. @@ -306,14 +306,14 @@ pub const PST_ENDGAME: Pst = Pst([ // king make_pst([ - -50, -50, -50, -50, -50, -50, -50, -50, // 8 - -50, -10, -10, -10, -10, -10, -10, -50, // 7 - -50, -10, 0, 0, 0, 0, -10, -50, // 6 - -50, -10, 0, 4, 4, 0, -10, -50, // 5 - -50, -10, 0, 4, 4, 0, -10, -50, // 4 - -50, -10, 0, 0, 0, 0, -10, -50, // 3 - -50, -10, -10, -10, -10, -10, -10, -50, // 2 - -50, -50, -50, -50, -50, -50, -50, -50, // 1 + -100,-100, -90, -70, -70, -90,-100,-100, // 8 + -100, -20, -15, -13, -13, -15, -20,-100, // 7 + -90, -15, -5, -5, -5, -5, -15, -90, // 6 + -70, -13, -5, 4, 4, -5, -13, -70, // 5 + -70, -13, -5, 4, 4, -5, -13, -70, // 4 + -90, -15, -5, -5, -5, -5, -15, -90, // 3 + -100, -20, -15, -13, -13, -15, -20,-100, // 2 + -100,-100, -90, -70, -70, -90,-100,-100, // 1 // a b c d e f g h ], 20_000), @@ -344,16 +344,57 @@ pub const PST_ENDGAME: Pst = Pst([ ], 100), ]); +/// Centipawn, signed, eval metrics. +pub struct EvalMetrics { + pub pst_eval: i32, + /// Manhattan distance between kings. + pub king_distance: usize, + pub king_distance_eval: i32, + pub total_eval: i32, + /// game phase, from 14 (start) to 0 (end endgame). + pub phase: i32, +} + +pub fn eval_metrics(board: &Board) -> EvalMetrics { + // we'll define endgame as the moment when there are 7 non pawn/king pieces left on the + // board in total. + // + // `phase` will range from 14 (game start) to 7 (endgame) to 0 (end end game). + let phase = i32::from(board.eval.min_maj_pieces); + + // piece square tables + let pst_eval = i32::from(board.eval.midgame.score) * max(7, phase - 7) / 7 + + i32::from(board.eval.endgame.score) * min(14 - phase, 7) / 7; + + // Signed factor of the color with the material advantage. + let advantage = pst_eval / 10; + + let kings1 = board[board.turn][Piece::King].into_iter(); + let kings2 = board[board.turn.flip()][Piece::King].into_iter(); + let king_distance = kings1 + .zip(kings2) + .next() + .map_or(0, |(k1, k2)| k1.manhattan(k2)); + + // attempt to minimize king distance for checkmates + let king_distance_eval = -advantage * i32::try_from(king_distance).unwrap() * max(7 - phase, 0) / 100; + + let eval = pst_eval + king_distance_eval; + + EvalMetrics { + pst_eval, + king_distance, + king_distance_eval, + total_eval: eval, + phase, + } +} + impl Eval for Board { fn eval(&self) -> EvalInt { - // we'll define endgame as the moment when there are 7 non pawn/king pieces left on the - // board in total. - // - // `phase` will range from 7 (game start) to 0 (endgame). - let phase = i32::from(self.eval.min_maj_pieces.saturating_sub(7)); - let eval = i32::from(self.eval.midgame.score) * phase / 7 - + i32::from(self.eval.endgame.score) * max(7 - phase, 0) / 7; - eval.try_into().unwrap() + let res = eval_metrics(self); + + res.total_eval.try_into().unwrap() } } diff --git a/src/lib.rs b/src/lib.rs index 3d23516..e0f5ecd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -205,14 +205,14 @@ macro_rules! from_row_col_generic { } impl Square { - fn from_row_col(r: usize, c: usize) -> Result { + pub fn from_row_col(r: usize, c: usize) -> Result { //! Get index of square based on row and column. from_row_col_generic!(usize, r, c) } - fn from_row_col_signed(r: isize, c: isize) -> Result { + pub fn from_row_col_signed(r: isize, c: isize) -> Result { from_row_col_generic!(isize, r, c) } - fn to_row_col(self) -> (usize, usize) { + pub fn to_row_col(self) -> (usize, usize) { //! Get row, column from index let div = usize::from(self.0) / BOARD_WIDTH; let rem = usize::from(self.0) % BOARD_WIDTH; @@ -220,19 +220,26 @@ impl Square { debug_assert!(rem <= 7); (div, rem) } - fn to_row_col_signed(self) -> (isize, isize) { + pub fn to_row_col_signed(self) -> (isize, isize) { //! Get row, column (signed) from index let (r, c) = self.to_row_col(); (r.try_into().unwrap(), c.try_into().unwrap()) } /// Vertically mirror a square. - fn mirror_vert(&self) -> Self { + pub fn mirror_vert(&self) -> Self { let (r, c) = self.to_row_col(); let (nr, nc) = (BOARD_HEIGHT - 1 - r, c); Square::from_row_col(nr, nc) .unwrap_or_else(|e| panic!("mirrored square should be valid: nr {nr} nc {nc}: {e:?}")) } + + /// Manhattan (grid-based) distance with another Square. + pub fn manhattan(&self, other: Self) -> usize { + let (r1, c1) = self.to_row_col(); + let (r2, c2) = other.to_row_col(); + r1.abs_diff(r2) + c1.abs_diff(c2) + } } impl Display for Square { @@ -761,4 +768,26 @@ mod tests { assert_eq!(tc.flip_colors(), expect); } } + + #[test] + fn manhattan_distance() { + let test_cases = [ + ("a3", "a3", 0), + ("a3", "a4", 1), + ("a3", "b3", 1), + ("a3", "b4", 2), + ("a1", "b8", 8), + ]; + + for (sq_str1, sq_str2, expected) in test_cases { + let sq1 = Square::from_str(sq_str1).unwrap(); + let sq2 = Square::from_str(sq_str2).unwrap(); + let res = sq1.manhattan(sq2); + assert_eq!( + res, expected, + "failed {sq_str1} and {sq_str2}: got manhattan {}, expected {}", + res, expected + ); + } + } }