Yacht in Csharp: Complete Solution & Deep Dive Guide


Mastering the Yacht Dice Game in C#: A Complete Guide to Scoring Logic

Calculating the score for the Yacht dice game involves evaluating a set of five dice against specific category rules. In C#, this is efficiently handled using a combination of an enum for categories and a method that employs LINQ for data manipulation and a switch expression for logic branching.


You’ve gathered your friends for a game night, and the classic dice game Yacht is on the table. The dice are rolled, a flurry of numbers appears, and then comes the pause. Everyone squints at the dice, then at the score sheet, trying to manually calculate the best possible score. It’s a moment of fun that can quickly turn into a tedious chore, prone to errors and debate. What if you could capture that complex, branching logic in a few lines of elegant, error-proof code?

This is where the power of programming, specifically with C#, shines. The Yacht game isn't just a pastime; it's a perfect real-world puzzle that challenges your problem-solving skills. It forces you to think about data manipulation, conditional logic, and efficient algorithm design. In this guide, we will transform the manual scoring process into a clean, automated C# solution. We'll dive deep into the logic, explore powerful C# features like LINQ and modern `switch` expressions, and build a scoring function that is both robust and easy to understand, turning you from a player into the master of the game's logic.


What Exactly is the Yacht Dice Game?

Before we write a single line of code, it's crucial to understand the game's domain. The Yacht dice game is a strategic game of chance and a direct ancestor of the more widely known Yahtzee. The core objective is simple: score as many points as possible by rolling five dice and assigning the result to one of twelve fixed categories over twelve rounds.

In each round, a player rolls five standard six-sided dice. Based on the outcome, they must choose one of the available scoring categories. Once a category is used, it cannot be used again. This introduces a layer of strategy, as players must decide whether to take a low score in a category now or save it for a potentially better roll later.

The scoring is what forms the heart of our programming challenge. The categories are diverse, ranging from simple sums to complex pattern recognition. Here is a detailed breakdown of each category, which will serve as the blueprint for our C# logic:

The Twelve Scoring Categories

Category Score Calculation Description Example Dice Roll Example Score
Ones Sum of dice showing 1 A simple sum of all the ones rolled. 1, 1, 2, 4, 5 2
Twos Sum of dice showing 2 A simple sum of all the twos rolled. 2, 2, 2, 3, 5 6
Threes Sum of dice showing 3 A simple sum of all the threes rolled. 3, 1, 2, 4, 3 6
Fours Sum of dice showing 4 A simple sum of all the fours rolled. 4, 4, 4, 4, 1 16
Fives Sum of dice showing 5 A simple sum of all the fives rolled. 5, 1, 5, 2, 5 15
Sixes Sum of dice showing 6 A simple sum of all the sixes rolled. 6, 6, 1, 2, 3 12
Full House Sum of all 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, 6 16
Little Straight 30 points Dice show 1, 2, 3, 4, 5. 1, 2, 3, 4, 5 30
Big Straight 30 points Dice show 2, 3, 4, 5, 6. 2, 3, 4, 5, 6 30
Choice Sum of all dice Any combination of dice. Also called "Chance". 1, 3, 4, 5, 6 19
Yacht 50 points All five dice showing the same number. 6, 6, 6, 6, 6 50

Why is Yacht a Perfect C# Programming Challenge?

The Yacht game might seem trivial at first glance, but it's a fantastic problem for honing your programming skills, particularly in C#. It serves as a practical exercise that touches upon several fundamental computer science and software engineering concepts.

Firstly, it demands robust conditional logic. The core of the solution is a mechanism that selects the correct scoring algorithm from twelve different possibilities. This is a classic use case for `switch` statements or the more modern `switch` expressions, allowing you to see their strengths and weaknesses in a real-world scenario.

Secondly, it's a masterclass in data manipulation and querying. The input is a simple array of five integers, but to evaluate categories like "Full House" or "Four of a Kind," you can't just look at the numbers. You need to transform the data. You must count occurrences, group identical values, and check for specific sequences. This is where C#'s Language-Integrated Query (LINQ) becomes incredibly powerful, allowing you to express these complex data transformations in a declarative and highly readable way.

Finally, it encourages good software design. How do you represent the categories? An `enum` is a perfect fit, providing type safety and readability. How do you structure the scoring logic? A static utility class with a single `Score` method is a clean, functional approach that encapsulates the logic without needing to maintain state. This module from the kodikra learning path is designed to build these foundational skills.


How to Architect the Scoring Logic: A High-Level Plan

Before writing the C# implementation, let's outline a strategic plan. A solid architecture will make the code cleaner, more maintainable, and easier to debug. Our approach can be broken down into a clear, logical flow from input to output.

The function will always receive two pieces of information: the five dice values (e.g., an array `int[] { 1, 3, 3, 5, 5 }`) and the target category to score against (e.g., `YachtCategory.FullHouse`). The goal is to return a single integer representing the score.

Here is a conceptual flowchart of our algorithm:

    ● Start: Receive Dice & Category
    │
    ▼
  ┌───────────────────┐
  │ Analyze Dice Roll │
  │ (Group & Count)   │
  └─────────┬─────────┘
            │
            ▼
    ◆ Select Scoring Rule
   ╱   (Based on Category)   ╲
  ├───────────┬───────────┬───...
  │           │           │
  ▼           ▼           ▼
[Rule: Ones] [Rule: Full House] [Rule: Yacht]
  │           │           │
  └───────────┼───────────┘
              │
              ▼
  ┌───────────────────┐
  │ Calculate Score   │
  └─────────┬─────────┘
            │
            ▼
    ● End: Return Score (int)

This flow highlights three key stages:

  1. Data Preparation: For many categories, the raw dice array isn't enough. We need to know the frequency of each number. A common first step for complex categories is to group the dice by their value and count how many of each exist. For example, `[3, 5, 3, 5, 3]` becomes "three 3s and two 5s".
  2. Logic Dispatching: We need a control structure to execute the correct scoring logic based on the input `category`. A `switch` is the natural choice in C# for this purpose.
  3. Calculation: Within each branch of our `switch`, we'll implement the specific formula for that category, using the prepared data from step one.

This systematic approach ensures that we handle each case correctly and that the overall structure remains organized, even with twelve different logical paths.


Where to Implement the Logic: A C# Solution Deep Dive

Now, let's translate our plan into C# code. Following best practices, we'll define our categories using an `enum` for clarity and type safety, and we'll place our scoring logic inside a `static` class, as it doesn't require any instance-specific data.

Step 1: Defining the Categories with an `enum`

An enumeration (`enum`) is the perfect way to represent a fixed set of constants. It makes the code more readable and prevents errors from using invalid category names.


public enum YachtCategory
{
    Ones = 1,
    Twos = 2,
    Threes = 3,
    Fours = 4,
    Fives = 5,
    Sixes = 6,
    FullHouse = 7,
    FourOfAKind = 8,
    LittleStraight = 9,
    BigStraight = 10,
    Choice = 11,
    Yacht = 12
}

By assigning integer values, we can, if needed, but for our purpose, the symbolic names are what matter. They allow us to write `YachtCategory.FullHouse` instead of a "magic number" like `7`.

Step 2: The Static `YachtGame` Class and `Score` Method

We'll create a `static` class named `YachtGame` to house our logic. A `static` class cannot be instantiated and can only contain `static` members. This is ideal for utility functions like our `Score` method.


public static class YachtGame
{
    public static int Score(int[] dice, YachtCategory category)
    {
        // Our scoring logic will go here
    }
}

Step 3: Implementing the Scoring Logic with a `switch` Statement

Inside the `Score` method, we'll use a `switch` statement to handle the twelve categories. We'll walk through the implementation for each category, explaining the LINQ queries in detail.


public static class YachtGame
{
    public static int Score(int[] dice, YachtCategory category)
    {
        switch (category)
        {
            // Case 1: Simple Sum Categories (Ones to Sixes)
            case YachtCategory.Ones:
                return dice.Where(d => d == 1).Sum();
            case YachtCategory.Twos:
                return dice.Where(d => d == 2).Sum();
            case YachtCategory.Threes:
                return dice.Where(d => d == 3).Sum();
            case YachtCategory.Fours:
                return dice.Where(d => d == 4).Sum();
            case YachtCategory.Fives:
                return dice.Where(d => d == 5).Sum();
            case YachtCategory.Sixes:
                return dice.Where(d => d == 6).Sum();

            // Case 2: The "Choice" Category
            case YachtCategory.Choice:
                return dice.Sum();

            // Case 3: The "Yacht" Category (All five dice the same)
            case YachtCategory.Yacht:
                // Group dice by value. If there's only one group and it has 5 dice, it's a Yacht.
                var groupsForYacht = dice.GroupBy(d => d);
                return groupsForYacht.Count() == 1 ? 50 : 0;

            // Case 4: Four of a Kind
            case YachtCategory.FourOfAKind:
                var groupsForFour = dice.GroupBy(d => d);
                // Find a group that has 4 or more dice.
                var fourOfAKindGroup = groupsForFour.FirstOrDefault(g => g.Count() >= 4);
                // If such a group exists, the score is 4 * its value. Otherwise, 0.
                return fourOfAKindGroup != null ? fourOfAKindGroup.Key * 4 : 0;

            // Case 5: Full House (Three of one, two of another)
            case YachtCategory.FullHouse:
                var groupsForFullHouse = dice.GroupBy(d => d);
                // A full house must have exactly two groups of numbers.
                // One group must have a count of 3, the other a count of 2.
                bool hasThreeOfAKind = groupsForFullHouse.Any(g => g.Count() == 3);
                bool hasPair = groupsForFullHouse.Any(g => g.Count() == 2);
                return (groupsForFullHouse.Count() == 2 && hasThreeOfAKind && hasPair) ? dice.Sum() : 0;

            // Case 6: The Straights
            case YachtCategory.LittleStraight:
                // Sort the dice and check if they are exactly 1,2,3,4,5
                Array.Sort(dice);
                return dice.SequenceEqual(new int[] { 1, 2, 3, 4, 5 }) ? 30 : 0;
            
            case YachtCategory.BigStraight:
                // Sort the dice and check if they are exactly 2,3,4,5,6
                Array.Sort(dice);
                return dice.SequenceEqual(new int[] { 2, 3, 4, 5, 6 }) ? 30 : 0;

            default:
                return 0;
        }
    }
}

Code Walkthrough: Deconstructing the Logic

  • Ones to Sixes: These are the simplest cases. We use the LINQ method .Where(d => d == N) to filter the array, keeping only the dice with the desired value N. Then, we use .Sum() to add them up. For example, for a roll of {1, 1, 2, 5, 5} and category Ones, Where creates a new sequence {1, 1} and Sum returns 2.
  • Choice: This is even simpler. We just need the total of all dice, which is a direct call to dice.Sum().
  • Yacht: Here, we introduce .GroupBy(d => d). This powerful LINQ method groups all identical elements together. For a roll of {4, 4, 4, 4, 4}, it creates a single group where the key is `4` and the elements are the five `4`s. If the .Count() of these groups is exactly 1, it means all dice were the same, and we have a Yacht.
  • Four of a Kind: We again use .GroupBy(). Then, we search for a group whose count is 4 or more using .FirstOrDefault(g => g.Count() >= 4). If a group is found (i.e., it's not `null`), we calculate the score by multiplying its key (the die's value) by 4.
  • Full House: This is the most complex pattern. A full house requires two distinct numbers, one appearing three times and the other twice. Our logic checks three conditions:
    1. groups.Count() == 2: There must be exactly two unique numbers in the roll.
    2. groups.Any(g => g.Count() == 3): One of those groups must contain three dice.
    3. groups.Any(g => g.Count() == 2): The other group must contain two dice.
    If all three are true, we score the sum of all dice.
  • Straights: For the straights, the easiest approach is to sort the dice array first using Array.Sort(). Then, we can use the LINQ method .SequenceEqual() to compare our sorted roll against the exact required sequence ({1, 2, 3, 4, 5} for Little Straight and {2, 3, 4, 5, 6} for Big Straight). This is a very clean and readable way to check for an exact sequence match.

This detailed logic flow for detecting a Full House can be visualized as follows:

    ● Input: Dice Array
    │ e.g., [3, 5, 3, 5, 3]
    ▼
  ┌───────────────────┐
  │ Group by Value    │
  └─────────┬─────────┘
            │ Result: {3: [3,3,3], 5: [5,5]}
            ▼
    ◆ Number of Groups == 2?
   ╱           ╲
 Yes (Groups=2) No (Score=0)
  │
  ▼
    ◆ Group Counts are {3, 2}?
   ╱           ╲
 Yes           No (Score=0)
  │
  ▼
  ┌───────────────────┐
  │ Calculate Sum()   │
  └─────────┬─────────┘
            │ Result: 19
            ▼
    ● Output: Score

When to Modernize: Refactoring to a C# `switch` Expression

The `switch` statement we used is perfectly functional and has been a staple of C# for years. However, since C# 8.0, a more modern, concise, and powerful alternative exists: the `switch` expression. It's an excellent fit for this problem and can significantly improve code readability.

A `switch` expression uses a more functional syntax, reduces boilerplate (no more `case` and `break` keywords), and can be directly assigned to a variable or returned from a method. It also enforces exhaustiveness, meaning the compiler will warn you if you forget to handle a possible value of your `enum`, which is a fantastic safety feature.

The Optimized Code with a `switch` Expression

Let's refactor our `Score` method to use this modern syntax. We'll also extract the grouping logic into a helper variable to avoid repetition.


public static class YachtGame
{
    public static int Score(int[] dice, YachtCategory category)
    {
        // Pre-calculate groups as it's used in multiple categories
        var groups = dice.GroupBy(d => d).ToList();

        return category switch
        {
            YachtCategory.Ones => dice.Count(d => d == 1) * 1,
            YachtCategory.Twos => dice.Count(d => d == 2) * 2,
            YachtCategory.Threes => dice.Count(d => d == 3) * 3,
            YachtCategory.Fours => dice.Count(d => d == 4) * 4,
            YachtCategory.Fives => dice.Count(d => d == 5) * 5,
            YachtCategory.Sixes => dice.Count(d => d == 6) * 6,
            YachtCategory.Choice => dice.Sum(),
            YachtCategory.Yacht => groups.Count == 1 ? 50 : 0,

            YachtCategory.FourOfAKind => 
                groups.FirstOrDefault(g => g.Count() >= 4)?.Key * 4 ?? 0,
            
            YachtCategory.FullHouse =>
                groups.Count == 2 && groups.Any(g => g.Count() == 3) ? dice.Sum() : 0,

            YachtCategory.LittleStraight =>
                dice.OrderBy(d => d).SequenceEqual(new[] { 1, 2, 3, 4, 5 }) ? 30 : 0,

            YachtCategory.BigStraight =>
                dice.OrderBy(d => d).SequenceEqual(new[] { 2, 3, 4, 5, 6 }) ? 30 : 0,

            // The discard `_` is a catch-all, required by the switch expression
            _ => 0
        };
    }
}

Why is this version better?

  • Conciseness: The code is much shorter and easier to scan. The `=>` syntax clearly separates the condition from the result.
  • Readability: The expression-based nature reads more like a specification or a mapping of categories to scoring rules.
  • Reduced Repetition: By calculating `groups` once at the top, we make the subsequent logic cleaner. Note the change from `dice.Where(...).Sum()` to `dice.Count(...) * N`, which is slightly more direct for the simple sum categories.
  • Null-Conditional Operator: For `FourOfAKind`, we use the null-conditional operator `?.` and the null-coalescing operator `??`. The expression `groups.FirstOrDefault(...)?.Key * 4 ?? 0` elegantly says: "Try to find the group; if you find it, get its Key and multiply by 4. If `FirstOrDefault` returns `null`, the whole chain short-circuits and the `?? 0` provides a default value of 0." This is a very idiomatic and safe way to handle potential nulls in modern C#.
  • LINQ `OrderBy` vs `Array.Sort`: In the refactored version, we use `dice.OrderBy(d => d)` instead of `Array.Sort(dice)`. `OrderBy` is a LINQ method that returns a *new* sorted sequence without modifying the original `dice` array. This is a better practice as it avoids side effects (mutating the input parameter), which is a core principle of functional programming.

Pros and Cons: `switch` Statement vs. `switch` Expression

Feature Traditional switch Statement Modern switch Expression
Syntax Verbose, with case, :, and break/return for each branch. Concise, using => for each arm. Reads like a map.
Value Returning Does not return a value directly. You must return or assign from each branch. Evaluates to a single value, which can be directly returned or assigned.
Exhaustiveness Compiler does not check if all enum values are handled (unless using specific analyzer settings). Compiler enforces exhaustiveness, issuing a warning if a possible input is not handled. Requires a default case `_`.
Pattern Matching Supports basic pattern matching since C# 7. Offers more advanced and integrated pattern matching capabilities.
Complexity Can become deeply nested and hard to read with complex logic in each case. Encourages simpler, single-expression logic per arm, promoting clarity.
Best For Executing blocks of statements or multiple actions per case. Mapping an input to a single output value based on patterns. Perfect for this problem.

For the Yacht scoring problem, the `switch` expression is the clear winner due to its conciseness, safety, and alignment with a functional programming style. For more in-depth C# concepts, explore our complete C# learning curriculum on kodikra.com.


Frequently Asked Questions (FAQ)

1. What is the main difference between Yacht and Yahtzee?

Yacht is the direct predecessor to Yahtzee and they are very similar. The primary differences are in the scoring. For example, in many Yahtzee rule sets, the straights have different scores (30 for a small straight, 40 for a large), and there are bonuses for getting multiple Yahtzees. The Yacht scoring rules, as implemented here, are simpler and form the basis from which Yahtzee evolved.

2. Why is LINQ so heavily used in this C# solution?

LINQ (Language-Integrated Query) is used because it provides a highly expressive and declarative way to query and manipulate data collections. Instead of writing manual `for` loops with `if` conditions to count, group, or sum dice, we can use methods like GroupBy, Where, Sum, and Count. This makes the code shorter, more readable, and less prone to common off-by-one or logic errors found in imperative loops.

3. How could you handle invalid dice inputs in this function?

A robust implementation would add input validation at the beginning of the Score method. You could check for several conditions:

  • Is the dice array null? (if (dice == null) throw new ArgumentNullException(...))
  • Does the array contain exactly five elements? (if (dice.Length != 5) throw new ArgumentException(...))
  • Are all dice values between 1 and 6? (if (dice.Any(d => d < 1 || d > 6)) throw new ArgumentOutOfRangeException(...))
Adding these checks makes your function safer and more predictable when used in a larger application.

4. Is a `switch` expression always better than a `switch` statement?

Not always. A `switch` expression is best when you are mapping an input to a single output value, as in our scoring function. If each case needs to perform multiple, complex actions (e.g., call several methods, modify state, perform I/O), a traditional `switch` statement with code blocks { ... } is often clearer and more appropriate.

5. What is the purpose of the "Choice" category?

The "Choice" category (often called "Chance" in Yahtzee) is a fallback or safety net. It allows a player to score points from a roll that doesn't fit well into any of the other pattern-based categories. Since its score is simply the sum of all dice, it guarantees some points from any roll, making it a crucial part of the game's strategy.

6. How does the code distinguish between a "Little Straight" and a "Big Straight"?

The code uses a precise sequence comparison. After sorting the dice, it checks for an exact match against two different, predefined arrays.

  • For a Little Straight, it checks if the sorted dice are exactly {1, 2, 3, 4, 5} using SequenceEqual.
  • For a Big Straight, it checks if they are exactly {2, 3, 4, 5, 6}.
A roll cannot be both at the same time, so the checks are mutually exclusive.

7. Can this logic be adapted for a game with more dice or different rules?

Absolutely. The core patterns (grouping, counting, summing) are highly adaptable. If you were playing with six dice, you would simply update the validation logic. If new categories were added (e.g., "Two Pairs"), you would add a new case to the `switch` expression with the corresponding LINQ query to detect that pattern, making the design very extensible.


Conclusion: From Game Rules to Elegant Code

We have successfully translated the complete rules of the Yacht dice game into a clean, efficient, and modern C# solution. By breaking down the problem, we saw how a seemingly complex set of twelve scoring rules could be implemented systematically. We leveraged the power of C# enums for type-safe categories and the expressive syntax of LINQ to perform complex data analysis on the dice rolls with remarkable clarity.

Furthermore, we explored the evolution of C# syntax by refactoring our initial `switch` statement into a more concise and safer `switch` expression. This demonstrated not just how to solve the problem, but how to solve it using best practices that lead to more maintainable and readable code. The Yacht problem, as featured in the kodikra.com C# curriculum, is a perfect example of how abstract programming concepts can be applied to create tangible, logical solutions for real-world puzzles.

The final takeaway is the importance of choosing the right tool for the job. LINQ excels at data querying, and `switch` expressions are perfect for conditional mapping. Mastering these features will significantly elevate your C# programming skills, enabling you to tackle even more complex challenges with confidence and elegance.

Disclaimer: All code examples are written and tested against .NET 8 and C# 12. Syntax and features, particularly `switch` expressions, may not be available in older versions of the .NET framework.


Published by Kodikra — Your trusted Csharp learning resource.