use crate::domain::time::Duration;
use crate::domain::time_control::{Clock, TimeControl};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tick {
Continued(Clock),
Flagged,
}
impl Tick {
#[inline]
#[must_use]
pub const fn is_flagged(self) -> bool {
matches!(self, Self::Flagged)
}
#[inline]
#[must_use]
pub const fn clock(self) -> Option<Clock> {
match self {
Self::Continued(clock) => Some(clock),
Self::Flagged => None,
}
}
}
#[must_use]
pub fn tick(tc: &TimeControl, clock: Clock, elapsed: Duration) -> Tick {
let Some(period) = tc.period(clock.period_index()) else {
return Tick::Flagged;
};
let increment = period.increment().unwrap_or(Duration::ZERO);
match period.plies() {
Some(quota) => {
let available = add(clock.remaining(), increment);
let Some(remaining_after) = available.checked_sub(elapsed) else {
return Tick::Flagged;
};
let plies_after = clock.plies_in_period().saturating_add(1);
if plies_after >= quota {
let next_index = clock.period_index().saturating_add(1);
let (index, reset) = match tc.period(next_index) {
Some(next) => (next_index, next),
None => (clock.period_index(), period),
};
return Tick::Continued(Clock::new(reset.duration(), index, 0));
}
Tick::Continued(Clock::new(
remaining_after,
clock.period_index(),
plies_after,
))
}
None => {
let Some(remaining_after_spend) = clock.remaining().checked_sub(elapsed) else {
let next_index = clock.period_index().saturating_add(1);
if let Some(next) = tc.period(next_index) {
let over = elapsed.saturating_sub(clock.remaining());
return tick(tc, Clock::new(next.duration(), next_index, 0), over);
}
return Tick::Flagged;
};
let remaining_after = add(remaining_after_spend, increment);
let plies_after = clock.plies_in_period().saturating_add(1);
Tick::Continued(Clock::new(
remaining_after,
clock.period_index(),
plies_after,
))
}
}
}
#[inline]
fn add(a: Duration, b: Duration) -> Duration {
Duration::from_secs(a.as_secs().saturating_add(b.as_secs()))
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
use super::{tick, Tick};
use crate::domain::time::Duration;
use crate::domain::time_control::{Clock, Period, TimeControl};
fn secs(n: u64) -> Duration {
Duration::from_secs(n)
}
fn period(duration: u64, increment: Option<u64>, plies: Option<u32>) -> Period {
Period::new(secs(duration), increment.map(secs), plies).expect("valid period")
}
fn tc(periods: Vec<Period>) -> TimeControl {
TimeControl::from_periods(periods).expect("valid time control")
}
#[test]
fn fischer_spend_and_increment() {
let control = tc(vec![period(300, Some(3), None)]);
let result = tick(&control, Clock::new(secs(300), 0, 0), secs(10));
assert_eq!(result, Tick::Continued(Clock::new(secs(293), 0, 1)));
}
#[test]
fn fischer_flags_when_out_of_time() {
let control = tc(vec![period(300, Some(3), None)]);
let result = tick(&control, Clock::new(secs(5), 0, 0), secs(10));
assert_eq!(result, Tick::Flagged);
assert!(result.is_flagged());
assert_eq!(result.clock(), None);
}
#[test]
fn fischer_increment_not_available_during_ply() {
let control = tc(vec![period(300, Some(3), None)]);
assert_eq!(
tick(&control, Clock::new(secs(5), 0, 0), secs(7)),
Tick::Flagged
);
}
#[test]
fn fixed_bank_depletes_linearly() {
let control = tc(vec![period(600, None, None)]);
let result = tick(&control, Clock::new(secs(600), 0, 0), secs(60));
assert_eq!(result, Tick::Continued(Clock::new(secs(540), 0, 1)));
}
#[test]
fn fixed_bank_terminal_flags() {
let control = tc(vec![period(600, None, None)]);
assert_eq!(
tick(&control, Clock::new(secs(50), 0, 0), secs(60)),
Tick::Flagged
);
}
#[test]
fn byoyomi_per_move_resets() {
let control = tc(vec![period(0, Some(30), Some(1))]);
let result = tick(&control, Clock::new(secs(0), 0, 0), secs(20));
assert_eq!(result, Tick::Continued(Clock::new(secs(0), 0, 0)));
assert_eq!(
tick(&control, Clock::new(secs(0), 0, 0), secs(40)),
Tick::Flagged
);
}
#[test]
fn quota_reached_advances_period() {
let control = tc(vec![
period(5400, Some(30), Some(40)),
period(1800, Some(30), None),
]);
let result = tick(&control, Clock::new(secs(1000), 0, 39), secs(10));
assert_eq!(result, Tick::Continued(Clock::new(secs(1800), 1, 0)));
}
#[test]
fn rollover_from_main_bank_to_overtime() {
let control = tc(vec![period(3600, None, None), period(0, Some(30), Some(1))]);
let result = tick(&control, Clock::new(secs(10), 0, 0), secs(25));
assert_eq!(result, Tick::Continued(Clock::new(secs(0), 1, 0)));
}
#[test]
fn rollover_then_flag_if_overtime_insufficient() {
let control = tc(vec![period(3600, None, None), period(0, Some(30), Some(1))]);
assert_eq!(
tick(&control, Clock::new(secs(10), 0, 0), secs(50)),
Tick::Flagged
);
}
}