crabchess/positions/
timer.rs

1//! This module exposes an incremental chess timer which can be used to track a chess player's time
2//! remaining and move history and read and format PGN-style clock annotations.
3
4#![allow(clippy::cast_precision_loss)]
5
6use crate::{
7    error::{Err, Result},
8    pieces::Color::{self, Black, White},
9};
10use compact_str::{format_compact, CompactString};
11use std::{collections::BTreeMap, str::FromStr};
12
13/// Struct to hold both players' timers in a game of chess.
14#[derive(Clone, Debug)]
15pub struct Timers {
16    pub white: Timer,
17    pub black: Timer,
18}
19
20impl Timers {
21    /// Create timers with given time controls.
22    #[must_use]
23    pub fn new(allocated: u32, increment: u32) -> Self {
24        Self {
25            white: Timer::new(allocated, increment),
26            black: Timer::new(allocated, increment),
27        }
28    }
29
30    /// Get PGN tag.
31    ///
32    /// # Errors
33    ///
34    /// Returns an error if `White` and `Black` have different time controls.
35    pub fn pgn_tag(&self) -> Result<String> {
36        if self.white.allocated != self.black.allocated
37            || self.white.increment != self.black.increment
38        {
39            return Err(Err::DifferentTimeControlsError(
40                self.white.clone(),
41                self.black.clone(),
42            ));
43        }
44
45        Ok(self.white.pgn_tag())
46    }
47
48    /// Get reference to timer by color.
49    #[must_use]
50    pub const fn get(&self, color: Color) -> &Timer {
51        match color {
52            White => &self.white,
53            Black => &self.black,
54        }
55    }
56
57    /// Get mutable reference to timer by color.
58    pub fn get_mut(&mut self, color: Color) -> &mut Timer {
59        match color {
60            White => &mut self.white,
61            Black => &mut self.black,
62        }
63    }
64
65    /// Get value from history, regardless of color.
66    #[must_use]
67    pub fn get_history(&self, ply_number: &u16) -> Option<&f32> {
68        self.white
69            .history
70            .get(ply_number)
71            .or_else(|| self.black.history.get(ply_number))
72    }
73}
74
75/// An incremental chess timer. The value in `allocated` represents the initial time in seconds a
76/// player has on the clock. After each move, the clock will decrement by the time elapsed during
77/// the move and increment by the value in `increment`.
78#[derive(Clone)]
79pub struct Timer {
80    pub allocated: u32,
81    pub increment: u32,
82    pub remaining: f32,
83    /// Timer history is recorded in a hash table with ply number keys and seconds remaining as
84    /// values.
85    pub history: BTreeMap<u16, f32>,
86}
87
88impl core::fmt::Debug for Timer {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        write!(
91            f,
92            "Timer(allocated={}, increment={}, remaining={})",
93            self.allocated, self.increment, self.remaining
94        )
95    }
96}
97
98impl Timer {
99    /// Create a timer with `allocated` and `increment` values.
100    #[must_use]
101    pub const fn new(allocated: u32, increment: u32) -> Self {
102        Self {
103            allocated,
104            increment,
105            remaining: allocated as f32,
106            history: BTreeMap::new(),
107        }
108    }
109
110    /// Create a timer from a time control PGN header formatted like `"300+5"` or `"1200"`.
111    ///
112    /// # Errors
113    ///
114    /// Returns an error if time controls do not match the format.
115    pub fn from_pgn_tag(controls: &str) -> Result<Self> {
116        let (allocated, increment) = Self::read_control(controls)?;
117
118        Ok(Self::new(allocated, increment))
119    }
120
121    /// Write a PGN tag representing the timer's time controls.
122    #[must_use]
123    pub fn pgn_tag(&self) -> String {
124        let mut output = format!("{}", self.allocated);
125
126        if self.increment > 0 {
127            output.push_str(&format!("+{}", self.increment));
128        }
129
130        output
131    }
132
133    /// Set to 0 seconds remaining.
134    pub fn zero(&mut self) {
135        self.remaining = 0.0;
136    }
137
138    /// Read a time control formatted like: `"300+5"`.
139    ///
140    /// # Errors
141    ///
142    /// Returns an error if time controls do not match the format.
143    pub fn read_control(controls: &str) -> Result<(u32, u32)> {
144        let allocated: &str;
145        let increment: &str;
146
147        if let Some(tup) = controls.split_once('+') {
148            (allocated, increment) = tup;
149        } else {
150            (allocated, increment) = (controls, "0");
151        }
152
153        let Ok(allocated) = allocated.parse() else {
154            return Err(Err::ParseError("allocated", allocated.into()));
155        };
156        let Ok(increment) = increment.parse() else {
157            return Err(Err::ParseError("increment", increment.into()));
158        };
159
160        Ok((allocated, increment))
161    }
162
163    /// Add a move to the timer, updating fields `seconds_remaining` and `history`.
164    pub fn update(&mut self, ply_number: u16, update: Update) {
165        match update {
166            Update::ElapsedInMove(elapsed) => {
167                self.remaining -= elapsed;
168
169                if self.remaining > 0.0 {
170                    self.remaining += self.increment as f32;
171                } else if self.remaining < 0.0 {
172                    self.remaining = 0.0;
173                }
174            }
175            Update::Remaining(remaining) => self.remaining = remaining,
176        }
177
178        self.history.insert(ply_number, self.remaining);
179    }
180
181    /// Write a clock annotation from the current `seconds_remaining` value.
182    #[must_use]
183    pub fn annotation(&self) -> CompactString {
184        Update::write_annotation(self.remaining)
185    }
186
187    /// Return a `bool` representing whether the clock has run out.
188    #[must_use]
189    pub fn timeout(&self) -> bool {
190        self.remaining == 0.0
191    }
192}
193
194impl FromStr for Timer {
195    type Err = Err;
196
197    fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> {
198        Self::from_pgn_tag(s)
199    }
200}
201
202impl TryFrom<&str> for Timer {
203    type Error = Err;
204
205    fn try_from(value: &str) -> std::prelude::v1::Result<Self, Self::Error> {
206        Self::from_pgn_tag(value)
207    }
208}
209
210/// An update to a chess timer, based either on the new total seconds remaining or the seconds
211/// elapsed during the turn.
212#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
213pub enum Update {
214    ElapsedInMove(f32),
215    Remaining(f32),
216}
217
218impl Update {
219    /// Reads a clock annotation in `[%clk 0:00:00.0]` format, returning `Self::Remaining`.
220    ///
221    /// # Errors
222    ///
223    /// Returns an error if annotation does not match format.
224    pub fn read_annotation(annotation: &str) -> Result<Self> {
225        let mut as_str = annotation;
226        if let Some(stripped) = as_str.strip_prefix("[%clk ") {
227            as_str = stripped;
228        }
229        if let Some(stripped) = as_str.strip_suffix(']') {
230            as_str = stripped;
231        }
232        let mut split_str = as_str.split(':');
233
234        let Some(hours_str) = split_str.next() else {
235            return Err(Err::ParseError("timer annotation", annotation.into()));
236        };
237        let Ok(hours) = hours_str.parse::<f32>() else {
238            return Err(Err::ParseError("timer annotation", annotation.into()));
239        };
240
241        let Some(minutes_str) = split_str.next() else {
242            return Err(Err::ParseError("timer annotation", annotation.into()));
243        };
244        let Ok(minutes) = minutes_str.parse::<f32>() else {
245            return Err(Err::ParseError("timer annotation", annotation.into()));
246        };
247
248        let Some(seconds_str) = split_str.next() else {
249            return Err(Err::ParseError("timer annotation", annotation.into()));
250        };
251        let Ok(seconds) = seconds_str.parse::<f32>() else {
252            return Err(Err::ParseError("timer annotation", annotation.into()));
253        };
254
255        Ok(Self::Remaining(
256            hours.mul_add(3600.0, minutes * 60.0) + seconds,
257        ))
258    }
259
260    /// Writes a clock annotation in `[%clk 0:00:00.0]` format.
261    #[must_use]
262    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
263    pub fn write_annotation(time: f32) -> CompactString {
264        let mut parts: f32 = time;
265
266        let hours = (parts / 3600.0).floor() as usize;
267        parts %= 3600.0;
268
269        let minutes = (parts / 60.0).floor() as u8;
270        parts %= 60.0;
271
272        format_compact!("[%clk {hours}:{minutes:02}:{parts:04.1}]")
273    }
274
275    /// Get an update's floating-point value.
276    #[must_use]
277    pub const fn value(&self) -> f32 {
278        let (Self::ElapsedInMove(r) | Self::Remaining(r)) = self;
279        *r
280    }
281}
282
283#[cfg(test)]
284#[allow(clippy::float_cmp)]
285mod tests {
286    use super::*;
287
288    #[test]
289    fn update_timer() {
290        let mut timer = Timer::new(400, 20);
291        timer.update(5, Update::ElapsedInMove(53.456));
292        assert_eq!(timer.remaining, 366.544);
293    }
294
295    #[test]
296    fn make_annotation() {
297        let mut timer = Timer::new(400, 20);
298        timer.update(5, Update::ElapsedInMove(53.456));
299        assert_eq!("[%clk 0:06:06.5]", timer.annotation());
300    }
301
302    #[test]
303    fn test_read_annotation() {
304        assert_eq!(
305            [366.5, 11532.2, 7203.2, 0.0],
306            [
307                "[%clk 0:06:06.5]",
308                "3:12:12.2",
309                "[%clk 2:00:03.2]",
310                "0:00:00.0",
311            ]
312            .map(|ann| {
313                let Update::Remaining(val) = Update::read_annotation(ann).unwrap() else {
314                    panic!("Expected Update::Remaining")
315                };
316
317                val
318            })
319        );
320    }
321
322    #[test]
323    fn test_write_annotation() {
324        assert_eq!(
325            [366.5, 11532.2, 7203.2, 0.0].map(Update::write_annotation),
326            [
327                "[%clk 0:06:06.5]",
328                "[%clk 3:12:12.2]",
329                "[%clk 2:00:03.2]",
330                "[%clk 0:00:00.0]",
331            ]
332        );
333    }
334}