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  /// Clones the round, setting its state to [`RoundState::Idle`].
158  /// This is useful for saving the game.
159  pub(crate) fn to_idle(&self) -> Self {
160    let mut round = self.clone();
161    round.state = RoundState::Idle;
162    round
163  }
164}
165
166#[derive(Clone, Copy, Debug, Deref, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
167pub struct RoundId(NonZeroU32);
168
169impl RoundId {
170  #[must_use]
171  const fn next(self) -> RoundId {
172    Self(self.0.saturating_add(1))
173  }
174}
175
176impl Default for RoundId {
177  fn default() -> Self {
178    Self(NonZeroU32::MIN)
179  }
180}
181
182impl PartialEq<u32> for RoundId {
183  fn eq(&self, other: &u32) -> bool {
184    self.0.get().eq(other)
185  }
186}
187
188#[derive(Clone, Default, Deserialize, Serialize, EnumIs)]
189#[serde(tag = "kind", rename_all = "kebab-case")]
190pub enum RoundState {
191  /// The game hasn't started yet.
192  #[default]
193  Idle,
194
195  /// There are players who haven't finished their turn yet.
196  Waiting {
197    pending: HashSet<PlayerId>,
198    ready: HashSet<PlayerId>,
199  },
200
201  /// The round is finished.
202  Done,
203}
204
205impl fmt::Debug for RoundState {
206  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207    match self {
208      Self::Idle => write!(f, "Idle"),
209      Self::Waiting { pending, ready } => {
210        f.debug_struct("Waiting")
211          .field("pending", &pending.len())
212          .field("ready", &ready.len())
213          .finish()
214      }
215      Self::Done => write!(f, "Done"),
216    }
217  }
218}