use crate::{Categories, Category, Dice, GameError, ScoreDelta, State};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Scorecard {
boxes: [Option<u8>; 13],
yahtzee_bonuses: u8,
}
impl Scorecard {
#[must_use]
pub const fn new() -> Self {
Self {
boxes: [None; 13],
yahtzee_bonuses: 0,
}
}
#[must_use]
pub const fn get(self, category: Category) -> Option<u8> {
self.boxes[category as usize]
}
#[must_use]
pub fn state(self) -> State {
let scored = Category::ALL
.into_iter()
.filter(|&c| self.get(c).is_some())
.fold(Categories::EMPTY, Categories::with);
State::new(
scored,
self.upper_subtotal().min(63),
self.get(Category::Yahtzee) == Some(50),
)
}
#[must_use]
pub fn upper_subtotal(self) -> u8 {
self.boxes[..6].iter().flatten().sum()
}
#[must_use]
pub fn upper_bonus(self) -> u8 {
if self.upper_subtotal() >= 63 { 35 } else { 0 }
}
#[must_use]
pub const fn yahtzee_bonuses(self) -> u8 {
self.yahtzee_bonuses
}
#[must_use]
pub fn total(self) -> u16 {
self.boxes
.iter()
.flatten()
.map(|&v| u16::from(v))
.sum::<u16>()
+ u16::from(self.upper_bonus())
+ 100 * u16::from(self.yahtzee_bonuses)
}
#[must_use]
pub fn legal_categories(self, dice: Dice) -> Categories {
self.state().legal_categories(dice)
}
pub fn score(&mut self, category: Category, dice: Dice) -> Result<ScoreDelta, GameError> {
let delta = self
.state()
.apply(category, dice)
.ok_or(GameError::IllegalCategory { category })?;
self.boxes[category as usize] = Some(delta.value);
self.yahtzee_bonuses += u8::from(delta.yahtzee_bonus);
Ok(delta)
}
#[must_use]
pub fn is_full(self) -> bool {
self.boxes.iter().all(Option::is_some)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dice(s: &str) -> Dice {
s.parse().expect("a valid roll")
}
#[test]
fn totals_add_boxes_and_bonuses() {
let mut card = Scorecard::new();
card.score(Category::Yahtzee, dice("44444")).expect("legal");
assert_eq!(card.total(), 50);
let delta = card.score(Category::Fours, dice("44444")).expect("legal");
assert!(delta.yahtzee_bonus);
assert_eq!(card.get(Category::Fours), Some(20));
assert_eq!(card.yahtzee_bonuses(), 1);
assert_eq!(card.total(), 170);
assert_eq!(card.upper_subtotal(), 20);
assert_eq!(card.upper_bonus(), 0);
}
#[test]
fn state_projection_matches_the_boxes() {
let mut card = Scorecard::new();
card.score(Category::Sixes, dice("66666")).expect("legal");
card.score(Category::Chance, dice("13446")).expect("legal");
let state = card.state();
assert_eq!(state.scored(), Category::Sixes.bit().with(Category::Chance));
assert_eq!(state.upper(), 30);
assert!(!state.yahtzee_50());
card.score(Category::Yahtzee, dice("13446")).expect("legal");
assert!(!card.state().yahtzee_50());
}
#[test]
fn rejections_leave_the_card_unchanged() {
let mut card = Scorecard::new();
card.score(Category::Chance, dice("13446")).expect("legal");
let before = card;
let result = card.score(Category::Chance, dice("22222"));
assert!(result.is_err());
assert_eq!(card, before);
}
#[test]
fn upper_bonus_shows_up_in_the_total() {
let mut card = Scorecard::new();
card.score(Category::Fours, dice("44444")).expect("legal");
card.score(Category::Fives, dice("55555")).expect("legal");
card.score(Category::Sixes, dice("66666")).expect("legal");
assert_eq!(card.upper_subtotal(), 75);
assert_eq!(card.upper_bonus(), 35);
assert_eq!(card.total(), 110);
}
}