blunders_engine/
eval.rs

1//! Static Evaluation Functions.
2//!
3//! An evaluation function may have two types of calls: relative or absolute.
4//!
5//! An absolute score treats White as a maxing player and Black as a minning player,
6//! so a centipawn score of +10 is winning for White, while -10 is winning for Black.
7//! A relative score treats the player to move as the maxing player, so if it is
8//! Black to move, +10 is winning for Black.
9
10use crate::bitboard::{self, Bitboard};
11use crate::coretypes::{Color, Cp, CpKind, PieceKind, SquareIndexable, NUM_RANKS, NUM_SQUARES};
12use crate::coretypes::{Color::*, PieceKind::*};
13use crate::movegen as mg;
14use crate::position::Position;
15
16impl PieceKind {
17    /// Default, independent value per piece.
18    pub const fn centipawns(&self) -> Cp {
19        Cp(match self {
20            Pawn => 100,   // 100 Centipawn == 1 Pawn
21            Knight => 305, // slightly prefer knight over 3 default pawns
22            Bishop => 310, // slightly prefer bishop over 3 default pawns
23            Rook => 510,
24            Queen => 950,
25            King => 10_000,
26        })
27    }
28}
29
30// Evaluation Constants
31const MOBILITY_CP: Cp = Cp(1);
32
33// Relative Evaluation Functions
34
35/// Given a terminal node, return a score representing a checkmate or a draw.
36/// The return score is relative to the player to move.
37pub fn terminal(position: &Position) -> Cp {
38    // Checkmate position is strictly bad for player to move.
39    if position.is_checkmate() {
40        -Cp::CHECKMATE
41    } else {
42        Cp::STALEMATE
43    }
44}
45
46/// Return a score representing a stalemate. Uses a contempt factor to indicate
47/// how bad a draw is for the engine.
48pub fn draw(is_engine: bool, contempt: Cp) -> Cp {
49    Cp::STALEMATE
50        + match is_engine {
51            true => -contempt,
52            false => contempt,
53        }
54}
55
56/// Primary hand-crafted evaluate function for engine, with return relative to player to move.
57/// Statically evaluates a non-terminal position.
58pub fn evaluate(position: &Position) -> Cp {
59    evaluate_abs(position) * position.player.sign()
60}
61
62// Absolute Evaluation Functions
63
64/// Given a terminal node (no moves can be made), return a score representing
65/// a checkmate for white/black, or a draw.
66pub fn terminal_abs(position: &Position) -> Cp {
67    if position.is_checkmate() {
68        match position.player {
69            White => -Cp::CHECKMATE,
70            Black => Cp::CHECKMATE,
71        }
72    } else {
73        Cp::STALEMATE
74    }
75}
76
77/// Primary evaluate function for engine.
78/// Statically evaluate a non-terminal position using a variety of heuristics.
79pub fn evaluate_abs(position: &Position) -> Cp {
80    let cp_material = material(position);
81    let cp_piece_sq = piece_square_lookup(position);
82    let cp_pass_pawns = pass_pawns(position);
83    let cp_xray_king = xray_king_attacks(position);
84    let cp_mobility = mobility(position);
85    let cp_king_safety = king_safety(position);
86
87    let cp_total =
88        cp_material + cp_piece_sq + cp_pass_pawns + cp_xray_king + cp_mobility + cp_king_safety;
89    cp_total
90}
91
92/// Returns relative strength difference of pieces in position.
93/// Is equivalent of piece_centipawn(White) - pieces_centipawn(Black).
94/// A positive value is an advantage for white, 0 is even, negative is advantage for black.
95pub fn material(position: &Position) -> Cp {
96    let w_piece_cp: Cp = PieceKind::iter()
97        .map(|pk| pk.centipawns() * position.pieces[(White, pk)].count_squares())
98        .fold(Cp::default(), |acc, value| acc + value);
99
100    let b_piece_cp: Cp = PieceKind::iter()
101        .map(|pk| pk.centipawns() * position.pieces[(Black, pk)].count_squares())
102        .fold(Cp::default(), |acc, value| acc + value);
103
104    w_piece_cp - b_piece_cp
105}
106
107pub fn king_safety(position: &Position) -> Cp {
108    let mut cp = Cp(0);
109
110    let occupied = position.pieces.occupied();
111    // Virtual mobility: treat king as a queen and the less squares it can attack the better.
112    let w_sliding = position.pieces[(White, Queen)]
113        | position.pieces[(White, Rook)]
114        | position.pieces[(White, Bishop)];
115    let b_sliding = position.pieces[(Black, Queen)]
116        | position.pieces[(Black, Rook)]
117        | position.pieces[(Black, Bishop)];
118    let w_num_sliding = w_sliding.count_squares();
119    let b_num_sliding = b_sliding.count_squares();
120    let w_king = position.pieces[(White, King)];
121    let b_king = position.pieces[(Black, King)];
122
123    let w_king_open_squares = mg::queen_attacks(w_king, occupied).count_squares();
124    let b_king_open_squares = mg::queen_attacks(b_king, occupied).count_squares();
125
126    // The more sliding pieces the enemy has, the more value each open square has.
127    let w_value = b_king_open_squares * w_num_sliding / 2;
128    let b_value = w_king_open_squares * b_num_sliding / 2;
129
130    let value_diff = Cp(w_value as CpKind - b_value as CpKind);
131    cp += value_diff;
132
133    cp
134}
135
136/// Return value of number of moves that can be made from a position.
137pub fn mobility(position: &Position) -> Cp {
138    let w_attacks = position.attacks(White, position.pieces().occupied());
139    let b_attacks = position.attacks(Black, position.pieces().occupied());
140
141    let attack_surface_area_diff =
142        w_attacks.count_squares() as CpKind - b_attacks.count_squares() as CpKind;
143
144    Cp(attack_surface_area_diff) * MOBILITY_CP
145}
146
147/// Returns Centipawn difference for passed pawns.
148pub fn pass_pawns(position: &Position) -> Cp {
149    // Base value of a passed pawn.
150    const SCALAR: Cp = Cp(20);
151    // Bonus value of passed pawn per rank. Pass pawns are very valuable on rank 7.
152    const RANK_CP: [CpKind; NUM_RANKS] = [0, 0, 1, 2, 10, 50, 250, 900];
153    let w_passed: Bitboard = pass_pawns_bb(position, White);
154    let b_passed: Bitboard = pass_pawns_bb(position, Black);
155    let w_num_passed = w_passed.count_squares() as CpKind;
156    let b_num_passed = b_passed.count_squares() as CpKind;
157
158    // Sum the bonus rank value of each pass pawn.
159    let w_rank_bonus = w_passed
160        .into_iter()
161        .map(|sq| sq.rank())
162        .fold(Cp(0), |acc, rank| acc + Cp(RANK_CP[rank as usize]));
163    let b_rank_bonus = b_passed
164        .into_iter()
165        .map(|sq| sq.rank().flip())
166        .fold(Cp(0), |acc, rank| acc + Cp(RANK_CP[rank as usize]));
167
168    Cp(w_num_passed - b_num_passed) * SCALAR + w_rank_bonus - b_rank_bonus
169}
170
171/// Returns value from sliding pieces attacking opposing king on otherwise empty chessboard.
172pub fn xray_king_attacks(position: &Position) -> Cp {
173    // Base value of xray attackers.
174    const SCALAR: Cp = Cp(8);
175    let w_king = position.pieces[(White, King)].get_lowest_square().unwrap();
176    let b_king = position.pieces[(Black, King)].get_lowest_square().unwrap();
177    let w_king_ortho = Bitboard::from(w_king.file()) | Bitboard::from(w_king.rank());
178    let b_king_ortho = Bitboard::from(b_king.file()) | Bitboard::from(b_king.rank());
179    let w_king_diags = mg::bishop_pattern(w_king);
180    let b_king_diags = mg::bishop_pattern(b_king);
181
182    let w_diags = position.pieces[(White, Queen)] | position.pieces[(White, Bishop)];
183    let b_diags = position.pieces[(Black, Queen)] | position.pieces[(Black, Bishop)];
184    let w_ortho = position.pieces[(White, Queen)] | position.pieces[(White, Rook)];
185    let b_ortho = position.pieces[(Black, Queen)] | position.pieces[(Black, Rook)];
186
187    let w_xray_attackers_bb = (b_king_diags & w_diags) | (b_king_ortho & w_ortho);
188    let b_xray_attackers_bb = (w_king_diags & b_diags) | (w_king_ortho & b_ortho);
189
190    let w_xray_attackers: CpKind = w_xray_attackers_bb.count_squares() as CpKind;
191    let b_xray_attackers: CpKind = b_xray_attackers_bb.count_squares() as CpKind;
192
193    Cp(w_xray_attackers - b_xray_attackers) * SCALAR
194}
195
196/// Returns value from looking up each piece square in precalculated tables.
197pub fn piece_square_lookup(position: &Position) -> Cp {
198    let mut w_values = Cp(0);
199    position.pieces[(White, Pawn)]
200        .into_iter()
201        .for_each(|sq| w_values += Cp(MG_PAWN_TABLE[sq.idx()]));
202    position.pieces[(White, Knight)]
203        .into_iter()
204        .for_each(|sq| w_values += Cp(MG_KNIGHT_TABLE[sq.idx()]));
205    position.pieces[(White, Bishop)]
206        .into_iter()
207        .for_each(|sq| w_values += Cp(MG_BISHOP_TABLE[sq.idx()]));
208    position.pieces[(White, King)]
209        .into_iter()
210        .for_each(|sq| w_values += Cp(MG_KING_TABLE[sq.idx()]));
211
212    let mut b_values = Cp(0);
213    position.pieces[(Black, Pawn)]
214        .into_iter()
215        .for_each(|sq| b_values += Cp(MG_PAWN_TABLE[sq.flip_rank().idx()]));
216    position.pieces[(Black, Knight)]
217        .into_iter()
218        .for_each(|sq| b_values += Cp(MG_KNIGHT_TABLE[sq.flip_rank().idx()]));
219    position.pieces[(Black, Bishop)]
220        .into_iter()
221        .for_each(|sq| b_values += Cp(MG_BISHOP_TABLE[sq.flip_rank().idx()]));
222    position.pieces[(Black, King)]
223        .into_iter()
224        .for_each(|sq| b_values += Cp(MG_KING_TABLE[sq.flip_rank().idx()]));
225
226    w_values - b_values
227}
228
229/// A pass pawn is one with no opponent pawns in front of it on same or adjacent files.
230/// This returns a bitboard with all pass pawns of given player.
231#[inline]
232fn pass_pawns_bb(position: &Position, player: Color) -> Bitboard {
233    use Bitboard as Bb;
234
235    let opponent_pawns = position.pieces[(!player, Pawn)];
236
237    let spans = opponent_pawns
238        .into_iter()
239        .map(|sq| {
240            let file = sq.file();
241            let mut span = Bb::from(file);
242            // Working with opponent pieces, so if finding w_pass, need to clear above sq.
243            match player {
244                Color::White => span.clear_square_and_above(sq),
245                Color::Black => span.clear_square_and_below(sq),
246            };
247
248            span | span.to_east() | span.to_west()
249        })
250        .fold(Bitboard::EMPTY, |acc, bb| acc | bb);
251
252    // Any pawn not in spans is a pass pawn.
253    position.pieces[(player, Pawn)] & !spans
254}
255
256// Piece Square Tables
257// Orientation:
258// A1, B1, C1, D1, ...,
259// ...             ...,
260// A8, B8, C8, D8, ...,
261
262/// Midgame Pawn square values
263///
264/// * Penalize not pushing D2/E2
265/// TODO:
266/// Dynamically change to consider where king is?
267#[rustfmt::skip]
268const MG_PAWN_TABLE: [CpKind; NUM_SQUARES] = [
269    0,   0,   0,   0,   0,   0,   0,   0,
270    5,   1,   0, -20, -20,   0,   1,   5,
271    5,  -2,   0,   0,   0,   0,  -2,   5,
272    0,   0,   0,  20,  20,   0,   0,   0,
273    2,   2,   2,  21,  21,   2,   2,   2,
274    3,   3,   3,  22,  22,   3,   3,   3,
275    4,   4,   4,  23,  23,   4,   4,   4,
276    0,   0,   0,   0,   0,   0,   0,   0,
277];
278
279/// Midgame Knight square values
280/// Encourage central squares, penalize edge squares.
281#[rustfmt::skip]
282const MG_KNIGHT_TABLE: [CpKind; NUM_SQUARES] = [
283    -50, -30, -20, -20, -20, -20, -30, -50,
284    -20,   0,   0,   5,   5,   0,   0, -20,
285    -10,   0,  10,  15,  15,  10,   0, -10,
286    -10,   0,  15,  20,  20,  15,   0, -10,
287    -10,   0,  15,  20,  20,  15,   0, -10,
288    -10,   0,  10,  15,  15,  10,   0, -10,
289    -20,   0,   0,   0,   0,   0,   0, -20,
290    -50, -10, -10, -10, -10, -10, -10, -50,
291];
292
293/// Midgame Bishop square values
294/// Avoid corners and borders
295#[rustfmt::skip]
296const MG_BISHOP_TABLE: [CpKind; NUM_SQUARES] = [
297    -20,  -8, -10,  -8,  -8, -10,  -8, -20,
298     -8,   5,   0,   0,   0,   0,   5,  -8,
299     -8,  10,  10,  10,  10,  10,  10,  -8,
300     -8,   0,  10,  10,  10,  10,   0,  -8,
301     -8,   0,  10,  10,  10,  10,   0,  -8,
302     -8,   0,  10,  10,  10,  10,   0,  -8,
303     -8,   0,   0,   0,   0,   0,   0,  -8,
304    -20,  -8,  -8,  -8,  -8,  -8,  -8, -20,
305];
306
307/// Midgame King square values
308/// Keep king in corner, in pawn shelter.
309#[rustfmt::skip]
310const MG_KING_TABLE: [CpKind; NUM_SQUARES] = [
311     20,  30,  10,   0,   0,  10,  30,  20,
312     20,  20,   0,   0,   0,   0,  20,  20,
313    -10, -10, -15, -15, -15, -15, -10, -10,
314    -10, -10, -10, -10, -10, -10, -10, -10,
315      0,   0,   0,   0,   0,   0,   0,   0,
316      0,   0,   0,   0,   0,   0,   0,   0,
317      0,   0,   0,   0,   0,   0,   0,   0,
318      0,   0,   0,   0,   0,   0,   0,   0,
319];
320
321// Const Data Generation
322
323/// Warning: Do not use, unfinished.
324pub const PASS_PAWN_SIZE: usize = (NUM_SQUARES - 24) * 2;
325pub const PASS_PAWN_PATTERN: [Bitboard; PASS_PAWN_SIZE] = generate_pass_pawn_pattern();
326
327// Repeats the form: array[num] = func[num];
328// where $array and $func are identifiers, followed by 1 or more literals to repeat on.
329// Need to use a macro because loops are not allowed in const fn currently.
330macro_rules! w_repeat_for_each {
331    ($array:ident, $func:ident, $($numbers:literal),+) => {
332        {
333            $($array[$numbers - 8] = $func($numbers);)*
334        }
335    };
336}
337
338/// TODO:
339/// FINISH FOR B_PAWNS.
340/// Unfinished until eval is working.
341/// NOTES:
342/// pass_pawn_pattern does not need to be generated for:
343/// * Rank 1 White (Pawns cannot be on squares)
344/// * Rank 7/8 White (Cannot be blocked by pawns)
345/// * Rank 8 Black ( Pawns cannot be on squares)
346/// * Rank 1/2 Black (Pawns cannot be blocked by pawns)
347const fn generate_pass_pawn_pattern() -> [Bitboard; PASS_PAWN_SIZE] {
348    let mut array = [Bitboard::EMPTY; PASS_PAWN_SIZE];
349
350    #[rustfmt::skip]
351    w_repeat_for_each!(
352        array,
353        w_pass_pawn_pattern_idx,
354        8, 9, 10, 11, 12, 13, 14, 15,
355        16, 17, 18, 19, 20, 21, 22, 23,
356        24, 25, 26, 27, 28, 29, 30, 31,
357        32, 33, 34, 35, 36, 37, 38, 39,
358        40, 41, 42, 43, 44, 45, 46, 47
359    );
360
361    array
362}
363
364const fn w_pass_pawn_pattern_idx(square: usize) -> Bitboard {
365    use Bitboard as Bb;
366    let square_bb: bitboard::BitboardKind = 1u64 << square;
367
368    if square_bb & Bitboard::FILE_A.0 > 0 {
369        // On File A
370        let mut pass_pawn_pat = Bitboard::FILE_A.0 | Bitboard::FILE_B.0;
371        pass_pawn_pat &= !square_bb; // Remove idx square.
372        pass_pawn_pat &= !(square_bb << 1); // Remove square to right of idx.
373        if square != 0 {
374            pass_pawn_pat &= !(square_bb - 1);
375        }
376        Bitboard(pass_pawn_pat)
377    } else if square_bb & Bitboard::FILE_H.0 > 0 {
378        // On File H
379        let mut pass_pawn_pat = Bitboard::FILE_G.0 | Bitboard::FILE_H.0;
380        pass_pawn_pat &= !(square_bb ^ (square_bb - 1)); // Remove square and below.
381        Bitboard(pass_pawn_pat)
382    } else {
383        // Not Files A or H
384        let mut pass_pawn_pat = match square_bb {
385            bb if bb & Bb::FILE_B.0 > 0 => Bb::FILE_A.0 | Bb::FILE_B.0 | Bb::FILE_C.0,
386            bb if bb & Bb::FILE_C.0 > 0 => Bb::FILE_B.0 | Bb::FILE_C.0 | Bb::FILE_D.0,
387            bb if bb & Bb::FILE_D.0 > 0 => Bb::FILE_C.0 | Bb::FILE_D.0 | Bb::FILE_E.0,
388            bb if bb & Bb::FILE_E.0 > 0 => Bb::FILE_D.0 | Bb::FILE_E.0 | Bb::FILE_F.0,
389            bb if bb & Bb::FILE_F.0 > 0 => Bb::FILE_E.0 | Bb::FILE_F.0 | Bb::FILE_G.0,
390            bb if bb & Bb::FILE_G.0 > 0 => Bb::FILE_F.0 | Bb::FILE_G.0 | Bb::FILE_H.0,
391            _ => 0,
392        };
393        // Remove Rank of square and all below.
394        pass_pawn_pat &= !(square_bb ^ (square_bb - 1)); // Remove square and below.
395        pass_pawn_pat &= !(square_bb << 1);
396
397        Bitboard(pass_pawn_pat)
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404    use crate::Fen;
405
406    #[test]
407    fn start_pos_equal_eval() {
408        // The start position is symmetric.
409        // Its eval should be the same for white to move and black to move.
410        let mut start = Position::start_position();
411        let w_eval = evaluate(&start);
412        start.player = Black;
413        let b_eval = evaluate(&start);
414        assert_eq!(w_eval, b_eval);
415
416        assert_eq!(w_eval, evaluate(&start.color_flip()));
417    }
418
419    #[test]
420    fn cp_min_and_max() {
421        let min = Cp::MIN;
422        let max = Cp::MAX;
423        assert_eq!(min.signum(), -1);
424        assert_eq!(max.signum(), 1);
425
426        // Negated
427        assert_eq!((-min).signum(), 1);
428        assert_eq!((-max).signum(), -1);
429    }
430
431    #[test]
432    fn large_eval_in_score_range() {
433        // Evaluate a position with largest possible advantage for one player.
434        // Score should sit within legal and score ranges, and outside of checkmate range.
435        let pos = Position::parse_fen("4k3/8/8/8/8/8/QQQQ1QQQ/QQQQKQQQ w - - 0 1").unwrap();
436        let score = evaluate(&pos);
437        assert!(score.is_score());
438        assert!(score.is_legal());
439        assert!(!score.is_mate());
440        println!("MAX POSSIBLE SCORE: {}", score);
441    }
442}