Yacht in Cairo: Complete Solution & Deep Dive Guide
The Complete Guide to Mastering Logic with Cairo: Solving the Yacht Dice Game
This guide provides a comprehensive walkthrough for solving the Yacht dice game challenge using the Cairo programming language. We will deconstruct the problem, design data structures, implement scoring logic with pattern matching, and explain every line of code, making it perfect for developers transitioning into the Starknet ecosystem.
You’ve been staring at the screen for hours. The rules of the game are clear in your mind, a simple set of conditions and scores. Yet, translating that human logic into the rigid, unforgiving syntax of a new programming language feels like trying to solve a Rubik's Cube in the dark. This is a common wall every developer hits, especially with a language as powerful and unique as Cairo.
What if you could break down that wall, brick by brick? This article is your sledgehammer. We will tackle the classic Yacht dice game, not just as a coding exercise, but as a masterclass in Cairo's core principles. You will learn to model real-world concepts with enums and structs, handle complex conditional logic with elegant pattern matching, and build a robust, testable solution from the ground up. By the end, you won't just have a working program; you'll have a deeper, more intuitive understanding of how to think in Cairo.
What is the Yacht Dice Game Challenge?
The Yacht dice game is a classic logic puzzle that serves as an excellent entry point into algorithmic thinking. Originating as a precursor to modern games like Yahtzee, its rules are straightforward, yet implementing them requires careful data handling and conditional logic. The challenge, as presented in the kodikra.com learning curriculum, is to write a function that calculates the score for a given set of five dice and a specific scoring category.
The core of the problem involves two inputs: an array of five dice, with each die value ranging from one to six, and a category chosen from a predefined list. Your task is to implement the scoring rules for each category. This exercise tests your ability to count occurrences, detect sequences, and manage different logical paths within your code.
The Scoring Rules Deconstructed
To build our solution, we must first have an ironclad understanding of the game's rules. The dice may be provided in any order, so our logic must be order-agnostic. Let's break down each of the twelve scoring categories.
| Category | Score Calculation | Description | Example Dice Roll | Example Score |
|---|---|---|---|---|
| Ones | Sum of all dice showing '1' | Counts only the ones. | [1, 1, 2, 4, 5] |
2 |
| Twos | Sum of all dice showing '2' | Counts only the twos. | [2, 3, 2, 5, 2] |
6 |
| Threes | Sum of all dice showing '3' | Counts only the threes. | [3, 3, 3, 4, 5] |
9 |
| Fours | Sum of all dice showing '4' | Counts only the fours. | [4, 1, 2, 4, 4] |
12 |
| Fives | Sum of all dice showing '5' | Counts only the fives. | [5, 5, 2, 4, 1] |
10 |
| Sixes | Sum of all dice showing '6' | Counts only the sixes. | [6, 1, 6, 6, 6] |
24 |
| 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 are the same number. | [4, 4, 4, 4, 1] |
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. | [1, 3, 4, 5, 6] |
19 |
| Yacht | 50 points | All five dice are the same number. | [4, 4, 4, 4, 4] |
50 |
Why Use Cairo for This Logic Puzzle?
At first glance, a dice game might seem like a trivial problem for a language designed for verifiable computation and zero-knowledge proofs. However, it's precisely this kind of foundational logic puzzle that illuminates why Cairo's design is so robust. Solving the Yacht challenge in Cairo forces you to engage with concepts that are paramount in smart contract development: data integrity, explicit state transitions, and exhaustive logic handling.
Cairo's strong, static typing system, particularly its implementation of enums, is perfect for modeling the game's fixed set of categories. This prevents invalid category inputs at compile time, a critical feature for secure applications. Furthermore, its powerful match statements ensure that every possible category is handled, eliminating entire classes of bugs that could arise from a forgotten if-else condition in other languages.
While this specific problem doesn't deeply involve memory management, using Cairo's ownership and borrowing rules even for simple data structures builds good habits. You learn to think about data flow explicitly, which is non-negotiable when dealing with state on a blockchain like Starknet. For anyone serious about a career in web3, mastering these fundamentals through problems like this is an invaluable step. To learn more about the language itself, explore our complete Cairo learning path on kodikra.com.
Pros and Cons of Using Cairo for this Task
- Pros:
- Type Safety: Using an
enumforCategorymakes the code robust and self-documenting, preventing invalid inputs. - Exhaustive Matching: The Cairo compiler forces you to handle every variant of the
Categoryenum in amatchstatement, preventing logical gaps. - Clarity and Readability: Pattern matching can lead to very declarative and easy-to-read code, especially when compared to nested if-else statements.
- Foundation for dApps: The skills practiced here—data modeling, state management, and strict logic—are directly transferable to building complex smart contracts on Starknet.
- Type Safety: Using an
- Cons:
- Verbosity: For a simple script, Cairo's setup and strictness can feel more verbose than a dynamically typed language like Python.
- Learning Curve: Concepts like ownership, traits (e.g.,
Drop,Copy), and the specific syntax can be challenging for newcomers. - Ecosystem Maturity: While rapidly growing, the tooling and library ecosystem is not as extensive as that of more established languages, which might be a factor in larger projects.
How to Structure the Cairo Solution: A Step-by-Step Breakdown
A robust solution begins with a solid structure. We will break down our approach into logical, manageable steps: defining our data types, creating a helper function to process the input, and finally, implementing the core scoring logic. This modular approach makes the code easier to write, debug, and understand.
Step 1: Defining the Core Data Structures
Before we can write any logic, we need to model our data. In Cairo, the best tools for this are enums and structs.
The `Category` Enum
An enumeration (enum) is the perfect way to represent the twelve possible scoring categories. It restricts the possible values to a known set, providing compile-time safety.
#[derive(Drop)]
pub enum Category {
Ones,
Twos,
Threes,
Fours,
Fives,
Sixes,
FullHouse,
FourOfAKind,
LittleStraight,
BigStraight,
Choice,
Yacht,
}
Here, pub enum Category declares a public enumeration. The #[derive(Drop)] attribute is crucial; it tells the compiler to automatically generate the code needed to handle memory deallocation when a Category value goes out of scope. In Cairo, types must have a defined "drop" behavior.
The `Counter` Struct
Most of our scoring logic will depend on counting how many of each die face (1s, 2s, etc.) we have. A struct is an ideal way to hold these counts together in a single, organized data structure.
#[derive(Copy, Default, Drop, PartialEq)]
struct Counter {
ones: u8,
twos: u8,
threes: u8,
fours: u8,
fives: u8,
sixes: u8,
}
struct Counter: Defines our custom type.ones: u8, ...: These are the fields of the struct, each holding the count of a specific die face as an 8-bit unsigned integer (u8), which is more than enough for 5 dice.#[derive(...)]: This attribute automatically implements several useful traits for our struct:Copy: Allows instances ofCounterto be copied by value, simplifying data handling.Default: Provides aCounter::default()method that initializes all counts to 0.Drop: As with the enum, this is required for memory management.PartialEq: Allows us to compare twoCounterinstances for equality (useful in testing).
Step 2: The Main Entry Point - The `score` Function
This is the public function that other parts of a program would call. Its signature clearly defines its contract: it takes five dice and a category and returns a score.
pub fn score(dice: [u8; 5], category: Category) -> u8 {
// ... logic will go here ...
}
pub fn score: A public function namedscore.dice: [u8; 5]: The first parameter isdice, a fixed-size array of 5 elements, where each element is au8.category: Category: The second parameter is one of the variants from ourCategoryenum.-> u8: The function is declared to return an 8-bit unsigned integer, which represents the final score.
Step 3: Pre-processing the Dice - The `count_dice` Helper Function
Instead of repeatedly counting dice inside our main scoring logic, it's far more efficient to do it once at the beginning. We'll create a helper function that takes the dice array and returns a populated Counter struct.
This process follows a clear, logical flow, which can be visualized as follows:
● Start with Dice Array
│ e.g., [1, 3, 3, 5, 6]
▼
┌──────────────────┐
│ Initialize Empty │
│ Counter Struct │
│ (all counts = 0) │
└────────┬─────────┘
│
▼
╭ Loop through each die ╮
│ in the array │
╰───────────┬───────────╯
│
▼
◆ Match die value?
╱ │ │ │ ╲
1 2 3 ... 6
│ │ │ │ │
▼ ▼ ▼ ... ▼
[+1 to [+1 to [+1 to [+1 to
ones] twos] threes] sixes]
│ │ │ │ │
└─────┴────┼────┴─────┘
│
▼
┌──────────────────┐
│ Return Populated │
│ Counter Struct │
└──────────────────┘
│ e.g., {ones:1, twos:0, threes:2, ...}
▼
● End
Now, let's implement this logic in Cairo:
fn count_dice(dice_span: Span<u8>) -> Counter {
let mut counter = Counter::default();
let mut i = 0;
loop {
if i >= dice_span.len() {
break;
}
let die = *dice_span.at(i);
match die {
1 => { counter.ones += 1; },
2 => { counter.twos += 1; },
3 => { counter.threes += 1; },
4 => { counter.fours += 1; },
5 => { counter.fives += 1; },
6 => { counter.sixes += 1; },
_ => { // Do nothing for invalid dice values },
};
i += 1;
};
counter
}
Code Walkthrough:
fn count_dice(dice_span: Span<u8>) -> Counter: Defines a private function that takes aSpan<u8>(a view into an array, which is efficient) and returns ourCounterstruct.let mut counter = Counter::default();: We create a new, mutableCounterinstance with all fields initialized to zero.loop { ... }: We use a standard loop to iterate through the dice. Aforloop could also be used here.let die = *dice_span.at(i);: We get the value of the die at the current indexi. The asterisk (*) dereferences the pointer to get the actualu8value.match die { ... }: This is the heart of the counting logic. We match the die's value and increment the corresponding field in ourcounterstruct._ => { ... }: The underscore is a wildcard pattern. This arm catches any value that isn't 1-6, ensuring our function doesn't crash on invalid input.counter: The last expression in a Cairo function is its return value. We return the fully populatedcounter.
Step 4: Implementing the Scoring Logic with Pattern Matching
With our helper function ready, the main score function becomes a clean and readable match statement. We'll call count_dice first and then use the resulting counts to determine the score for the given category.
pub fn score(dice: [u8; 5], category: Category) -> u8 {
let span = dice.span();
let counter = count_dice(span);
match category {
// ... detailed match arms go here ...
}
}
Simple Categories: Ones to Sixes
These are the most straightforward. The score is simply the count of the die face multiplied by its value.
Category::Ones => counter.ones * 1,
Category::Twos => counter.twos * 2,
Category::Threes => counter.threes * 3,
Category::Fours => counter.fours * 4,
Category::Fives => counter.fives * 5,
Category::Sixes => counter.sixes * 6,
Complex Categories: Full House and Four of a Kind
These categories require us to inspect the counts within our Counter struct. For this, it's cleaner to create another helper function that checks the counts.
The decision logic for a "Full House" can be visualized like this:
● Start with Counter
│
▼
┌───────────────────┐
│ Scan through counts │
│ (ones, twos, etc.) │
└─────────┬─────────┘
│
▼
◆ Found a count of 3?
╱ ╲
Yes No
│ │
▼ ▼
┌───────────────────┐ ┌─────────┐
│ Scan counts again │ │ Return │
└─────────┬─────────┘ │ False │
│ │ (Score 0)
▼ └─────────┘
◆ Found a count of 2?
╱ ╲
Yes No
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ Return │ │ Return │
│ True │ │ False │
│(Full House!) │ (Score 0)
└─────────┘ └─────────┘
Let's implement this logic. We can iterate through the counts to find if a pair of 3 and 2 exists.
Category::FullHouse => {
let mut has_three = false;
let mut has_two = false;
let counts = [
counter.ones, counter.twos, counter.threes, counter.fours, counter.fives, counter.sixes
];
let mut i = 0;
loop {
if i >= counts.len() { break; }
let count = counts[i];
if count == 3 {
has_three = true;
} else if count == 2 {
has_two = true;
}
i += 1;
};
if has_three && has_two {
sum_all_dice(span)
} else {
0
}
},
Category::FourOfAKind => {
// Logic to find which number has a count >= 4
let mut four_kind_value = 0;
if counter.ones >= 4 { four_kind_value = 1; }
else if counter.twos >= 4 { four_kind_value = 2; }
else if counter.threes >= 4 { four_kind_value = 3; }
else if counter.fours >= 4 { four_kind_value = 4; }
else if counter.fives >= 4 { four_kind_value = 5; }
else if counter.sixes >= 4 { four_kind_value = 6; }
four_kind_value * 4
},
Note: We need a `sum_all_dice` helper for `FullHouse` and `Choice`.
fn sum_all_dice(dice_span: Span<u8>) -> u8 {
let mut sum = 0_u8;
let mut i = 0;
loop {
if i >= dice_span.len() { break; }
sum += *dice_span.at(i);
i += 1;
};
sum
}
Sequential Categories: Straights
For straights, we need to check for the presence of specific sequences. The `Counter` struct makes this easy.
Category::LittleStraight => {
if counter.ones == 1 && counter.twos == 1 && counter.threes == 1 && counter.fours == 1 && counter.fives == 1 {
30
} else {
0
}
},
Category::BigStraight => {
if counter.twos == 1 && counter.threes == 1 && counter.fours == 1 && counter.fives == 1 && counter.sixes == 1 {
30
} else {
0
}
},
Special Categories: Choice and Yacht
These are the final two. `Choice` is the sum of all dice, and `Yacht` is a fixed score if all five dice are the same.
Category::Choice => sum_all_dice(span),
Category::Yacht => {
if counter.ones == 5 || counter.twos == 5 || counter.threes == 5 || counter.fours == 5 || counter.fives == 5 || counter.sixes == 5 {
50
} else {
0
}
},
Putting It All Together: The Complete Cairo Code
Here is the complete, runnable Cairo module that combines all the pieces we've discussed. This code is structured for clarity and correctness, following best practices within the kodikra.com learning path.
use core::traits::{Copy, Default, Drop, PartialEq};
#[derive(Drop)]
pub enum Category {
Ones,
Twos,
Threes,
Fours,
Fives,
Sixes,
FullHouse,
FourOfAKind,
LittleStraight,
BigStraight,
Choice,
Yacht,
}
#[derive(Copy, Default, Drop, PartialEq)]
struct Counter {
ones: u8,
twos: u8,
threes: u8,
fours: u8,
fives: u8,
sixes: u8,
}
fn count_dice(dice_span: Span<u8>) -> Counter {
let mut counter = Counter::default();
let mut i = 0;
loop {
if i >= dice_span.len() {
break;
}
let die = *dice_span.at(i);
match die {
1 => { counter.ones += 1; },
2 => { counter.twos += 1; },
3 => { counter.threes += 1; },
4 => { counter.fours += 1; },
5 => { counter.fives += 1; },
6 => { counter.sixes += 1; },
_ => {},
};
i += 1;
};
counter
}
fn sum_all_dice(dice_span: Span<u8>) -> u8 {
let mut sum = 0_u8;
let mut i = 0;
loop {
if i >= dice_span.len() { break; }
sum += *dice_span.at(i);
i += 1;
};
sum
}
pub fn score(dice: [u8; 5], category: Category) -> u8 {
let span = dice.span();
let counter = count_dice(span);
match category {
Category::Ones => counter.ones * 1,
Category::Twos => counter.twos * 2,
Category::Threes => counter.threes * 3,
Category::Fours => counter.fours * 4,
Category::Fives => counter.fives * 5,
Category::Sixes => counter.sixes * 6,
Category::FullHouse => {
let mut has_three = false;
let mut has_two = false;
let counts = [
counter.ones, counter.twos, counter.threes, counter.fours, counter.fives, counter.sixes
];
let mut i = 0;
loop {
if i >= counts.len() { break; }
let count = counts[i];
if count == 3 {
has_three = true;
} else if count == 2 {
has_two = true;
}
i += 1;
};
if has_three && has_two {
sum_all_dice(span)
} else {
0
}
},
Category::FourOfAKind => {
let mut four_kind_value = 0_u8;
if counter.ones >= 4 { four_kind_value = 1; }
else if counter.twos >= 4 { four_kind_value = 2; }
else if counter.threes >= 4 { four_kind_value = 3; }
else if counter.fours >= 4 { four_kind_value = 4; }
else if counter.fives >= 4 { four_kind_value = 5; }
else if counter.sixes >= 4 { four_kind_value = 6; }
four_kind_value * 4
},
Category::LittleStraight => {
if counter.ones == 1 && counter.twos == 1 && counter.threes == 1 && counter.fours == 1 && counter.fives == 1 {
30
} else {
0
}
},
Category::BigStraight => {
if counter.twos == 1 && counter.threes == 1 && counter.fours == 1 && counter.fives == 1 && counter.sixes == 1 {
30
} else {
0
}
},
Category::Choice => sum_all_dice(span),
Category::Yacht => {
if counter.ones == 5 || counter.twos == 5 || counter.threes == 5 || counter.fours == 5 || counter.fives == 5 || counter.sixes == 5 {
50
} else {
0
}
},
}
}
Where to Test and Run Your Cairo Code
Writing code is only half the battle; testing it is just as important. The official Cairo toolchain, managed by Scarb, provides a seamless way to compile and test your code.
Setting Up Your Project
First, ensure you have the Cairo toolchain installed. Then, create a new project with Scarb:
$ scarb new yacht_solver
This command creates a new directory named yacht_solver with a standard project structure, including a src/ directory and a Scarb.toml manifest file. Place the complete code from the previous section into src/lib.cairo.
Writing a Test
Scarb has a built-in test runner. To test our score function, we add a test module to the bottom of our src/lib.cairo file.
#[cfg(test)]
mod tests {
use super::{score, Category};
#[test]
fn test_yacht_category() {
let dice = [5, 5, 5, 5, 5];
let category = Category::Yacht;
assert(score(dice, category) == 50, 'Yacht score should be 50');
}
#[test]
fn test_full_house() {
let dice = [3, 3, 3, 6, 6];
let category = Category::FullHouse;
assert(score(dice, category) == 21, 'Full House score incorrect');
}
#[test]
fn test_no_full_house() {
let dice = [3, 3, 1, 6, 6];
let category = Category::FullHouse;
assert(score(dice, category) == 0, 'Should not be a Full House');
}
}
The #[cfg(test)] attribute tells the compiler to only include this code when running tests. Each function with the #[test] attribute is a separate test case. The assert function checks if a condition is true and panics with a message if it's false, failing the test.
Running the Tests
Navigate to your project's root directory in the terminal and run:
$ scarb test
Testing yacht_solver ...
running 3 tests
test yacht_solver::tests::test_yacht_category ... ok
test yacht_solver::tests::test_full_house ... ok
test yacht_solver::tests::test_no_full_house ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 filtered out;
A successful output like this confirms that your logic is working correctly for the cases you've tested. It is highly recommended to add test cases for every single category to ensure your solution is fully robust.
Frequently Asked Questions (FAQ)
- Why is `#[derive(Drop)]` necessary for the `Category` enum in Cairo?
- Cairo has a strict ownership-based memory model similar to Rust. Every value must have a clear owner, and when the owner goes out of scope, the value is "dropped" (its memory is deallocated). The `Drop` trait provides the logic for this deallocation. By using `#[derive(Drop)]`, we instruct the compiler to generate this boilerplate code for us automatically, which is required for almost all custom types.
- Could I use a `HashMap` or dictionary instead of a `Counter` struct?
- While a `HashMap` is a viable option in many languages, it's often overkill for a problem with a small, fixed set of keys like dice faces (1-6). A `struct` is more performant because it's a compile-time, stack-allocated data structure with no overhead for hashing or dynamic memory allocation. For this specific problem, the `Counter` struct is simpler, faster, and more memory-efficient.
- How can this logic be optimized for performance?
- The current implementation is already quite efficient because it iterates through the dice only once to create the `Counter`. For the straight categories, an alternative approach would be to first sort the dice array. A sorted array `[1, 2, 3, 4, 5]` is very easy to check for a sequence. However, sorting adds its own overhead (typically O(n log n)), so for a tiny array of 5 elements, the current counting method is likely faster and is definitely simpler to implement.
- What are some common pitfalls when implementing the straight categories?
- A common mistake is failing to account for unordered dice. If you simply check if `dice[0]==1, dice[1]==2`, etc., you will fail on an input like `[5, 1, 4, 2, 3]`. This is why pre-processing the dice into a `Counter` struct or sorting them first is the correct approach, as it makes the logic independent of the input order.
- How does Cairo's ownership model affect this simple program?
- In this particular program, the impact is minimal but foundational. When you pass the `dice` array to the `score` function, ownership rules apply. We use `dice.span()` to create a `Span`, which is a non-owning "view" or "slice" of the array. This is more efficient than copying the entire array, a practice that becomes critical when dealing with large data structures in more complex Starknet contracts.
- What is the difference between Yacht and Yahtzee?
- Yacht is the ancestor of Yahtzee. They are very similar, but have some key differences in scoring. For instance, Yahtzee includes "3 of a Kind" and "Chance" categories, and the scoring for straights and full houses can differ. The core concept of rolling five dice and matching categories is the same.
- What's the next step after mastering this kind of logic in Cairo?
- Congratulations! You've built a solid foundation. The next logical step is to explore more complex data structures, learn about storing state, and understand how to write functions that can be executed on the Starknet network. The best way to continue your journey is by following the structured modules in our official Kodikra learning roadmap, which will guide you from these fundamentals to building full-fledged smart contracts.
Conclusion: From Game Logic to Smart Contract Mastery
We've successfully journeyed from a set of simple game rules to a complete, tested, and robust Cairo program. More than just solving a puzzle, you've practiced the essential building blocks of the Cairo language: modeling data with enums and structs, managing control flow with exhaustive match statements, and structuring code into logical, reusable functions.
This Yacht solver is a microcosm of the challenges you'll face in smart contract development. Every decentralized application, at its core, is a state machine governed by strict, unambiguous rules—just like a game. By mastering this level of logical precision, you are well on your way to writing secure, efficient, and verifiable code for the decentralized future on Starknet.
Technology Disclaimer: The code and concepts in this article are based on Cairo 2.x and the Scarb v2.6.x toolchain. The Cairo language and its ecosystem are under active development, so syntax and best practices may evolve. Always refer to the official documentation for the latest updates.
Ready to take on the next challenge? Continue your learning journey by exploring the full Kodikra Cairo curriculum and advancing to the next module on your personalized learning roadmap.
Published by Kodikra — Your trusted Cairo learning resource.
Post a Comment