From f60c573ba71207c18a28413e3940a4e21b07c73f Mon Sep 17 00:00:00 2001 From: eug-vs Date: Tue, 21 Feb 2023 17:49:51 +0300 Subject: refactor: create grossmeister module --- src/grossmeister/evaluation.rs | 466 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 466 insertions(+) create mode 100644 src/grossmeister/evaluation.rs (limited to 'src/grossmeister/evaluation.rs') 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()); + } + +} -- cgit v1.2.3