aboutsummaryrefslogtreecommitdiff
path: root/src/grossmeister/evaluation.rs
diff options
context:
space:
mode:
authoreug-vs <eugene@eug-vs.xyz>2023-02-21 17:49:51 +0300
committereug-vs <eugene@eug-vs.xyz>2023-02-23 14:01:03 +0300
commitf60c573ba71207c18a28413e3940a4e21b07c73f (patch)
tree3e50e9ea6cd0129414db92cd50805ebeb65a4676 /src/grossmeister/evaluation.rs
parent69f3c48fb99d96f3fbc4ab49f5fb6d1d8e90e270 (diff)
downloadchessnost-f60c573ba71207c18a28413e3940a4e21b07c73f.tar.gz
refactor: create grossmeister module
Diffstat (limited to 'src/grossmeister/evaluation.rs')
-rw-r--r--src/grossmeister/evaluation.rs466
1 files changed, 466 insertions, 0 deletions
diff --git a/src/grossmeister/evaluation.rs b/src/grossmeister/evaluation.rs
new file mode 100644
index 0000000..201aaed
--- /dev/null
+++ b/src/grossmeister/evaluation.rs
@@ -0,0 +1,466 @@
+use crate::{board::{piece::Piece, color::Color, CastlingSide}, bitboard::{BitboardFns, Bitboard}, square::Square};
+
+use super::Grossmeister;
+
+static A_FILE: Bitboard = 0x0101010101010101;
+
+const PAWN_BONUS: [f32; 64] = [
+ // A B C D E F G H
+ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00,
+ 0.50, 0.50, 0.50, 0.50, 0.50, 0.50, 0.50, 0.50,
+ 0.10, 0.10, 0.20, 0.30, 0.30, 0.20, 0.10, 0.10,
+ 0.05, 0.05, 0.10, 0.25, 0.25, 0.10, 0.05, 0.05,
+ 0.00, 0.00, 0.00, 0.20, 0.20, 0.00, 0.00, 0.00,
+ 0.05, -0.05, -0.10, 0.00, 0.00, -0.10, -0.05, 0.05,
+ 0.05, 0.10, 0.10, -0.20, -0.20, 0.10, 0.10, 0.05,
+ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00
+];
+
+const KNIGHT_BONUS: [f32; 64] = [
+ // A B C D E F G H
+ -0.50, -0.40, -0.30, -0.30, -0.30, -0.30, -0.40, -0.50,
+ -0.40, -0.20, 0.00, 0.00, 0.00, 0.00, -0.20, -0.40,
+ -0.30, 0.00, 0.10, 0.15, 0.15, 0.10, 0.00, -0.30,
+ -0.30, 0.05, 0.15, 0.20, 0.20, 0.15, 0.05, -0.30,
+ -0.30, 0.00, 0.15, 0.20, 0.20, 0.15, 0.00, -0.30,
+ -0.30, 0.05, 0.10, 0.15, 0.15, 0.10, 0.05, -0.30,
+ -0.40, -0.20, 0.00, 0.05, 0.05, 0.00, -0.20, -0.40,
+ -0.50, -0.40, -0.30, -0.30, -0.30, -0.30, -0.40, -0.50,
+];
+
+const BISHOP_BONUS: [f32; 64] = [
+ // A B C D E F G H
+ -0.20, -0.10, -0.10, -0.10, -0.10, -0.10, -0.10, -0.20,
+ -0.10, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, -0.10,
+ -0.10, 0.00, 0.05, 0.10, 0.10, 0.05, 0.00, -0.10,
+ -0.10, 0.05, 0.05, 0.10, 0.10, 0.05, 0.05, -0.10,
+ -0.10, 0.00, 0.10, 0.10, 0.10, 0.10, 0.00, -0.10,
+ -0.10, 0.10, 0.10, 0.10, 0.10, 0.10, 0.10, -0.10,
+ -0.10, 0.25, 0.00, 0.00, 0.00, 0.00, 0.25, -0.10,
+ -0.20, -0.10, -0.10, -0.10, -0.10, -0.10, -0.10, -0.20,
+];
+
+const ROOK_BONUS: [f32; 64] = [
+ // A B C D E F G H
+ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00,
+ 0.05, 0.30, 0.30, 0.30, 0.30, 0.30, 0.30, 0.05,
+ -0.05, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, -0.05,
+ -0.05, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, -0.05,
+ -0.05, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, -0.05,
+ -0.05, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, -0.05,
+ -0.05, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, -0.05,
+ 0.00, 0.00, 0.00, 0.05, 0.05, 0.00, 0.00, 0.00
+];
+
+const QUEEN_BONUS: [f32; 64] = [
+ // A B C D E F G H
+ -0.20, -0.10, -0.10, -0.05, -0.05, -0.10, -0.10, -0.20,
+ -0.10, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, -0.10,
+ -0.10, 0.00, 0.05, 0.05, 0.05, 0.05, 0.00, -0.10,
+ -0.05, 0.00, 0.05, 0.05, 0.05, 0.05, 0.00, -0.05,
+ 0.00, 0.00, 0.05, 0.05, 0.05, 0.05, 0.00, -0.05,
+ -0.10, 0.05, 0.05, 0.05, 0.05, 0.05, 0.00, -0.10,
+ -0.10, 0.00, 0.05, 0.00, 0.00, 0.00, 0.00, -0.10,
+ -0.20, -0.10, -0.10, -0.05, -0.05, -0.10, -0.10, -0.20
+];
+
+const KING_BONUS: [f32; 64] = [
+ // A B C D E F G H
+ -0.30, -0.40, -0.40, -0.50, -0.50, -0.40, -0.40, -0.30,
+ -0.30, -0.40, -0.40, -0.50, -0.50, -0.40, -0.40, -0.30,
+ -0.30, -0.40, -0.40, -0.50, -0.50, -0.40, -0.40, -0.30,
+ -0.30, -0.40, -0.40, -0.50, -0.50, -0.40, -0.40, -0.30,
+ -0.20, -0.30, -0.30, -0.40, -0.40, -0.30, -0.30, -0.20,
+ -0.10, -0.20, -0.20, -0.20, -0.20, -0.20, -0.20, -0.10,
+ 0.20, 0.20, 0.00, -0.10, -0.10, 0.00, 0.20, 0.20,
+ 0.20, 0.10, 0.30, -0.20, 0.00, 0.10, 0.30, 0.20
+];
+
+impl Grossmeister {
+ /// Evaluate a position relative to the current player
+ pub fn evaluate(&self) -> f32 {
+ let color = self.board.color();
+ let opponent_color = color.flip();
+
+ let mobility_advantage = self.mobility(color) - self.mobility(opponent_color);
+
+ let opponent_material = self.material(opponent_color);
+ let material_advantage = self.material(color) - opponent_material;
+
+ let pawn_structure_penalty = self.pawn_structure_penalty(color) - self.pawn_structure_penalty(opponent_color);
+
+ let king_tropism_penalty = self.king_tropism(color) - self.king_tropism(opponent_color);
+
+ material_advantage
+ + 0.15 * mobility_advantage
+ - 0.3 * pawn_structure_penalty
+ + king_tropism_penalty * (opponent_material / 40.0) * 0.07
+ }
+
+ /// Count player pieces' material, giving bonus for pieces standing well
+ pub fn material(&self, color: Color) -> f32 {
+ let mut material = 0f32;
+ for (piece_index, bitboard) in self.board.pieces_by_color(color).iter().enumerate() {
+ let piece_type = Piece::from(piece_index);
+
+ let bonus_table = match piece_type {
+ Piece::Pawn => PAWN_BONUS,
+ Piece::Knight => KNIGHT_BONUS,
+ Piece::Bishop => BISHOP_BONUS,
+ Piece::Rook => ROOK_BONUS,
+ Piece::King => KING_BONUS,
+ Piece::Queen => QUEEN_BONUS,
+ _ => panic!("Unreachable")
+ };
+
+ material += bitboard.serialize().iter().fold(0., |acc, square| {
+ acc + piece_type.static_eval() + bonus_table[
+ match color {
+ Color::White => square.mirror() as usize,
+ Color::Black => *square as usize,
+ }
+ ]
+ });
+ }
+ material
+ }
+
+ /// Returns sum of the doubled, blocked and isolated pawns
+ /// The greater result is, the worse is the pawn structure
+ pub fn pawn_structure_penalty(&self, color: Color) -> f32 {
+ let mut result = 0.0;
+
+ let pawns = match color {
+ Color::White => self.board.piece_sets[Piece::Pawn as usize],
+ Color::Black => self.board.piece_sets[Piece::PawnBlack as usize],
+ };
+
+ for file in 0..8 {
+ let file_mask = A_FILE << file;
+ let pawns_on_file = (pawns & file_mask).pop_count() as f32;
+
+ // Doubled pawns (-1 because one pawn on a file is ok)
+ result += (pawns_on_file - 1.).max(0.0);
+
+ // Isolated pawns (no pawns on neighbor files)
+ if [
+ A_FILE << (file - 1).max(0), // File to the left (if any)
+ A_FILE << (file + 1).min(7), // File to the right (if any)
+ ].iter().all(|file| file & pawns == 0) {
+ result += pawns_on_file;
+ }
+ }
+
+ // Blocked pawns
+ let blocked_mask = match color {
+ Color::White => self.board.occupancy >> 8,
+ Color::Black => self.board.occupancy << 8,
+ };
+ result += (pawns & blocked_mask).pop_count() as f32;
+
+ result
+ }
+
+ /// Returns the weighted sum of distances from attacking pieces to a king
+ /// The higher this value, the safer is the king
+ pub fn king_tropism(&self, color: Color) -> f32 {
+ let mut result = 0.0;
+
+ let king_square = match color {
+ Color::White => self.board.piece_sets[Piece::King as usize],
+ Color::Black => self.board.piece_sets[Piece::KingBlack as usize],
+ }.bitscan();
+
+ for (piece_type, bitboard) in self.board.pieces_by_color(color.flip()).iter().enumerate() {
+ if piece_type != Piece::King as usize && piece_type != Piece::Pawn as usize {
+ for square in bitboard.serialize() {
+ let distance =
+ (king_square.rank() as f32 - square.rank() as f32).abs() +
+ (king_square.file() as f32 - square.file() as f32).abs();
+
+ result += distance / Piece::from(piece_type).static_eval();
+ }
+ }
+ }
+ result
+ }
+
+ /// Count pseudo-legal moves without actually generating them
+ /// Also exclude all moves that put a piece under attack of a pawn - so called safe mobility
+ pub fn mobility(&self, color: Color) -> f32 {
+ let mut mobility = 0.;
+ let opponent_occupancy = self.board.color_occupancy(color.flip());
+ let player_pieces = self.board.pieces_by_color(color);
+
+ let opponent_pawns = match color {
+ Color::Black => self.board.piece_sets[Piece::Pawn as usize],
+ Color::White => self.board.piece_sets[Piece::PawnBlack as usize],
+ };
+
+ let pawn_attacked_squares = opponent_pawns.serialize().iter().fold(0u64, |acc, square| {
+ acc | self.attacks.pawn[color.flip() as usize][*square as usize]
+ });
+
+ // Exclude squares controlled by enemy pawns from mobility
+ let empty = self.board.empty() & !pawn_attacked_squares;
+
+ for (piece_type, piece) in player_pieces.iter().enumerate() {
+ match Piece::from(piece_type) {
+ Piece::Pawn => {
+ for source in piece.serialize() {
+ let ep_bitboard = match self.board.ep_target {
+ Some(square) => {
+ let rank = square.rank();
+ if (rank == 2 && color == Color::Black) || (rank == 5 && color == Color::White) {
+ square.to_bitboard()
+ } else {
+ 0
+ }
+ }
+ None => 0,
+ };
+ mobility += (self.attacks.pawn[color as usize][source as usize] & (opponent_occupancy | ep_bitboard)).pop_count() as f32;
+ mobility += (self.attacks.pawn_pushes[color as usize][source as usize] & empty).pop_count() as f32;
+ }
+ let able_to_double_push_mask = match color {
+ Color::White => empty >> 8,
+ Color::Black => empty << 8,
+ };
+ for source in (*piece & able_to_double_push_mask).serialize() {
+ mobility += (self.attacks.pawn_double_pushes[color as usize][source as usize] & empty).pop_count() as f32;
+ }
+ }
+ Piece::King => {
+ for source in piece.serialize() {
+ mobility += (self.attacks.king[source as usize] & (empty | opponent_occupancy)).pop_count() as f32;
+
+ // Castling
+ let king_home_position = match color {
+ Color::White => Square::E1,
+ Color::Black => Square::E8,
+ };
+ if *piece == king_home_position.to_bitboard() {
+ for rook_square in player_pieces[Piece::Rook as usize]
+ .serialize()
+ .iter()
+ .filter(|rook_square| rook_square.rank() == king_home_position.rank())
+ {
+ match rook_square.file() {
+ 0 => {
+ let castle_line = [
+ king_home_position.west_one(),
+ king_home_position.west_one().west_one(),
+ ];
+
+ let all_empty = castle_line.iter().all(|square| empty & square.to_bitboard() > 0);
+ let any_checks = castle_line.iter().any(|square| self.board.is_square_attacked(*square, color.flip()));
+
+ if all_empty && !any_checks && self.board.castling_rights[color as usize][CastlingSide::Queen as usize] {
+ mobility += 1.;
+ }
+ },
+ 7 => {
+ let castle_line = [
+ king_home_position.east_one(),
+ king_home_position.east_one().east_one(),
+ ];
+
+ let all_empty = castle_line.iter().all(|square| empty & square.to_bitboard() > 0);
+ let any_checks = castle_line.iter().any(|square| self.board.is_square_attacked(*square, color.flip()));
+
+ if all_empty && !any_checks && self.board.castling_rights[color as usize][CastlingSide::King as usize] {
+ mobility += 1.;
+ }
+ },
+ _ => {},
+ }
+ }
+ }
+ }
+ }
+ Piece::Knight => {
+ for source in piece.serialize() {
+ mobility += (self.attacks.knight[source as usize] & (empty | opponent_occupancy)).pop_count() as f32;
+ }
+ }
+ Piece::Bishop => {
+ for source in piece.serialize() {
+ mobility += (self.attacks.bishop(self.board.occupancy, source) & (empty | opponent_occupancy)).pop_count() as f32;
+ }
+ }
+ Piece::Rook => {
+ for source in piece.serialize() {
+ mobility += (self.attacks.rook(self.board.occupancy, source) & (empty | opponent_occupancy)).pop_count() as f32;
+ }
+ }
+ Piece::Queen => {
+ // Do not account queen in mobility
+ }
+ incorrect_type => panic!("Incorrect piece type: {:?}", incorrect_type),
+ }
+ }
+ mobility
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::{board::{Board, io::IO}, moves::{Move, MoveKind}};
+
+ use super::*;
+
+ #[test]
+ fn castle_bonus() {
+ assert_eq!(KING_BONUS[Square::E1.mirror() as usize], 0.0);
+ assert!(KING_BONUS[Square::G1.mirror() as usize] > 0.0);
+ assert!(KING_BONUS[Square::C1.mirror() as usize] > 0.0);
+ assert!(KING_BONUS[Square::D1.mirror() as usize] < 0.0);
+ assert!(KING_BONUS[Square::E2.mirror() as usize] < 0.0);
+
+ assert_eq!(KING_BONUS[Square::E8 as usize], 0.0);
+ assert!(KING_BONUS[Square::G8 as usize] > 0.0);
+ assert!(KING_BONUS[Square::C8 as usize] > 0.0);
+ assert!(KING_BONUS[Square::D8 as usize] < 0.0);
+ assert!(KING_BONUS[Square::E2 as usize] < 0.0);
+ }
+
+ #[test]
+ fn mobility() {
+ let board = Board::new();
+ let gm = Grossmeister::new(board);
+ let white = gm.mobility(Color::White);
+ let black = gm.mobility(Color::Black);
+
+ assert_eq!(white, 20.);
+ assert_eq!(black, 20.);
+ }
+
+ #[test]
+ fn material() {
+ let board = Board::new();
+ let gm = Grossmeister::new(board);
+ assert_eq!(gm.material(Color::Black), gm.material(Color::White));
+
+ }
+
+ #[test]
+ fn initial_eval() {
+ let board = Board::new();
+ let gm = Grossmeister::new(board);
+ assert_eq!(gm.evaluate(), 0.0);
+ }
+
+ #[test]
+ fn king_tropism() {
+ let board = Board::new();
+ let mut gm = Grossmeister::new(board);
+ gm.board.make_move(Move { source: Square::D1, target: Square::F5, kind: MoveKind::Quiet });
+ let score = gm.evaluate();
+ gm.board.print();
+ println!("Score {}", score);
+
+ assert!(score < 0.0);
+ assert!(score > -1.0);
+ }
+
+ #[test]
+ fn white_winning() {
+ let fen = String::from("8/5pk1/6p1/R4b1p/3P4/1P2N3/P1r2PPP/R5K1 b - - 1 27");
+ let board = Board::from_FEN(fen);
+ let gm = Grossmeister::new(board);
+ let score = gm.evaluate();
+ gm.board.print();
+ println!("Score {}", score);
+
+ assert!(score > 7.0);
+ }
+
+ #[test]
+ fn black_winning() {
+ let fen = String::from("8/p7/1k4K1/8/4P3/8/PP5r/8 b - - 1 38");
+ let board = Board::from_FEN(fen);
+ let gm = Grossmeister::new(board);
+ let score = gm.evaluate();
+ gm.board.print();
+ println!("Score {}", score);
+
+ assert!(score < -3.0);
+ }
+
+ #[test]
+ fn encourage_center_pawns() {
+ let score1 = {
+ let fen = String::from("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2");
+ let board = Board::from_FEN(fen);
+ let gm = Grossmeister::new(board);
+ let score = gm.evaluate();
+ gm.board.print();
+ println!("Score {}", score);
+ score
+ };
+
+ let score2 = {
+ let fen = String::from("rnbqkbnr/pppp1ppp/8/4p3/2P5/8/PP1PPPPP/RNBQKBNR w KQkq - 0 2");
+ let board = Board::from_FEN(fen);
+ let gm = Grossmeister::new(board);
+ let score = gm.evaluate();
+ gm.board.print();
+ println!("Score {}", score);
+ score
+ };
+
+ assert!(score1 > score2);
+ }
+
+ #[test]
+ fn discourage_edge_knights() {
+ let score1 = {
+ let fen = String::from("r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3");
+ let board = Board::from_FEN(fen);
+ let gm = Grossmeister::new(board);
+ let score = gm.evaluate();
+ gm.board.print();
+ println!("Score {}", score);
+ score
+ };
+
+ let score2 = {
+ let fen = String::from("r1bqkbnr/pppp1ppp/2n5/4p3/4P3/7N/PPPP1PPP/RNBQKB1R w KQkq - 2 3");
+ let board = Board::from_FEN(fen);
+ let gm = Grossmeister::new(board);
+ let score = gm.evaluate();
+ gm.board.print();
+ println!("Score {}", score);
+ score
+ };
+
+ assert!(score1 > score2);
+ }
+
+ #[test]
+ fn mirrored_evaluation() {
+ let score1 = {
+ let fen = String::from("r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1");
+ let board = Board::from_FEN(fen);
+ let gm = Grossmeister::new(board);
+ let score = gm.evaluate();
+ gm.board.print();
+ println!("Score {}", score);
+ score
+ };
+
+ let score2 = {
+ let fen = String::from("r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 1 ");
+ let mut board = Board::from_FEN(fen);
+ let gm = Grossmeister::new(board);
+ board.ply += 1; // TODO: remove me when FEN parsing includes side to move
+ let score = gm.evaluate();
+ gm.board.print();
+ println!("Score {}", score);
+ score
+ };
+
+ assert_eq!(score1.abs(), score2.abs());
+ }
+
+}