Skip to main content

yahtzee_engine/
score.rs

1//! Base scoring: what each category pays for a roll, before joker rules.
2//!
3//! These are the plain card values.  Joker forcing and joker values for
4//! extra Yahtzees live in [`State`](crate::State), which is the single
5//! authority on legality and valuation during play.
6
7use crate::{Category, Dice};
8
9/// Whether the roll holds `run` consecutive faces.
10fn has_run(dice: Dice, run: u8) -> bool {
11    let mut streak = 0;
12    (1..=6).any(|face| {
13        streak = if dice.count(face) > 0 { streak + 1 } else { 0 };
14        streak >= run
15    })
16}
17
18impl Category {
19    /// The base score of `dice` in this category, ignoring joker rules.
20    ///
21    /// A five-of-a-kind is not a base full house, small straight, or
22    /// large straight; those boxes pay for extra Yahtzees only through
23    /// the joker values in [`State::apply`](crate::State::apply).
24    #[must_use]
25    pub fn score(self, dice: Dice) -> u8 {
26        let of_a_kind = |n| (1..=6).any(|face| dice.count(face) >= n);
27        match self {
28            Self::Aces | Self::Twos | Self::Threes | Self::Fours | Self::Fives | Self::Sixes => {
29                let face = self.upper_face().expect("an upper category");
30                dice.count(face) * face
31            }
32            Self::ThreeOfAKind => {
33                if of_a_kind(3) {
34                    dice.sum()
35                } else {
36                    0
37                }
38            }
39            Self::FourOfAKind => {
40                if of_a_kind(4) {
41                    dice.sum()
42                } else {
43                    0
44                }
45            }
46            Self::FullHouse => {
47                let counts = dice.counts();
48                if counts.contains(&3) && counts.contains(&2) {
49                    25
50                } else {
51                    0
52                }
53            }
54            Self::SmallStraight => {
55                if has_run(dice, 4) {
56                    30
57                } else {
58                    0
59                }
60            }
61            Self::LargeStraight => {
62                if has_run(dice, 5) {
63                    40
64                } else {
65                    0
66                }
67            }
68            Self::Yahtzee => {
69                if dice.yahtzee_face().is_some() {
70                    50
71                } else {
72                    0
73                }
74            }
75            Self::Chance => dice.sum(),
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    fn dice(s: &str) -> Dice {
85        s.parse().expect("a valid roll")
86    }
87
88    fn scores(s: &str) -> [u8; 13] {
89        Category::ALL.map(|c| c.score(dice(s)))
90    }
91
92    #[test]
93    fn upper_section_counts_one_face() {
94        assert_eq!(scores("13446")[..6], [1, 0, 3, 8, 0, 6]);
95        assert_eq!(scores("55555")[..6], [0, 0, 0, 0, 25, 0]);
96    }
97
98    #[test]
99    fn n_of_a_kind_pays_the_whole_roll() {
100        assert_eq!(Category::ThreeOfAKind.score(dice("33345")), 18);
101        assert_eq!(Category::ThreeOfAKind.score(dice("33445")), 0);
102        assert_eq!(Category::FourOfAKind.score(dice("33334")), 16);
103        assert_eq!(Category::FourOfAKind.score(dice("33345")), 0);
104        // A Yahtzee also counts as three and four of a kind.
105        assert_eq!(Category::ThreeOfAKind.score(dice("66666")), 30);
106        assert_eq!(Category::FourOfAKind.score(dice("66666")), 30);
107    }
108
109    #[test]
110    fn full_house_is_exactly_three_and_two() {
111        assert_eq!(Category::FullHouse.score(dice("22333")), 25);
112        assert_eq!(Category::FullHouse.score(dice("22334")), 0);
113        assert_eq!(Category::FullHouse.score(dice("22223")), 0);
114        // Five of a kind is not a base full house.
115        assert_eq!(Category::FullHouse.score(dice("33333")), 0);
116    }
117
118    #[test]
119    fn straights() {
120        assert_eq!(Category::SmallStraight.score(dice("12346")), 30);
121        assert_eq!(Category::SmallStraight.score(dice("23345")), 30);
122        assert_eq!(Category::SmallStraight.score(dice("12356")), 0);
123        assert_eq!(Category::LargeStraight.score(dice("12345")), 40);
124        assert_eq!(Category::LargeStraight.score(dice("23456")), 40);
125        assert_eq!(Category::LargeStraight.score(dice("12346")), 0);
126        // A large straight is also a small straight.
127        assert_eq!(Category::SmallStraight.score(dice("23456")), 30);
128    }
129
130    #[test]
131    fn yahtzee_and_chance() {
132        assert_eq!(Category::Yahtzee.score(dice("44444")), 50);
133        assert_eq!(Category::Yahtzee.score(dice("44443")), 0);
134        assert_eq!(Category::Chance.score(dice("13446")), 18);
135        assert_eq!(Category::Chance.score(dice("66666")), 30);
136    }
137}