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.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  /// Clones the round, setting its state to [`RoundState::Idle`].
147  /// This is useful for saving the game.
148  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  /// The game hasn't started yet.
181  #[default]
182  Idle,
183
184  /// There are players who haven't finished their turn yet.
185  Waiting {
186    pending: HashSet<PlayerId>,
187    ready: HashSet<PlayerId>,
188  },
189
190  /// The round is finished.
191  Done,
192}