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