Yacht in Cpp: Complete Solution & Deep Dive Guide
The Ultimate Guide to Mastering the Yacht Dice Game Logic in C++
Implementing the scoring logic for a dice game like Yacht is a fantastic challenge for any C++ developer. It requires careful handling of collections, conditional logic, and algorithm application. This guide provides a comprehensive, step-by-step walkthrough of building a robust Yacht scoring system in modern C++, leveraging the power of the Standard Template Library (STL).
The Thrill of the Dice Roll: A Programmer's Challenge
Remember those classic dice games? The suspense of the roll, the quick mental math to find the best scoring category, the satisfaction of hitting a perfect combination. Games like Yacht, a precursor to Yahtzee, are not just about luck; they're about strategy, pattern recognition, and decision-making—skills that directly mirror those of a programmer.
When you translate these game rules into code, you're doing more than just building a program. You're designing a system of logic, a state machine that can interpret any combination of dice and assign a value to it. This specific module from the kodikra C++ learning path is designed to sharpen your algorithmic thinking and your command of the C++ STL.
This article will guide you from zero to hero. We'll dissect the rules of Yacht, implement a complete scoring solution in C++, and then refactor it for superior efficiency and elegance. Get ready to turn game logic into clean, powerful code.
What Exactly is the Yacht Dice Game?
Yacht is a dice game played with five standard six-sided dice. The objective is to score points by rolling the dice and then selecting one of twelve scoring categories. After a category is chosen and scored for a particular roll, it cannot be used again in subsequent rounds.
The core of our programming task is to create a function that, given five dice values and a specific category, can accurately calculate the score. This requires understanding each category's unique scoring rule.
The Scoring Categories Explained
The twelve categories are a mix of simple sums, set combinations (like a full house), and sequences (like a straight). Let's break them down in a clear table for reference.
| Category | Score Calculation | Description | Example Dice Roll | Example Score |
|---|---|---|---|---|
| Ones | Sum of all dice showing '1' | Adds up only the ones. | {1, 1, 2, 4, 5} |
2 |
| Twos | Sum of all dice showing '2' | Adds up only the twos. | {2, 2, 2, 1, 3} |
6 |
| Threes | Sum of all dice showing '3' | Adds up only the threes. | {3, 1, 2, 4, 5} |
3 |
| Fours | Sum of all dice showing '4' | Adds up only the fours. | {4, 4, 4, 4, 1} |
16 |
| Fives | Sum of all dice showing '5' | Adds up only the fives. | {5, 5, 1, 2, 3} |
10 |
| Sixes | Sum of all dice showing '6' | Adds up only the sixes. | {6, 6, 6, 6, 6} |
30 |
| Full House | Sum of all five dice | Three of one number and two of another. | {3, 3, 3, 5, 5} |
19 |
| Four of a Kind | Sum of the four matching dice | At least four dice showing the same number. | {4, 4, 4, 4, 1} |
16 |
| Little Straight | Fixed score of 30 | Dice show 1, 2, 3, 4, 5. | {1, 2, 3, 4, 5} |
30 |
| Big Straight | Fixed score of 30 | Dice show 2, 3, 4, 5, 6. | {2, 3, 4, 5, 6} |
30 |
| Choice | Sum of all five dice | Any combination. A "catch-all" category. | {1, 3, 4, 5, 6} |
19 |
| Yacht | Fixed score of 50 | All five dice are the same. | {4, 4, 4, 4, 4} |
50 |
Why Use C++ for This Logic Puzzle?
While you could solve this problem in many languages, C++ offers a compelling blend of performance and high-level abstraction, making it an excellent choice. Its strengths shine brightly in tasks that involve algorithmic manipulation of data collections.
- The Standard Template Library (STL): The C++ STL is a treasure trove of powerful algorithms and data structures. For the Yacht problem, we can use functions like
std::accumulate,std::sort,std::all_of, and containers likestd::arrayandstd::mapto write expressive and efficient code. - Performance: As a compiled language, C++ provides performance that is critical for more complex simulations or games. While not strictly necessary for a single scoring function, this foundation is valuable for building larger applications.
- Type Safety: C++'s static typing helps catch errors at compile time. Using types like
std::array<int, 5>ensures we are always working with exactly five dice, preventing a whole class of runtime bugs. - Control and Precision: The language gives you fine-grained control over memory and data manipulation, which is a core skill for any serious software engineer.
This problem serves as a perfect practical exercise to move from theoretical knowledge of the STL to applied skill. Master C++ with our comprehensive curriculum to build these foundational skills.
How to Implement the Scoring Logic: A Detailed Code Walkthrough
Let's start by analyzing a solid, working solution provided by the kodikra.com curriculum. The strategy here is to create a primary score function that acts as a dispatcher, calling specialized helper functions for each complex category. This keeps the code organized and readable.
The core of this implementation relies on a central dispatcher function that selects the correct logic based on the chosen category. This is a clean and common pattern in software design.
● Start (dice, category)
│
▼
┌───────────────────┐
│ Switch (category) │
└─────────┬─────────┘
│
╭─────────┼─────────╮
│ │ │
▼ ▼ ▼
Case: Case: Case:
[ONES] [FULL_HOUSE] [YACHT]
│ │ │
▼ ▼ ▼
call() call() call()
`simple_scores` `is_full_house` `is_yacht`
│ │ │
╰─────────┼─────────╯
│
▼
┌──────────┐
│ Return │
│ Score │
└──────────┘
│
▼
● End
The Header File: yacht.h
First, we define our function signatures and the category enum in a header file. This separates the interface from the implementation, a fundamental practice in C++.
#if !defined(YACHT_H)
#define YACHT_H
#include <array>
#include <string>
namespace yacht {
enum category {
ONES,
TWOS,
THREES,
FOURS,
FIVES,
SIXES,
FULL_HOUSE,
FOUR_OF_A_KIND,
LITTLE_STRAIGHT,
BIG_STRAIGHT,
CHOICE,
YACHT
};
int score(const std::array<int, 5>& dice, category c);
} // namespace yacht
#endif // YACHT_H
Using an enum for the categories is much safer and more readable than using raw strings or integers. The const std::array<int, 5>& dice parameter ensures we receive exactly five dice and we don't make an unnecessary copy of the data, thanks to the reference (&).
The Implementation File: yacht.cpp
Now, let's dive into the logic, function by function.
Helper Function: simple_scores
The first six categories (Ones, Twos, ..., Sixes) all follow the same pattern: sum the dice that match a target value. We can create a single helper function for this.
#include "yacht.h"
#include <numeric>
#include <map>
#include <algorithm>
namespace yacht {
int simple_scores(const std::array<int, 5>& dice, int target) {
return std::accumulate(dice.begin(), dice.end(), 0,
[target](int accumulator, int die_value) {
return die_value == target ? accumulator + die_value : accumulator;
});
}
// ... other functions
std::accumulate: This powerful algorithm from the<numeric>header is perfect for summing up elements in a range.- It takes the start and end of the range (
dice.begin(),dice.end()), an initial value for the sum (0), and a binary operation. - Lambda Function: We provide a lambda function
[target](int accumulator, int die_value) { ... }as the operation.[target]is the capture clause, making thetargetvalue available inside the lambda.- For each
die_valuein the array, it checks if it equals thetarget. If it does, it adds thedie_valueto theaccumulator; otherwise, it leaves theaccumulatorunchanged.
Helper Function: count_dice
Many categories depend on the counts of each die value (e.g., three 4s, two 6s). A function to create a frequency map is incredibly useful. This is a key refactoring that simplifies many subsequent checks.
std::map<int, int> count_dice(const std::array<int, 5>& dice) {
std::map<int, int> counts;
for (int die : dice) {
counts[die]++;
}
return counts;
}
std::map<int, int>: We use a map to store the die face (e.g., 4) as the key and its frequency (e.g., 3) as the value.- Range-based for loop: The loop
for (int die : dice)is a clean and modern way to iterate through each element of the container. counts[die]++: This is a concise feature ofstd::map. If the keydiedoesn't exist, it's created and its value is default-initialized to 0, then incremented to 1. If it already exists, its value is simply incremented.
The Main score Function
This function acts as the central router. It takes the dice and the category, then calls the appropriate logic.
int score(const std::array<int, 5>& dice, category c) {
auto counts = count_dice(dice);
switch (c) {
case ONES:
return simple_scores(dice, 1);
case TWOS:
return simple_scores(dice, 2);
case THREES:
return simple_scores(dice, 3);
case FOURS:
return simple_scores(dice, 4);
case FIVES:
return simple_scores(dice, 5);
case SIXES:
return simple_scores(dice, 6);
case CHOICE:
return std::accumulate(dice.begin(), dice.end(), 0);
case YACHT: {
bool is_yacht = (counts.size() == 1);
return is_yacht ? 50 : 0;
}
case FULL_HOUSE: {
bool has_three = false;
bool has_two = false;
for (auto const& [val, count] : counts) {
if (count == 3) has_three = true;
if (count == 2) has_two = true;
}
return (has_three && has_two) ? std::accumulate(dice.begin(), dice.end(), 0) : 0;
}
case FOUR_OF_A_KIND: {
for (auto const& [val, count] : counts) {
if (count >= 4) {
return val * 4;
}
}
return 0;
}
case LITTLE_STRAIGHT: {
auto sorted_dice = dice;
std::sort(sorted_dice.begin(), sorted_dice.end());
std::array<int, 5> expected = {1, 2, 3, 4, 5};
return (sorted_dice == expected) ? 30 : 0;
}
case BIG_STRAIGHT: {
auto sorted_dice = dice;
std::sort(sorted_dice.begin(), sorted_dice.end());
std::array<int, 5> expected = {2, 3, 4, 5, 6};
return (sorted_dice == expected) ? 30 : 0;
}
}
return 0; // Should not be reached
}
} // namespace yacht
auto counts = count_dice(dice);: We call our helper right at the start. The result, the frequency map, is used by multiple categories. This is efficient as we only count once.case YACHT: The logic is now trivial. If there's only one unique key in our frequency map (e.g., only the key '4' exists), it must be a Yacht. We return 50, otherwise 0.case FULL_HOUSE: We iterate through the map's key-value pairs. If we find a count of 3 and a count of 2, it's a full house. The score is the sum of all dice.case FOUR_OF_A_KIND: We look for any die with a count of 4 or more. If found, the score is that die's value multiplied by 4.case LITTLE_STRAIGHT / BIG_STRAIGHT: For straights, sorting is the easiest approach. We create a mutable copy of the dice, sort it, and then compare it to the expected sequence for a perfect match.
A More Optimized and Unified Approach
The solution above is good, but we can refine it further. The core idea is to rely entirely on the frequency map for almost all category checks, avoiding sorting unless absolutely necessary. This creates a more consistent and often more performant solution, especially if sorting becomes a bottleneck in a larger application.
This approach emphasizes a "prepare once, query many times" philosophy. We process the dice into a frequency map, which is a more useful data structure for our specific queries than the raw, unsorted array.
● Start (dice)
│
▼
┌────────────────────┐
│ Create Frequency Map │
│ (e.g., std::map) │
└──────────┬─────────┘
│
▼
┌─────────────────────────┐
│ Iterate through dice & │
│ populate map counts │
└──────────┬──────────────┘
│
▼
◆ Category Check?
╱ (using map values) ╲
╱ ╲
Is count of any die == 5? Is there one pair & one triple?
(Yacht) (Full House)
│ │
▼ ▼
[Score 50] [Score Sum]
│ │
└────────────┬───────────────┘
▼
● End
Here's how we could refactor the score function to fully embrace this pattern.
// Inside yacht.cpp, a revised score function
int score_optimized(const std::array<int, 5>& dice, category c) {
// Phase 1: Data Preparation
std::map<int, int> counts;
int sum = 0;
for (int die : dice) {
counts[die]++;
sum += die;
}
// Phase 2: Logic Dispatch
switch (c) {
case ONES: return counts.count(1) ? counts[1] * 1 : 0;
case TWOS: return counts.count(2) ? counts[2] * 2 : 0;
case THREES: return counts.count(3) ? counts[3] * 3 : 0;
case FOURS: return counts.count(4) ? counts[4] * 4 : 0;
case FIVES: return counts.count(5) ? counts[5] * 5 : 0;
case SIXES: return counts.count(6) ? counts[6] * 6 : 0;
case CHOICE: return sum;
case YACHT:
return (counts.size() == 1 && counts.begin()->second == 5) ? 50 : 0;
case FULL_HOUSE: {
bool has_pair = false;
bool has_triple = false;
if (counts.size() != 2) return 0; // Optimization: must have exactly 2 unique values
for (auto const& [val, count] : counts) {
if (count == 2) has_pair = true;
if (count == 3) has_triple = true;
}
return (has_pair && has_triple) ? sum : 0;
}
case FOUR_OF_A_KIND: {
for (auto const& [val, count] : counts) {
if (count >= 4) {
return val * 4;
}
}
return 0;
}
case LITTLE_STRAIGHT: {
// Straights are the exception where checking counts is less clean than sorting.
return (counts.size() == 5 && !counts.count(6)) ? 30 : 0;
}
case BIG_STRAIGHT: {
return (counts.size() == 5 && !counts.count(1)) ? 30 : 0;
}
}
return 0;
}
Analysis of the Optimized Approach
- Single Pass: We now iterate through the dice array only once at the beginning to build the map and calculate the total sum.
- Simplified Simple Scores: The logic for ONES through SIXES becomes a direct map lookup.
counts.count(1)checks for existence, thencounts[1] * 1calculates the score. This is arguably more readable thanstd::accumulatefor this specific case. - More Efficient Straights: The straight logic is now much faster. A straight must have 5 unique dice values, so
counts.size() == 5is a necessary condition. For a Little Straight, the missing value must be 6 (!counts.count(6)). For a Big Straight, the missing value must be 1. This avoids sorting entirely! - DRY Principle: The
sumis calculated once and reused forFULL_HOUSEandCHOICE, adhering to the "Don't Repeat Yourself" principle.
Pros and Cons of Different Strategies
Choosing an implementation strategy involves trade-offs. Here's a comparison of the initial approach versus the frequency-map-centric one.
| Aspect | Initial Approach (Multiple Helpers, Sorting) | Optimized Approach (Frequency Map) |
|---|---|---|
| Readability | Can be high if helper functions are well-named. The intent of is_full_house() is very clear. |
Highly readable for most cases. The logic is centralized and relies on a single data structure (the map). |
| Performance | Generally good, but sorting for straights is O(N log N). Can involve multiple passes over the data. | Excellent. Building the map is O(N). All subsequent lookups are O(log K) where K is number of unique dice faces (max 6). This is faster than sorting. |
| Extensibility | Adding a new category might require a new helper function, increasing file length. | Adding a new category is as simple as adding a new `case` to the switch, likely reusing the existing `counts` map. |
| Code Duplication | Higher risk. Multiple functions might need to count or sum dice independently. | Lower risk. Data preparation (counting, summing) is done once at the beginning. |
How to Compile and Run Your Solution
To test your C++ code, you need a compiler like g++. Let's create a simple main file to run some tests.
Create a Test File: main.cpp
#include <iostream>
#include "yacht.h"
void test_score(const std::string& test_name, int expected, int actual) {
std::cout << "[" << (expected == actual ? "PASS" : "FAIL") << "] "
<< test_name << ": Expected " << expected << ", Got " << actual << std::endl;
}
int main() {
// Test cases
test_score("Yacht", 50, yacht::score({5, 5, 5, 5, 5}, yacht::YACHT));
test_score("Not Yacht", 0, yacht::score({1, 3, 3, 2, 5}, yacht::YACHT));
test_score("Full House", 19, yacht::score({3, 3, 3, 5, 5}, yacht::FULL_HOUSE));
test_score("Little Straight", 30, yacht::score({1, 2, 3, 4, 5}, yacht::LITTLE_STRAIGHT));
test_score("Fours", 12, yacht::score({4, 4, 1, 4, 5}, yacht::FOURS));
return 0;
}
Compilation and Execution Commands
Open your terminal, navigate to the directory containing yacht.h, yacht.cpp, and main.cpp, and run the following commands.
# Step 1: Compile the C++ source files into an executable named 'yacht_game'
# The -std=c++17 flag ensures we can use modern C++ features like structured bindings.
g++ -std=c++17 -o yacht_game main.cpp yacht.cpp
# Step 2: Run the compiled executable
./yacht_game
You should see the output of your test cases, indicating whether each calculation was successful.
[PASS] Yacht: Expected 50, Got 50
[PASS] Not Yacht: Expected 0, Got 0
[PASS] Full House: Expected 19, Got 19
[PASS] Little Straight: Expected 30, Got 30
[PASS] Fours: Expected 12, Got 12
Frequently Asked Questions (FAQ)
- Why use
std::array<int, 5>instead of astd::vector<int>? -
std::arrayis a fixed-size container. Since the game of Yacht always uses exactly five dice,std::arrayis the perfect choice. It communicates this fixed-size constraint in the type itself, making the code safer and self-documenting. It also avoids the overhead of dynamic memory allocation that comes withstd::vector. - What is the most complex category to implement?
-
"Full House" and "Four of a Kind" can be tricky if you don't use a frequency map. Without one, you often need to sort the dice and then write complex loop conditions to check for adjacent matching values. Using a frequency map, as shown in the optimized solution, makes these checks significantly simpler.
- Could a
std::unordered_mapbe used instead ofstd::map? -
Absolutely. For this problem, a
std::unordered_mapwould likely be slightly faster as it provides average O(1) lookups compared tostd::map's O(log N). However, with only 6 possible die faces, the performance difference is negligible.std::mapkeeps keys sorted, which can be useful for debugging or for logic that depends on order, but that is not required here. - How would you adapt this logic for Yahtzee?
-
The logic is very similar. Yahtzee has categories like "3 of a Kind" and separate "Small Straight" (4 dice in sequence) and "Large Straight" (5 dice in sequence) categories. You would adapt the
scorefunction's switch statement, adding new cases and modifying the logic for straights. The core strategy of using a frequency map would still be highly effective. - What does the
[target]in the lambda function mean? -
This is the "capture clause" of a C++ lambda. It specifies which variables from the surrounding scope the lambda function needs to access.
[target]means the lambda "captures" thetargetvariable by value, making a copy of it available for use inside the lambda's body. - Is there a way to implement this without using the C++ STL?
-
Yes, but it would be far more verbose and error-prone. You would have to manually write sorting algorithms, create frequency counts using plain C-style arrays (e.g.,
int counts[6] = {0};), and write loops for everything. The STL exists to provide robust, efficient, and well-tested implementations of these common operations so you don't have to reinvent the wheel. - What are the technology trends for this type of logic programming?
-
For C++, the trend is towards more expressive and safer code using modern features (C++17, C++20, and beyond). This includes using ranges (
std::ranges), concepts for better template error messages, and more powerful algorithms. The core idea of transforming data into a more queryable structure (like a frequency map) is a timeless programming pattern that is also central to functional programming paradigms, which are gaining popularity.
Conclusion: From Game Rules to Elegant Code
We've successfully translated the rules of the Yacht dice game into a clean, efficient, and robust C++ solution. This journey took us through understanding the problem domain, applying fundamental STL algorithms, and progressively refactoring our code for better performance and readability. The final frequency-map-based approach is a testament to how choosing the right data structure can dramatically simplify complex logic.
This exercise from the kodikra.com curriculum is more than just a game; it's a practical lesson in algorithmic thinking. The skills you've applied here—data processing, pattern matching, and logical dispatching—are fundamental to countless real-world programming challenges.
As you continue your journey, remember the power of the C++ STL. Before you write a complex loop, ask yourself if there's an algorithm in <algorithm> or <numeric> that can do the job more elegantly. Continue to build on this foundation by exploring more challenges in the C++ learning path and dive deeper into the language with our complete C++ guide.
Disclaimer: The code in this article is based on modern C++ standards (C++17 and later). Ensure your compiler is configured to support these versions for full compatibility.
Published by Kodikra — Your trusted Cpp learning resource.
Post a Comment