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}