myopic_brain/
quiescent.rs

1use crate::eval::EvalChessBoard;
2use crate::{eval, see};
3use anyhow::Result;
4use myopic_board::{BitBoard, Move, MoveComputeType, Reflectable, Termination};
5use std::cmp;
6
7const Q_DEPTH_CAP: i32 = -8;
8const Q_CHECK_CAP: i32 = -2;
9
10// TODO Do we want the quiescent search to have interruption finishing checks too?
11
12/// Performs a depth limited search looking to evaluate only quiet positions,
13/// i.e. those with no attack moves.
14pub fn search<B: EvalChessBoard>(
15    state: &mut B,
16    mut alpha: i32,
17    beta: i32,
18    depth: i32,
19) -> Result<i32> {
20    if depth == Q_DEPTH_CAP || state.termination_status().is_some() {
21        return Ok(match state.termination_status() {
22            Some(Termination::Loss) => eval::LOSS_VALUE,
23            Some(Termination::Draw) => eval::DRAW_VALUE,
24            None => state.static_eval(),
25        });
26    }
27    // If we aren't in check then we can use the static eval as the initial
28    // result under the sound assumption that there exists a move
29    // (which might not be considered here) we can make in the position
30    // which will improve our score. We cannot make this assumption if we
31    // are in check because we will consider all the moves and so we
32    // assume lost until proven otherwise.
33    let mut result = if state.in_check() {
34        -eval::INFTY
35    } else {
36        state.static_eval()
37    };
38
39    // Break immediately if the stand pat is greater than beta.
40    if result >= beta {
41        return Ok(beta);
42    }
43    if alpha < result {
44        alpha = result;
45    }
46
47    for evolve in compute_quiescent_moves(state, depth) {
48        state.make(evolve)?;
49        let next_result = -search(state, -beta, -alpha, depth - 1)?;
50        state.unmake()?;
51        result = cmp::max(result, next_result);
52        alpha = cmp::max(alpha, result);
53        if alpha > beta {
54            return Ok(beta);
55        }
56    }
57    return Ok(result);
58}
59
60fn compute_quiescent_moves<B: EvalChessBoard>(state: &mut B, depth: i32) -> Vec<Move> {
61    let mut moves = if depth < Q_CHECK_CAP {
62        state.compute_moves(MoveComputeType::Attacks)
63    } else {
64        state.compute_moves(MoveComputeType::AttacksChecks)
65    };
66    let enemies = state.side(state.active().reflect());
67    let is_attack_filter = |mv: &Move| is_attack(mv, enemies);
68    // If in check don't filter out any attacks, we must check all available moves.
69    let good_attack_threshold = if state.in_check() { -eval::INFTY } else { 0 };
70    let split_index = itertools::partition(&mut moves, is_attack_filter);
71    // Score attacks using see and filter bad exchanges before sorting and
72    // recombining.
73    let mut attacks: Vec<_> = moves
74        .iter()
75        .take(split_index)
76        .map(|mv| (mv, score_attack(state, mv)))
77        .filter(|(_, score)| *score > good_attack_threshold)
78        .collect();
79    attacks.sort_by_key(|(_, score)| -*score);
80
81    moves
82        .iter()
83        .cloned()
84        .skip(split_index)
85        .chain(attacks.into_iter().map(|(mv, _)| mv.clone()))
86        .collect()
87}
88
89fn score_attack<B: EvalChessBoard>(state: &mut B, attack: &Move) -> i32 {
90    match attack {
91        &Move::Enpassant { .. } => 10000,
92        &Move::Promotion { .. } => 20000,
93        &Move::Standard { from, dest, .. } => {
94            see::exchange_value(state, from, dest, state.piece_values())
95        }
96        // Should never get here
97        _ => 0,
98    }
99}
100
101fn is_attack(query: &Move, enemies: BitBoard) -> bool {
102    match query {
103        &Move::Enpassant { .. } => true,
104        &Move::Castle { .. } => false,
105        &Move::Promotion { dest, .. } => enemies.contains(dest),
106        &Move::Standard { dest, .. } => enemies.contains(dest),
107    }
108}