Skip to main content

turnbase/
active.rs

1//! The set of players who owe a decision right now.
2
3use std::collections::BTreeSet;
4
5use crate::player::PlayerId;
6
7/// An ordered, deterministic set of players who owe a decision.
8///
9/// Wraps a [`BTreeSet`] so iteration order is stable (ascending by seat index)
10/// rather than the per-process random order of a hashed set. Determinism is the
11/// engine's core promise, so ordering is part of the observable contract: two
12/// replays of the same seed and inputs must visit active players in the same
13/// order. The backing collection is intentionally not exposed.
14///
15/// Cardinality carries meaning:
16/// - empty during engine-only resolution steps (adjudicating simultaneous
17///   orders, running a deterministic resolution pass),
18/// - one for strictly alternating games (the common case),
19/// - many during simultaneous or secret phases.
20#[derive(Clone, PartialEq, Eq, Default, Debug)]
21pub struct ActivePlayers(BTreeSet<PlayerId>);
22
23impl ActivePlayers {
24    /// Returns the empty set (no player owes a decision).
25    #[must_use]
26    pub const fn none() -> Self {
27        Self(BTreeSet::new())
28    }
29
30    /// Returns a singleton set containing just `player`.
31    #[must_use]
32    pub fn one(player: PlayerId) -> Self {
33        let mut set = BTreeSet::new();
34        set.insert(player);
35        Self(set)
36    }
37
38    /// Returns the set of all real seats `0..num_players`.
39    ///
40    /// Does not include [`PlayerId::CHANCE`].
41    #[must_use]
42    pub fn all(num_players: u32) -> Self {
43        (0..num_players).map(PlayerId::new).collect()
44    }
45
46    /// Returns true if `player` owes a decision.
47    #[must_use]
48    pub fn contains(&self, player: PlayerId) -> bool {
49        self.0.contains(&player)
50    }
51
52    /// Returns the number of active players.
53    #[must_use]
54    pub fn len(&self) -> usize {
55        self.0.len()
56    }
57
58    /// Returns true if no player owes a decision.
59    #[must_use]
60    pub fn is_empty(&self) -> bool {
61        self.0.is_empty()
62    }
63
64    /// Iterates the active players in ascending seat order.
65    pub fn iter(&self) -> impl Iterator<Item = PlayerId> + '_ {
66        self.0.iter().copied()
67    }
68}
69
70impl FromIterator<PlayerId> for ActivePlayers {
71    fn from_iter<I: IntoIterator<Item = PlayerId>>(iter: I) -> Self {
72        Self(iter.into_iter().collect())
73    }
74}
75
76impl<'a> IntoIterator for &'a ActivePlayers {
77    type Item = PlayerId;
78    type IntoIter = std::iter::Copied<std::collections::btree_set::Iter<'a, PlayerId>>;
79
80    fn into_iter(self) -> Self::IntoIter {
81        self.0.iter().copied()
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::ActivePlayers;
88    use crate::player::PlayerId;
89
90    #[test]
91    fn none_is_empty() {
92        let a = ActivePlayers::none();
93        assert!(a.is_empty());
94        assert_eq!(a.len(), 0);
95    }
96
97    #[test]
98    fn one_contains_only_that_player() {
99        let a = ActivePlayers::one(PlayerId::new(2));
100        assert_eq!(a.len(), 1);
101        assert!(a.contains(PlayerId::new(2)));
102        assert!(!a.contains(PlayerId::new(0)));
103    }
104
105    #[test]
106    fn all_covers_the_range_excluding_chance() {
107        let a = ActivePlayers::all(3);
108        assert_eq!(a.len(), 3);
109        assert!(a.contains(PlayerId::new(0)));
110        assert!(a.contains(PlayerId::new(2)));
111        assert!(!a.contains(PlayerId::CHANCE));
112    }
113
114    #[test]
115    fn iteration_is_ascending_and_deduplicated() {
116        let a: ActivePlayers = [PlayerId::new(2), PlayerId::new(0), PlayerId::new(2)]
117            .into_iter()
118            .collect();
119        let order: Vec<u32> = a.iter().map(PlayerId::index).collect();
120        assert_eq!(order, vec![0, 2]);
121    }
122}