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.dangerously_set_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.dangerously_set_done();
87 }
88 }
89 }
90
91 pub(crate) fn dangerously_set_done(&mut self) {
92 debug_assert!(!self.state.is_idle());
93 self.state = RoundState::Done;
94 }
95
96 #[inline]
97 pub fn id(&self) -> RoundId {
98 self.id
99 }
100
101 #[inline]
102 pub fn is_idle(&self) -> bool {
103 self.state.is_idle()
104 }
105
106 #[inline]
107 pub fn is_done(&self) -> bool {
108 self.state.is_done()
109 }
110
111 #[inline]
112 pub fn is_waiting(&self) -> bool {
113 self.state.is_waiting()
114 }
115
116 #[inline]
117 pub fn is_waiting_player(&self, player: &PlayerId) -> bool {
118 if let RoundState::Waiting { pending, ready } = &self.state {
119 pending.contains(player) || ready.contains(player)
120 } else {
121 false
122 }
123 }
124
125 #[inline]
126 pub fn is_player_pending(&self, player: &PlayerId) -> bool {
127 if let RoundState::Waiting { pending, .. } = &self.state {
128 pending.contains(player)
129 } else {
130 false
131 }
132 }
133
134 #[inline]
135 pub fn is_player_ready(&self, player: &PlayerId) -> bool {
136 if let RoundState::Waiting { ready, .. } = &self.state {
137 ready.contains(player)
138 } else {
139 false
140 }
141 }
142
143 #[inline]
144 pub fn started_at(&self) -> Result<&Zoned> {
145 self
146 .started_at
147 .as_ref()
148 .ok_or(Error::RoundNotStarted)
149 }
150
151 pub(crate) fn to_idle(&self) -> Self {
154 let mut round = self.clone();
155 round.state = RoundState::Idle;
156 round
157 }
158}
159
160#[derive(Clone, Copy, Debug, Deref, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
161pub struct RoundId(NonZeroU32);
162
163impl RoundId {
164 #[must_use]
165 const fn next(self) -> RoundId {
166 Self(self.0.saturating_add(1))
167 }
168}
169
170impl Default for RoundId {
171 fn default() -> Self {
172 Self(NonZeroU32::MIN)
173 }
174}
175
176impl PartialEq<u32> for RoundId {
177 fn eq(&self, other: &u32) -> bool {
178 self.0.get().eq(other)
179 }
180}
181
182#[derive(Clone, Debug, Default, Deserialize, Serialize, EnumIs)]
183#[serde(tag = "kind", rename_all = "kebab-case")]
184enum RoundState {
185 #[default]
187 Idle,
188
189 Waiting {
191 pending: HashSet<PlayerId>,
192 ready: HashSet<PlayerId>,
193 },
194
195 Done,
197}