Skip to main content

turnbase/
pile.rs

1//! An ordered pile of items: a deck, hand, discard, or any zone of cards.
2
3use crate::Prng;
4
5/// An ordered pile of items with deterministic operations.
6///
7/// The "top" is the end of the pile: [`draw`](Pile::draw) takes from the top
8/// and [`put`](Pile::put) adds to it. Shuffling is deterministic given a
9/// [`Prng`], and the pile carries no generator of its own, so it snapshots and
10/// replays with whatever state it lives in. This is an opt-in helper; a game's
11/// `State` can hold piles or plain fields as it prefers.
12#[derive(Clone, PartialEq, Eq, Debug, Default)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct Pile<T> {
15    items: Vec<T>,
16}
17
18impl<T> Pile<T> {
19    /// Creates an empty pile.
20    #[must_use]
21    pub const fn new() -> Self {
22        Self { items: Vec::new() }
23    }
24
25    /// Creates a pile from `items`, with the last element on top.
26    #[must_use]
27    pub const fn from_items(items: Vec<T>) -> Self {
28        Self { items }
29    }
30
31    /// Returns the number of items.
32    #[must_use]
33    pub const fn len(&self) -> usize {
34        self.items.len()
35    }
36
37    /// Returns true if the pile is empty.
38    #[must_use]
39    pub const fn is_empty(&self) -> bool {
40        self.items.is_empty()
41    }
42
43    /// Removes and returns the top item, or `None` if empty.
44    pub fn draw(&mut self) -> Option<T> {
45        self.items.pop()
46    }
47
48    /// Removes up to `n` items from the top, returned top-first.
49    pub fn draw_n(&mut self, n: usize) -> Vec<T> {
50        let mut drawn = Vec::new();
51        for _ in 0..n {
52            match self.items.pop() {
53                Some(item) => drawn.push(item),
54                None => break,
55            }
56        }
57        drawn
58    }
59
60    /// Adds an item to the top.
61    pub fn put(&mut self, item: T) {
62        self.items.push(item);
63    }
64
65    /// Adds an item to the bottom.
66    pub fn put_bottom(&mut self, item: T) {
67        self.items.insert(0, item);
68    }
69
70    /// Inserts an item at `index` (clamped to the length), preserving order.
71    ///
72    /// Use this to reverse a draw exactly, e.g. in a `Reversible` undo.
73    pub fn insert(&mut self, index: usize, item: T) {
74        self.items.insert(index.min(self.items.len()), item);
75    }
76
77    /// Removes and returns the item at `index`, or `None` if out of range.
78    pub fn remove(&mut self, index: usize) -> Option<T> {
79        if index < self.items.len() {
80            Some(self.items.remove(index))
81        } else {
82            None
83        }
84    }
85
86    /// Shuffles the pile in place with a deterministic Fisher-Yates using `rng`.
87    pub fn shuffle(&mut self, rng: &mut Prng) {
88        rng.shuffle(&mut self.items);
89    }
90
91    /// Returns the items, bottom to top.
92    #[must_use]
93    pub fn as_slice(&self) -> &[T] {
94        &self.items
95    }
96
97    /// Iterates the items, bottom to top.
98    pub fn iter(&self) -> std::slice::Iter<'_, T> {
99        self.items.iter()
100    }
101}
102
103impl<T: PartialEq> Pile<T> {
104    /// Returns true if the pile contains `item`.
105    #[must_use]
106    pub fn contains(&self, item: &T) -> bool {
107        self.items.contains(item)
108    }
109
110    /// Returns the index of the first item equal to `item`, or `None`.
111    #[must_use]
112    pub fn position(&self, item: &T) -> Option<usize> {
113        self.items.iter().position(|candidate| candidate == item)
114    }
115
116    /// Removes and returns the first item equal to `item`, or `None`.
117    pub fn remove_item(&mut self, item: &T) -> Option<T> {
118        self.position(item).and_then(|index| self.remove(index))
119    }
120}
121
122impl<T> FromIterator<T> for Pile<T> {
123    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
124        Self {
125            items: iter.into_iter().collect(),
126        }
127    }
128}
129
130impl<'a, T> IntoIterator for &'a Pile<T> {
131    type Item = &'a T;
132    type IntoIter = std::slice::Iter<'a, T>;
133
134    fn into_iter(self) -> Self::IntoIter {
135        self.items.iter()
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::Pile;
142    use crate::Prng;
143
144    #[test]
145    fn draw_takes_from_the_top() {
146        let mut pile = Pile::from_items(vec![1, 2, 3]);
147        assert_eq!(pile.draw(), Some(3));
148        assert_eq!(pile.draw(), Some(2));
149        assert_eq!(pile.len(), 1);
150    }
151
152    #[test]
153    fn draw_n_stops_at_empty() {
154        let mut pile = Pile::from_items(vec![1, 2]);
155        assert_eq!(pile.draw_n(5), vec![2, 1]);
156        assert!(pile.is_empty());
157    }
158
159    #[test]
160    fn remove_then_insert_restores_order() {
161        let mut pile = Pile::from_items(vec!['a', 'b', 'c', 'd']);
162        let removed = pile.remove(1).unwrap();
163        assert_eq!(removed, 'b');
164        pile.insert(1, removed);
165        assert_eq!(pile.as_slice(), &['a', 'b', 'c', 'd']);
166    }
167
168    #[test]
169    fn shuffle_is_a_deterministic_permutation() {
170        let base: Vec<u32> = (0..50).collect();
171        let mut a = Pile::from_items(base.clone());
172        let mut b = Pile::from_items(base.clone());
173        a.shuffle(&mut Prng::new(7));
174        b.shuffle(&mut Prng::new(7));
175        assert_eq!(a, b, "same seed shuffles identically");
176
177        let mut sorted: Vec<u32> = a.iter().copied().collect();
178        sorted.sort_unstable();
179        assert_eq!(sorted, base, "shuffle is a permutation");
180    }
181
182    #[test]
183    fn contains_and_remove_item_use_equality() {
184        let mut pile = Pile::from_items(vec![10, 20, 30]);
185        assert!(pile.contains(&20));
186        assert_eq!(pile.position(&30), Some(2));
187        assert_eq!(pile.remove_item(&20), Some(20));
188        assert!(!pile.contains(&20));
189    }
190}