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