blunders_engine/
moveorder.rs

1//! Move Ordering
2//!
3//! Functions used for ordering a list of moves from best to worst,
4//! or for picking the best move out of a list of moves.
5//!
6//! Move ordering is important for alpha-beta pruning performance.
7//! If the best or good moves are searched early on in an alpha-beta search,
8//! pruning occurs more frequently.
9//!
10//! Two strategies are used for when to move order.
11//! 1. Sort an entire list of moves before processing.
12//! 2. Pick and remove the best move from the move list each time a move is needed.
13//!
14//! There are several strategies for move ordering which may be used.
15//! 1. Sort first by principal variation moves, then by hash moves, then by Captures (SEE)
16
17use crate::arrayvec::ArrayVec;
18use crate::coretypes::{Cp, Move, MoveInfo, MAX_MOVES};
19use crate::movelist::MoveInfoList;
20
21// General considerations for move ordering and searching:
22// For tt look ups during a search, a node only needs to search itself, not it's children.
23// a/b can only be inherited, so getting a tt value from a child node within
24// a call is the same as getting it from a recursive call.
25
26// When looking up a value, we only return right away if it meets some conditions.
27// 1. The tt hit depth >= current search depth. Otherwise value is not valid.
28// 2. If depth if great enough, then we only return immediately if
29
30/// Simple move ordering strategy. The following information is extracted from a move,
31/// and used for sorting. The values go from most-to-least important based on
32/// top-to-bottom declaration of fields.
33#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
34pub(crate) struct OrderStrategy {
35    is_tt_move: bool,      // Move listed as best move for root position in tt.
36    promotion: Option<Cp>, // Cp value of promoting piece, or none.
37    mvv_lva: (bool, Cp),   // is capture, followed by mvv-lva.
38                           // All other nodes remain with lowest but equal priority.
39}
40
41/// OrderStrategy defaults to all false.
42impl Default for OrderStrategy {
43    fn default() -> Self {
44        OrderStrategy {
45            is_tt_move: false,
46            promotion: None,
47            mvv_lva: (false, Cp(0)),
48        }
49    }
50}
51
52impl From<(MoveInfo, Option<Move>)> for OrderStrategy {
53    fn from((move_info, key_move): (MoveInfo, Option<Move>)) -> Self {
54        // Give high priority to move if root position listed it in tt.
55        let is_tt_move = key_move == Some(move_info.move_());
56
57        // Set promotion CP.
58        let promotion = move_info.promotion.map(|pk| pk.centipawns());
59
60        // Sort by most-valuable-victim -> least-valuable-aggressor.
61        // A decent heuristic that prioritizes capturing enemy most valuable pieces first.
62        // Also prioritizes positive capture above all.
63        let mvv_lva = if let Some(victim) = move_info.captured() {
64            let attacker = move_info.piece_kind.centipawns();
65            let victim = victim.centipawns();
66            (true, victim - attacker)
67        } else {
68            (false, Cp(0))
69        };
70
71        Self {
72            is_tt_move,
73            promotion,
74            mvv_lva,
75        }
76    }
77}
78
79/// Order all moves in a container completely, in order of worst move to best move.
80/// Best moves are near the end to allow for iterating from best to worst move by using
81/// `while let Some(move_) = move_list.pop() ...` or `for move_ in move_list.into_iter().rev() ...`
82///
83/// # Arguments
84///
85/// * `legal_moves`: List of MoveInfos for all legal moves of current position.
86/// * `maybe_key_move`: Transposition Table move for current position.
87pub fn order_all_moves(legal_moves: MoveInfoList, maybe_key_move: Option<Move>) -> MoveInfoList {
88    let mut ordering_vec: ArrayVec<(MoveInfo, OrderStrategy), MAX_MOVES> = legal_moves
89        .into_iter()
90        .map(|move_info| (move_info, OrderStrategy::from((move_info, maybe_key_move))))
91        .collect();
92
93    // Sort all moves using their OrderStrategy as a key.
94    ordering_vec.sort_unstable_by_key(|pair| pair.1);
95
96    // Convert ordering_vec back into a MoveInfoList with sorted moves.
97    ordering_vec.into_iter().map(|pair| pair.0).collect()
98}
99
100/// Pick and return the best move from a move list without allocation.
101/// When run to completion, this acts as a selection sort.
102pub fn pick_best_move(legal_moves: &mut MoveInfoList, key_move: Option<Move>) -> Option<MoveInfo> {
103    legal_moves
104        .iter()
105        .enumerate()
106        .max_by(|left, right| {
107            let left = OrderStrategy::from((*left.1, key_move));
108            let right = OrderStrategy::from((*right.1, key_move));
109
110            left.cmp(&right)
111        })
112        .map(|(index, _)| index)
113        .map(|index| legal_moves.swap_remove(index))
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::coretypes::{Move, PieceKind, Square::*};
120    use crate::fen::Fen;
121    use crate::transposition::NodeKind;
122    use crate::Position;
123
124    #[test]
125    fn order_all_moves_one_capture() {
126        let pos = Position::parse_fen("rnb1k1nr/pppp1ppp/8/4p3/3P4/8/PPP1PPPP/RN2KBNR b - - 3 11")
127            .unwrap();
128        let capture = Move::new(E5, D4, None);
129        let num_moves = 24; // Checked manually.
130        let legal_moves = pos
131            .get_legal_moves()
132            .into_iter()
133            .map(|move_| pos.move_info(move_))
134            .collect();
135        let mut ordered_legal_moves = order_all_moves(legal_moves, None);
136
137        assert_eq!(ordered_legal_moves.len(), num_moves);
138        assert_eq!(ordered_legal_moves.pop().unwrap().move_(), capture);
139    }
140
141    #[test]
142    fn node_kind_ordering() {
143        assert!(NodeKind::Pv > NodeKind::Cut);
144        assert!(NodeKind::Cut > NodeKind::All);
145    }
146
147    #[test]
148    fn order_strategy_cmp() {
149        let os = OrderStrategy::default();
150        let mut gt_os = OrderStrategy::default();
151        gt_os.is_tt_move = true;
152
153        let mut lt_os = OrderStrategy::default();
154        lt_os.promotion = Some(PieceKind::Queen.centipawns());
155
156        assert!(gt_os > os);
157        assert!(gt_os > lt_os);
158    }
159}