stub: static exchange evaluation

not yet used, but will be useful hopefully
This commit is contained in:
dogeystamp 2024-12-22 16:34:04 -05:00
parent 1d651de4a0
commit a36f394b99
No known key found for this signature in database
4 changed files with 345 additions and 67 deletions

View File

@ -13,7 +13,7 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
//! Position evaluation.
use crate::{Board, Color, Piece, Square, N_COLORS, N_PIECES, N_SQUARES};
use crate::prelude::*;
use core::cmp::{max, min};
use core::ops::Index;
@ -29,6 +29,23 @@ pub trait Eval {
fn eval(&self) -> EvalInt;
}
pub trait EvalSEE {
/// Evaluate the outcome of an exchange at a square (static exchange evaluation).
///
/// # Arguments
///
/// * dest: Square where the exchange happens.
/// * first_move_side: Side to move first in the exchange.
///
/// This function may panic if a piece already at the destination is the same color as the side
/// to move.
///
/// # Returns
///
/// Expected gain from this exchange.
fn eval_see(&self, dest: Square, first_move_side: Color) -> EvalInt;
}
pub(crate) mod eval_score {
//! Opaque "score" counters to be used in the board.
@ -195,7 +212,7 @@ pub const PST_MIDGAME: Pst = Pst([
-5, 0, 0, 0, 0, 0, 0, -5, // 2
-5, -3, 0, 0, 0, 2, -3, -5, // 1
// a b c d e f g h
], 500),
], Piece::Rook.value()),
// bishop
make_pst([
@ -208,7 +225,7 @@ pub const PST_MIDGAME: Pst = Pst([
0, 0, 0, 0, 0, 0, 0, 0, // 2
0, 0, -10, 0, 0, -10, 0, 0, // 1
// a b c d e f g h
], 300),
], Piece::Bishop.value()),
// knight
make_pst([
@ -221,7 +238,7 @@ pub const PST_MIDGAME: Pst = Pst([
-100, 1, 0, 0, 0, 0, 0,-100, // 2
-100,-100,-100,-100,-100,-100,-100,-100, // 1
// a b c d e f g h
], 300),
], Piece::Knight.value()),
// king
make_pst([
@ -234,7 +251,7 @@ pub const PST_MIDGAME: Pst = Pst([
-70, -70, -70, -70, -70, -70, -70, -70, // 2
0, 0, 10, 0, 0, 0, 20, 0, // 1
// a b c d e f g h
], 20_000),
], Piece::King.value()),
// queen
make_pst([
@ -247,7 +264,7 @@ pub const PST_MIDGAME: Pst = Pst([
0, 0, 0, 0, 0, 0, 0, 0, // 2
0, 0, 0, 0, 0, 0, 0, 0, // 1
// a b c d e f g h
], 900),
], Piece::Queen.value()),
// pawn
make_pst([
@ -260,7 +277,7 @@ pub const PST_MIDGAME: Pst = Pst([
0, 0, 0, 0, 0, 0, 0, 0, // 2
0, 0, 0, 0, 0, 0, 0, 0, // 1
// a b c d e f g h
], 100),
], Piece::Pawn.value()),
]);
#[rustfmt::skip]
@ -276,7 +293,7 @@ pub const PST_ENDGAME: Pst = Pst([
0, 0, 0, 0, 0, 0, 0, 0, // 2
0, 0, 0, 0, 0, 0, 0, 0, // 1
// a b c d e f g h
], 500),
], Piece::Rook.value()),
// bishop
make_pst([
@ -289,7 +306,7 @@ pub const PST_ENDGAME: Pst = Pst([
0, 0, 0, 0, 0, 0, 0, 0, // 2
0, 0, 0, 0, 0, 0, 0, 0, // 1
// a b c d e f g h
], 300),
], Piece::Bishop.value()),
// knight
make_pst([
@ -302,7 +319,7 @@ pub const PST_ENDGAME: Pst = Pst([
-100, 0, 0, 0, 0, 0, 0,-100, // 2
-100,-100,-100,-100,-100,-100,-100,-100, // 1
// a b c d e f g h
], 300),
], Piece::Knight.value()),
// king
make_pst([
@ -315,7 +332,7 @@ pub const PST_ENDGAME: Pst = Pst([
-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),
], Piece::King.value()),
// queen
make_pst([
@ -328,7 +345,7 @@ pub const PST_ENDGAME: Pst = Pst([
0, 0, 0, 0, 0, 0, 0, 0, // 2
0, 0, 0, 0, 0, 0, 0, 0, // 1
// a b c d e f g h
], 900),
], Piece::Queen.value()),
// pawn
make_pst([
@ -341,7 +358,7 @@ pub const PST_ENDGAME: Pst = Pst([
0, 0, 0, 0, 0, 0, 0, 0, // 2
0, 0, 0, 0, 0, 0, 0, 0, // 1
// a b c d e f g h
], 100),
], Piece::Pawn.value()),
]);
/// Centipawn, signed, eval metrics.
@ -399,6 +416,66 @@ impl Eval for Board {
}
}
impl EvalSEE for Board {
fn eval_see(&self, dest: Square, first_mv_side: Color) -> EvalInt {
let attackers = self.gen_attackers(dest, false, None);
// indexed by the Piece enum order
let mut atk_qty = [[0u8; N_PIECES]; N_COLORS];
// counting sort
for (attacker, _src) in attackers {
atk_qty[attacker.col as usize][attacker.pc as usize] += 1;
}
let dest_pc = self.get_piece(dest);
// it doesn't make sense if the piece already on the square is first to move
debug_assert!(!dest_pc.is_some_and(|pc| pc.col == first_mv_side));
// Simulate the exchange.
//
// Returns the expected gain for the side in the exchange.
//
// TODO: promotions aren't accounted for.
fn sim_exchange(
side: Color,
dest_pc: Option<ColPiece>,
atk_qty: &mut [[u8; N_PIECES]; N_COLORS],
) -> EvalInt {
use Piece::*;
let val_idxs = [Pawn, Knight, Bishop, Rook, Queen, King];
let mut ptr = 0;
let mut eval = 0;
// while the count of this piece is zero, move to the next piece
while atk_qty[side as usize][val_idxs[ptr] as usize] == 0 {
ptr += 1;
if ptr == N_PIECES {
return eval;
}
}
let cur_pc = val_idxs[ptr];
let pc_ptr = cur_pc as usize;
debug_assert!(atk_qty[side as usize][pc_ptr] > 0);
atk_qty[side as usize][pc_ptr] -= 1;
if let Some(dest_pc) = dest_pc {
eval += dest_pc.pc.value();
// this player may either give up now, or capture. pick the best (max score).
// anything the other player gains is taken from us, hence the minus.
eval = max(0, eval - sim_exchange(side.flip(), Some(dest_pc), atk_qty))
}
eval
}
sim_exchange(first_mv_side, dest_pc, &mut atk_qty)
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -415,4 +492,34 @@ mod tests {
assert!(eval1 > 0, "got eval {eval1} ({:?})", board1.eval);
assert!(eval2 < 0, "got eval {eval2} ({:?})", board2.eval);
}
/// Static exchange evaluation tests.
#[test]
fn test_see_eval() {
// set side to move appropriately in the fen
//
// otherwise the exchange doesn't work
let test_cases = [
(
// fen
"8/4n3/8/2qRr3/8/4N3/8/8 b - - 0 1",
// square where exchange happens
"d5",
// expected (signed) value gain of exchange
Piece::Rook.value(),
),
(
"8/8/4b3/2kq4/2PKP3/8/8/8 w - - 0 1",
"d5",
Piece::Queen.value(),
),
];
for (fen, dest, expected) in test_cases {
let board = Board::from_fen(fen).unwrap();
let dest: Square = dest.parse().unwrap();
let res = board.eval_see(dest, board.turn);
assert_eq!(res, expected, "failed {}", fen);
}
}
}

View File

@ -29,6 +29,7 @@ pub mod prelude;
use crate::fen::{FromFen, ToFen, START_POSITION};
use crate::hash::Zobrist;
use crate::movegen::GenAttackers;
use eval::eval_score::EvalScores;
pub const BOARD_WIDTH: usize = 8;
@ -45,13 +46,13 @@ pub const N_COLORS: usize = 2;
impl Color {
/// Return opposite color (does not assign).
pub fn flip(self) -> Self {
pub const fn flip(self) -> Self {
match self {
Color::White => Color::Black,
Color::Black => Color::White,
}
}
pub fn sign(&self) -> i8 {
pub const fn sign(&self) -> i8 {
match self {
Color::White => 1,
Color::Black => -1,
@ -79,6 +80,21 @@ pub enum Piece {
}
pub const N_PIECES: usize = 6;
impl Piece {
/// Get a piece's base value.
pub const fn value(&self) -> crate::eval::EvalInt {
use Piece::*;
(match self {
Rook => 6,
Bishop => 3,
Knight => 3,
King => 200,
Queen => 9,
Pawn => 1,
}) * 100
}
}
pub struct PieceErr;
/// Color and piece.
@ -482,6 +498,16 @@ impl Board {
(0..N_SQUARES).map(Square::try_from).map(|x| x.unwrap())
}
/// Get the 8th rank from a given player's perspective.
///
/// Useful for promotions.
pub fn last_rank(pl: Color) -> usize {
match pl {
Color::White => usize::from(BOARD_HEIGHT) - 1,
Color::Black => 0,
}
}
/// Create a new piece in a location, and pop any existing piece in the destination.
pub fn set_piece(&mut self, sq: Square, pc: ColPiece) -> Option<ColPiece> {
let dest_pc = self.del_piece(sq);
@ -563,47 +589,13 @@ impl Board {
/// Is a given player in check?
pub fn is_check(&self, pl: Color) -> bool {
for src in self[pl][Piece::King] {
macro_rules! detect_checker {
($dirs: ident, $pc: pat, $keep_going: expr) => {
for dir in $dirs.into_iter() {
let (mut r, mut c) = src.to_row_col_signed();
loop {
let (nr, nc) = (r + dir.0, c + dir.1);
if let Ok(sq) = Square::from_row_col_signed(nr, nc) {
if let Some(pc) = self.get_piece(sq) {
if matches!(pc.pc, $pc) && pc.col != pl {
return true;
} else {
break;
}
}
} else {
break;
}
if (!($keep_going)) {
break;
}
r = nr;
c = nc;
}
}
};
}
let dirs_white_pawn = [(-1, 1), (-1, -1)];
let dirs_black_pawn = [(1, 1), (1, -1)];
use Piece::*;
use movegen::{DIRS_DIAG, DIRS_KNIGHT, DIRS_STAR, DIRS_STRAIGHT};
detect_checker!(DIRS_DIAG, Bishop | Queen, true);
detect_checker!(DIRS_STRAIGHT, Rook | Queen, true);
detect_checker!(DIRS_STAR, King, false);
detect_checker!(DIRS_KNIGHT, Knight, false);
match pl {
Color::White => detect_checker!(dirs_black_pawn, Pawn, false),
Color::Black => detect_checker!(dirs_white_pawn, Pawn, false),
if self
.gen_attackers(src, true, Some(pl.flip()))
.into_iter()
.next()
.is_some()
{
return true;
}
}
false

View File

@ -454,6 +454,134 @@ impl ToUCIAlgebraic for Move {
}
}
pub trait GenAttackers {
/// Generate attackers/attacks for a given square.
///
/// # Arguments
///
/// * `dest`: Square that is attacked.
/// * `single`: Exit early if any attack is found.
/// * `filter_color`: Matches only attackers of this color, if given.
fn gen_attackers(
&self,
dest: Square,
single: bool,
filter_color: Option<Color>,
) -> impl IntoIterator<Item = (ColPiece, Move)>;
}
impl GenAttackers for Board {
fn gen_attackers(
&self,
dest: Square,
single: bool,
filter_color: Option<Color>,
) -> impl IntoIterator<Item = (ColPiece, Move)> {
let mut ret: Vec<(ColPiece, Move)> = Vec::new();
/// Filter attackers and add them to the return vector.
///
/// Returns true if attacker was added.
fn push_ans(
pc: ColPiece,
sq: Square,
dest: Square,
ret: &mut Vec<(ColPiece, Move)>,
filter_color: Option<Color>,
) -> bool {
if let Some(filter_color) = filter_color {
if filter_color != pc.col {
return false;
}
}
let (r, _c) = dest.to_row_col();
let is_promotion = matches!(pc.pc, Piece::Pawn) && r == Board::last_rank(pc.col);
if is_promotion {
use PromotePiece::*;
for prom_pc in [Queen, Knight, Rook, Bishop] {
ret.push((
pc,
Move {
src: sq,
dest,
move_type: MoveType::Promotion(prom_pc),
},
));
}
} else {
ret.push((
pc,
Move {
src: sq,
dest,
move_type: MoveType::Normal,
},
));
}
true
}
macro_rules! detect_checker {
($dirs: ident, $pc: pat, $color: pat, $keep_going: expr) => {
for dir in $dirs.into_iter() {
let (mut r, mut c) = dest.to_row_col_signed();
loop {
let (nr, nc) = (r + dir.0, c + dir.1);
if let Ok(sq) = Square::from_row_col_signed(nr, nc) {
if let Some(pc) = self.get_piece(sq) {
if matches!(pc.pc, $pc) && matches!(pc.col, $color) {
let added = push_ans(pc, sq, dest, &mut ret, filter_color);
if single && added {
return ret;
}
}
break;
}
} else {
break;
}
if (!($keep_going)) {
break;
}
r = nr;
c = nc;
}
}
};
}
// inverted because our perspective is from the attacked square
let dirs_white_pawn = [(-1, 1), (-1, -1)];
let dirs_black_pawn = [(1, 1), (1, -1)];
use Piece::*;
macro_rules! both {
() => {
Color::White | Color::Black
};
}
detect_checker!(DIRS_DIAG, Bishop | Queen, both!(), true);
detect_checker!(DIRS_STRAIGHT, Rook | Queen, both!(), true);
// this shouldn't happen in legal chess but we're using this function in a pseudo-legal
// move gen context
detect_checker!(DIRS_STAR, King, both!(), false);
detect_checker!(DIRS_KNIGHT, Knight, both!(), false);
if filter_color.is_none_or(|c| matches!(c, Color::Black)) {
detect_checker!(dirs_black_pawn, Pawn, Color::Black, false);
}
if filter_color.is_none_or(|c| matches!(c, Color::White)) {
detect_checker!(dirs_white_pawn, Pawn, Color::White, false);
}
ret
}
}
#[derive(Debug, Clone, Copy)]
enum MoveGenType {
/// Legal move generation.
@ -561,6 +689,7 @@ fn move_slider(
}
}
}
fn is_legal(board: &mut Board, mv: Move) -> bool {
// mut required for check checking
// disallow friendly fire
@ -678,10 +807,7 @@ impl MoveGenInternal for Board {
for src in squares!(Pawn) {
let (r, c) = src.to_row_col_signed();
let last_row = match self.turn {
Color::White => isize::try_from(BOARD_HEIGHT).unwrap() - 1,
Color::Black => 0,
};
let last_row = isize::try_from(Board::last_rank(self.turn)).unwrap();
let nr = r + isize::from(self.turn.sign());
let is_promotion = nr == last_row;
@ -763,10 +889,11 @@ impl MoveGenInternal for Board {
}
}
}
ret.into_iter().filter(move |mv| match gen_type {
ret.retain(move |mv| match gen_type {
MoveGenType::Legal => is_legal(self, *mv),
MoveGenType::_Pseudo => true,
})
});
ret
}
}
@ -1156,6 +1283,18 @@ mod tests {
let all_cases = check_cases.iter().chain(&not_check_cases);
for (fen, expected) in all_cases {
let board = Board::from_fen(fen).unwrap();
eprintln!(
"got attackers {:?} for {}",
board
.gen_attackers(
board[Color::White][Piece::King].into_iter().next().unwrap(),
false,
Some(Color::Black)
)
.into_iter()
.collect::<Vec<_>>(),
fen
);
assert_eq!(board.is_check(Color::White), *expected, "failed on {}", fen);
let board_anti = board.flip_colors();
@ -1431,4 +1570,38 @@ mod tests {
assert_eq!(mv.to_uci_algebraic(), tc);
}
}
#[test]
fn test_gen_attackers() {
let test_cases = [(
// fen
"3q4/3rn3/3r2b1/3rb3/rnpkbN2/2qKK2r/1nPPpN2/2rr4 w - - 0 1",
// attacked square
"d3",
// expected results
"c3 c2 e3 b4 d4 e4 f2 f4 c4 b2",
)];
for (fen, attacked, expected) in test_cases {
let mut expected = expected
.split_whitespace()
.map(str::parse::<Square>)
.map(|x| x.expect("test case has invalid square"))
.collect::<Vec<_>>();
expected.sort();
let attacked = attacked.parse::<Square>().unwrap();
let board = Board::from_fen(fen).unwrap();
let mut attackers = board
.gen_attackers(attacked, false, None)
.into_iter()
.map(|(_pc, mv)| mv.src)
.collect::<Vec<_>>();
attackers.sort();
assert_eq!(attackers, expected);
}
}
}

View File

@ -13,9 +13,15 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
//! Prelude that you can import entirely to use the library conveniently.
pub use crate::eval::{eval_metrics, EvalMetrics, EvalInt, Eval};
pub use crate::coordination::{
GoMessage, MsgBestmove, MsgToEngine, MsgToMain, UCIMode, UCIModeMachine, UCIModeTransition,
};
pub use crate::eval::{eval_metrics, Eval, EvalInt, EvalMetrics};
pub use crate::fen::{FromFen, ToFen};
pub use crate::movegen::{FromUCIAlgebraic, Move, MoveGen, ToUCIAlgebraic};
pub use crate::search::{best_line, best_move, SearchEval, TranspositionTable, EngineState, SearchConfig, TimeLimits};
pub use crate::{Board, Color, BOARD_HEIGHT, BOARD_WIDTH, N_COLORS, N_PIECES, N_SQUARES, ColPiece, Piece};
pub use crate::coordination::{UCIMode, UCIModeTransition, UCIModeMachine, MsgBestmove, MsgToMain, MsgToEngine, GoMessage};
pub use crate::movegen::{FromUCIAlgebraic, GenAttackers, Move, MoveGen, ToUCIAlgebraic};
pub use crate::search::{
best_line, best_move, EngineState, SearchConfig, SearchEval, TimeLimits, TranspositionTable,
};
pub use crate::{
Board, ColPiece, Color, Piece, BOARD_HEIGHT, BOARD_WIDTH, N_COLORS, N_PIECES, N_SQUARES, Square,
};