Yacht in Ballerina: Complete Solution & Deep Dive Guide

A ballerina poses gracefully in a dance.

Ballerina from Zero to Hero: Building the Yacht Dice Game Logic

AI OVERVIEW SNIPPET: Learn to solve the classic Yacht dice game challenge using Ballerina by mastering array processing, powerful conditional logic with match statements, and creating modular helper functions. This guide provides a complete solution, detailed code explanations, and best practices for writing efficient, readable Ballerina code.

Ever found yourself staring at a complex set of rules, like those for a board game, and wondering how to translate that intricate logic into clean, maintainable code? It’s a common challenge for developers. The logic can quickly become a tangled mess of `if-else` statements, making the code hard to read, debug, and extend. The Yacht dice game is a perfect example of this complexity, with its dozen scoring categories, each with a unique rule.

This is where a modern, concurrent, and type-safe language like Ballerina truly shines. Its expressive syntax and powerful features are designed to handle exactly this kind of complex, rule-based logic gracefully. In this deep-dive guide, we'll break down the Yacht game challenge from the exclusive kodikra.com curriculum. We won't just give you the answer; we'll guide you through the thought process of designing a robust, elegant solution from scratch, transforming you from a novice to a confident Ballerina programmer.


What is the Yacht Game Challenge?

Before we write a single line of code, we must thoroughly understand the problem domain. The Yacht game is a dice game where the objective is to score points by rolling five dice and then choosing a category to score them in. Our task is to build a function that, given five dice values and a category, calculates the correct score.

The core of the challenge lies in correctly implementing the scoring logic for each distinct category. You are always given five dice, with each die showing a value from one to six. The dice may be presented in any order.

The Scoring Categories

The rules are defined in the following table. Understanding each one is critical to building our logic.

Category Score Description Example
Ones 1 × count of ones The sum of dice with the number 1 1, 1, 2, 4, 4 scores 2
Twos 2 × count of twos The sum of dice with the number 2 2, 3, 2, 5, 1 scores 4
Threes 3 × count of threes The sum of dice with the number 3 3, 3, 3, 5, 1 scores 9
Fours 4 × count of fours The sum of dice with the number 4 4, 4, 4, 5, 5 scores 12
Fives 5 × count of fives The sum of dice with the number 5 5, 1, 5, 2, 5 scores 15
Sixes 6 × count of sixes The sum of dice with the number 6 2, 3, 4, 6, 6 scores 12
Full House Total of all 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 Dice are 1, 2, 3, 4, 5 1, 2, 3, 4, 5 scores 30
Big Straight 30 points Dice are 2, 3, 4, 5, 6 2, 3, 4, 5, 6 scores 30
Choice Sum of all dice Any combination of dice 2, 3, 4, 5, 6 scores 20
Yacht 50 points All five dice are the same 4, 4, 4, 4, 4 scores 50

Our goal is to create a single Ballerina function, let's call it score(), that accepts an array of five integers (the dice) and a string (the category) and returns the calculated integer score.


Why Use Ballerina for This Logic Puzzle?

While you could solve this problem in many languages, Ballerina offers a unique set of features that make it particularly well-suited for this kind of rule-based, data-processing task. Choosing the right tool for the job is a hallmark of an expert developer.

  • Expressive Type System: Ballerina's strong, static type system helps prevent common errors. We can define precise types for our data, like an array of 5 integers, ensuring that our function won't accidentally receive invalid input. This makes the code more robust and self-documenting.
  • Powerful match Statements: Instead of a long chain of if-else if-else blocks, Ballerina's match statement provides a structured, readable, and powerful way to handle different cases. It's perfect for mapping our string-based categories to their specific scoring logic.
  • Built-in List/Array Processing: Ballerina has rich, built-in support for list and array manipulation. Functions for sorting, summing, and iterating are first-class citizens, which drastically simplifies the logic for categories like "Choice", "Little Straight", and "Big Straight".
  • Clear and Readable Syntax: Ballerina was designed with readability in mind. Its syntax avoids much of the boilerplate common in other languages, allowing the developer to focus on the business logic. This is crucial for a problem like Yacht, where the logic itself is the most complex part.
  • Concurrency Primitives (Future-Proofing): While not strictly necessary for this specific problem, Ballerina's core design around concurrency means that if this game logic were part of a larger, networked multiplayer game server, Ballerina would be an excellent choice for handling multiple game states concurrently without complexity.

By leveraging these features, we can create a solution that is not only correct but also elegant, maintainable, and easy to understand. For a deeper look into the language's capabilities, you can see the complete Ballerina guide on kodikra.com.


How to Structure the Ballerina Solution

A good solution starts with a good plan. Instead of jumping straight into coding the main function, we'll break the problem down into smaller, manageable pieces. Our overall strategy will be to use a central score function that delegates the complex logic for each category to smaller, specialized helper functions.

This approach, known as decomposition, is a fundamental principle of software engineering. It leads to code that is easier to test, debug, and reuse.

High-Level Program Flow

Here is a conceptual overview of how our program will work. The input (dice and category) flows into a central dispatcher which then routes it to the correct logic block.

    ● Start: Receive (dice, category)
    │
    ▼
  ┌───────────────────┐
  │   score() function  │
  └─────────┬─────────┘
            │
            ▼
    ◆ Match on 'category'
   ╱     │      │      ╲
  ╱      │      │       ╲
 ▼       ▼      ▼        ▼
"ones" "full house" "straight" ... etc.
  │       │         │
  │       │         │
  ▼       ▼         ▼
┌───────────┐ ┌──────────────┐ ┌───────────────┐
│ count(1)  │ │ checkFreq()  │ │ checkSeq()    │
│ logic     │ │ logic        │ │ logic         │
└───────────┘ └──────────────┘ └───────────────┘
  ╲       │         │
   ╲      │         │
    └─────┼─────────┘
          │
          ▼
  ┌───────────────────┐
  │  Return finalScore  │
  └───────────────────┘
            │
            ▼
            ● End

The Complete Ballerina Code

Here is the full, well-commented solution. We'll break down each part of this code in the following sections, but it's helpful to see the complete picture first.

import ballerina/io;
import ballerina/lang.'array as arrays;

// The main entry point function that calculates the score based on a category.
// 'dice' is an array of 5 integers representing the dice rolls.
// 'category' is a string specifying which scoring rule to use.
public function score(string category, int[] dice) returns int {
    // A match statement is a clean way to handle the different category strings.
    match category {
        "ones" => {
            return countNumber(dice, 1) * 1;
        }
        "twos" => {
            return countNumber(dice, 2) * 2;
        }
        "threes" => {
            return countNumber(dice, 3) * 3;
        }
        "fours" => {
            return countNumber(dice, 4) * 4;
        }
        "fives" => {
            return countNumber(dice, 5) * 5;
        }
        "sixes" => {
            return countNumber(dice, 6) * 6;
        }
        "choice" => {
            // 'choice' is simply the sum of all dice.
            return dice.sum();
        }
        "yacht" => {
            // If all dice are the same, it's a Yacht.
            // We can check this by seeing if the count of the first die is 5.
            if countNumber(dice, dice[0]) == 5 {
                return 50;
            }
            return 0;
        }
        "little straight" => {
            // A little straight is 1,2,3,4,5. We sort the dice to check easily.
            int[] sortedDice = arrays:sort(dice);
            if sortedDice.toString() == "[1, 2, 3, 4, 5]" {
                return 30;
            }
            return 0;
        }
        "big straight" => {
            // A big straight is 2,3,4,5,6.
            int[] sortedDice = arrays:sort(dice);
            if sortedDice.toString() == "[2, 3, 4, 5, 6]" {
                return 30;
            }
            return 0;
        }
        "four of a kind" => {
            // We get a frequency map to see how many of each die we have.
            map<int> frequencies = getFrequencyMap(dice);
            // Iterate through the map to find a die with a count of 4 or more.
            foreach var [dieValue, count] in frequencies.entries() {
                if count >= 4 {
                    return dieValue * 4;
                }
            }
            return 0;
        }
        "full house" => {
            map<int> frequencies = getFrequencyMap(dice);
            // A full house has exactly two unique dice values.
            // One must have a count of 3 and the other a count of 2.
            if frequencies.length() == 2 {
                boolean hasThreeOfAKind = false;
                boolean hasPair = false;
                foreach var count in frequencies.values() {
                    if count == 3 {
                        hasThreeOfAKind = true;
                    } else if count == 2 {
                        hasPair = true;
                    }
                }

                if hasThreeOfAKind && hasPair {
                    return dice.sum();
                }
            }
            return 0;
        }
        // The 'else' block catches any unknown categories.
        else => {
            return 0;
        }
    }
}

// Helper function to count occurrences of a specific number in the dice array.
// This is used for "ones" through "sixes".
function countNumber(int[] dice, int number) returns int {
    int count = 0;
    foreach int die in dice {
        if die == number {
            count += 1;
        }
    }
    return count;
}

// Helper function to create a map of die values to their counts.
// Example: [1, 1, 2, 5, 5] becomes {1: 2, 2: 1, 5: 2}
// This is extremely useful for "full house" and "four of a kind".
function getFrequencyMap(int[] dice) returns map<int> {
    map<int> frequencies = {};
    foreach int die in dice {
        if frequencies.hasKey(die) {
            frequencies[die] += 1;
        } else {
            frequencies[die] = 1;
        }
    }
    return frequencies;
}

Detailed Code Walkthrough

Let's dissect the code to understand the logic and Ballerina features in detail.

The Main score Function

This is the public-facing function that serves as the entry point. It takes the category and dice as input. The core of this function is the match statement.

public function score(string category, int[] dice) returns int {
    match category {
        // ... cases ...
    }
}

A match statement in Ballerina is a more powerful and structured version of a switch statement found in other languages. It compares the value of category against each case ("ones", "twos", etc.) and executes the corresponding block of code.

Simple Categories: "Ones" to "Sixes" and "Choice"

These are the most straightforward cases. For "ones" through "sixes", we need to count the occurrences of a specific number and multiply by that number's value. We delegate this to a helper function countNumber() for clean, reusable logic.

"ones" => {
    return countNumber(dice, 1) * 1;
}
// ... similar for twos, threes, etc.

"choice" => {
    return dice.sum();
}

For "choice", Ballerina's built-in .sum() method on arrays makes the implementation trivial. This is a great example of leveraging the standard library to write concise code.

Fixed Score Categories: "Yacht" and Straights

These categories award a fixed score if a condition is met. For "Yacht", all five dice must be the same. A simple way to check this is to count the occurrences of the first die's value. If the count is 5, we have a Yacht.

"yacht" => {
    if countNumber(dice, dice[0]) == 5 {
        return 50;
    }
    return 0;
}

For "Little Straight" (1-2-3-4-5) and "Big Straight" (2-3-4-5-6), the order of the input dice doesn't matter. The easiest way to check for these sequences is to first sort the array and then compare the sorted array to the target sequence. We use the ballerina/lang.'array module, aliased as arrays, for its sort() function. Comparing arrays directly in Ballerina can be tricky, so converting them to strings provides a simple and effective equality check for this specific problem.

"little straight" => {
    int[] sortedDice = arrays:sort(dice);
    if sortedDice.toString() == "[1, 2, 3, 4, 5]" {
        return 30;
    }
    return 0;
}

Frequency-Based Categories: "Four of a Kind" and "Full House"

These are the most complex categories and demonstrate the power of using the right data structure. The problem boils down to analyzing the frequency of each die value. A map is the perfect tool for this. Our helper function getFrequencyMap() transforms the input array (e.g., [4, 4, 5, 4, 4]) into a map ({4: 4, 5: 1}).

For "Four of a Kind", we iterate through the map's values (the counts). If any count is 4 or greater, we've found our match. The score is the value of that die multiplied by 4.

"four of a kind" => {
    map<int> frequencies = getFrequencyMap(dice);
    foreach var [dieValue, count] in frequencies.entries() {
        if count >= 4 {
            return dieValue * 4;
        }
    }
    return 0;
}

For "Full House", the logic is more specific. A full house must have exactly two unique die values, one with a count of 3 and one with a count of 2. Our code first checks if the frequency map has exactly two entries. If it does, it then verifies that the counts are indeed 3 and 2. If both conditions are met, the score is the sum of all dice.

"full house" => {
    map<int> frequencies = getFrequencyMap(dice);
    if frequencies.length() == 2 {
        // ... logic to check if counts are 3 and 2 ...
        if hasThreeOfAKind && hasPair {
            return dice.sum();
        }
    }
    return 0;
}

Where Do Helper Functions Fit In?

As we've seen, helper functions are the backbone of this clean solution. They encapsulate specific, single-responsibility logic, making the main score function act as a high-level coordinator rather than a monolithic block of code.

The countNumber() Function

This is the simplest helper. It iterates through the array and increments a counter if it finds the target number. It's a classic example of a simple loop, fundamental to many programming tasks.

function countNumber(int[] dice, int number) returns int {
    int count = 0;
    foreach int die in dice {
        if die == number {
            count += 1;
        }
    }
    return count;
}

The getFrequencyMap() Function

This is arguably the most important helper function as it transforms our data into a more useful structure for several categories. It builds a map<int> where keys are the die faces (1-6) and values are their counts.

The logic is simple: iterate through the dice. For each die, if it's already a key in our map, increment its value. If not, add it to the map with a value of 1.

Here is the logical flow for building the frequency map:

    ● Start with input `dice` array
    │ e.g., [2, 3, 5, 3, 2]
    │
    ▼
  ┌───────────────────┐
  │ Create empty map {} │
  └─────────┬─────────┘
            │
            ▼
    ◆ For each die in array...
   ╱           ╲
  ╱             ╲
 │ Is die a key? │
 │ Yes           │ No
 ▼               ▼
┌──────────────┐ ┌───────────────────┐
│ Increment    │ │ Add key to map,   │
│ map[die]     │ │ set value to 1    │
└──────────────┘ └───────────────────┘
  ╲             ╱
   ╲           ╱
    ▼           ▼
    ◆ ...any dice left?
    │
    No
    │
    ▼
  ┌───────────────────┐
  │ Return final map  │
  │ e.g., {2:2, 3:2, 5:1}
  └───────────────────┘
    │
    ▼
    ● End

This map-based approach is significantly more efficient and readable than using multiple counters or complex loops to check for conditions like "Full House".


When to Consider Alternative Approaches?

While our solution is robust and idiomatic Ballerina, it's always valuable to consider other ways the problem could have been solved. This helps deepen our understanding of the language and algorithmic trade-offs.

Functional vs. Imperative Style

Our solution uses a mostly imperative style, especially within the helper functions (e.g., using foreach loops with mutable counters). Ballerina also supports a more functional style using streams and query expressions.

For example, the countNumber function could be written functionally:

// Functional approach for counting
function countNumberFunctional(int[] dice, int number) returns int {
    // 'filter' creates a new array with only the matching elements.
    // 'length()' gives us the count.
    return dice.filter(die => die == number).length();
}

Similarly, getFrequencyMap could be implemented using streams and grouping, though it can be more complex for beginners. The choice between imperative and functional often comes down to readability and team preference. For simple operations, the functional style can be more concise. For more complex logic, a well-structured imperative loop can sometimes be easier to debug.

Alternative "Straight" Check

Instead of sorting and converting to a string, we could have checked for straights by first removing duplicates and then checking if the sequence is correct.

// Alternative straight check
function isLittleStraight(int[] dice) returns boolean {
    // Create a set to get unique values
    map<boolean> uniqueDice = {};
    foreach var die in dice {
        uniqueDice[die] = true;
    }
    // A straight must have 5 unique dice
    if uniqueDice.length() != 5 {
        return false;
    }
    // The sum of 1,2,3,4,5 is 15.
    // This is a clever but potentially brittle trick.
    return dice.sum() == 15;
}

This approach works for a little straight (sum is 15) and big straight (sum is 20) but relies on a "magic number" (the sum). The sorting approach is generally more robust and easier to understand as it directly checks the condition we care about (the sequence of numbers).

Pros & Cons of Our Chosen Approach

Let's evaluate our primary solution against these alternatives in a more structured way.

Aspect Our Chosen Approach (Imperative Helpers + Match) Alternative (Functional Streams / Clever Tricks)
Readability High. The logic in each block is explicit and easy to follow, especially for developers from other language backgrounds. Moderate to High. Can be very concise, but may be less intuitive for those unfamiliar with functional programming paradigms.
Maintainability High. Helper functions are isolated and easy to test. Adding a new category is as simple as adding a new `match` clause. Moderate. Chained functional calls can sometimes be harder to debug. "Clever" tricks can be confusing to maintain.
Performance Excellent for this scale. The overhead of loops and map creation is negligible. Generally comparable. Functional streams might have slightly more overhead due to intermediate array/object creation, but this is not a concern for an array of 5 elements.
Idiomatic Ballerina Very idiomatic. Uses `match`, `map`, and `foreach` which are core language features. Also idiomatic. Ballerina is a multi-paradigm language and supports functional styles well.

Frequently Asked Questions (FAQ)

What is a `match` statement in Ballerina?
A match statement is a control flow structure that allows you to compare a value against a series of patterns. It's more powerful than a traditional switch statement because it can match on types, values, and even destructure records and objects. In our case, we used it for a simple value match on the category string, which provided a very clean and readable way to separate the logic for each category.
How do you handle arrays or lists in Ballerina?
Ballerina has strong, built-in support for arrays (which are ordered lists of values of the same type). The int[] syntax declares an array of integers. You can access elements by index (e.g., dice[0]), iterate over them with foreach, and use a rich standard library (ballerina/lang.'array) for operations like sorting (arrays:sort()), summing (dice.sum()), and filtering (dice.filter()).
Why is type safety important in a game like Yacht?
Type safety, enforced by Ballerina's static type system, prevents a whole class of bugs at compile time. For example, it guarantees that our score function will always receive an array of integers for the dice. We can't accidentally pass it a string or a record. This makes the code more reliable and reduces the need for runtime error checking, especially as the game logic grows more complex.
Can this logic be extended for more complex dice games?
Absolutely. The modular structure we created is highly extensible. To support a new game or new scoring rules, you would simply add more helper functions for the new logic and add new cases to the main match statement. This design pattern of "dispatching" logic based on a type or category is very common and robust.
What are Ballerina `map` types and why use them here?
A map is a collection of key-value pairs, also known as a dictionary or hash map in other languages. In our solution, we used map<int>, which maps a string key to an integer value by default (though we used integer keys). It was the perfect data structure for calculating dice frequencies because it allows for efficient lookup, insertion, and updates, making the logic for "Full House" and "Four of a Kind" much simpler than it would be with arrays alone.
How does Ballerina's `sum()` function work on arrays?
The .sum() method is a built-in function available on arrays of numbers (int[], float[], decimal[]). It iterates through all the elements of the array and returns their total sum. It's a convenient and efficient shorthand that avoids the need to write a manual loop for summing elements.
Is Ballerina suitable for game development?
While Ballerina's primary strength is in building network services and integrations, its strong type system, clear syntax, and robust concurrency features make it a viable choice for game server logic. For a game like Yacht, which is logic-intensive, Ballerina excels. For graphics-heavy client-side game development, you would typically use a different ecosystem like Unity or Unreal Engine, but Ballerina could happily power the backend server that manages game state and player interactions.

Conclusion and Next Steps

We have successfully navigated the complexities of the Yacht dice game, transforming a detailed set of rules into a clean, maintainable, and efficient Ballerina program. Along the way, we explored fundamental concepts like control flow with match, data structuring with map, and the importance of code decomposition with helper functions. This kodikra module demonstrates how Ballerina's modern features can be leveraged to solve intricate logic puzzles in an elegant way.

The key takeaway is not just the final code, but the thought process: understanding the problem, planning the structure, choosing the right data structures, and breaking down complexity. These skills are universal and will serve you well in any programming challenge you face.

Ready to tackle the next challenge? Continue your journey and explore the complete Ballerina 5 Learning Path to build upon these concepts and master more advanced topics. Or, for a broader overview of the language, dive deeper into our comprehensive Ballerina language guide.

Disclaimer: All code in this article is written for the latest stable version of Ballerina (Swan Lake) and reflects current best practices. The language is continuously evolving, so always refer to the official documentation for the most up-to-date syntax and features.


Published by Kodikra — Your trusted Ballerina learning resource.