Skip to main content

nil_core/round/
mod.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4#[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  /// Tries to advance to the next round.
41  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  /// Sets the round state to [`RoundState::Waiting`],
60  /// where players are expected to take their turns.
61  ///
62  /// If `players` is empty, the round will be set to [`RoundState::Done`] instead.
63  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  /// The game hasn't started yet.
184  #[default]
185  Idle,
186
187  /// There are players who haven't finished their turn yet.
188  Waiting {
189    pending: HashSet<PlayerId>,
190    ready: HashSet<PlayerId>,
191  },
192
193  /// The round is finished.
194  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}