Skip to main content

yahtzee_engine/
category.rs

1//! The thirteen scoring categories and sets thereof.
2
3use core::fmt;
4
5/// A scoring category on the card.
6///
7/// The discriminant is the category's bit position in [`Categories`],
8/// upper section first, so `Aces..=Sixes` are bits `0..=5`.
9#[repr(u8)]
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub enum Category {
12    /// Total of dice showing 1.
13    Aces,
14    /// Total of dice showing 2.
15    Twos,
16    /// Total of dice showing 3.
17    Threes,
18    /// Total of dice showing 4.
19    Fours,
20    /// Total of dice showing 5.
21    Fives,
22    /// Total of dice showing 6.
23    Sixes,
24    /// Total of all dice, if at least three show the same face.
25    ThreeOfAKind,
26    /// Total of all dice, if at least four show the same face.
27    FourOfAKind,
28    /// 25 points for three of one face and two of another.
29    FullHouse,
30    /// 30 points for four faces in sequence.
31    SmallStraight,
32    /// 40 points for five faces in sequence.
33    LargeStraight,
34    /// 50 points for five of a kind.
35    Yahtzee,
36    /// Total of all dice, no requirement.
37    Chance,
38}
39
40impl Category {
41    /// All thirteen categories in card order.
42    pub const ALL: [Self; 13] = [
43        Self::Aces,
44        Self::Twos,
45        Self::Threes,
46        Self::Fours,
47        Self::Fives,
48        Self::Sixes,
49        Self::ThreeOfAKind,
50        Self::FourOfAKind,
51        Self::FullHouse,
52        Self::SmallStraight,
53        Self::LargeStraight,
54        Self::Yahtzee,
55        Self::Chance,
56    ];
57
58    /// The face this upper-section category counts, or [`None`] for the
59    /// lower section.
60    #[must_use]
61    pub const fn upper_face(self) -> Option<u8> {
62        let index = self as u8;
63        if index < 6 { Some(index + 1) } else { None }
64    }
65
66    /// The upper-section category counting `face` (`1..=6`).
67    ///
68    /// Returns [`None`] for faces outside `1..=6`.
69    #[must_use]
70    pub const fn upper(face: u8) -> Option<Self> {
71        match face {
72            1 => Some(Self::Aces),
73            2 => Some(Self::Twos),
74            3 => Some(Self::Threes),
75            4 => Some(Self::Fours),
76            5 => Some(Self::Fives),
77            6 => Some(Self::Sixes),
78            _ => None,
79        }
80    }
81
82    /// This category as a one-element set.
83    #[must_use]
84    pub const fn bit(self) -> Categories {
85        Categories(1 << self as u16)
86    }
87
88    /// The category's display name, as printed on the card.
89    #[must_use]
90    pub const fn name(self) -> &'static str {
91        match self {
92            Self::Aces => "Aces",
93            Self::Twos => "Twos",
94            Self::Threes => "Threes",
95            Self::Fours => "Fours",
96            Self::Fives => "Fives",
97            Self::Sixes => "Sixes",
98            Self::ThreeOfAKind => "Three of a Kind",
99            Self::FourOfAKind => "Four of a Kind",
100            Self::FullHouse => "Full House",
101            Self::SmallStraight => "Small Straight",
102            Self::LargeStraight => "Large Straight",
103            Self::Yahtzee => "Yahtzee",
104            Self::Chance => "Chance",
105        }
106    }
107}
108
109impl fmt::Display for Category {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        // `pad`, not `write_str`, so width and alignment flags work.
112        f.pad(self.name())
113    }
114}
115
116/// A set of categories, one bit per [`Category`].
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
118pub struct Categories(u16);
119
120impl Categories {
121    /// The empty set.
122    pub const EMPTY: Self = Self(0);
123
124    /// All thirteen categories.
125    pub const ALL: Self = Self(0x1FFF);
126
127    /// The six upper-section categories.
128    pub const UPPER: Self = Self(0x003F);
129
130    /// The seven lower-section categories.
131    pub const LOWER: Self = Self(0x1FC0);
132
133    /// Whether `category` is in the set.
134    #[must_use]
135    pub const fn contains(self, category: Category) -> bool {
136        self.0 & category.bit().0 != 0
137    }
138
139    /// The set with `category` added.
140    #[must_use]
141    pub const fn with(self, category: Category) -> Self {
142        Self(self.0 | category.bit().0)
143    }
144
145    /// The number of categories in the set.
146    #[must_use]
147    pub const fn len(self) -> u32 {
148        self.0.count_ones()
149    }
150
151    /// Whether the set is empty.
152    #[must_use]
153    pub const fn is_empty(self) -> bool {
154        self.0 == 0
155    }
156
157    /// The raw 13-bit representation, bit `i` for `Category::ALL[i]`.
158    #[must_use]
159    pub const fn bits(self) -> u16 {
160        self.0
161    }
162
163    /// Rebuilds a set from [`Self::bits`].
164    ///
165    /// Returns [`None`] if any bit beyond the thirteen categories is set.
166    #[must_use]
167    pub const fn from_bits(bits: u16) -> Option<Self> {
168        if bits & !Self::ALL.0 == 0 {
169            Some(Self(bits))
170        } else {
171            None
172        }
173    }
174
175    /// The categories in the set, in card order.
176    pub fn iter(self) -> impl Iterator<Item = Category> {
177        Category::ALL.into_iter().filter(move |&c| self.contains(c))
178    }
179}
180
181impl From<Category> for Categories {
182    fn from(category: Category) -> Self {
183        category.bit()
184    }
185}
186
187impl core::ops::BitOr for Categories {
188    type Output = Self;
189
190    fn bitor(self, rhs: Self) -> Self {
191        Self(self.0 | rhs.0)
192    }
193}
194
195impl core::ops::BitAnd for Categories {
196    type Output = Self;
197
198    fn bitand(self, rhs: Self) -> Self {
199        Self(self.0 & rhs.0)
200    }
201}
202
203impl core::ops::Not for Categories {
204    type Output = Self;
205
206    /// The complement within the thirteen categories.
207    fn not(self) -> Self {
208        Self(!self.0 & Self::ALL.0)
209    }
210}
211
212impl core::ops::Sub for Categories {
213    type Output = Self;
214
215    /// Set difference.
216    fn sub(self, rhs: Self) -> Self {
217        Self(self.0 & !rhs.0)
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn bits_are_card_order() {
227        for (i, category) in Category::ALL.into_iter().enumerate() {
228            assert_eq!(category as usize, i);
229            assert_eq!(category.bit().bits(), 1 << i);
230        }
231        assert_eq!(Categories::UPPER | Categories::LOWER, Categories::ALL);
232        assert_eq!(Categories::UPPER & Categories::LOWER, Categories::EMPTY);
233    }
234
235    #[test]
236    fn display_honors_width_flags() {
237        assert_eq!(format!("{}", Category::FullHouse), "Full House");
238        assert_eq!(format!("{:16}", Category::Aces), "Aces            ");
239        assert_eq!(format!("{:>6}", Category::Twos), "  Twos");
240    }
241
242    #[test]
243    fn upper_face_round_trips() {
244        for face in 1..=6 {
245            let category = Category::upper(face).expect("an upper category");
246            assert_eq!(category.upper_face(), Some(face));
247            assert!(Categories::UPPER.contains(category));
248        }
249        assert_eq!(Category::upper(0), None);
250        assert_eq!(Category::upper(7), None);
251        assert_eq!(Category::Yahtzee.upper_face(), None);
252    }
253
254    #[test]
255    fn set_operations() {
256        let set = Category::Aces.bit().with(Category::Chance);
257        assert_eq!(set.len(), 2);
258        assert!(set.contains(Category::Aces));
259        assert!(!set.contains(Category::Twos));
260        assert_eq!(
261            set.iter().collect::<Vec<_>>(),
262            [Category::Aces, Category::Chance]
263        );
264        assert_eq!(!Categories::EMPTY, Categories::ALL);
265        assert_eq!(Categories::ALL - set, !set);
266        assert_eq!(Categories::from_bits(set.bits()), Some(set));
267        assert_eq!(Categories::from_bits(0x2000), None);
268    }
269}