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