feat: better endgame heuristics
it may or may not be able to deliver checkmate
This commit is contained in:
parent
c0e8766fee
commit
0591f29c66
@ -15,6 +15,7 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
|
||||
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!(),
|
||||
}
|
||||
|
||||
|
75
src/eval.rs
75
src/eval.rs
@ -14,7 +14,7 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
|
||||
//! 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
39
src/lib.rs
39
src/lib.rs
@ -205,14 +205,14 @@ macro_rules! from_row_col_generic {
|
||||
}
|
||||
|
||||
impl Square {
|
||||
fn from_row_col(r: usize, c: usize) -> Result<Self, SquareError> {
|
||||
pub fn from_row_col(r: usize, c: usize) -> Result<Self, SquareError> {
|
||||
//! 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<Self, SquareError> {
|
||||
pub fn from_row_col_signed(r: isize, c: isize) -> Result<Self, SquareError> {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user