Yacht in Common-lisp: Complete Solution & Deep Dive Guide

A picture of a very tall object in the water

The Complete Guide to Solving the Yacht Dice Game in Common Lisp

Calculating scores for the Yacht dice game requires analyzing the frequency of five dice rolls and applying specific rules for categories like 'Yacht' or 'Full House'. This is efficiently solved in Common Lisp by using a hash table to count dice frequencies and a dispatcher function to select the correct scoring logic for the given category.

Have you ever looked at the rules of a simple game and thought, "I could code that," only to find yourself tangled in a web of `if-else` statements? Translating real-world rules, especially for games with numerous scoring categories like Yacht, into clean, maintainable code is a classic programming challenge. It’s easy to end up with a monolithic function that’s impossible to debug or extend.

This is a common pain point for developers learning a new language. You understand the logic, but expressing it elegantly is the real hurdle. This guide is your solution. We will dissect the Yacht dice game problem from the exclusive kodikra.com learning path, transforming its rules into a robust and idiomatic Common Lisp program. You'll learn not just how to solve the problem, but how to think in Lisp, leveraging its powerful features to write code that is both concise and incredibly expressive.


What is the Yacht Dice Game?

The Yacht dice game is a classic logic puzzle and a precursor to modern games like Yahtzee. The core of the game is simple: a player rolls five standard six-sided dice and must choose one of twelve scoring categories to apply to that roll. The challenge, and the fun, comes from the unique scoring calculation for each category.

This problem is a fantastic exercise for programmers because it isn't about a single complex algorithm. Instead, it tests your ability to organize and manage a dozen small, distinct logical rules. It forces you to think about data representation (how do you handle the five dice?) and control flow (how do you select the correct scoring logic?).

Mastering this challenge from the kodikra module demonstrates a solid grasp of fundamental programming concepts: data aggregation, conditional logic, and function composition. Let's examine the specific rules you'll need to implement.

The Scoring Categories Explained

Here are the twelve categories and their corresponding scoring rules. You will always be given five dice, each with a value from 1 to 6.

Category Score Calculation Description Example Dice Roll Example Score
Ones Sum of dice showing 1 The sum of the dice with the number 1. (1 1 2 4 5) 2
Twos Sum of dice showing 2 The sum of the dice with the number 2. (2 2 2 3 5) 6
Threes Sum of dice showing 3 The sum of the dice with the number 3. (1 2 3 3 3) 9
Fours Sum of dice showing 4 The sum of the dice with the number 4. (4 4 4 4 4) 20
Fives Sum of dice showing 5 The sum of the dice with the number 5. (1 2 3 5 5) 10
Sixes Sum of dice showing 6 The sum of the dice with the number 6. (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 Use Common Lisp for This Challenge?

While you could solve this problem in any language, Common Lisp offers a unique set of tools that make the solution particularly elegant. It's a multi-paradigm language that shines in scenarios requiring powerful data manipulation and symbolic computation.

For the Yacht game, Common Lisp's strengths are immediately apparent. Its built-in hash-table is a perfect and highly efficient tool for counting the frequency of dice rolls. The functional programming paradigm, exemplified by functions like reduce, allows us to build this frequency map in a single, expressive line of code.

Furthermore, Lisp's powerful conditional constructs like cond and case provide a much cleaner way to build the main scoring dispatcher than a long series of nested if statements. The interactive development style, centered around the REPL (Read-Eval-Print Loop), also allows for rapid testing of individual scoring functions, making the development process faster and more reliable. For a deeper dive into the language, you can always consult our complete Common Lisp guide.


How to Approach the Problem: The Core Logic

Before writing a single line of code, a good developer forms a strategy. Our strategy for the Yacht problem can be broken down into three main steps, forming a logical pipeline from input to output.

First, we need to transform the raw input—a list of five numbers—into a more useful data structure. Simply looking at `(5 3 5 3 3)` isn't immediately helpful. We need to know that we have three 3s and two 5s. This is a classic frequency counting problem, and the ideal tool for this is a hash table (or dictionary/map in other languages).

Second, once we have our frequency map, we need a central control mechanism. We'll receive a category (like `:full-house` or `:yacht`) and must execute the correct scoring logic. A main `score` function will act as a dispatcher or a router, examining the category and delegating the work to a specialized helper function.

Third, we'll implement a small, focused helper function for each of the twelve scoring categories. Each function will take the dice roll as input, perform its specific calculation, and return the score. This approach keeps our code modular, easy to test, and easy to read.

The Logical Flow Diagram

This ASCII art diagram illustrates the high-level flow of our program, from receiving the initial dice roll and category to returning the final calculated score.

    ● Start
    │
    ▼
  ┌─────────────────────────────┐
  │ Input: (dice-list, category)│
  │ e.g., ((3 3 5 3 5), :full-house) │
  └─────────────┬───────────────┘
                │
                ▼
  ┌─────────────────────────────┐
  │ 1. Calculate Frequencies    │
  │    (Create a hash-table)    │
  │    {3 -> 3, 5 -> 2}         │
  └─────────────┬───────────────┘
                │
                ▼
  ◆ Dispatch based on `category`
  │
  ├─ :full-house ───> run full_house_logic() ──┐
  │                                            │
  ├─ :yacht ────────> run yacht_logic() ───────┤
  │                                            │
  ├─ :ones ─────────> run ones_logic() ────────┤
  │                                            │
  └─ ... (other categories) ... ──────────────┘
                                               │
                                               ▼
                                          ┌──────────┐
                                          │  Return  │
                                          │  Score   │
                                          └──────────┘
                                               │
                                               ▼
                                           ● End

Where is the Code Implemented? A Detailed Solution Walkthrough

Now, let's dive into the complete Common Lisp solution from the kodikra curriculum. We'll analyze it piece by piece, explaining the purpose of each function and the Lisp concepts being used. This structured approach ensures you understand not just what the code does, but how it does it.

Setting Up the Package

Every Common Lisp project should start with a package definition. This avoids naming conflicts with other Lisp code or the base language itself.

(defpackage :yacht
  (:use :cl)
  (:export :score))

(in-package :yacht)
  • (defpackage :yacht ...): This defines a new package named yacht. A package is a namespace for symbols.
  • (:use :cl): This line specifies that our new package should inherit all the public symbols from the standard :common-lisp package. This gives us access to core functions like defun, let, and loop without needing to prefix them (e.g., cl:defun).
  • (:export :score): This makes the symbol score public. It means that other packages can import and use our main score function. All other functions we define will be private to the yacht package by default.
  • (in-package :yacht): This command switches the current working namespace to our newly defined yacht package. All subsequent definitions will belong to it.

Core Utility: The `frequencies` Function

This is the heart of our data processing. It takes a list of dice and returns a hash table mapping each die value to its count.

(defun frequencies (sequence)
  (let ((occurences (make-hash-table)))
    (reduce (lambda (acc n)
              (incf (gethash n acc 0))
              acc)
            sequence
            :initial-value occurences)))

This function is a beautiful example of functional programming in Lisp. Let's break it down:

  • (defun frequencies (sequence)): Defines a function named frequencies that accepts one argument, sequence (our list of dice).
  • (let ((occurences (make-hash-table)))): Inside the function, we create a local variable named occurences and initialize it as a new, empty hash table. This hash table will store our frequency counts.
  • (reduce ...): The reduce function is a powerful functional tool. It iterates over a sequence, applying a function to accumulate a single result.
    • (lambda (acc n) ...): This is the function that reduce applies at each step. It's an anonymous function (a lambda) that takes two arguments: acc (the accumulator, which is our hash table) and n (the current element from the dice list).
    • (incf (gethash n acc 0)): This is the magic. gethash tries to find the key n (the die value) in the hash table acc. If it's not found, it returns the default value, which we've specified as 0. incf then increments this value by one and updates the hash table. So, the first time we see a '4', it finds nothing, gets 0, increments it to 1, and stores it. The second time, it finds 1, increments it to 2, and so on.
    • acc: The lambda must return the updated accumulator for the next iteration. Here, we simply return the modified hash table.
  • sequence: This is the input list that reduce will iterate over.
  • :initial-value occurences: This tells reduce what the starting value of the accumulator acc should be. We provide the empty hash table we created earlier.

Visualizing the `reduce` Operation

This diagram shows how `reduce` builds the hash table step-by-step for an input of `(1 3 1 5 3)`.

    ● Start with sequence (1 3 1 5 3) and empty hash-table {}
    │
    ▼
  ┌───────────────────────────┐
  │ Iteration 1: n = 1        │
  │ `incf (gethash 1 acc 0)`  │
  │ acc becomes {1 -> 1}      │
  └───────────┬───────────────┘
              │
              ▼
  ┌───────────────────────────┐
  │ Iteration 2: n = 3        │
  │ `incf (gethash 3 acc 0)`  │
  │ acc becomes {1 -> 1, 3 -> 1}│
  └───────────┬───────────────┘
              │
              ▼
  ┌───────────────────────────┐
  │ Iteration 3: n = 1        │
  │ `incf (gethash 1 acc 0)`  │
  │ acc becomes {1 -> 2, 3 -> 1}│
  └───────────┬───────────────┘
              │
              ▼
  ┌───────────────────────────┐
  │ Iteration 4: n = 5        │
  │ `incf (gethash 5 acc 0)`  │
  │ acc becomes {1 -> 2, 3 -> 1, 5 -> 1}│
  └───────────┬───────────────┘
              │
              ▼
  ┌───────────────────────────┐
  │ Iteration 5: n = 3        │
  │ `incf (gethash 3 acc 0)`  │
  │ acc becomes {1 -> 2, 3 -> 2, 5 -> 1}│
  └───────────┬───────────────┘
              │
              ▼
    ● Final result: hash-table {1 -> 2, 3 -> 2, 5 -> 1}

Helper Functions for Scoring Logic

The solution uses several small helper functions to determine if a set of dice matches a complex category. This is excellent practice, as it keeps the main scoring function clean.

Extracting Hash Values

(defun hash-values (ht)
  (loop for v being each hash-value of ht collect v))

This is a simple utility to get a list of all values from a hash table. The loop macro is Common Lisp's incredibly versatile iteration tool. Here, it iterates through each value v in the hash table ht and collects them into a list.

Checking for a Full House

(defun full-house-p (occurences)
  (let ((vals (hash-values occurences)))
    (and (= 2 (length vals))
         (find 3 vals)
         (find 2 vals))))

This function checks if the dice constitute a full house. A full house means there are only two unique die faces, with counts of 3 and 2.

  • (let ((vals (hash-values occurences)))): It first gets the list of frequency counts (e.g., `(2 3)`).
  • (and (= 2 (length vals)) ...): The and macro ensures all conditions are true. First, it checks if there are exactly two unique dice values (`length` of the values list is 2).
  • (find 3 vals) and (find 2 vals): It then confirms that the counts '3' and '2' are present in the list of values. This is a more robust check than sorting and comparing to `'(2 3)`, as the order of values from a hash table is not guaranteed. I've renamed the function to `full-house-p` which is a Lisp convention for predicates (functions that return true/false).

Checking for Four-Of-A-Kind

(defun four-of-a-kind-value (occurences)
  (loop for k being each hash-key of occurences using (hash-value v)
        if (>= v 4)
          return k))

This function finds the die value that appears four or more times.

  • (loop for k ... using (hash-value v)): This loop iterates over both the keys (k, the die face) and values (v, the count) of the occurences hash table.
  • if (>= v 4) return k: If it finds a count (v) that is greater than or equal to 4, it immediately stops and returns the corresponding key (k). If no such key is found, the loop finishes and the function returns nil.

The Main `score` Dispatcher Function

This is the public-facing function that brings everything together. It takes the dice and a category, calculates the frequencies, and then uses a cond statement to call the appropriate scoring logic.

(defun score (dice category)
  (let ((freqs (frequencies dice)))
    (cond
      ((eq category :yacht)
       (if (= 1 (hash-table-count freqs)) 50 0))

      ((eq category :full-house)
       (if (full-house-p freqs) (reduce #'+ dice) 0))

      ((eq category :four-of-a-kind)
       (let ((val (four-of-a-kind-value freqs)))
         (if val (* val 4) 0)))

      ((or (eq category :little-straight) (eq category :big-straight))
       (let ((sorted (sort (copy-list dice) #'<)))
         (if (or (and (eq category :little-straight) (equal sorted '(1 2 3 4 5)))
                 (and (eq category :big-straight) (equal sorted '(2 3 4 5 6))))
             30
             0)))

      ((eq category :choice) (reduce #'+ dice))

      ((eq category :ones) (* 1 (gethash 1 freqs 0)))
      ((eq category :twos) (* 2 (gethash 2 freqs 0)))
      ((eq category :threes) (* 3 (gethash 3 freqs 0)))
      ((eq category :fours) (* 4 (gethash 4 freqs 0)))
      ((eq category :fives) (* 5 (gethash 5 freqs 0)))
      ((eq category :sixes) (* 6 (gethash 6 freqs 0)))

      (t 0))))

The cond macro is Lisp's primary conditional expression. It evaluates each clause in order. A clause looks like (test-expression result-expression). If test-expression evaluates to true, its result-expression is evaluated and its value is returned, and no further clauses are checked.

  • :yacht: A Yacht is five of a kind. This means the frequency hash table will have only one entry. (hash-table-count freqs) checks this. If it's 1, score 50, otherwise 0.
  • :full-house: Calls our full-house-p helper. If it's true, we sum all the dice using (reduce #'+ dice). Otherwise, the score is 0.
  • :four-of-a-kind: Calls our four-of-a-kind-value helper. If it returns a value (not nil), we score four times that value (e.g., if we have four 6s, the value is 6, score is 24). Otherwise, 0.
  • Straights: This logic handles both straight types. It first sorts a copy of the dice list (important not to modify the original input). Then it checks if the sorted list is equal to `(1 2 3 4 5)` for a little straight or `(2 3 4 5 6)` for a big straight. If either condition matches, score 30.
  • :choice: The score is simply the sum of all dice.
  • Numbered Categories (:ones to :sixes): These are the simplest. We use gethash to look up the count of the required die face in our frequency map. For example, for :threes, (gethash 3 freqs 0) gets the number of 3s rolled (defaulting to 0 if none). We then multiply this count by the die's value (e.g., count * 3).
  • (t 0): This is the default "else" clause. t is always true, so if no other category matches, this will be executed, returning a score of 0.

When to Optimize? Refactoring for Idiomatic Lisp

The provided solution is correct and functional, but we can make it more "Lispy" and arguably cleaner by using a different construct for the main dispatcher. The long cond statement works, but for dispatching on a single value like category, the case macro is often a better fit.

The case macro is specifically designed for comparing a keyform against a series of literal values.

An Alternative `score` function using `case`

(defun score-refactored (dice category)
  (let ((freqs (frequencies dice)))
    (case category
      (:yacht
       (if (= 1 (hash-table-count freqs)) 50 0))
      (:full-house
       (if (full-house-p freqs) (reduce #'+ dice) 0))
      (:four-of-a-kind
       (let ((val (four-of-a-kind-value freqs)))
         (if val (* val 4) 0)))
      (:little-straight
       (if (equal (sort (copy-list dice) #'<) '(1 2 3 4 5)) 30 0))
      (:big-straight
       (if (equal (sort (copy-list dice) #'<) '(2 3 4 5 6)) 30 0))
      (:choice
       (reduce #'+ dice))
      (:ones   (* 1 (gethash 1 freqs 0)))
      (:twos   (* 2 (gethash 2 freqs 0)))
      (:threes (* 3 (gethash 3 freqs 0)))
      (:fours  (* 4 (gethash 4 freqs 0)))
      (:fives  (* 5 (gethash 5 freqs 0)))
      (:sixes  (* 6 (gethash 6 freqs 0)))
      (otherwise 0))))

What changed?

  1. We replaced cond with case category. This makes the intent clearer: we are switching based on the value of category.
  2. Each clause is now just the keyword (e.g., :yacht) followed by the code to execute. This reduces verbosity compared to (eq category :yacht).
  3. The default clause is now otherwise instead of t, which is the standard for case.
  4. The straight logic was separated for clarity, although the original combined approach is also perfectly valid.

This refactoring doesn't change the core logic but improves readability and is considered more idiomatic for this specific type of dispatch table.

Pros & Cons of the Hash Table Approach

Our chosen strategy of using a hash table for frequency counting is central to the solution. It's worth evaluating its strengths and weaknesses.

Pros Cons
Efficiency: Hash table lookups are, on average, O(1) constant time. This makes checking for counts of specific dice extremely fast, regardless of the number of dice (though here it's fixed at 5). Overhead: For a very small, fixed input size of 5, creating a hash table has a small amount of memory and computational overhead compared to a simpler approach like sorting an array.
Clarity: The code (gethash 4 freqs 0) is highly readable. It clearly states "get the frequency of the number 4, or 0 if it's not there." Complexity: For a beginner, the `reduce` function combined with `incf` and `gethash` can seem more complex than a simple `for` loop.
Flexibility: This approach easily scales. If the game used 20 dice instead of 5, the `frequencies` function would work without any changes. Order Loss: Hash tables are inherently unordered. For problems where the original sequence of dice matters (not this one), a hash table would be the wrong tool.

Frequently Asked Questions (FAQ)

Why use a hash table instead of a simple list or array for frequencies?

A hash table provides a direct mapping from a die's face value (the key) to its count (the value). To do this with a list, you'd need to constantly search the list. With an array, you could use indices 1-6, which is also a valid and fast approach. However, the hash table is more general and arguably more expressive, as you don't need to worry about array indices or off-by-one errors.

What does `incf (gethash n acc 0)` do exactly?

This is a powerful Lisp idiom. It's a "read-modify-write" operation in one step.

  1. (gethash n acc 0): Reads the value associated with key `n` from the hash table `acc`. If `n` doesn't exist as a key, it returns the default value `0`.
  2. incf: Takes the value returned by `gethash` and increments it by one.
  3. The crucial part: incf then writes this new, incremented value back into the location it was read from. In this case, it updates the value for key `n` in the hash table `acc`.

How can I test my Common Lisp code for this problem?

The best way is to use a testing framework. A popular choice in the Common Lisp ecosystem is FiveAM. You would write a series of tests, each defining a set of dice, a category, and the expected score, and then assert that your score function returns the correct result. This is a core part of modern software development and is heavily encouraged in the kodikra learning path.

Is Common Lisp still relevant for modern programming?

Absolutely. While not as mainstream as Python or JavaScript, Common Lisp is a powerhouse for specific domains like AI, symbolic computation, scheduling, and building highly resilient systems. Its features, like macros, a powerful object system (CLOS), and interactive development, are still ahead of many modern languages. Its stability is also a huge asset; code written decades ago often runs with minimal changes on modern implementations like SBCL.

What's the difference between `cond`, `case`, and `if` in Common Lisp?

  • if: The most basic. It handles a single condition with a "then" branch and an optional "else" branch. `(if condition then-form else-form)`.
  • cond: The general-purpose conditional. It handles a series of conditions, evaluating arbitrary Lisp expressions for each test. It's like a chain of `if-elseif-elseif-else`.
  • case: A specialized conditional. It's optimized for when you are comparing a single variable against a list of constant, literal values (like keywords, numbers, or characters). It's often cleaner and can be more efficient than `cond` for this specific use case.

What does `defpackage` actually do?

defpackage creates a namespace, which is a container for symbols (like function and variable names). This prevents name collisions. For example, if you define a function called list in your own package, it won't conflict with the built-in Common Lisp list function. The :use option imports symbols from another package, and :export makes your package's symbols available to other packages.

Could this problem be solved recursively?

While you could use recursion to, for example, calculate the frequency map, it would be less natural and less efficient in Common Lisp than the iterative `reduce` or `loop` approach. Lisp's functional tools are often a better fit for sequence processing than explicit recursion, unless the problem has a naturally recursive structure (like traversing a tree).


Conclusion: From Game Rules to Elegant Code

We have successfully translated the complete ruleset of the Yacht dice game into a clean, modular, and idiomatic Common Lisp solution. The journey involved more than just writing code; it was an exercise in strategic thinking. By first choosing the right data structure—the hash table—to represent dice frequencies, we simplified the entire problem. The rest of the solution flowed naturally from that single, powerful decision.

The key takeaways are the importance of data transformation, the power of functional constructs like reduce for processing sequences, and the clarity that specialized control-flow macros like case can bring to your code. This solution is not just a passing grade; it's a template for how to approach similar logic-heavy problems with confidence and elegance.

Technology Disclaimer: The Common Lisp code provided in this article is written in the ANSI Common Lisp standard and is compatible with modern implementations such as Steel Bank Common Lisp (SBCL) 2.4.x and Clozure CL (CCL).


Published by Kodikra — Your trusted Common-lisp learning resource.