use crate::domain::time::Timestamp;
use crate::domain::time_control::{Clocks, TimeControl};
use crate::position::Position;
use crate::terminal::{move_cap, move_limit, repetition};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct SessionState {
position: Position,
clocks: Clocks,
time_control: TimeControl,
last_attestation: Timestamp,
history: HashMap<String, u32>,
repetition_count: u32,
halfmove_clock: u32,
half_move: u32,
}
impl SessionState {
#[must_use]
pub fn start(position: Position, time_control: TimeControl, anchor: Timestamp) -> Self {
let clocks = Clocks::start(&time_control);
let mut history = HashMap::new();
history.insert(position.to_feen(), 1);
Self {
position,
clocks,
time_control,
last_attestation: anchor,
history,
repetition_count: 1,
halfmove_clock: 0,
half_move: 1,
}
}
#[inline]
#[must_use]
pub const fn position(&self) -> &Position {
&self.position
}
#[inline]
#[must_use]
pub const fn clocks(&self) -> Clocks {
self.clocks
}
#[inline]
#[must_use]
pub const fn time_control(&self) -> &TimeControl {
&self.time_control
}
#[inline]
#[must_use]
pub const fn last_attestation(&self) -> Timestamp {
self.last_attestation
}
#[inline]
#[must_use]
pub const fn half_move(&self) -> u32 {
self.half_move
}
#[inline]
#[must_use]
pub const fn halfmove_clock(&self) -> u32 {
self.halfmove_clock
}
#[inline]
#[must_use]
pub const fn threefold_repetition(&self) -> bool {
self.repetition_count >= repetition::THREEFOLD as u32
}
#[inline]
#[must_use]
pub fn move_limit_reached(&self) -> bool {
move_limit::limit_reached(self.halfmove_clock)
}
#[inline]
#[must_use]
pub fn move_cap_reached(&self) -> bool {
move_cap::cap_reached(self.half_move.saturating_sub(1))
}
#[must_use]
pub fn advance(
mut self,
position: Position,
clocks: Clocks,
attestation_at: Timestamp,
irreversible: bool,
) -> Self {
let repetition_count = *self
.history
.entry(position.to_feen())
.and_modify(|count| *count = count.saturating_add(1))
.or_insert(1);
let halfmove_clock = if irreversible {
0
} else {
self.halfmove_clock.saturating_add(1)
};
let half_move = self.half_move.saturating_add(1);
Self {
position,
clocks,
time_control: self.time_control,
last_attestation: self.last_attestation.max(attestation_at),
history: self.history,
repetition_count,
halfmove_clock,
half_move,
}
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
use super::SessionState;
use crate::domain::time::{Duration, Timestamp};
use crate::domain::time_control::{Clocks, Period, TimeControl};
use crate::position::Position;
fn pos(feen: &str) -> Position {
Position::parse(feen).expect("valid Sanki FEEN")
}
fn time_control() -> TimeControl {
let period = Period::new(Duration::from_secs(60), None, None).expect("valid period");
TimeControl::new(period, Vec::new())
}
fn ts(secs: i64) -> Timestamp {
Timestamp::from_unix(secs)
}
const START_FEEN: &str = "4k^3/8/8/8/8/8/8/4K^3 / W/w";
#[test]
fn start_initializes_the_state() {
let tc = time_control();
let expected_clocks = Clocks::start(&tc);
let state = SessionState::start(pos(START_FEEN), tc, ts(1000));
assert_eq!(state.half_move(), 1);
assert_eq!(state.halfmove_clock(), 0);
assert_eq!(state.last_attestation(), ts(1000));
assert_eq!(state.clocks(), expected_clocks);
assert!(!state.threefold_repetition());
assert!(!state.move_limit_reached());
assert_eq!(state.position().to_feen(), START_FEEN);
}
#[test]
fn advance_increments_half_move_and_counter() {
let state = SessionState::start(pos(START_FEEN), time_control(), ts(1000));
let next_feen = "3k^4/8/8/8/8/8/8/4K^3 / w/W";
let clocks = state.clocks();
let next = state.advance(pos(next_feen), clocks, ts(1030), false);
assert_eq!(next.half_move(), 2);
assert_eq!(next.halfmove_clock(), 1);
assert_eq!(next.last_attestation(), ts(1030));
assert_eq!(next.position().to_feen(), next_feen);
}
#[test]
fn advance_never_rewinds_the_attestation_anchor() {
let state = SessionState::start(pos(START_FEEN), time_control(), ts(1000));
let next_feen = "3k^4/8/8/8/8/8/8/4K^3 / w/W";
let clocks = state.clocks();
let next = state.advance(pos(next_feen), clocks, ts(950), false); assert_eq!(next.last_attestation(), ts(1000)); }
#[test]
fn advance_irreversible_resets_the_counter() {
let state = SessionState::start(pos(START_FEEN), time_control(), ts(1000));
let other = "3k^4/8/8/8/8/8/8/4K^3 / w/W";
let clocks = state.clocks();
let state = state.advance(pos(other), clocks, ts(1010), false);
let state = state.advance(pos(START_FEEN), clocks, ts(1020), false);
assert_eq!(state.halfmove_clock(), 2);
let state = state.advance(pos(other), clocks, ts(1030), true);
assert_eq!(state.halfmove_clock(), 0);
}
#[test]
fn move_limit_reached_at_a_hundred_plies() {
let mut state = SessionState::start(pos(START_FEEN), time_control(), ts(0));
let clocks = state.clocks();
let other = "3k^4/8/8/8/8/8/8/4K^3 / w/W";
for i in 0..99 {
let feen = if i % 2 == 0 { other } else { START_FEEN };
state = state.advance(pos(feen), clocks, ts(i64::from(i)), false);
}
assert_eq!(state.halfmove_clock(), 99);
assert!(!state.move_limit_reached());
state = state.advance(pos(other), clocks, ts(100), false);
assert_eq!(state.halfmove_clock(), 100);
assert!(state.move_limit_reached());
}
#[test]
fn threefold_at_the_third_occurrence() {
let state = SessionState::start(pos(START_FEEN), time_control(), ts(0));
let clocks = state.clocks();
let state = state.advance(pos(START_FEEN), clocks, ts(10), false);
assert!(!state.threefold_repetition());
let state = state.advance(pos(START_FEEN), clocks, ts(20), false);
assert!(state.threefold_repetition());
}
#[test]
fn move_cap_reached_at_600_plies() {
let mut state = SessionState::start(pos(START_FEEN), time_control(), ts(0));
let clocks = state.clocks();
let other = "3k^4/8/8/8/8/8/8/4K^3 / w/W";
for i in 0..599 {
let feen = if i % 2 == 0 { other } else { START_FEEN };
state = state.advance(pos(feen), clocks, ts(i64::from(i)), true);
}
assert!(!state.move_cap_reached());
state = state.advance(pos(other), clocks, ts(600), true);
assert!(state.move_cap_reached());
}
}