1#[cfg(test)]
5mod tests;
6
7use crate::error::{Error, Result};
8use crate::player::PlayerId;
9use derive_more::Display;
10use jiff::Zoned;
11use nil_util::ConstDeref;
12use nil_util::iter::IterExt;
13use serde::{Deserialize, Serialize};
14use std::collections::HashSet;
15use std::fmt;
16use std::num::NonZeroU32;
17use strum::EnumIs;
18
19#[derive(Clone, Debug, Deserialize, Serialize)]
20#[derive_const(Default)]
21#[serde(rename_all = "camelCase")]
22#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
23pub struct Round {
24 id: RoundId,
25 state: RoundState,
26 started_at: Option<Zoned>,
27}
28
29impl Round {
30 pub(crate) fn start<I>(&mut self, players: I) -> Result<()>
31 where
32 I: IntoIterator<Item = PlayerId>,
33 {
34 if let RoundState::Idle = &self.state {
35 self.started_at = Some(Zoned::now());
36 self.wait_players(players);
37 Ok(())
38 } else {
39 Err(Error::RoundAlreadyStarted)
40 }
41 }
42
43 pub(crate) fn next<I>(&mut self, players: I) -> Result<()>
45 where
46 I: IntoIterator<Item = PlayerId>,
47 {
48 match &self.state {
49 RoundState::Idle => Err(Error::RoundNotStarted),
50 RoundState::Waiting { pending, .. } if !pending.is_empty() => {
51 Err(Error::RoundHasPendingPlayers)
52 }
53 RoundState::Waiting { .. } | RoundState::Done => {
54 self.id = self.id.next();
55 self.started_at = Some(Zoned::now());
56 self.wait_players(players);
57 Ok(())
58 }
59 }
60 }
61
62 fn wait_players<I>(&mut self, players: I)
67 where
68 I: IntoIterator<Item = PlayerId>,
69 {
70 let pending = players.into_iter().collect_set();
71 if pending.is_empty() {
72 self.dangerously_set_done();
73 } else {
74 let ready = HashSet::with_capacity(pending.len());
75 self.state = RoundState::Waiting { pending, ready };
76 }
77 }
78
79 pub(crate) fn set_ready(&mut self, player: &PlayerId, is_ready: bool) {
80 if let RoundState::Waiting { pending, ready } = &mut self.state {
81 #[expect(clippy::collapsible_else_if)]
82 if is_ready {
83 if pending.remove(player) {
84 ready.insert(player.clone());
85 }
86 } else {
87 if ready.remove(player) {
88 pending.insert(player.clone());
89 }
90 }
91
92 if pending.is_empty() {
93 self.dangerously_set_done();
94 }
95 }
96 }
97
98 pub(crate) fn dangerously_set_done(&mut self) {
99 debug_assert!(!self.state.is_idle());
100 self.state = RoundState::Done;
101 }
102
103 #[inline]
104 pub fn id(&self) -> RoundId {
105 self.id
106 }
107
108 #[inline]
109 pub fn state(&self) -> &RoundState {
110 &self.state
111 }
112
113 #[inline]
114 pub fn is_idle(&self) -> bool {
115 self.state.is_idle()
116 }
117
118 #[inline]
119 pub fn is_done(&self) -> bool {
120 self.state.is_done()
121 }
122
123 #[inline]
124 pub fn is_waiting(&self) -> bool {
125 self.state.is_waiting()
126 }
127
128 #[inline]
129 pub fn is_waiting_player(&self, player: &PlayerId) -> bool {
130 if let RoundState::Waiting { pending, ready } = &self.state {
131 pending.contains(player) || ready.contains(player)
132 } else {
133 false
134 }
135 }
136
137 #[inline]
138 pub fn is_player_pending(&self, player: &PlayerId) -> bool {
139 if let RoundState::Waiting { pending, .. } = &self.state {
140 pending.contains(player)
141 } else {
142 false
143 }
144 }
145
146 #[inline]
147 pub fn is_player_ready(&self, player: &PlayerId) -> bool {
148 if let RoundState::Waiting { ready, .. } = &self.state {
149 ready.contains(player)
150 } else {
151 false
152 }
153 }
154
155 #[inline]
156 pub fn started_at(&self) -> Result<&Zoned> {
157 self
158 .started_at
159 .as_ref()
160 .ok_or(Error::RoundNotStarted)
161 }
162}
163
164#[derive(Copy, Debug, Display, Deserialize, Serialize, ConstDeref)]
165#[derive_const(Clone, PartialEq, Eq, PartialOrd, Ord)]
166#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
167pub struct RoundId(NonZeroU32);
168
169impl RoundId {
170 #[must_use]
171 const fn next(self) -> RoundId {
172 Self(self.0.saturating_add(1))
173 }
174}
175
176impl const Default for RoundId {
177 fn default() -> Self {
178 Self(NonZeroU32::MIN)
179 }
180}
181
182impl const PartialEq<u32> for RoundId {
183 fn eq(&self, other: &u32) -> bool {
184 self.0.get().eq(other)
185 }
186}
187
188#[derive(Clone, Deserialize, Serialize, EnumIs)]
189#[derive_const(Default)]
190#[serde(tag = "kind", rename_all = "kebab-case")]
191#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
192pub enum RoundState {
193 #[default]
195 Idle,
196
197 Waiting {
199 pending: HashSet<PlayerId>,
200 ready: HashSet<PlayerId>,
201 },
202
203 Done,
205}
206
207impl fmt::Debug for RoundState {
208 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209 match self {
210 Self::Idle => write!(f, "Idle"),
211 Self::Done => write!(f, "Done"),
212 Self::Waiting { pending, ready } => {
213 f.debug_struct("Waiting")
214 .field("pending", &pending.len())
215 .field("ready", &ready.len())
216 .finish()
217 }
218 }
219 }
220}