blunders_engine/
timeman.rs

1//! Time Management
2
3use std::convert::TryFrom;
4use std::time::Instant;
5
6use crate::coretypes::{Color, PlyKind};
7use crate::error::{self, ErrorKind};
8use crate::uci::SearchControls;
9
10const TIME_RATIO: u32 = 15; // Use 1/15th of remaining time per timed move.
11const OVERHEAD: u128 = 10; // Expected amount of time loss in ms.
12
13/// There are 4 supported search modes currently, Infinite, Standard, Depth, and MoveTime.  
14/// Infinite mode: do not stop searching. Search must be signaled externally to stop.  
15/// Standard mode: standard chess time controls with time per side.  
16/// Depth mode: search to a given depth.  
17/// MoveTime mode: search for a specified time per move.
18#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
19pub enum Mode {
20    Infinite,           // Search until told to stop. Requires `infinite`.
21    Standard(Standard), // Each player has a time limit. Requires `wtime`, `btime`.
22    Depth(Depth),       // Search to a given depth. Requires `depth`.
23    MoveTime(MoveTime), // Search for a specified amount of time. Requires `movetime`.
24}
25
26impl Mode {
27    /// Returns true if a search should be stopped.
28    pub fn stop(&self, root_player: Color, ply: PlyKind) -> bool {
29        match self {
30            Mode::Infinite => Infinite::stop(),
31            Mode::Depth(depth_mode) => depth_mode.stop(ply),
32            Mode::MoveTime(movetime_mode) => movetime_mode.stop(ply),
33            Mode::Standard(standard_mode) => standard_mode.stop(root_player, ply),
34        }
35    }
36
37    /// Returns a new Infinite Mode.
38    pub fn infinite() -> Self {
39        Self::Infinite
40    }
41
42    /// Returns a new Depth Mode.
43    pub fn depth(ply: PlyKind, movetime: Option<u32>) -> Self {
44        Self::Depth(Depth {
45            depth: ply,
46            instant: Instant::now(),
47            movetime,
48        })
49    }
50
51    /// Returns a new MoveTime mode.
52    pub fn movetime(movetime: u32, ply: Option<PlyKind>) -> Self {
53        Self::MoveTime(MoveTime {
54            movetime,
55            instant: Instant::now(),
56            depth: ply,
57        })
58    }
59
60    pub fn standard(
61        wtime: i32,
62        btime: i32,
63        winc: Option<u32>,
64        binc: Option<u32>,
65        moves_to_go: Option<u32>,
66        ply: Option<PlyKind>,
67    ) -> Self {
68        Self::Standard(Standard {
69            wtime,
70            btime,
71            winc,
72            binc,
73            moves_to_go,
74            depth: ply,
75            instant: Instant::now(),
76        })
77    }
78}
79
80impl TryFrom<SearchControls> for Mode {
81    type Error = error::Error;
82    fn try_from(controls: SearchControls) -> error::Result<Self> {
83        if Infinite::satisfied(&controls) {
84            Ok(Mode::Infinite)
85        } else if Standard::satisfied(&controls) {
86            Ok(Mode::standard(
87                controls.wtime.unwrap(),
88                controls.btime.unwrap(),
89                controls.winc,
90                controls.binc,
91                controls.moves_to_go,
92                controls.depth,
93            ))
94        } else if MoveTime::satisfied(&controls) {
95            Ok(Mode::movetime(controls.move_time.unwrap(), controls.depth))
96        } else if Depth::satisfied(&controls) {
97            Ok(Mode::depth(controls.depth.unwrap(), controls.move_time))
98        } else {
99            Err(ErrorKind::ModeNotSatisfied.into())
100        }
101    }
102}
103
104#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
105pub struct Infinite;
106
107impl Infinite {
108    fn stop() -> bool {
109        false
110    }
111    /// Returns true if search controls has all required fields for Infinite mode.
112    fn satisfied(search_controls: &SearchControls) -> bool {
113        search_controls.infinite
114    }
115}
116
117#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
118pub struct Depth {
119    pub depth: PlyKind,
120    instant: Instant,
121    movetime: Option<u32>,
122}
123
124impl Depth {
125    /// Depth mode stops when its depth limit is passed, or optionally if movetime is met.
126    fn stop(&self, ply: PlyKind) -> bool {
127        if ply > self.depth {
128            return true;
129        }
130
131        if let Some(movetime) = self.movetime {
132            let elapsed_ms = self.instant.elapsed().as_millis();
133            if elapsed_ms >= (movetime as u128).saturating_sub(OVERHEAD) {
134                return true;
135            }
136        }
137
138        return false;
139    }
140
141    /// Returns true if search controls has all required fields for Depth mode.
142    fn satisfied(search_controls: &SearchControls) -> bool {
143        search_controls.depth.is_some()
144    }
145}
146
147#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
148pub struct MoveTime {
149    movetime: u32,
150    instant: Instant,
151    depth: Option<PlyKind>,
152}
153
154impl MoveTime {
155    /// MoveTime mode stops after a given time has passed, or optionally if its depth is passed.
156    fn stop(&self, ply: PlyKind) -> bool {
157        let elapsed_ms = self.instant.elapsed().as_millis();
158        if elapsed_ms >= (self.movetime as u128).saturating_sub(OVERHEAD) {
159            return true;
160        }
161
162        if let Some(depth) = self.depth {
163            if ply > depth {
164                return true;
165            }
166        }
167
168        return false;
169    }
170
171    /// Returns true if search controls has all required fields for MoveTime mode.
172    fn satisfied(search_controls: &SearchControls) -> bool {
173        search_controls.move_time.is_some()
174    }
175}
176
177#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
178pub struct Standard {
179    instant: Instant,
180    wtime: i32,
181    btime: i32,
182    winc: Option<u32>,
183    binc: Option<u32>,
184    moves_to_go: Option<u32>,
185    depth: Option<PlyKind>,
186}
187
188impl Standard {
189    /// Standard stops after using some heuristic to determine how much of remaining time to use.
190    /// Optionally, stops when a depth is passed.
191    fn stop(&self, root_player: Color, ply: PlyKind) -> bool {
192        let target_elapsed = self.target_elapsed_ms(root_player);
193        let elapsed_ms = self.instant.elapsed().as_millis();
194
195        if elapsed_ms >= target_elapsed {
196            return true;
197        }
198
199        // Optional depth
200        if let Some(depth) = self.depth {
201            if ply > depth {
202                return true;
203            }
204        }
205
206        false
207    }
208
209    fn target_elapsed_ms(&self, root_player: Color) -> u128 {
210        let remaining_time = match root_player {
211            Color::White => self.wtime,
212            Color::Black => self.btime,
213        };
214
215        // Clamp to lower bound of 0.
216        let remaining_time: u128 = if remaining_time.is_negative() {
217            0
218        } else {
219            remaining_time as u128
220        };
221
222        (remaining_time / TIME_RATIO as u128).saturating_sub(OVERHEAD)
223    }
224
225    /// Returns true if search controls has all required fields for Standard Mode.
226    fn satisfied(search_controls: &SearchControls) -> bool {
227        search_controls.wtime.is_some() && search_controls.btime.is_some()
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn standard() {
237        let mut controls = SearchControls::default();
238        controls.wtime = Some(5000);
239        controls.btime = Some(5000);
240
241        let mode = Mode::try_from(controls);
242
243        assert!(mode.is_ok());
244        let mode = mode.unwrap();
245        assert!(matches!(mode, Mode::Standard(_)));
246    }
247}