Exercism - Yacht
This post shows you how to get Yacht exercise of Exercism.
Preparation
Before we click on our next exercise, let’s see what concepts of DART we need to consider

So we need to use the following concepts.
Extension Methods
Extension methods allow you to add new functionality to existing types without modifying their source code. You can add methods to built-in types like List using extensions.
extension on List<int> {
// Add sum method to List<int>
int sum() => fold(0, (a, b) => a + b);
// Add frequency counting method
Map<int, int> freq() => fold({}, (m, d) => m..update(d, (v) => v + 1, ifAbsent: () => 1));
}
void main() {
List<int> dice = [1, 2, 2, 3, 3];
// Use extension methods
int total = dice.sum();
print(total); // 11
Map<int, int> frequencies = dice.freq();
print(frequencies); // {1: 1, 2: 2, 3: 2}
}
Enums
Enums define a set of named constants. They’re perfect for representing a fixed set of categories or options, like Yacht scoring categories.
enum Category {
ones,
twos,
threes,
fours,
fives,
sixes,
yacht,
choice,
full_house,
four_of_a_kind,
little_straight,
big_straight,
}
void main() {
// Use enum values
Category category = Category.yacht;
print(category); // Category.yacht
// Switch on enum
switch (category) {
case Category.yacht:
print('Yacht category!');
break;
case Category.choice:
print('Choice category!');
break;
// ... other cases
}
}
Switch Expressions
Switch expressions allow you to return values directly from switch cases. They’re perfect for mapping enum values to scores.
enum Category { ones, twos, yacht }
void main() {
Category category = Category.ones;
// Switch expression - returns a value
int score = switch (category) {
Category.ones => 5,
Category.twos => 10,
Category.yacht => 50,
};
print(score); // 5
// Use with pattern matching
int calculateScore(Category cat, Map<int, int> freq) => switch (cat) {
Category.ones => (freq[1] ?? 0),
Category.twos => (freq[2] ?? 0) * 2,
Category.yacht => freq.length == 1 ? 50 : 0,
};
}
List fold() Method
The fold() method reduces a list to a single value by applying a function to each element and an accumulator. It’s perfect for summing values or building maps.
void main() {
List<int> dice = [1, 2, 3, 4, 5];
// Sum all values
int sum = dice.fold(0, (acc, value) => acc + value);
print(sum); // 15
// Build frequency map
Map<int, int> freq = dice.fold({}, (map, value) {
map.update(value, (count) => count + 1, ifAbsent: () => 1);
return map;
});
print(freq); // {1: 1, 2: 1, 3: 1, 4: 1, 5: 1}
// Shorter version with cascade
Map<int, int> freq2 = dice.fold({}, (m, d) => m..update(d, (v) => v + 1, ifAbsent: () => 1));
}
Map update() Method
The update() method updates a value in a map. It takes a key, a function to transform the existing value, and an ifAbsent callback if the key doesn’t exist.
void main() {
Map<int, int> freq = {};
// Update existing value
freq[1] = 2;
freq.update(1, (value) => value + 1);
print(freq[1]); // 3
// Update with ifAbsent (creates if doesn't exist)
freq.update(2, (value) => value + 1, ifAbsent: () => 1);
print(freq[2]); // 1
// Use in frequency counting
List<int> dice = [1, 1, 2, 3];
Map<int, int> counts = {};
for (int d in dice) {
counts.update(d, (v) => v + 1, ifAbsent: () => 1);
}
print(counts); // {1: 2, 2: 1, 3: 1}
}
Cascade Operator (..)
The cascade operator (..) allows you to perform multiple operations on the same object. It’s useful for method chaining and updating maps.
void main() {
Map<int, int> map = {};
// Cascade operator - returns the object after operations
map..update(1, (v) => v + 1, ifAbsent: () => 1)
..update(2, (v) => v + 1, ifAbsent: () => 1);
print(map); // {1: 1, 2: 1}
// Use in fold
List<int> dice = [1, 2, 2];
Map<int, int> freq = dice.fold({}, (m, d) => m..update(d, (v) => v + 1, ifAbsent: () => 1));
print(freq); // {1: 1, 2: 2}
}
List toList() Method
The toList() method creates a new list from an iterable. It’s useful for converting map values or other iterables to lists.
void main() {
Map<int, int> freq = {1: 2, 2: 3, 3: 1};
// Get values as list
List<int> values = freq.values.toList();
print(values); // [2, 3, 1]
// Sort the list
values.sort();
print(values); // [1, 2, 3]
// Use cascade to sort in place
List<int> sorted = freq.values.toList()..sort();
print(sorted); // [1, 2, 3]
}
List sort() Method
The sort() method sorts a list in place. It modifies the original list and doesn’t return a value (returns void).
void main() {
List<int> dice = [3, 1, 4, 1, 5];
// Sort in place
dice.sort();
print(dice); // [1, 1, 3, 4, 5]
// Use cascade to sort and use
List<int> sorted = dice.toList()..sort();
print(sorted); // [1, 1, 3, 4, 5]
// Sort map values
Map<int, int> freq = {1: 3, 2: 1, 3: 2};
List<int> values = freq.values.toList()..sort();
print(values); // [1, 2, 3]
}
List join() Method
The join() method combines list elements into a single string. It’s useful for checking if a sorted list matches a pattern.
void main() {
List<int> dice = [1, 2, 3, 4, 5];
// Join without separator
String joined = dice.join();
print(joined); // "12345"
// Join with separator
String withSpace = dice.join(' ');
print(withSpace); // "1 2 3 4 5"
// Check if matches pattern
List<int> sorted = dice.toList()..sort();
bool isStraight = sorted.join() == '12345';
print(isStraight); // true
}
Map entries Property
The entries property returns an iterable of map entries (key-value pairs). It’s useful for filtering and processing map data.
void main() {
Map<int, int> freq = {1: 2, 2: 1, 3: 4, 4: 1};
// Get entries
for (var entry in freq.entries) {
print('${entry.key}: ${entry.value}');
// 1: 2, 2: 1, 3: 4, 4: 1
}
// Filter entries
var fourOrMore = freq.entries.where((e) => e.value >= 4);
print(fourOrMore.map((e) => e.key).toList()); // [3]
// Use in scoring
int score = freq.entries
.where((e) => e.value >= 4)
.map((e) => e.key * 4)
.firstOrNull ?? 0;
}
Iterable where() Method
The where() method filters an iterable based on a condition. It returns a new iterable with only matching elements.
void main() {
List<int> numbers = [1, 2, 3, 4, 5, 6];
// Filter even numbers
var evens = numbers.where((n) => n % 2 == 0);
print(evens.toList()); // [2, 4, 6]
// Filter map entries
Map<int, int> freq = {1: 2, 2: 4, 3: 1};
var fourOrMore = freq.entries.where((e) => e.value >= 4);
print(fourOrMore.map((e) => e.key).toList()); // [2]
}
Iterable map() Method
The map() method transforms each element in an iterable. It returns a new iterable with transformed values.
void main() {
List<int> numbers = [1, 2, 3];
// Double each number
var doubled = numbers.map((n) => n * 2);
print(doubled.toList()); // [2, 4, 6]
// Transform map entries
Map<int, int> freq = {1: 4, 2: 1};
var scores = freq.entries.map((e) => e.key * 4);
print(scores.toList()); // [4, 8]
}
Iterable firstOrNull Property
The firstOrNull property returns the first element of an iterable, or null if the iterable is empty. It’s safer than first which throws if empty.
void main() {
List<int> numbers = [1, 2, 3];
// Get first element
int? first = numbers.firstOrNull;
print(first); // 1
// Empty list returns null
List<int> empty = [];
int? firstEmpty = empty.firstOrNull;
print(firstEmpty); // null
// Use with null-aware operator
int score = numbers.firstOrNull ?? 0;
print(score); // 1
// Use with map chain
Map<int, int> freq = {1: 4};
int result = freq.entries
.where((e) => e.value >= 4)
.map((e) => e.key * 4)
.firstOrNull ?? 0;
print(result); // 4
}
Null-Aware Operator (??)
The null-aware operator (??) provides a default value if the left side is null. It’s useful for handling optional values.
void main() {
Map<int, int> freq = {1: 2, 2: 3};
// Get value or default
int count1 = freq[1] ?? 0;
print(count1); // 2
int count3 = freq[3] ?? 0;
print(count3); // 0 (key doesn't exist, so null, use 0)
// Use in scoring
int score = (freq[1] ?? 0) * 1; // Count of ones
print(score); // 2
}
Classes and Constructors
Classes define blueprints for objects. Constructors initialize instances with specific values.
class Yacht {
final List<int> dice;
// Constructor - initializes dice
Yacht(this.dice);
int score(Category category) {
// Scoring logic
return 0;
}
}
void main() {
List<int> dice = [1, 2, 3, 4, 5];
Yacht yacht = Yacht(dice);
int result = yacht.score(Category.choice);
print(result); // 15
}
Introduction
Each year, something new is “all the rage” in your high school. This year it is a dice game: Yacht.
The game of Yacht is from the same family as Poker Dice, Generala and particularly Yahtzee, of which it is a precursor. The game consists of twelve rounds. In each, five dice are rolled and the player chooses one of twelve categories. The chosen category is then used to score the throw of the dice.
Instructions
Given five dice and a category, calculate the score of the dice for that category.
Note
You’ll always be presented with five dice. Each dice’s value will be between one and six inclusively. The dice may be unordered.
Scores in Yacht
| Category | Score | Description | Example |
|---|---|---|---|
| Ones | 1 × number of ones | Any combination | 1 1 1 4 5 scores 3 |
| Twos | 2 × number of twos | Any combination | 2 2 3 4 5 scores 4 |
| Threes | 3 × number of threes | Any combination | 3 3 3 3 3 scores 15 |
| Fours | 4 × number of fours | Any combination | 1 2 3 3 5 scores 0 |
| Fives | 5 × number of fives | Any combination | 5 1 5 2 5 scores 15 |
| Sixes | 6 × number of sixes | Any combination | 2 3 4 5 6 scores 6 |
| Full House | Total of the dice | Three of one number and two of another | 3 3 3 5 5 scores 19 |
| Four of a Kind | Total of the four dice | At least four dice showing the same face | 4 4 4 4 6 scores 16 |
| Little Straight | 30 points | 1-2-3-4-5 | 1 2 3 4 5 scores 30 |
| Big Straight | 30 points | 2-3-4-5-6 | 2 3 4 5 6 scores 30 |
| Choice | Sum of the dice | Any combination | 2 3 3 4 6 scores 18 |
| Yacht | 50 points | All five dice showing the same face | 4 4 4 4 4 scores 50 |
If the dice do not satisfy the requirements of a category, the score is zero. If, for example, Four Of A Kind is entered in the Yacht category, zero points are scored. A Yacht scores zero if entered in the Full House category.
How do we calculate Yacht scores?
To calculate scores for different categories:
- Count frequencies: Build a map of each die value to its count
- Number categories (Ones-Sixes): Multiply the count by the number value
- Yacht: Check if all dice are the same (frequency map has only one key)
- Choice: Sum all dice values
- Full House: Check if frequencies are exactly [2, 3] (sorted)
- Four of a Kind: Find a value with count >= 4, multiply by 4
- Little Straight: Check if sorted dice equal [1, 2, 3, 4, 5]
- Big Straight: Check if sorted dice equal [2, 3, 4, 5, 6]
The key insight is using frequency counting to identify patterns (pairs, three-of-a-kind, four-of-a-kind, etc.) and pattern matching with switch expressions to handle each category elegantly.
Solution
import 'categories.dart';
extension on List<int> {
int sum() => fold(0, (a, b) => a + b);
Map<int, int> freq() => fold({}, (m, d) => m..update(d, (v) => v + 1, ifAbsent: () => 1));
}
class Yacht {
final List<int> dice;
Yacht(this.dice);
int score(Category category) {
final f = dice.freq();
final v = f.values.toList()..sort();
final s = dice.toList()..sort();
return switch (category) {
Category.ones => (f[1] ?? 0),
Category.twos => (f[2] ?? 0) * 2,
Category.threes => (f[3] ?? 0) * 3,
Category.fours => (f[4] ?? 0) * 4,
Category.fives => (f[5] ?? 0) * 5,
Category.sixes => (f[6] ?? 0) * 6,
Category.yacht => f.length == 1 ? 50 : 0,
Category.choice => dice.sum(),
Category.full_house => v.length == 2 && v[0] == 2 && v[1] == 3 ? dice.sum() : 0,
Category.four_of_a_kind => f.entries.where((e) => e.value >= 4).map((e) => e.key * 4).firstOrNull ?? 0,
Category.little_straight => s.join() == '12345' ? 30 : 0,
Category.big_straight => s.join() == '23456' ? 30 : 0,
};
}
}
Let’s break down the solution:
-
import 'categories.dart'- Import Category enum:- Imports the
Categoryenum that defines all scoring categories - Enum values: ones, twos, threes, fours, fives, sixes, yacht, choice, full_house, four_of_a_kind, little_straight, big_straight
- Imports the
-
extension on List<int>- Extension methods:- Adds helper methods to
List<int>for dice operations - Makes code more readable and reusable
- Adds helper methods to
-
int sum() => fold(0, (a, b) => a + b)- Sum method:- Uses
fold()to sum all dice values - Starts with 0, adds each value to accumulator
- Example: [1, 2, 3] → 6
- Uses
-
Map<int, int> freq() => fold({}, (m, d) => m..update(d, (v) => v + 1, ifAbsent: () => 1))- Frequency method:- Builds a frequency map of die values
- Uses
fold()with empty map as starting point - Uses cascade operator (
..) to update map in place update()increments count if key exists, creates with count 1 if absent- Example: [1, 1, 2, 3, 3] → {1: 2, 2: 1, 3: 2}
-
class Yacht- Main class:- Encapsulates dice and scoring logic
- Stores the five dice values
-
final List<int> dice- Dice field:- Stores the five dice values
- Each value is between 1 and 6
-
Yacht(this.dice)- Constructor:- Initializes dice field
- Takes list of five dice values
-
int score(Category category)- Scoring method:- Takes a category and returns the score
- Uses switch expression to handle all categories
-
final f = dice.freq()- Calculate frequencies:- Builds frequency map of all dice values
- Used for pattern detection
-
final v = f.values.toList()..sort()- Sorted frequency values:- Gets all frequency counts as a list
- Sorts them for pattern matching (e.g., [2, 3] for full house)
-
final s = dice.toList()..sort()- Sorted dice:- Creates sorted copy of dice for straight detection
- Used to check if dice form a sequence
-
return switch (category) { ... }- Switch expression:- Returns value directly based on category
- Each case handles a specific scoring rule
-
Category.ones => (f[1] ?? 0)- Ones category:- Gets count of ones from frequency map
- Uses null-aware operator for missing values
- Returns count (multiplied by 1, which is just the count)
-
Category.twos => (f[2] ?? 0) * 2- Twos category:- Gets count of twos and multiplies by 2
- Example: [2, 2, 3, 4, 5] → count=2, score=4
-
Category.threes => (f[3] ?? 0) * 3- Threes category:- Gets count of threes and multiplies by 3
- Similar pattern for fours, fives, sixes
-
Category.yacht => f.length == 1 ? 50 : 0- Yacht category:- Checks if frequency map has only one key (all dice same)
- Returns 50 if yacht, 0 otherwise
- Example: [4, 4, 4, 4, 4] → {4: 5} → length=1 → 50
-
Category.choice => dice.sum()- Choice category:- Returns sum of all dice values
- Always valid, no pattern required
-
Category.full_house => v.length == 2 && v[0] == 2 && v[1] == 3 ? dice.sum() : 0- Full House:- Checks if sorted frequencies are exactly [2, 3]
- Means one value appears twice, another appears three times
- Returns sum if valid, 0 otherwise
- Example: [3, 3, 3, 5, 5] → frequencies {3: 3, 5: 2} → sorted [2, 3] → valid
-
Category.four_of_a_kind => f.entries.where((e) => e.value >= 4).map((e) => e.key * 4).firstOrNull ?? 0- Four of a Kind:- Filters entries with count >= 4
- Maps to die value × 4 (score of the four dice)
- Uses
firstOrNullto get first match (or null) - Returns 0 if no four-of-a-kind found
- Example: [4, 4, 4, 4, 6] → {4: 4, 6: 1} → filter 4:4 → map 4×4=16
-
Category.little_straight => s.join() == '12345' ? 30 : 0- Little Straight:- Checks if sorted dice form sequence 1-2-3-4-5
- Joins sorted dice into string and compares
- Returns 30 if matches, 0 otherwise
-
Category.big_straight => s.join() == '23456' ? 30 : 0- Big Straight:- Checks if sorted dice form sequence 2-3-4-5-6
- Similar to little straight but different sequence
- Returns 30 if matches, 0 otherwise
The solution efficiently calculates scores using frequency counting and pattern matching. Extension methods make the code more readable, and the switch expression provides a clean way to handle all categories.
A video tutorial for this exercise is coming soon! In the meantime, check out my YouTube channel for more Dart and Flutter tutorials. 😉
Visit My YouTube Channel