Yacht in Common-lisp: Complete Solution & Deep Dive Guide
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 namedyacht. 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-lisppackage. This gives us access to core functions likedefun,let, andloopwithout needing to prefix them (e.g.,cl:defun).(:export :score): This makes the symbolscorepublic. It means that other packages can import and use our mainscorefunction. All other functions we define will be private to theyachtpackage by default.(in-package :yacht): This command switches the current working namespace to our newly definedyachtpackage. 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 namedfrequenciesthat accepts one argument,sequence(our list of dice).(let ((occurences (make-hash-table)))): Inside the function, we create a local variable namedoccurencesand initialize it as a new, empty hash table. This hash table will store our frequency counts.(reduce ...): Thereducefunction 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 thatreduceapplies at each step. It's an anonymous function (a lambda) that takes two arguments:acc(the accumulator, which is our hash table) andn(the current element from the dice list).(incf (gethash n acc 0)): This is the magic.gethashtries to find the keyn(the die value) in the hash tableacc. If it's not found, it returns the default value, which we've specified as0.incfthen 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 thatreducewill iterate over.:initial-value occurences: This tellsreducewhat the starting value of the accumulatoraccshould 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)) ...): Theandmacro 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 theoccurenceshash 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 returnsnil.
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-phelper. 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-valuehelper. If it returns a value (notnil), 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
gethashto 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.
tis 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?
- We replaced
condwithcase category. This makes the intent clearer: we are switching based on the value ofcategory. - Each clause is now just the keyword (e.g.,
:yacht) followed by the code to execute. This reduces verbosity compared to(eq category :yacht). - The default clause is now
otherwiseinstead oft, which is the standard forcase. - 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.
(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`.incf: Takes the value returned by `gethash` and increments it by one.- The crucial part:
incfthen 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
scorefunction 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?
-
defpackagecreates a namespace, which is a container for symbols (like function and variable names). This prevents name collisions. For example, if you define a function calledlistin your own package, it won't conflict with the built-in Common Lisplistfunction. The:useoption imports symbols from another package, and:exportmakes 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.
Post a Comment