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}