use crate::event::{EventId, PublicKey};
use crate::selection::CANDIDATE_CAP;
use core::num::NonZeroUsize;
use sashite_sanki_engine::domain::side::Side;
use sashite_sanki_engine::domain::status::Outcome3;
use sashite_sanki_engine::domain::time::Timestamp;
use sashite_sanki_engine::domain::time_control::TimeControl;
use sashite_sanki_engine::kernel::state::SessionState;
use sashite_sanki_engine::position::Position;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Seats {
first: PublicKey,
second: PublicKey,
}
impl Seats {
#[inline]
#[must_use]
pub fn new(first: PublicKey, second: PublicKey) -> Option<Self> {
(first != second).then_some(Self { first, second })
}
#[inline]
#[must_use]
pub const fn first(&self) -> PublicKey {
self.first
}
#[inline]
#[must_use]
pub const fn second(&self) -> PublicKey {
self.second
}
#[inline]
#[must_use]
pub const fn player(&self, side: Side) -> PublicKey {
match side {
Side::First => self.first,
Side::Second => self.second,
}
}
#[inline]
#[must_use]
pub fn side_of(&self, pubkey: PublicKey) -> Option<Side> {
if pubkey == self.first {
Some(Side::First)
} else if pubkey == self.second {
Some(Side::Second)
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct SessionParams {
session: EventId,
timestamper: Option<PublicKey>,
seats: Seats,
time_control: TimeControl,
initial_position: Position,
start: Timestamp,
candidate_cap: NonZeroUsize,
}
impl SessionParams {
#[inline]
#[must_use]
pub fn new(
session: EventId,
timestamper: Option<PublicKey>,
seats: Seats,
time_control: TimeControl,
initial_position: Position,
start: Timestamp,
) -> Option<Self> {
(initial_position.active_side() == Side::First).then_some(Self {
session,
timestamper,
seats,
time_control,
initial_position,
start,
candidate_cap: CANDIDATE_CAP,
})
}
#[inline]
#[must_use]
pub const fn with_candidate_cap(mut self, candidate_cap: NonZeroUsize) -> Self {
self.candidate_cap = candidate_cap;
self
}
#[inline]
#[must_use]
pub const fn session(&self) -> EventId {
self.session
}
#[inline]
#[must_use]
pub const fn timestamper(&self) -> Option<PublicKey> {
self.timestamper
}
#[inline]
#[must_use]
pub const fn seats(&self) -> &Seats {
&self.seats
}
#[inline]
#[must_use]
pub const fn time_control(&self) -> &TimeControl {
&self.time_control
}
#[inline]
#[must_use]
pub const fn initial_position(&self) -> &Position {
&self.initial_position
}
#[inline]
#[must_use]
pub const fn start(&self) -> Timestamp {
self.start
}
#[inline]
#[must_use]
pub const fn candidate_cap(&self) -> NonZeroUsize {
self.candidate_cap
}
#[inline]
#[must_use]
pub const fn player(&self, side: Side) -> PublicKey {
self.seats.player(side)
}
#[inline]
#[must_use]
pub fn side_of(&self, pubkey: PublicKey) -> Option<Side> {
self.seats.side_of(pubkey)
}
#[inline]
#[must_use]
pub fn is_player(&self, pubkey: PublicKey) -> bool {
self.seats.side_of(pubkey).is_some()
}
#[inline]
#[must_use]
pub fn is_timestamper(&self, pubkey: PublicKey) -> bool {
self.timestamper == Some(pubkey)
}
#[must_use]
pub fn outcome_from_scores(&self, scores: [(PublicKey, u8); 2]) -> Option<Outcome3> {
let [(a, score_a), (b, score_b)] = scores;
let side_a = self.side_of(a)?;
let side_b = self.side_of(b)?;
if side_a == side_b {
return None;
}
let (first, second) = match side_a {
Side::First => (score_a, score_b),
Side::Second => (score_b, score_a),
};
match (first, second) {
(100, 0) => Some(Outcome3::FirstWins),
(50, 50) => Some(Outcome3::Draw),
(0, 100) => Some(Outcome3::SecondWins),
_ => None,
}
}
#[inline]
#[must_use]
pub const fn side_at(&self, half_move: u32) -> Side {
if half_move & 1 == 1 {
Side::First
} else {
Side::Second
}
}
#[inline]
#[must_use]
pub const fn step_at(&self, half_move: u32) -> u32 {
half_move.div_ceil(2)
}
#[inline]
#[must_use]
pub const fn player_at(&self, half_move: u32) -> PublicKey {
self.player(self.side_at(half_move))
}
#[inline]
#[must_use]
pub fn initial_state(&self) -> SessionState {
SessionState::start(
self.initial_position.clone(),
self.time_control.clone(),
self.start,
)
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
use super::{Seats, SessionParams};
use crate::event::{EventId, PublicKey};
use sashite_sanki_engine::domain::side::Side;
use sashite_sanki_engine::domain::status::Outcome3;
use sashite_sanki_engine::domain::time::{Duration, Timestamp};
use sashite_sanki_engine::domain::time_control::{Period, TimeControl};
use sashite_sanki_engine::position::Position;
fn pk(byte: u8) -> PublicKey {
PublicKey::from_bytes([byte; 32])
}
fn id(byte: u8) -> EventId {
EventId::from_bytes([byte; 32])
}
fn time_control() -> TimeControl {
let period = Period::new(Duration::from_secs(600), None, None).expect("valid period");
TimeControl::new(period, Vec::new())
}
const START_FEEN: &str = "4k^3/8/8/8/8/8/8/4K^3 / W/w";
fn params() -> SessionParams {
SessionParams::new(
id(1),
Some(pk(3)),
Seats::new(pk(10), pk(20)).expect("distinct players"),
time_control(),
Position::parse(START_FEEN).expect("valid Sanki FEEN"),
Timestamp::from_unix(1000),
)
.expect("first to move")
}
#[test]
fn maps_pubkey_to_side() {
let p = params();
assert_eq!(p.side_of(pk(10)), Some(Side::First));
assert_eq!(p.side_of(pk(20)), Some(Side::Second));
assert_eq!(p.side_of(pk(99)), None); }
#[test]
fn maps_side_to_player() {
let p = params();
assert_eq!(p.player(Side::First), pk(10));
assert_eq!(p.player(Side::Second), pk(20));
}
#[test]
fn recognizes_player_and_timestamper() {
let p = params();
assert!(p.is_player(pk(10)));
assert!(p.is_player(pk(20)));
assert!(!p.is_player(pk(3))); assert!(p.is_timestamper(pk(3)));
assert!(!p.is_timestamper(pk(10)));
}
#[test]
fn self_timed_session_designates_no_timestamper() {
let p = SessionParams::new(
id(1),
None,
Seats::new(pk(10), pk(20)).expect("distinct players"),
time_control(),
Position::parse(START_FEEN).expect("valid Sanki FEEN"),
Timestamp::from_unix(1000),
)
.expect("first to move");
assert_eq!(p.timestamper(), None);
assert!(!p.is_timestamper(pk(3)));
assert!(!p.is_timestamper(pk(10)));
assert_eq!(p.player(Side::First), pk(10));
assert_eq!(p.player_at(2), pk(20));
}
#[test]
fn play_order_positions_map_to_slots() {
let p = params();
assert_eq!(p.side_at(1), Side::First);
assert_eq!(p.side_at(2), Side::Second);
assert_eq!(p.side_at(3), Side::First);
assert_eq!(p.side_at(4), Side::Second);
assert_eq!(p.step_at(1), 1);
assert_eq!(p.step_at(2), 1);
assert_eq!(p.step_at(3), 2);
assert_eq!(p.step_at(4), 2);
assert_eq!(p.step_at(5), 3);
assert_eq!(p.player_at(1), pk(10));
assert_eq!(p.player_at(2), pk(20));
assert_eq!(p.player_at(3), pk(10));
}
#[test]
fn play_order_mapping_is_exact_at_the_u32_extremes() {
let p = params();
for n in [1_u32, 2, 3, 500, 2_147_483_646, 2_147_483_647] {
let odd = n
.checked_mul(2)
.and_then(|d| d.checked_sub(1))
.expect("fits");
let even = n.checked_mul(2).expect("fits");
assert_eq!(p.side_at(odd), Side::First, "position {odd}");
assert_eq!(p.step_at(odd), n, "position {odd}");
assert_eq!(p.side_at(even), Side::Second, "position {even}");
assert_eq!(p.step_at(even), n, "position {even}");
}
assert_eq!(p.side_at(u32::MAX), Side::First);
assert_eq!(p.step_at(u32::MAX), 2_147_483_648);
assert_eq!(p.player_at(u32::MAX), pk(10));
assert_eq!(p.side_at(u32::MAX - 1), Side::Second);
assert_eq!(p.step_at(u32::MAX - 1), 2_147_483_647);
assert_eq!(p.player_at(u32::MAX - 1), pk(20));
assert_eq!(p.step_at(0), 0);
}
#[test]
fn side_of_maps_only_the_two_players() {
let p = params();
assert_eq!(p.side_of(pk(10)), Some(Side::First));
assert_eq!(p.side_of(pk(20)), Some(Side::Second));
for stranger in [
pk(2), pk(3), pk(0), pk(255),
] {
assert_eq!(p.side_of(stranger), None, "{stranger} is not a player");
assert!(!p.is_player(stranger));
}
for player in [10_u8, 20] {
let mut near = [player; 32];
near[31] = player.wrapping_add(1);
let near = PublicKey::from_bytes(near);
assert_eq!(p.side_of(near), None);
let mut near = [player; 32];
near[0] = player.wrapping_sub(1);
let near = PublicKey::from_bytes(near);
assert_eq!(p.side_of(near), None);
}
}
#[test]
fn initial_kernel_state_starts_in_the_first_period() {
let main = Period::new(Duration::from_secs(900), None, None).expect("valid period");
let overtime = Period::new(
Duration::from_secs(0),
Some(Duration::from_secs(30)),
Some(1),
)
.expect("valid period");
let p = SessionParams::new(
id(1),
Some(pk(3)),
Seats::new(pk(10), pk(20)).expect("distinct players"),
TimeControl::new(main, vec![overtime]),
Position::parse(START_FEEN).expect("valid Sanki FEEN"),
Timestamp::from_unix(1000),
)
.expect("first to move");
assert_eq!(p.time_control().period_count(), 2);
let state = p.initial_state();
for side in [Side::First, Side::Second] {
let clock = state.clocks().get(side);
assert_eq!(clock.remaining(), Duration::from_secs(900), "{side:?}");
assert_eq!(clock.period_index(), 0, "{side:?}");
assert_eq!(clock.plies_in_period(), 0, "{side:?}");
}
assert_eq!(state.last_attestation(), Timestamp::from_unix(1000));
assert_eq!(state.position().active_side(), p.side_at(state.half_move()));
}
#[test]
fn seats_reject_a_player_against_themselves() {
assert!(Seats::new(pk(10), pk(10)).is_none());
let seats = Seats::new(pk(10), pk(20)).expect("distinct");
assert_eq!(seats.first(), pk(10));
assert_eq!(seats.second(), pk(20));
assert_eq!(seats.player(Side::Second), pk(20));
assert_eq!(seats.side_of(pk(20)), Some(Side::Second));
assert_eq!(seats.side_of(pk(30)), None);
}
#[test]
fn scores_map_to_the_seat_axis_outcome_whatever_the_tag_order() {
let p = params();
assert_eq!(
p.outcome_from_scores([(pk(10), 100), (pk(20), 0)]),
Some(Outcome3::FirstWins)
);
assert_eq!(
p.outcome_from_scores([(pk(20), 0), (pk(10), 100)]),
Some(Outcome3::FirstWins)
);
assert_eq!(
p.outcome_from_scores([(pk(20), 100), (pk(10), 0)]),
Some(Outcome3::SecondWins)
);
assert_eq!(
p.outcome_from_scores([(pk(10), 50), (pk(20), 50)]),
Some(Outcome3::Draw)
);
assert_eq!(p.outcome_from_scores([(pk(99), 100), (pk(20), 0)]), None);
assert_eq!(p.outcome_from_scores([(pk(10), 100), (pk(10), 0)]), None);
assert_eq!(p.outcome_from_scores([(pk(10), 70), (pk(20), 30)]), None);
assert_eq!(p.outcome_from_scores([(pk(10), 100), (pk(20), 100)]), None);
}
#[test]
fn initial_kernel_state() {
let p = params();
let state = p.initial_state();
assert_eq!(state.half_move(), 1);
assert_eq!(state.last_attestation(), Timestamp::from_unix(1000));
assert_eq!(state.position().to_feen(), START_FEEN);
assert!(!state.move_limit_reached());
}
#[test]
fn accessors() {
let p = params();
assert_eq!(p.session(), id(1));
assert_eq!(p.timestamper(), Some(pk(3)));
assert_eq!(p.start(), Timestamp::from_unix(1000));
assert_eq!(p.candidate_cap().get(), 8);
let three = core::num::NonZeroUsize::new(3).expect("non-zero");
assert_eq!(p.clone().with_candidate_cap(three).candidate_cap(), three);
assert_eq!(p.initial_position().to_feen(), START_FEEN);
}
}