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::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  /// Tries to advance to the next round.
40  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  /// Sets the round state to [`RoundState::Waiting`],
59  /// where players are expected to take their turns.
60  ///
61  /// If `players` is empty, the round will be set to [`RoundState::Done`] instead.
62  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  /// Clones the round, setting its state to [`RoundState::Idle`].
152  /// This is useful for saving the game.
153  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  /// The game hasn't started yet.
186  #[default]
187  Idle,
188
189  /// There are players who haven't finished their turn yet.
190  Waiting {
191    pending: HashSet<PlayerId>,
192    ready: HashSet<PlayerId>,
193  },
194
195  /// The round is finished.
196  Done,
197}