yahtzee_engine/
scorecard.rs1use crate::{Categories, Category, Dice, GameError, ScoreDelta, State};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
11pub struct Scorecard {
12 boxes: [Option<u8>; 13],
13 yahtzee_bonuses: u8,
14}
15
16impl Scorecard {
17 #[must_use]
19 pub const fn new() -> Self {
20 Self {
21 boxes: [None; 13],
22 yahtzee_bonuses: 0,
23 }
24 }
25
26 #[must_use]
28 pub const fn get(self, category: Category) -> Option<u8> {
29 self.boxes[category as usize]
30 }
31
32 #[must_use]
34 pub fn state(self) -> State {
35 let scored = Category::ALL
36 .into_iter()
37 .filter(|&c| self.get(c).is_some())
38 .fold(Categories::EMPTY, Categories::with);
39 State::new(
40 scored,
41 self.upper_subtotal().min(63),
42 self.get(Category::Yahtzee) == Some(50),
43 )
44 }
45
46 #[must_use]
48 pub fn upper_subtotal(self) -> u8 {
49 self.boxes[..6].iter().flatten().sum()
50 }
51
52 #[must_use]
54 pub fn upper_bonus(self) -> u8 {
55 if self.upper_subtotal() >= 63 { 35 } else { 0 }
56 }
57
58 #[must_use]
60 pub const fn yahtzee_bonuses(self) -> u8 {
61 self.yahtzee_bonuses
62 }
63
64 #[must_use]
66 pub fn total(self) -> u16 {
67 self.boxes
68 .iter()
69 .flatten()
70 .map(|&v| u16::from(v))
71 .sum::<u16>()
72 + u16::from(self.upper_bonus())
73 + 100 * u16::from(self.yahtzee_bonuses)
74 }
75
76 #[must_use]
79 pub fn legal_categories(self, dice: Dice) -> Categories {
80 self.state().legal_categories(dice)
81 }
82
83 pub fn score(&mut self, category: Category, dice: Dice) -> Result<ScoreDelta, GameError> {
90 let delta = self
91 .state()
92 .apply(category, dice)
93 .ok_or(GameError::IllegalCategory { category })?;
94 self.boxes[category as usize] = Some(delta.value);
95 self.yahtzee_bonuses += u8::from(delta.yahtzee_bonus);
96 Ok(delta)
97 }
98
99 #[must_use]
101 pub fn is_full(self) -> bool {
102 self.boxes.iter().all(Option::is_some)
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 fn dice(s: &str) -> Dice {
111 s.parse().expect("a valid roll")
112 }
113
114 #[test]
115 fn totals_add_boxes_and_bonuses() {
116 let mut card = Scorecard::new();
117 card.score(Category::Yahtzee, dice("44444")).expect("legal");
118 assert_eq!(card.total(), 50);
119 let delta = card.score(Category::Fours, dice("44444")).expect("legal");
122 assert!(delta.yahtzee_bonus);
123 assert_eq!(card.get(Category::Fours), Some(20));
124 assert_eq!(card.yahtzee_bonuses(), 1);
125 assert_eq!(card.total(), 170);
126 assert_eq!(card.upper_subtotal(), 20);
127 assert_eq!(card.upper_bonus(), 0);
128 }
129
130 #[test]
131 fn state_projection_matches_the_boxes() {
132 let mut card = Scorecard::new();
133 card.score(Category::Sixes, dice("66666")).expect("legal");
134 card.score(Category::Chance, dice("13446")).expect("legal");
135 let state = card.state();
136 assert_eq!(state.scored(), Category::Sixes.bit().with(Category::Chance));
137 assert_eq!(state.upper(), 30);
138 assert!(!state.yahtzee_50());
139 card.score(Category::Yahtzee, dice("13446")).expect("legal");
141 assert!(!card.state().yahtzee_50());
142 }
143
144 #[test]
145 fn rejections_leave_the_card_unchanged() {
146 let mut card = Scorecard::new();
147 card.score(Category::Chance, dice("13446")).expect("legal");
148 let before = card;
149 let result = card.score(Category::Chance, dice("22222"));
150 assert!(result.is_err());
151 assert_eq!(card, before);
152 }
153
154 #[test]
155 fn upper_bonus_shows_up_in_the_total() {
156 let mut card = Scorecard::new();
157 card.score(Category::Fours, dice("44444")).expect("legal");
158 card.score(Category::Fives, dice("55555")).expect("legal");
159 card.score(Category::Sixes, dice("66666")).expect("legal");
160 assert_eq!(card.upper_subtotal(), 75);
161 assert_eq!(card.upper_bonus(), 35);
162 assert_eq!(card.total(), 110);
163 }
164}