From a36f394b99774ea30e7b2b4c3f1e2499915ff182 Mon Sep 17 00:00:00 2001 From: dogeystamp Date: Sun, 22 Dec 2024 16:34:04 -0500 Subject: [PATCH] stub: static exchange evaluation not yet used, but will be useful hopefully --- src/eval.rs | 133 +++++++++++++++++++++++++++++++---- src/lib.rs | 78 ++++++++++----------- src/movegen.rs | 185 +++++++++++++++++++++++++++++++++++++++++++++++-- src/prelude.rs | 16 +++-- 4 files changed, 345 insertions(+), 67 deletions(-) diff --git a/src/eval.rs b/src/eval.rs index 9c13929..461de9e 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -13,7 +13,7 @@ Copyright © 2024 dogeystamp //! 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, + 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); + } + } } diff --git a/src/lib.rs b/src/lib.rs index 8972e53..e5a9c35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 { 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 diff --git a/src/movegen.rs b/src/movegen.rs index 74166d0..9b1d063 100644 --- a/src/movegen.rs +++ b/src/movegen.rs @@ -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, + ) -> impl IntoIterator; +} + +impl GenAttackers for Board { + fn gen_attackers( + &self, + dest: Square, + single: bool, + filter_color: Option, + ) -> impl IntoIterator { + 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, + ) -> 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(¬_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::>(), + 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::) + .map(|x| x.expect("test case has invalid square")) + .collect::>(); + expected.sort(); + + let attacked = attacked.parse::().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::>(); + attackers.sort(); + + assert_eq!(attackers, expected); + } + } } diff --git a/src/prelude.rs b/src/prelude.rs index 048f350..a53002b 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -13,9 +13,15 @@ Copyright © 2024 dogeystamp //! 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, +};