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 PAWN_PASSER_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, 2.00, 2.00, 2.00, 2.00, 2.00, 2.00, 2.00, 2.00, 1.50, 1.50, 1.50, 1.50, 1.50, 1.50, 1.50, 1.50, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 0.70, 0.70, 0.70, 0.70, 0.70, 0.70, 0.70, 0.70, 0.50, 0.50, 0.50, 0.50, 0.50, 0.50, 0.50, 0.50, 0.20, 0.20, 0.20, 0.20, 0.20, 0.20, 0.20, 0.20, 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 ]; const KING_BONUS_ENGAME: [f32; 64] = [ // A B C D E F G H -0.50, -0.40, -0.30, -0.20, -0.20, -0.30, -0.40, -0.50, -0.30, -0.20, -0.10, 0.00, 0.00, -0.10, -0.20, -0.30, -0.30, -0.10, 0.20, 0.30, 0.30, 0.20, -0.10, -0.30, -0.30, -0.10, 0.30, 0.40, 0.40, 0.30, -0.10, -0.30, -0.30, -0.10, 0.30, 0.40, 0.40, 0.30, -0.10, -0.30, -0.30, -0.10, 0.20, 0.30, 0.30, 0.20, -0.10, -0.30, -0.30, -0.30, 0.00, 0.00, 0.00, 0.00, -0.30, -0.30, -0.50, -0.30, -0.30, -0.30, -0.30, -0.30, -0.30, -0.50 ]; impl Grossmeister { pub fn is_endgame(&self) -> bool { // No queens on the board self.board.piece_sets[Piece::Queen as usize].pop_count() == 0 && self.board.piece_sets[Piece::QueenBlack as usize].pop_count() == 0 } pub fn passer_mask(&self, color: Color) -> Bitboard { let black_pawns = self.board.piece_sets[Piece::PawnBlack as usize]; let white_pawns = self.board.piece_sets[Piece::Pawn as usize]; match color { Color::Black => { let mut front_fill = white_pawns.nort_fill().nort_one(); front_fill |= front_fill.east_one() | front_fill.west_one(); black_pawns & !front_fill } Color::White => { let mut front_fill = black_pawns.sout_fill().sout_one(); front_fill |= front_fill.east_one() | front_fill.west_one(); white_pawns & !front_fill } } } pub fn is_dead_position(&self) -> bool { let non_minor_exists = [ Piece::Pawn, Piece::PawnBlack, Piece::Rook, Piece::RookBlack, Piece::Queen, Piece::QueenBlack ].iter().any(|&piece| { self.board.piece_sets[piece as usize].pop_count() > 0 }); if non_minor_exists { return false; } let minor_pieces = [ Piece::Knight, Piece::KnightBlack, Piece::Bishop, Piece::BishopBlack ].iter().fold(0, |acc, &piece| { acc + self.board.piece_sets[piece as usize].pop_count() }); minor_pieces <= 1 } pub fn evaluate_endgame(&self) -> f32 { let color = self.board.color(); let opponent_color = color.flip(); let opponent_material = self.material(opponent_color, true); self.material(color, true) - opponent_material } /// Return number of pawns above the king (+left/right) pub fn pawn_shield(&self, color: Color) -> f32 { let behind_pawns = match color { Color::White => self.board.piece_sets[Piece::Pawn as usize].sout_one(), Color::Black => self.board.piece_sets[Piece::PawnBlack as usize].nort_one(), }; let king = match color { Color::White => self.board.piece_sets[Piece::King as usize], Color::Black => self.board.piece_sets[Piece::KingBlack as usize], }; (behind_pawns & (king | king.west_one() | king.east_one())).pop_count() as f32 } pub fn evaluate_middlegame(&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, false); let material_advantage = self.material(color, false) - opponent_material; let king_tropism_penalty = self.king_tropism(color) - self.king_tropism(opponent_color); material_advantage + 0.15 * mobility_advantage + king_tropism_penalty * (opponent_material / 40.0) * 0.07 } // Returns a value in [0, 240] representing how // much material is left in the game // Note: not related to actual material counting pub fn phase(&self) -> u8 { let knight_phase = 10; let bishop_phase = 10; let rook_phase = 20; let queen_phase = 40; let total_phase = knight_phase*4 + bishop_phase*4 + rook_phase*4 + queen_phase*2; // If you change it, make sure to update denominator in interpolation debug_assert_eq!(total_phase, 240); self.board.piece_sets.iter().enumerate().fold(0, |acc, (piece_index, &bitboard)| { acc + match Piece::from(piece_index).without_color() { Piece::King => 0, Piece::Pawn => 0, Piece::Knight => knight_phase, Piece::Bishop => bishop_phase, Piece::Rook => rook_phase, Piece::Queen => queen_phase, _ => panic!("Unreachable") } * bitboard.pop_count() }) } /// Evaluate a position relative to the current player pub fn evaluate(&self) -> f32 { if self.is_dead_position() { return 0.0 } let color = self.board.color(); let opponent_color = color.flip(); let pawn_structure_penalty = self.pawn_structure_penalty(color) - self.pawn_structure_penalty(opponent_color); let middlegame_eval = self.evaluate_middlegame(); let endgame_eval = self.evaluate_endgame(); let phase = self.phase(); let tapered_eval = (middlegame_eval * phase as f32 + endgame_eval * (240 - phase) as f32) / 240.; tapered_eval - 0.3 * pawn_structure_penalty } /// Count player pieces' material, giving bonus for pieces standing well pub fn material(&self, color: Color, is_endgame: bool) -> f32 { let passer_mask = self.passer_mask(color); 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 => if bitboard & passer_mask != 0 { PAWN_PASSER_BONUS } else { PAWN_BONUS } Piece::Knight => KNIGHT_BONUS, Piece::Bishop => BISHOP_BONUS, Piece::Rook => ROOK_BONUS, Piece::King => if is_endgame { KING_BONUS_ENGAME } else { 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.board.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.board.attacks.pawn[color as usize][source as usize] & (opponent_occupancy | ep_bitboard)).pop_count() as f32; mobility += (self.board.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.board.attacks.pawn_double_pushes[color as usize][source as usize] & empty).pop_count() as f32; } } Piece::King => { for source in piece.serialize() { mobility += (self.board.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.board.attacks.knight[source as usize] & (empty | opponent_occupancy)).pop_count() as f32; } } Piece::Bishop => { for source in piece.serialize() { mobility += (self.board.attacks.bishop(self.board.occupancy, source) & (empty | opponent_occupancy)).pop_count() as f32; } } Piece::Rook => { for source in piece.serialize() { mobility += (self.board.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, false), gm.material(Color::White, false)); } #[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 w - - 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 board = Board::from_FEN(fen); let gm = Grossmeister::new(board); let score = gm.evaluate(); gm.board.print(); println!("Score {}", score); score }; assert_eq!(score1.abs(), score2.abs()); } #[test] fn passer() { let fen = String::from("rnbqkbnr/ppp3pp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); let board = Board::from_FEN(fen); let gm = Grossmeister::new(board); let mask = gm.passer_mask(Color::White); assert_eq!(mask, Square::E2.to_bitboard()); } #[test] fn no_passer() { let board = Board::new(); let gm = Grossmeister::new(board); let mask = gm.passer_mask(Color::White); assert_eq!(mask, 0); } }