Yacht Dice Scoring
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
[u8; 5]) instead of lists for fixed-length dataList.find / Not_found with Option-based .find().unwrap_or()🦀 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
type category = Ones | ... maps directly to pub enum Category { Ones, ... } — the concept is identical, the syntax is similar.'a list for dice; Rust uses [u8; 5], making the "exactly five dice" invariant a compile-time guarantee.List.find raises Not_found; Rust Iterator::find returns Option, handled with .map().unwrap_or(0) — no exceptions.[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);
}
}#[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
| Concept | OCaml | Rust |
|---|---|---|
| Category type | type category = Ones \| Twos \| ... | pub enum Category { Ones, Twos, ... } |
| Dice parameter | 'a list (unbounded) | &[u8; 5] (exactly 5 dice) |
| Score result | int | u32 |
| Count helper | dice -> int -> int | fn count(dice: &[u8], n: u8) -> u8 |
| Missing value | Not_found exception | Option<T> → .unwrap_or(0) |
Key Insights
'a list; Rust uses [u8; 5]. The array type encodes "exactly five" at compile time, eliminating a class of runtime errors with no overhead.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.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
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).best_category function that, given a set of five dice, returns the category that yields the highest score for that roll.