Yacht in Bash: Complete Solution & Deep Dive Guide

A blue and white boat is featured in this image.

The Complete Guide to Solving the Yacht Dice Game in Bash

Implementing game logic in Bash scripts requires a solid understanding of shell functions, argument parsing, and data manipulation. This guide provides a comprehensive solution for the Yacht dice game, breaking down how to calculate scores for categories like "Full House" or "Yacht" using core Bash utilities and structured scripting.

Have you ever looked at a Bash script and wondered if it could do more than just move files or run system commands? Many developers see Bash as a simple glue language, but its true power lies in its ability to process text and data, making it a surprisingly capable tool for solving logic puzzles, like a classic dice game. You might feel that implementing complex rules in a shell script is daunting, but this challenge is the perfect opportunity to level up your skills from a simple command-user to a sophisticated script author. This article will guide you through every step, transforming a complex problem into a clean, modular, and understandable Bash solution.


What is the Yacht Dice Game?

The Yacht dice game is a classic game of chance and strategy, a direct precursor to the more widely known Yahtzee. It belongs to a family of games that includes Poker Dice and Generala, all centered around rolling five dice to achieve specific combinations. The game is played over twelve rounds, where in each round, a player rolls five dice and must assign the outcome to one of twelve scoring categories.

The core challenge, which we will tackle in Bash, is to take a set of five dice rolls and a specific category, then calculate the score according to the game's rules. Each die is a standard six-sided die, with values ranging from 1 to 6. The dice are presented as a simple list of numbers, which may be unordered.

Understanding the scoring categories is crucial. They range from simple sums to complex patterns. Let's break down each category in detail.

Scoring Categories Explained

The twelve categories determine how a player's roll is scored. Mastering these rules is the first step to building our Bash script.

Category Score Calculation Description Example Dice Roll Example Score
Ones Sum of dice showing 1 Score is the total of all dice with the value 1. 1 1 2 4 5 2
Twos Sum of dice showing 2 Score is the total of all dice with the value 2. 2 2 2 3 4 6
Threes Sum of dice showing 3 Score is the total of all dice with the value 3. 3 1 3 5 3 9
Fours Sum of dice showing 4 Score is the total of all dice with the value 4. 4 4 1 2 3 8
Fives Sum of dice showing 5 Score is the total of all dice with the value 5. 5 1 2 3 4 5
Sixes Sum of dice showing 6 Score is the total of all dice with the value 6. 6 6 6 6 1 24
Full House Sum of all dice Three of one number and two of another. Scores 0 if not met. 3 3 3 5 5 19
Four of a Kind Sum of the four matching dice At least four dice showing the same number. Scores 0 if not met. 4 4 4 4 1 16
Little Straight Fixed score of 30 Dice show 1, 2, 3, 4, 5. Scores 0 if not met. 1 2 3 4 5 30
Big Straight Fixed score of 30 Dice show 2, 3, 4, 5, 6. Scores 0 if not met. 2 3 4 5 6 30
Choice Sum of all dice Any combination of dice. This is a "catch-all" category. 1 3 4 5 6 19
Yacht Fixed score of 50 All five dice showing the same number. Scores 0 if not met. 4 4 4 4 4 50

Why Solve This Challenge with Bash?

At first glance, Bash might not seem like the ideal language for implementing game logic. Languages like Python or JavaScript offer more built-in data structures and a more conventional programming paradigm. However, tackling this problem in Bash provides an incredible learning opportunity and highlights the shell's surprising versatility.

This challenge, drawn from the exclusive kodikra.com learning curriculum, forces you to master fundamental concepts that are directly applicable to real-world system administration and DevOps tasks:

  • Argument Parsing: The script needs to accept a category and a list of dice rolls as command-line arguments. This is a core skill for creating any command-line tool.
  • Control Flow: A central case statement is perfect for routing logic based on the chosen category, mirroring how a script might handle different sub-commands (e.g., git commit, git push).
  • Text and Data Manipulation: The heart of the problem involves counting and analyzing the dice rolls. This is where Bash shines. Using a pipeline of commands like sort, uniq, and awk is analogous to parsing log files, processing CSV data, or filtering system outputs.
  • Modular Design with Functions: Breaking down each category's scoring logic into separate functions makes the code clean, reusable, and easy to debug. This principle of modularity is universal in software development.

By solving the Yacht problem, you aren't just building a game scorer; you're building a mental toolkit for processing structured and semi-structured data directly from the command line, a skill that remains highly valuable in any server environment.


How to Structure the Bash Solution

A robust solution requires a clear structure. We'll design our script around a main controller that delegates tasks to specialized helper functions. This approach keeps our code organized and readable.

Here is the high-level logic flow:

    ● Start Script
    │
    ▼
  ┌───────────────────────┐
  │ Read Category & Dice  │
  │ (from command line)   │
  └──────────┬────────────┘
             │
             ▼
    ◆  Select Category (case statement)
   ╱           ├───────────╲
  │            │            │
  ▼            ▼            ▼
[Call "ones"] [Call "full_house"] [Call "yacht"] ...
  │            │            │
  └────────────┼────────────┘
               │
               ▼
  ┌───────────────────────┐
  │ Calculate & Echo Score│
  └──────────┬────────────┘
             │
             ▼
    ● End Script

The Core Components

  1. Main Function: A main() function will serve as the entry point. It will parse the command-line arguments, storing the category and the dice rolls.
  2. Case Statement Router: Inside main(), a case statement will read the category variable and call the appropriate scoring function. This is the script's central nervous system.
  3. Helper Functions: We will create a function for each unique type of scoring logic.
    • xs(): A generic function to handle the simple sum categories (Ones, Twos, ..., Sixes).
    • full_house(): A function to check for the 3-and-2 dice pattern.
    • four_of_a_kind(): A function to check for four matching dice.
    • straight(): A function to check for "Little Straight" and "Big Straight".
    • yacht(): A function to check if all five dice are the same.
    • choice(): A simple function to sum all dice.

This modular design makes it easy to test each piece of logic independently and simplifies future maintenance or extensions.


Detailed Code Walkthrough

Let's dissect a complete and functional Bash script that solves the Yacht problem. We will go through it section by section, explaining the purpose of each function and command.

The Full Script

#!/usr/bin/env bash

# This script calculates the score for a given category in the Yacht dice game.

# Helper function to count dice frequencies.
# Usage: get_counts ${dice[@]}
# Output: A sorted list of "count value", e.g., "3 5" for three 5s.
get_counts() {
    # tr replaces spaces with newlines, sort groups numbers, uniq -c counts them.
    tr ' ' '\n' <<< "$@" | sort -n | uniq -c | sed 's/^[ ]*//'
}

# Scores the simple categories: ones, twos, threes, fours, fives, sixes.
# Usage: xs <number_to_count> ${dice[@]}
xs() {
    local target=$1
    shift
    local sum=0
    for die in "$@"; do
        if (( die == target )); then
            sum=$(( sum + die ))
        fi
    done
    echo "$sum"
}

# Scores "Full House".
# Must be three of one number and two of another.
# Score is the sum of all dice.
full_house() {
    local counts
    counts=$(get_counts "$@")
    
    # A full house must have exactly two unique dice values.
    if (( $(echo "$counts" | wc -l) != 2 )); then
        echo 0
        return
    fi

    # Check if the counts are 2 and 3 (or 3 and 2).
    local first_count=$(echo "$counts" | head -n1 | cut -d' ' -f1)
    if (( first_count == 2 || first_count == 3 )); then
        local total=0
        for die in "$@"; do
            total=$(( total + die ))
        done
        echo "$total"
    else
        echo 0
    fi
}

# Scores "Four of a Kind".
# At least four dice must be the same.
# Score is the sum of those four dice.
four_of_a_kind() {
    local counts
    counts=$(get_counts "$@")
    
    # Find a line in counts where the count is 4 or 5.
    local match=$(echo "$counts" | grep -E '^[45] ')

    if [[ -n "$match" ]]; then
        # Extract the die value from the match.
        local value=$(echo "$match" | cut -d' ' -f2)
        echo $(( 4 * value ))
    else
        echo 0
    fi
}

# Scores "Little Straight" (1-2-3-4-5) and "Big Straight" (2-3-4-5-6).
# Usage: straight <type> ${dice[@]} where type is 1 for little, 2 for big.
straight() {
    local type=$1
    shift
    local sorted_dice=$(echo "$@" | tr ' ' '\n' | sort -n | tr '\n' ' ')
    
    local expected=""
    if (( type == 1 )); then
        expected="1 2 3 4 5 " # Little Straight
    else
        expected="2 3 4 5 6 " # Big Straight
    fi

    if [[ "$sorted_dice" == "$expected" ]]; then
        echo 30
    else
        echo 0
    fi
}

# Scores "Yacht".
# All five dice must be the same.
# Score is 50.
yacht() {
    local counts
    counts=$(get_counts "$@")

    # If there is only one unique die value, it's a Yacht.
    if (( $(echo "$counts" | wc -l) == 1 )); then
        echo 50
    else
        echo 0
    fi
}

# Scores "Choice".
# Score is the sum of all dice.
choice() {
    local total=0
    for die in "$@"; do
        total=$(( total + die ))
    done
    echo "$total"
}


main() {
    local category=$1
    shift
    local dice=("$@")

    case $category in
        ones)           xs 1 "${dice[@]}" ;;
        twos)           xs 2 "${dice[@]}" ;;
        threes)         xs 3 "${dice[@]}" ;;
        fours)          xs 4 "${dice[@]}" ;;
        fives)          xs 5 "${dice[@]}" ;;
        sixes)          xs 6 "${dice[@]}" ;;
        "full house")   full_house "${dice[@]}" ;;
        "four of a kind") four_of_a_kind "${dice[@]}" ;;
        "little straight") straight 1 "${dice[@]}" ;;
        "big straight") straight 2 "${dice[@]}" ;;
        yacht)          yacht "${dice[@]}" ;;
        choice)         choice "${dice[@]}" ;;
        *)              echo "Invalid category" >&2; exit 1 ;;
    esac
}

# Execute the main function with all command-line arguments.
main "$@"

Analysis of Key Functions

1. The main() Function

This is the script's entry point. - local category=$1: It grabs the first command-line argument (e.g., "yacht", "full house") and stores it in the category variable. - shift: This command discards the first argument, so that $@ now contains only the dice rolls. - local dice=("$@"): The remaining arguments (the dice) are stored in an array named dice. Using an array is best practice as it correctly handles spaces and other special characters. - case $category in ... esac: This is the central router. It matches the category variable against a list of patterns and executes the corresponding function, passing the dice array to it using the "${dice[@]}" syntax, which expands each array element as a separate word.

2. The get_counts() Helper Function

This is a brilliant piece of shell scripting that calculates the frequency of each die value. It's used by several other functions (full_house, four_of_a_kind, yacht) to avoid code duplication. - tr ' ' '\n' <<< "$@": This takes the input dice (e.g., "5 5 5 3 3"), replaces the spaces with newlines, creating a multi-line string. - | sort -n: The pipe (|) sends this output to the sort -n command, which sorts the numbers numerically. - | uniq -c: The sorted list is piped to uniq -c, which counts consecutive identical lines. The output is now something like ` 2 3` and ` 3 5`. - | sed 's/^[ ]*//': The sed command removes any leading spaces from the uniq -c output for cleaner parsing later. The final output is a clean list like `2 3\n3 5`.

3. The xs() Function for Simple Sums

This function is a great example of generalization. Instead of writing six separate functions for "Ones" through "Sixes," we have one function that takes the target number as an argument. - local target=$1: Stores the number we are looking for (1, 2, 3, etc.). - shift: Discards that first argument, leaving only the dice. - for die in "$@"; do ... done: It loops through each die. - if (( die == target )): It checks if the current die matches the target. The ((...)) syntax is Bash's modern arithmetic evaluation. - sum=$(( sum + die )): If it matches, the die's value is added to the running total.

4. The full_house() Function Logic

This function demonstrates how to use the get_counts output to check for a complex pattern. A full house is a combination of three of a kind and a pair.

    ● Start full_house()
    │
    ▼
  ┌──────────────────┐
  │ get_counts()     │◀─ Get frequency of each die
  └────────┬─────────┘
           │
           ▼
    ◆ Number of unique dice == 2?
   ╱           ╲
 Yes           No ───────────► Echo 0 & Exit
  │
  ▼
    ◆ Are counts "2 and 3"?
   ╱           ╲
 Yes           No ───────────► Echo 0 & Exit
  │
  ▼
┌──────────────────┐
│ Sum all dice     │
└────────┬─────────┘
         │
         ▼
    ● Echo Sum

The code implements this logic perfectly: - It first checks if there are exactly two lines of output from get_counts using wc -l. If not, it can't be a full house. - It then checks if the count of one of the groups is 2 or 3. Since we already know there are only two groups and a total of five dice, if one group is 2 or 3, the other must be 3 or 2, respectively. This is a clever shortcut. - If both conditions are met, it calculates and echoes the total sum of the dice; otherwise, it echoes 0.

5. The straight() Function

This function checks for two specific, ordered sequences. - local sorted_dice=$(...): It first sorts the dice numerically and joins them back into a single string with spaces. This "normalizes" the input. For example, 5 1 3 4 2 becomes 1 2 3 4 5 . - if [[ "$sorted_dice" == "$expected" ]]: It then performs a simple string comparison against the expected patterns for a little or big straight. This is a clean and highly readable way to check for a fixed sequence.


Real-World Applicability and Best Practices

The patterns used in this Yacht solver are not just for games. They are fundamental to robust shell scripting in professional environments.

Pros and Cons of Using Bash for This Task

Pros (Why Bash is a Good Choice Here) Cons (Where Bash Might Fall Short)
Ubiquity: Bash is available on virtually every Linux, macOS, and even Windows (via WSL) system, making scripts highly portable. Complex Data Structures: Bash lacks native support for complex structures like nested dictionaries or objects. Associative arrays exist but can be cumbersome.
Powerful Text Processing: The integration with core utilities like grep, sed, awk, sort, and uniq is seamless and incredibly powerful for data manipulation. Error Handling: Robust error handling requires careful use of set -e, set -o pipefail, and explicit checks, which can make the code verbose.
Ideal for CLI Tools: Bash is designed for creating command-line interfaces. Parsing arguments and piping data is its primary strength. Performance: For computationally intensive tasks, the overhead of forking processes for external commands (like grep, sed) can make Bash slower than compiled languages or even Python.
Excellent for Automation: This kind of logic is perfect for automating tasks like parsing server logs, analyzing report data, or managing file systems based on content. Readability at Scale: As scripts grow very large and complex, maintaining readability and managing state can become challenging compared to languages with stricter syntax and typing.

Future-Proofing Your Bash Scripts

As of today, Bash 5.x is prevalent. The script we've analyzed uses modern but widely supported features. Looking ahead to the next 1-2 years, the core principles of Bash scripting will remain stable. However, a few trends are worth noting:

  • ShellCheck is King: The importance of static analysis tools like ShellCheck cannot be overstated. It catches common errors, style issues, and portability problems before they become bugs. Integrating it into your workflow is a must.
  • Alternatives Gaining Traction: While Bash remains the de-facto standard, tools like Zsh (with frameworks like Oh My Zsh) offer more interactive features. For complex scripting, languages like Python and Go are increasingly used to create CLI tools due to better dependency management and testing frameworks.
  • Focus on POSIX Compliance: For maximum portability, especially in containerized environments with minimal base images (like Alpine Linux, which uses ash), writing POSIX-compliant shell scripts is a valuable skill. Our script uses some Bash-specific features (((...)), arrays), so it's not strictly POSIX-compliant.

To learn more about advanced scripting and other powerful tools, explore the full Bash language guide on kodikra.com.


Frequently Asked Questions (FAQ)

How can I handle invalid dice rolls in this Bash script?

You can add a validation loop at the beginning of the main function. Iterate through the dice array and use a regular expression or arithmetic comparison to ensure each die is an integer between 1 and 6. If an invalid die is found, print an error message to stderr and exit with a non-zero status code.

What is the difference between "$@" and "$*" in Bash?

This is a critical distinction. "$@" expands each argument as a separate, quoted string (e.g., "arg1" "arg2" "arg3"). This is almost always what you want, as it preserves arguments containing spaces. In contrast, "$*" expands all arguments into a single quoted string, joined by the first character of the IFS variable (e.g., "arg1 arg2 arg3"). Using "$@" is essential for passing arguments to other functions correctly.

Can I use associative arrays in Bash to count dice frequencies?

Yes, if you are using Bash version 4.0 or newer. You could declare an associative array with declare -A counts, loop through the dice, and increment the count for each value (e.g., ((counts[$die]++))). This can be more efficient as it avoids forking external processes like sort and uniq, making it a "pure Bash" solution.

Why is #!/usr/bin/env bash preferred over #!/bin/bash?

The #!/usr/bin/env bash shebang is more portable. It tells the system to find the bash executable in the user's $PATH. This is useful because the location of Bash can differ across systems (e.g., /bin/bash, /usr/local/bin/bash). The #!/bin/bash shebang uses a hardcoded path, which might fail if Bash is installed elsewhere.

What's the main difference between the Yacht and Yahtzee games?

They are very similar, but have key scoring differences. Yahtzee introduced the "Yahtzee Bonus" (extra points for subsequent Yahtzees), has different scoring for straights (Small Straight is 30, Large Straight is 40), and includes a "3 of a Kind" category. Yacht is considered the simpler, ancestral version of the game.

How could I make this script more user-friendly?

You could add a --help flag that prints a usage message explaining the available categories and the expected input format. You could also add more descriptive error messages for invalid categories or incorrect numbers of dice. Wrapping the core logic in functions makes adding these features straightforward without cluttering the main logic.


Conclusion

We have successfully navigated the logic of the Yacht dice game, translating its rules into a clean, modular, and effective Bash script. This journey has demonstrated that Bash is far more than a simple command runner; it is a powerful environment for data processing and automation. By leveraging core utilities, functions, and control structures, we built a solution that is both readable and robust.

The key takeaways from this kodikra module are the importance of structured design, the power of command-line pipelines for data analysis, and the real-world applicability of these scripting skills. The techniques used here—parsing arguments, routing logic with case, and creating reusable helper functions—are the building blocks for creating sophisticated command-line tools and automation scripts that can save you countless hours in your daily work.

Disclaimer: The code in this article is based on the latest stable versions of Bash (v5.x). While most features are backward-compatible, behavior may vary on very old systems. Always test scripts in your target environment.


Published by Kodikra — Your trusted Bash learning resource.