#[cfg(test)]
mod tests;
use crate::error::{Error, Result};
use crate::player::PlayerId;
use derive_more::Display;
use jiff::Zoned;
use nil_util::ConstDeref;
use nil_util::iter::IterExt;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt;
use std::num::NonZeroU32;
use strum::EnumIs;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive_const(Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct Round {
id: RoundId,
state: RoundState,
started_at: Option<Zoned>,
}
impl Round {
pub(crate) fn start<I>(&mut self, players: I) -> Result<()>
where
I: IntoIterator<Item = PlayerId>,
{
if let RoundState::Idle = &self.state {
self.started_at = Some(Zoned::now());
self.wait_players(players);
Ok(())
} else {
Err(Error::RoundAlreadyStarted)
}
}
pub(crate) fn next<I>(&mut self, players: I) -> Result<()>
where
I: IntoIterator<Item = PlayerId>,
{
match &self.state {
RoundState::Idle => Err(Error::RoundNotStarted),
RoundState::Waiting { pending, .. } if !pending.is_empty() => {
Err(Error::RoundHasPendingPlayers)
}
RoundState::Waiting { .. } | RoundState::Done => {
self.id = self.id.next();
self.started_at = Some(Zoned::now());
self.wait_players(players);
Ok(())
}
}
}
fn wait_players<I>(&mut self, players: I)
where
I: IntoIterator<Item = PlayerId>,
{
let pending = players.into_iter().collect_set();
if pending.is_empty() {
self.dangerously_set_done();
} else {
let ready = HashSet::with_capacity(pending.len());
self.state = RoundState::Waiting { pending, ready };
}
}
pub(crate) fn set_ready(&mut self, player: &PlayerId, is_ready: bool) {
if let RoundState::Waiting { pending, ready } = &mut self.state {
#[expect(clippy::collapsible_else_if)]
if is_ready {
if pending.remove(player) {
ready.insert(player.clone());
}
} else {
if ready.remove(player) {
pending.insert(player.clone());
}
}
if pending.is_empty() {
self.dangerously_set_done();
}
}
}
pub(crate) fn dangerously_set_done(&mut self) {
debug_assert!(!self.state.is_idle());
self.state = RoundState::Done;
}
#[inline]
pub fn id(&self) -> RoundId {
self.id
}
#[inline]
pub fn state(&self) -> &RoundState {
&self.state
}
#[inline]
pub fn is_idle(&self) -> bool {
self.state.is_idle()
}
#[inline]
pub fn is_done(&self) -> bool {
self.state.is_done()
}
#[inline]
pub fn is_waiting(&self) -> bool {
self.state.is_waiting()
}
#[inline]
pub fn is_waiting_player(&self, player: &PlayerId) -> bool {
if let RoundState::Waiting { pending, ready } = &self.state {
pending.contains(player) || ready.contains(player)
} else {
false
}
}
#[inline]
pub fn is_player_pending(&self, player: &PlayerId) -> bool {
if let RoundState::Waiting { pending, .. } = &self.state {
pending.contains(player)
} else {
false
}
}
#[inline]
pub fn is_player_ready(&self, player: &PlayerId) -> bool {
if let RoundState::Waiting { ready, .. } = &self.state {
ready.contains(player)
} else {
false
}
}
#[inline]
pub fn started_at(&self) -> Result<&Zoned> {
self
.started_at
.as_ref()
.ok_or(Error::RoundNotStarted)
}
}
#[derive(Copy, Debug, Display, Deserialize, Serialize, ConstDeref)]
#[derive_const(Clone, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct RoundId(NonZeroU32);
impl RoundId {
#[must_use]
const fn next(self) -> RoundId {
Self(self.0.saturating_add(1))
}
}
impl const Default for RoundId {
fn default() -> Self {
Self(NonZeroU32::MIN)
}
}
impl const PartialEq<u32> for RoundId {
fn eq(&self, other: &u32) -> bool {
self.0.get().eq(other)
}
}
#[derive(Clone, Deserialize, Serialize, EnumIs)]
#[derive_const(Default)]
#[serde(tag = "kind", rename_all = "kebab-case")]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub enum RoundState {
#[default]
Idle,
Waiting {
pending: HashSet<PlayerId>,
ready: HashSet<PlayerId>,
},
Done,
}
impl fmt::Debug for RoundState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Idle => write!(f, "Idle"),
Self::Done => write!(f, "Done"),
Self::Waiting { pending, ready } => {
f.debug_struct("Waiting")
.field("pending", &pending.len())
.field("ready", &ready.len())
.finish()
}
}
}
}