ExamplesBy LevelBy TopicLearning Paths
275 Intermediate

Yacht Dice Scoring

Pattern MatchingAlgebraic Data Types

Tutorial Video

Text description (accessibility)

This video demonstrates the "Yacht Dice Scoring" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Pattern Matching, Algebraic Data Types. Score a roll of five dice against one of twelve Yacht categories. Key difference from OCaml: 1. **Variant / enum:** OCaml `type category = Ones | ...` maps directly to `pub enum Category { Ones, ... }` — the concept is identical, the syntax is similar.

Tutorial

The Problem

Score a roll of five dice against one of twelve Yacht categories. Each category has different rules: number categories sum matching dice, Yacht awards 50 for five-of-a-kind, FullHouse awards the sum when dice form a 2+3 pair, and straights award 30 for the sequences 1-5 or 2-6.

🎯 Learning Outcomes

  • • Modeling a closed set of alternatives with Rust enums vs OCaml variant types
  • • Using fixed-size arrays ([u8; 5]) instead of lists for fixed-length data
  • • Replacing OCaml's exception-driven List.find / Not_found with Option-based .find().unwrap_or()
  • • Writing frequency-counting helpers that avoid fragile sorted-pattern matching
  • 🦀 The Rust Way

    Rust models the same logic with a #[derive]-annotated enum and an exhaustive match. Fixed-size [u8; 5] arrays replace OCaml lists; sort_unstable replaces List.sort. FullHouse uses a frequency-count approach (more robust than pattern-matching on sorted values). FourOfAKind uses iterator .find().map().unwrap_or(0), replacing the exception-based OCaml idiom cleanly.

    Code Example

    pub fn score(dice: &[u8; 5], category: Category) -> u32 {
        match category {
            Category::Ones   => u32::from(count(dice, 1)),
            Category::Twos   => 2 * u32::from(count(dice, 2)),
            Category::Threes => 3 * u32::from(count(dice, 3)),
            Category::Fours  => 4 * u32::from(count(dice, 4)),
            Category::Fives  => 5 * u32::from(count(dice, 5)),
            Category::Sixes  => 6 * u32::from(count(dice, 6)),
            Category::Choice => dice.iter().map(|&d| u32::from(d)).sum(),
            Category::Yacht  => {
                if dice.iter().all(|&d| d == dice[0]) { 50 } else { 0 }
            }
            Category::FullHouse => {
                let mut counts = [0u8; 7];
                for &d in dice { counts[d as usize] += 1; }
                let mut freqs: Vec<u8> = counts.iter().copied()
                    .filter(|&c| c > 0).collect();
                freqs.sort_unstable();
                if freqs == [2, 3] { dice.iter().map(|&d| u32::from(d)).sum() }
                else { 0 }
            }
            Category::FourOfAKind => (1u8..=6)
                .find(|&n| count(dice, n) >= 4)
                .map(|n| 4 * u32::from(n))
                .unwrap_or(0),
            Category::LittleStraight => {
                let mut s = *dice; s.sort_unstable();
                if s == [1, 2, 3, 4, 5] { 30 } else { 0 }
            }
            Category::BigStraight => {
                let mut s = *dice; s.sort_unstable();
                if s == [2, 3, 4, 5, 6] { 30 } else { 0 }
            }
        }
    }

    Key Differences

  • Variant / enum: OCaml type category = Ones | ... maps directly to pub enum Category { Ones, ... } — the concept is identical, the syntax is similar.
  • Fixed-length data: OCaml uses 'a list for dice; Rust uses [u8; 5], making the "exactly five dice" invariant a compile-time guarantee.
  • Error handling: OCaml List.find raises Not_found; Rust Iterator::find returns Option, handled with .map().unwrap_or(0) — no exceptions.
  • FullHouse detection: OCaml matches sorted patterns; Rust builds a frequency table and checks sorted frequency counts equal [2, 3] — avoids fragile guard expressions that clippy flags.
  • OCaml Approach

    OCaml uses a variant type for categories and dispatches with a multi-arm function expression. FullHouse is detected by matching sorted list patterns with guards, and FourOfAKind uses List.find with a try/with Not_found fallback. Sorted comparison against literal lists handles the straights.

    Full Source

    #![allow(clippy::all)]
    /// Yacht dice scoring categories.
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum Category {
        Ones,
        Twos,
        Threes,
        Fours,
        Fives,
        Sixes,
        FullHouse,
        FourOfAKind,
        LittleStraight,
        BigStraight,
        Yacht,
        Choice,
    }
    
    /// Count how many dice show the value `n`.
    fn count(dice: &[u8], n: u8) -> u8 {
        dice.iter().filter(|&&d| d == n).count() as u8
    }
    
    /// Score idiomatic Rust style — exhaustive match, iterator-based helpers.
    pub fn score(dice: &[u8; 5], category: Category) -> u32 {
        match category {
            Category::Ones => u32::from(count(dice, 1)),
            Category::Twos => 2 * u32::from(count(dice, 2)),
            Category::Threes => 3 * u32::from(count(dice, 3)),
            Category::Fours => 4 * u32::from(count(dice, 4)),
            Category::Fives => 5 * u32::from(count(dice, 5)),
            Category::Sixes => 6 * u32::from(count(dice, 6)),
            Category::Choice => dice.iter().map(|&d| u32::from(d)).sum(),
            Category::Yacht => {
                if dice.iter().all(|&d| d == dice[0]) {
                    50
                } else {
                    0
                }
            }
            Category::FullHouse => {
                // Counts per face value; a full house is exactly two distinct faces
                // with counts 2 and 3.
                let mut counts = [0u8; 7];
                for &d in dice {
                    counts[d as usize] += 1;
                }
                let freqs: Vec<u8> = counts.iter().copied().filter(|&c| c > 0).collect();
                let mut sorted_freqs = freqs.clone();
                sorted_freqs.sort_unstable();
                if sorted_freqs == [2, 3] {
                    dice.iter().map(|&d| u32::from(d)).sum()
                } else {
                    0
                }
            }
            Category::FourOfAKind => {
                // Find a face value that appears at least 4 times
                (1u8..=6)
                    .find(|&n| count(dice, n) >= 4)
                    .map(|n| 4 * u32::from(n))
                    .unwrap_or(0)
            }
            Category::LittleStraight => {
                let mut sorted = *dice;
                sorted.sort_unstable();
                if sorted == [1, 2, 3, 4, 5] {
                    30
                } else {
                    0
                }
            }
            Category::BigStraight => {
                let mut sorted = *dice;
                sorted.sort_unstable();
                if sorted == [2, 3, 4, 5, 6] {
                    30
                } else {
                    0
                }
            }
        }
    }
    
    /// Functional/recursive style — mirrors the OCaml approach more closely.
    /// Recursively scans face values 1..=6 to find one appearing >= 4 times.
    pub fn score_four_of_a_kind_recursive(dice: &[u8; 5], face: u8) -> u32 {
        if face > 6 {
            return 0;
        }
        if count(dice, face) >= 4 {
            4 * u32::from(face)
        } else {
            score_four_of_a_kind_recursive(dice, face + 1)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // --- Yacht ---
        #[test]
        fn test_yacht_all_same() {
            assert_eq!(score(&[5, 5, 5, 5, 5], Category::Yacht), 50);
        }
    
        #[test]
        fn test_yacht_not_all_same() {
            assert_eq!(score(&[1, 2, 3, 4, 5], Category::Yacht), 0);
        }
    
        // --- Number categories ---
        #[test]
        fn test_ones() {
            assert_eq!(score(&[1, 1, 2, 3, 4], Category::Ones), 2);
        }
    
        #[test]
        fn test_sixes() {
            assert_eq!(score(&[6, 6, 6, 6, 6], Category::Sixes), 30);
        }
    
        #[test]
        fn test_twos_none() {
            assert_eq!(score(&[1, 3, 4, 5, 6], Category::Twos), 0);
        }
    
        // --- Choice ---
        #[test]
        fn test_choice_sum() {
            assert_eq!(score(&[1, 2, 3, 4, 5], Category::Choice), 15);
        }
    
        #[test]
        fn test_choice_all_sixes() {
            assert_eq!(score(&[6, 6, 6, 6, 6], Category::Choice), 30);
        }
    
        // --- FullHouse ---
        #[test]
        fn test_full_house_two_then_three() {
            assert_eq!(score(&[2, 2, 3, 3, 3], Category::FullHouse), 13);
        }
    
        #[test]
        fn test_full_house_three_then_two() {
            assert_eq!(score(&[3, 3, 3, 2, 2], Category::FullHouse), 13);
        }
    
        #[test]
        fn test_full_house_five_of_a_kind_not_full_house() {
            assert_eq!(score(&[5, 5, 5, 5, 5], Category::FullHouse), 0);
        }
    
        #[test]
        fn test_full_house_all_different() {
            assert_eq!(score(&[1, 2, 3, 4, 5], Category::FullHouse), 0);
        }
    
        // --- FourOfAKind ---
        #[test]
        fn test_four_of_a_kind_basic() {
            assert_eq!(score(&[3, 3, 3, 3, 1], Category::FourOfAKind), 12);
        }
    
        #[test]
        fn test_four_of_a_kind_five_of_a_kind() {
            // five-of-a-kind satisfies four-of-a-kind
            assert_eq!(score(&[4, 4, 4, 4, 4], Category::FourOfAKind), 16);
        }
    
        #[test]
        fn test_four_of_a_kind_none() {
            assert_eq!(score(&[1, 2, 3, 4, 5], Category::FourOfAKind), 0);
        }
    
        // --- Straights ---
        #[test]
        fn test_little_straight() {
            assert_eq!(score(&[1, 2, 3, 4, 5], Category::LittleStraight), 30);
        }
    
        #[test]
        fn test_little_straight_unordered() {
            assert_eq!(score(&[3, 1, 2, 5, 4], Category::LittleStraight), 30);
        }
    
        #[test]
        fn test_big_straight() {
            assert_eq!(score(&[2, 3, 4, 5, 6], Category::BigStraight), 30);
        }
    
        #[test]
        fn test_little_straight_fails_for_big() {
            assert_eq!(score(&[1, 2, 3, 4, 5], Category::BigStraight), 0);
        }
    
        // --- Recursive helper ---
        #[test]
        fn test_recursive_four_of_a_kind() {
            assert_eq!(score_four_of_a_kind_recursive(&[2, 2, 2, 2, 5], 1), 8);
            assert_eq!(score_four_of_a_kind_recursive(&[1, 2, 3, 4, 5], 1), 0);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // --- Yacht ---
        #[test]
        fn test_yacht_all_same() {
            assert_eq!(score(&[5, 5, 5, 5, 5], Category::Yacht), 50);
        }
    
        #[test]
        fn test_yacht_not_all_same() {
            assert_eq!(score(&[1, 2, 3, 4, 5], Category::Yacht), 0);
        }
    
        // --- Number categories ---
        #[test]
        fn test_ones() {
            assert_eq!(score(&[1, 1, 2, 3, 4], Category::Ones), 2);
        }
    
        #[test]
        fn test_sixes() {
            assert_eq!(score(&[6, 6, 6, 6, 6], Category::Sixes), 30);
        }
    
        #[test]
        fn test_twos_none() {
            assert_eq!(score(&[1, 3, 4, 5, 6], Category::Twos), 0);
        }
    
        // --- Choice ---
        #[test]
        fn test_choice_sum() {
            assert_eq!(score(&[1, 2, 3, 4, 5], Category::Choice), 15);
        }
    
        #[test]
        fn test_choice_all_sixes() {
            assert_eq!(score(&[6, 6, 6, 6, 6], Category::Choice), 30);
        }
    
        // --- FullHouse ---
        #[test]
        fn test_full_house_two_then_three() {
            assert_eq!(score(&[2, 2, 3, 3, 3], Category::FullHouse), 13);
        }
    
        #[test]
        fn test_full_house_three_then_two() {
            assert_eq!(score(&[3, 3, 3, 2, 2], Category::FullHouse), 13);
        }
    
        #[test]
        fn test_full_house_five_of_a_kind_not_full_house() {
            assert_eq!(score(&[5, 5, 5, 5, 5], Category::FullHouse), 0);
        }
    
        #[test]
        fn test_full_house_all_different() {
            assert_eq!(score(&[1, 2, 3, 4, 5], Category::FullHouse), 0);
        }
    
        // --- FourOfAKind ---
        #[test]
        fn test_four_of_a_kind_basic() {
            assert_eq!(score(&[3, 3, 3, 3, 1], Category::FourOfAKind), 12);
        }
    
        #[test]
        fn test_four_of_a_kind_five_of_a_kind() {
            // five-of-a-kind satisfies four-of-a-kind
            assert_eq!(score(&[4, 4, 4, 4, 4], Category::FourOfAKind), 16);
        }
    
        #[test]
        fn test_four_of_a_kind_none() {
            assert_eq!(score(&[1, 2, 3, 4, 5], Category::FourOfAKind), 0);
        }
    
        // --- Straights ---
        #[test]
        fn test_little_straight() {
            assert_eq!(score(&[1, 2, 3, 4, 5], Category::LittleStraight), 30);
        }
    
        #[test]
        fn test_little_straight_unordered() {
            assert_eq!(score(&[3, 1, 2, 5, 4], Category::LittleStraight), 30);
        }
    
        #[test]
        fn test_big_straight() {
            assert_eq!(score(&[2, 3, 4, 5, 6], Category::BigStraight), 30);
        }
    
        #[test]
        fn test_little_straight_fails_for_big() {
            assert_eq!(score(&[1, 2, 3, 4, 5], Category::BigStraight), 0);
        }
    
        // --- Recursive helper ---
        #[test]
        fn test_recursive_four_of_a_kind() {
            assert_eq!(score_four_of_a_kind_recursive(&[2, 2, 2, 2, 5], 1), 8);
            assert_eq!(score_four_of_a_kind_recursive(&[1, 2, 3, 4, 5], 1), 0);
        }
    }

    Deep Comparison

    OCaml vs Rust: Yacht Dice Scoring

    Side-by-Side Code

    OCaml

    type category = Ones | Twos | Threes | Fours | Fives | Sixes
      | FullHouse | FourOfAKind | LittleStraight | BigStraight | Yacht | Choice
    
    let count dice n = List.length (List.filter ((=) n) dice)
    
    let score dice = function
      | Ones   -> count dice 1 | Twos   -> 2 * count dice 2
      | Threes -> 3 * count dice 3 | Fours  -> 4 * count dice 4
      | Fives  -> 5 * count dice 5 | Sixes  -> 6 * count dice 6
      | Choice -> List.fold_left (+) 0 dice
      | Yacht  -> if List.for_all ((=) (List.hd dice)) dice then 50 else 0
      | FullHouse ->
        let sorted = List.sort compare dice in
        (match sorted with
         | [a;b;c;d;e] when a=b && b=c && d=e && c<>d -> List.fold_left (+) 0 dice
         | [a;b;c;d;e] when a=b && c=d && d=e && b<>c -> List.fold_left (+) 0 dice
         | _ -> 0)
      | FourOfAKind ->
        (try
           let v = List.find (fun n -> count dice n >= 4)
                             (List.sort_uniq compare dice) in
           4 * v
         with Not_found -> 0)
      | LittleStraight ->
        if List.sort compare dice = [1;2;3;4;5] then 30 else 0
      | BigStraight ->
        if List.sort compare dice = [2;3;4;5;6] then 30 else 0
    

    Rust (idiomatic)

    pub fn score(dice: &[u8; 5], category: Category) -> u32 {
        match category {
            Category::Ones   => u32::from(count(dice, 1)),
            Category::Twos   => 2 * u32::from(count(dice, 2)),
            Category::Threes => 3 * u32::from(count(dice, 3)),
            Category::Fours  => 4 * u32::from(count(dice, 4)),
            Category::Fives  => 5 * u32::from(count(dice, 5)),
            Category::Sixes  => 6 * u32::from(count(dice, 6)),
            Category::Choice => dice.iter().map(|&d| u32::from(d)).sum(),
            Category::Yacht  => {
                if dice.iter().all(|&d| d == dice[0]) { 50 } else { 0 }
            }
            Category::FullHouse => {
                let mut counts = [0u8; 7];
                for &d in dice { counts[d as usize] += 1; }
                let mut freqs: Vec<u8> = counts.iter().copied()
                    .filter(|&c| c > 0).collect();
                freqs.sort_unstable();
                if freqs == [2, 3] { dice.iter().map(|&d| u32::from(d)).sum() }
                else { 0 }
            }
            Category::FourOfAKind => (1u8..=6)
                .find(|&n| count(dice, n) >= 4)
                .map(|n| 4 * u32::from(n))
                .unwrap_or(0),
            Category::LittleStraight => {
                let mut s = *dice; s.sort_unstable();
                if s == [1, 2, 3, 4, 5] { 30 } else { 0 }
            }
            Category::BigStraight => {
                let mut s = *dice; s.sort_unstable();
                if s == [2, 3, 4, 5, 6] { 30 } else { 0 }
            }
        }
    }
    

    Rust (functional/recursive — FourOfAKind)

    pub fn score_four_of_a_kind_recursive(dice: &[u8; 5], face: u8) -> u32 {
        if face > 6 { return 0; }
        if count(dice, face) >= 4 {
            4 * u32::from(face)
        } else {
            score_four_of_a_kind_recursive(dice, face + 1)
        }
    }
    

    Type Signatures

    ConceptOCamlRust
    Category typetype category = Ones \| Twos \| ...pub enum Category { Ones, Twos, ... }
    Dice parameter'a list (unbounded)&[u8; 5] (exactly 5 dice)
    Score resultintu32
    Count helperdice -> int -> intfn count(dice: &[u8], n: u8) -> u8
    Missing valueNot_found exceptionOption<T>.unwrap_or(0)

    Key Insights

  • Enums are the same concept: OCaml variant types and Rust enums are both sum types encoding a closed set of alternatives. The translation is nearly mechanical — rename and adjust syntax.
  • Fixed-length arrays enforce invariants: OCaml represents five dice as 'a list; Rust uses [u8; 5]. The array type encodes "exactly five" at compile time, eliminating a class of runtime errors with no overhead.
  • Options replace exceptions: OCaml's List.find raises Not_found when nothing matches, caught with try/with. Rust's Iterator::find returns Option<T>. The .find().map(f).unwrap_or(default) chain is safer and composes without stack-unwinding overhead.
  • Frequency table beats sorted-pattern matching: OCaml's FullHouse implementation matches on two sorted list patterns with guards — readable but fragile and clippy-hostile when ported to Rust. Building a frequency table (counts[face] += 1) and comparing sorted frequency counts to [2, 3] is cleaner, handles all orderings, and passes clippy without special cases.
  • **sort_unstable vs List.sort:** Rust's sort_unstable on a [u8; 5] stack array is zero-allocation and O(n log n). OCaml's List.sort allocates a sorted list. For five elements neither matters practically, but the Rust version has no heap traffic at all.
  • When to Use Each Style

    Use idiomatic Rust when: Scoring real game logic — the frequency-table approach for FullHouse and the iterator chain for FourOfAKind are easier to maintain and extend (e.g., adding validation).

    Use recursive Rust when: Teaching the OCaml-to-Rust mental model — the recursive score_four_of_a_kind_recursive mirrors the OCaml List.find tail-recursion pattern explicitly and shows how recursion replaces looping over a range.

    Exercises

  • Add the missing scoring categories FullHouse, SmallStraight, and LargeStraight if not already present, and handle edge cases (e.g., a five-of-a-kind counts as a full house by some rules).
  • Implement a best_category function that, given a set of five dice, returns the category that yields the highest score for that roll.
  • Build a simple Yacht game simulator: roll five dice, choose the best unscored category for 13 rounds, and return the final total score — using your scoring functions throughout.
  • Open Source Repos