yahtzee-engine 0.1.0

Yahtzee rules, scoring, and bots: a fast heuristic and an exact optimal expected-value solver
Documentation
//! Base scoring: what each category pays for a roll, before joker rules.
//!
//! These are the plain card values.  Joker forcing and joker values for
//! extra Yahtzees live in [`State`](crate::State), which is the single
//! authority on legality and valuation during play.

use crate::{Category, Dice};

/// Whether the roll holds `run` consecutive faces.
fn has_run(dice: Dice, run: u8) -> bool {
    let mut streak = 0;
    (1..=6).any(|face| {
        streak = if dice.count(face) > 0 { streak + 1 } else { 0 };
        streak >= run
    })
}

impl Category {
    /// The base score of `dice` in this category, ignoring joker rules.
    ///
    /// A five-of-a-kind is not a base full house, small straight, or
    /// large straight; those boxes pay for extra Yahtzees only through
    /// the joker values in [`State::apply`](crate::State::apply).
    #[must_use]
    pub fn score(self, dice: Dice) -> u8 {
        let of_a_kind = |n| (1..=6).any(|face| dice.count(face) >= n);
        match self {
            Self::Aces | Self::Twos | Self::Threes | Self::Fours | Self::Fives | Self::Sixes => {
                let face = self.upper_face().expect("an upper category");
                dice.count(face) * face
            }
            Self::ThreeOfAKind => {
                if of_a_kind(3) {
                    dice.sum()
                } else {
                    0
                }
            }
            Self::FourOfAKind => {
                if of_a_kind(4) {
                    dice.sum()
                } else {
                    0
                }
            }
            Self::FullHouse => {
                let counts = dice.counts();
                if counts.contains(&3) && counts.contains(&2) {
                    25
                } else {
                    0
                }
            }
            Self::SmallStraight => {
                if has_run(dice, 4) {
                    30
                } else {
                    0
                }
            }
            Self::LargeStraight => {
                if has_run(dice, 5) {
                    40
                } else {
                    0
                }
            }
            Self::Yahtzee => {
                if dice.yahtzee_face().is_some() {
                    50
                } else {
                    0
                }
            }
            Self::Chance => dice.sum(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn dice(s: &str) -> Dice {
        s.parse().expect("a valid roll")
    }

    fn scores(s: &str) -> [u8; 13] {
        Category::ALL.map(|c| c.score(dice(s)))
    }

    #[test]
    fn upper_section_counts_one_face() {
        assert_eq!(scores("13446")[..6], [1, 0, 3, 8, 0, 6]);
        assert_eq!(scores("55555")[..6], [0, 0, 0, 0, 25, 0]);
    }

    #[test]
    fn n_of_a_kind_pays_the_whole_roll() {
        assert_eq!(Category::ThreeOfAKind.score(dice("33345")), 18);
        assert_eq!(Category::ThreeOfAKind.score(dice("33445")), 0);
        assert_eq!(Category::FourOfAKind.score(dice("33334")), 16);
        assert_eq!(Category::FourOfAKind.score(dice("33345")), 0);
        // A Yahtzee also counts as three and four of a kind.
        assert_eq!(Category::ThreeOfAKind.score(dice("66666")), 30);
        assert_eq!(Category::FourOfAKind.score(dice("66666")), 30);
    }

    #[test]
    fn full_house_is_exactly_three_and_two() {
        assert_eq!(Category::FullHouse.score(dice("22333")), 25);
        assert_eq!(Category::FullHouse.score(dice("22334")), 0);
        assert_eq!(Category::FullHouse.score(dice("22223")), 0);
        // Five of a kind is not a base full house.
        assert_eq!(Category::FullHouse.score(dice("33333")), 0);
    }

    #[test]
    fn straights() {
        assert_eq!(Category::SmallStraight.score(dice("12346")), 30);
        assert_eq!(Category::SmallStraight.score(dice("23345")), 30);
        assert_eq!(Category::SmallStraight.score(dice("12356")), 0);
        assert_eq!(Category::LargeStraight.score(dice("12345")), 40);
        assert_eq!(Category::LargeStraight.score(dice("23456")), 40);
        assert_eq!(Category::LargeStraight.score(dice("12346")), 0);
        // A large straight is also a small straight.
        assert_eq!(Category::SmallStraight.score(dice("23456")), 30);
    }

    #[test]
    fn yahtzee_and_chance() {
        assert_eq!(Category::Yahtzee.score(dice("44444")), 50);
        assert_eq!(Category::Yahtzee.score(dice("44443")), 0);
        assert_eq!(Category::Chance.score(dice("13446")), 18);
        assert_eq!(Category::Chance.score(dice("66666")), 30);
    }
}