Skip to main content

turnbase/
state.rs

1//! Provided state shape for games with hidden information.
2
3use std::collections::BTreeMap;
4
5use crate::{PlayerId, Prng};
6
7/// State split into a public zone and per-player private zones, plus the
8/// match's random generator.
9///
10/// Redaction is mechanical: a player's observation is the public zone plus
11/// *their own* private entry, so there is no field to forget to strip. The
12/// backing map is not exposed; games reach private data through the accessors.
13/// Using this type is a convenience, not a requirement (`Game::State` is an
14/// associated type and can be any shape) but it makes the common hidden-info
15/// game turnkey.
16///
17/// The [`Prng`] lives here so it clones, serializes, and rewinds together with
18/// everything else: snapshot and resume are O(1), and a `Reversible` undo
19/// record can restore the stream position (see `ARCHITECTURE.md`). Games with
20/// their own `State` type should embed a [`Prng`] the same way.
21#[derive(Clone, PartialEq, Eq, Debug)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub struct State<P, Q> {
24    public: P,
25    private: BTreeMap<PlayerId, Q>,
26    prng: Prng,
27}
28
29impl<P, Q> State<P, Q> {
30    /// Creates a state with the given public zone, no private entries, and a
31    /// generator seeded from `seed`.
32    #[must_use]
33    pub const fn new(public: P, seed: u64) -> Self {
34        Self {
35            public,
36            private: BTreeMap::new(),
37            prng: Prng::new(seed),
38        }
39    }
40
41    /// Returns the public zone, visible to everyone.
42    #[must_use]
43    pub const fn public(&self) -> &P {
44        &self.public
45    }
46
47    /// Returns the public zone for mutation inside `apply`.
48    pub const fn public_mut(&mut self) -> &mut P {
49        &mut self.public
50    }
51
52    /// Sets `player`'s private zone, returning the previous value if any.
53    pub fn insert_private(&mut self, player: PlayerId, value: Q) -> Option<Q> {
54        self.private.insert(player, value)
55    }
56
57    /// Returns `player`'s private zone, or `None` if they have none.
58    #[must_use]
59    pub fn private(&self, player: PlayerId) -> Option<&Q> {
60        self.private.get(&player)
61    }
62
63    /// Returns a mutable reference to `player`'s private zone.
64    pub fn private_mut(&mut self, player: PlayerId) -> Option<&mut Q> {
65        self.private.get_mut(&player)
66    }
67
68    /// Removes and returns `player`'s private zone, if any.
69    ///
70    /// Needed to reverse a deal exactly: undoing a chance move that dealt a
71    /// private card must leave no stale entry behind, so a `Reversible::undo`
72    /// calls this to drop the card it handed out.
73    ///
74    /// # Example
75    /// ```
76    /// use turnbase::{PlayerId, State};
77    /// let mut state: State<(), u8> = State::new((), 0);
78    /// state.insert_private(PlayerId::new(0), 7); // deal
79    /// assert_eq!(state.remove_private(PlayerId::new(0)), Some(7)); // undo the deal
80    /// assert_eq!(state.private(PlayerId::new(0)), None);
81    /// ```
82    pub fn remove_private(&mut self, player: PlayerId) -> Option<Q> {
83        self.private.remove(&player)
84    }
85
86    /// Returns the match's generator.
87    #[must_use]
88    pub const fn rng(&self) -> &Prng {
89        &self.prng
90    }
91
92    /// Returns the match's generator for drawing randomness inside `apply`.
93    pub const fn rng_mut(&mut self) -> &mut Prng {
94        &mut self.prng
95    }
96}
97
98impl<P: Clone, Q: Clone> State<P, Q> {
99    /// Produces the standard observation for `viewer`: the public zone plus
100    /// their own private zone (`None` viewer, a spectator, sees public only).
101    ///
102    /// This is the default visibility rule. Games whose rule is inverted or
103    /// otherwise non-standard (e.g. Hanabi) build their [`PlayerView`] directly
104    /// instead of calling this. Clones the observed data, so games with a large
105    /// public zone may prefer a cheaper custom projection.
106    #[must_use]
107    pub fn view_for(&self, viewer: Option<PlayerId>) -> PlayerView<P, Q> {
108        PlayerView {
109            public: self.public.clone(),
110            own_private: viewer.and_then(|p| self.private(p)).cloned(),
111        }
112    }
113}
114
115/// The standard observation produced by [`State::view_for`]: the public zone
116/// and the viewer's own private zone, if any.
117#[derive(Clone, PartialEq, Eq, Debug)]
118#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
119pub struct PlayerView<P, Q> {
120    /// The public zone, visible to everyone.
121    pub public: P,
122    /// The viewer's own private zone, or `None` for a spectator.
123    pub own_private: Option<Q>,
124}
125
126#[cfg(test)]
127mod tests {
128    use super::State;
129    use crate::PlayerId;
130
131    #[test]
132    fn private_zones_are_per_player() {
133        let mut state: State<u32, &str> = State::new(0, 1);
134        state.insert_private(PlayerId::new(0), "a");
135        state.insert_private(PlayerId::new(1), "b");
136        assert_eq!(state.private(PlayerId::new(0)), Some(&"a"));
137        assert_eq!(state.private(PlayerId::new(1)), Some(&"b"));
138        assert_eq!(state.private(PlayerId::new(2)), None);
139    }
140
141    #[test]
142    fn view_for_shows_only_the_viewers_private_zone() {
143        let mut state: State<u32, &str> = State::new(7, 1);
144        state.insert_private(PlayerId::new(0), "secret0");
145        state.insert_private(PlayerId::new(1), "secret1");
146
147        let view = state.view_for(Some(PlayerId::new(0)));
148        assert_eq!(view.public, 7);
149        assert_eq!(view.own_private, Some("secret0"));
150
151        let spectator = state.view_for(None);
152        assert_eq!(spectator.public, 7);
153        assert_eq!(spectator.own_private, None);
154    }
155}