yahtzee-engine 0.1.0

Yahtzee rules, scoring, and bots: a fast heuristic and an exact optimal expected-value solver
Documentation
//! The thirteen scoring categories and sets thereof.

use core::fmt;

/// A scoring category on the card.
///
/// The discriminant is the category's bit position in [`Categories`],
/// upper section first, so `Aces..=Sixes` are bits `0..=5`.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Category {
    /// Total of dice showing 1.
    Aces,
    /// Total of dice showing 2.
    Twos,
    /// Total of dice showing 3.
    Threes,
    /// Total of dice showing 4.
    Fours,
    /// Total of dice showing 5.
    Fives,
    /// Total of dice showing 6.
    Sixes,
    /// Total of all dice, if at least three show the same face.
    ThreeOfAKind,
    /// Total of all dice, if at least four show the same face.
    FourOfAKind,
    /// 25 points for three of one face and two of another.
    FullHouse,
    /// 30 points for four faces in sequence.
    SmallStraight,
    /// 40 points for five faces in sequence.
    LargeStraight,
    /// 50 points for five of a kind.
    Yahtzee,
    /// Total of all dice, no requirement.
    Chance,
}

impl Category {
    /// All thirteen categories in card order.
    pub const ALL: [Self; 13] = [
        Self::Aces,
        Self::Twos,
        Self::Threes,
        Self::Fours,
        Self::Fives,
        Self::Sixes,
        Self::ThreeOfAKind,
        Self::FourOfAKind,
        Self::FullHouse,
        Self::SmallStraight,
        Self::LargeStraight,
        Self::Yahtzee,
        Self::Chance,
    ];

    /// The face this upper-section category counts, or [`None`] for the
    /// lower section.
    #[must_use]
    pub const fn upper_face(self) -> Option<u8> {
        let index = self as u8;
        if index < 6 { Some(index + 1) } else { None }
    }

    /// The upper-section category counting `face` (`1..=6`).
    ///
    /// Returns [`None`] for faces outside `1..=6`.
    #[must_use]
    pub const fn upper(face: u8) -> Option<Self> {
        match face {
            1 => Some(Self::Aces),
            2 => Some(Self::Twos),
            3 => Some(Self::Threes),
            4 => Some(Self::Fours),
            5 => Some(Self::Fives),
            6 => Some(Self::Sixes),
            _ => None,
        }
    }

    /// This category as a one-element set.
    #[must_use]
    pub const fn bit(self) -> Categories {
        Categories(1 << self as u16)
    }

    /// The category's display name, as printed on the card.
    #[must_use]
    pub const fn name(self) -> &'static str {
        match self {
            Self::Aces => "Aces",
            Self::Twos => "Twos",
            Self::Threes => "Threes",
            Self::Fours => "Fours",
            Self::Fives => "Fives",
            Self::Sixes => "Sixes",
            Self::ThreeOfAKind => "Three of a Kind",
            Self::FourOfAKind => "Four of a Kind",
            Self::FullHouse => "Full House",
            Self::SmallStraight => "Small Straight",
            Self::LargeStraight => "Large Straight",
            Self::Yahtzee => "Yahtzee",
            Self::Chance => "Chance",
        }
    }
}

impl fmt::Display for Category {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // `pad`, not `write_str`, so width and alignment flags work.
        f.pad(self.name())
    }
}

/// A set of categories, one bit per [`Category`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Categories(u16);

impl Categories {
    /// The empty set.
    pub const EMPTY: Self = Self(0);

    /// All thirteen categories.
    pub const ALL: Self = Self(0x1FFF);

    /// The six upper-section categories.
    pub const UPPER: Self = Self(0x003F);

    /// The seven lower-section categories.
    pub const LOWER: Self = Self(0x1FC0);

    /// Whether `category` is in the set.
    #[must_use]
    pub const fn contains(self, category: Category) -> bool {
        self.0 & category.bit().0 != 0
    }

    /// The set with `category` added.
    #[must_use]
    pub const fn with(self, category: Category) -> Self {
        Self(self.0 | category.bit().0)
    }

    /// The number of categories in the set.
    #[must_use]
    pub const fn len(self) -> u32 {
        self.0.count_ones()
    }

    /// Whether the set is empty.
    #[must_use]
    pub const fn is_empty(self) -> bool {
        self.0 == 0
    }

    /// The raw 13-bit representation, bit `i` for `Category::ALL[i]`.
    #[must_use]
    pub const fn bits(self) -> u16 {
        self.0
    }

    /// Rebuilds a set from [`Self::bits`].
    ///
    /// Returns [`None`] if any bit beyond the thirteen categories is set.
    #[must_use]
    pub const fn from_bits(bits: u16) -> Option<Self> {
        if bits & !Self::ALL.0 == 0 {
            Some(Self(bits))
        } else {
            None
        }
    }

    /// The categories in the set, in card order.
    pub fn iter(self) -> impl Iterator<Item = Category> {
        Category::ALL.into_iter().filter(move |&c| self.contains(c))
    }
}

impl From<Category> for Categories {
    fn from(category: Category) -> Self {
        category.bit()
    }
}

impl core::ops::BitOr for Categories {
    type Output = Self;

    fn bitor(self, rhs: Self) -> Self {
        Self(self.0 | rhs.0)
    }
}

impl core::ops::BitAnd for Categories {
    type Output = Self;

    fn bitand(self, rhs: Self) -> Self {
        Self(self.0 & rhs.0)
    }
}

impl core::ops::Not for Categories {
    type Output = Self;

    /// The complement within the thirteen categories.
    fn not(self) -> Self {
        Self(!self.0 & Self::ALL.0)
    }
}

impl core::ops::Sub for Categories {
    type Output = Self;

    /// Set difference.
    fn sub(self, rhs: Self) -> Self {
        Self(self.0 & !rhs.0)
    }
}

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

    #[test]
    fn bits_are_card_order() {
        for (i, category) in Category::ALL.into_iter().enumerate() {
            assert_eq!(category as usize, i);
            assert_eq!(category.bit().bits(), 1 << i);
        }
        assert_eq!(Categories::UPPER | Categories::LOWER, Categories::ALL);
        assert_eq!(Categories::UPPER & Categories::LOWER, Categories::EMPTY);
    }

    #[test]
    fn display_honors_width_flags() {
        assert_eq!(format!("{}", Category::FullHouse), "Full House");
        assert_eq!(format!("{:16}", Category::Aces), "Aces            ");
        assert_eq!(format!("{:>6}", Category::Twos), "  Twos");
    }

    #[test]
    fn upper_face_round_trips() {
        for face in 1..=6 {
            let category = Category::upper(face).expect("an upper category");
            assert_eq!(category.upper_face(), Some(face));
            assert!(Categories::UPPER.contains(category));
        }
        assert_eq!(Category::upper(0), None);
        assert_eq!(Category::upper(7), None);
        assert_eq!(Category::Yahtzee.upper_face(), None);
    }

    #[test]
    fn set_operations() {
        let set = Category::Aces.bit().with(Category::Chance);
        assert_eq!(set.len(), 2);
        assert!(set.contains(Category::Aces));
        assert!(!set.contains(Category::Twos));
        assert_eq!(
            set.iter().collect::<Vec<_>>(),
            [Category::Aces, Category::Chance]
        );
        assert_eq!(!Categories::EMPTY, Categories::ALL);
        assert_eq!(Categories::ALL - set, !set);
        assert_eq!(Categories::from_bits(set.bits()), Some(set));
        assert_eq!(Categories::from_bits(0x2000), None);
    }
}