Scrabble Score in Cairo: Complete Solution & Deep Dive Guide

Pyramids visible over buildings and street traffic

The Complete Guide to Calculating Scrabble Scores in Cairo

Calculating a Scrabble score in Cairo is a foundational exercise that involves mapping letter values using a Felt252Dict, a specialized dictionary. The process requires iterating through an input word, represented as a ByteArray, converting each character to lowercase, and summing the corresponding point values to compute the total score.

You’ve just placed your tiles on the board, spelling out a magnificent word. The satisfaction is immense, but it's immediately followed by a crucial question: "How many points is that?" This simple act of summing letter values, a cornerstone of word games, is also a perfect problem to sharpen your programming skills, especially in a powerful and unique language like Cairo.

Many developers diving into Cairo, the language of Starknet, find themselves grappling with fundamental data manipulation tasks. How do you handle strings? What's the most efficient way to map keys to values? This guide tackles these questions head-on. We will dissect the Scrabble Score problem from the exclusive kodikra.com curriculum, transforming a real-world scenario into clean, efficient, and provable Cairo code. By the end, you won't just have a solution; you'll have a deeper understanding of Cairo's core data structures and control flow.


What is the Scrabble Score Problem?

At its heart, the Scrabble Score problem is a straightforward mapping and summation task. The goal is to write a function that takes a single word as input and returns its total score based on a predefined value for each letter of the alphabet. It's a classic algorithm challenge that tests your ability to work with collections and perform basic arithmetic.

The Rules of the Game

In the game, each letter tile has a point value. More common letters like 'E' or 'A' are worth less, while rarer letters like 'Q' or 'Z' are worth much more. For this programming challenge, we will use the standard English letter values. The calculation is case-insensitive, meaning 'a' and 'A' are both worth 1 point.

The complete mapping of letters to their corresponding values is as follows:

Letter Value
A, E, I, O, U, L, N, R, S, T 1
D, G 2
B, C, M, P 3
F, H, V, W, Y 4
K 5
J, X 8
Q, Z 10

Defining the Task in Code

From a programming perspective, we need to define the inputs and outputs clearly.

  • Input: The function will accept a single argument, a word. In Cairo, strings are often handled as a ByteArray, which is a dynamic array of bytes representing the UTF-8 characters of the string.
  • Output: The function must return a single numerical value representing the total score. An unsigned integer type like u16 is a suitable choice, as scores are always positive and unlikely to exceed the limit of 65,535.

For example, if the input word is "cabbage", the calculation would be: 3 (c) + 1 (a) + 3 (b) + 3 (b) + 1 (a) + 2 (g) + 1 (e) = 14.


Why Use Cairo for This Task?

While you could solve this problem in any programming language, implementing it in Cairo offers unique insights into the language designed for provable computation on Starknet. Cairo enforces strictness and safety, which encourages robust and bug-free code, even for simple tasks like this one.

Leveraging Cairo's Data Structures: Felt252Dict

The most critical part of this problem is storing the letter-to-value mapping. Cairo provides a powerful and efficient data structure for this purpose: the Felt252Dict<V>. This is a dictionary (or hash map) where keys are of type felt252 (a 252-bit field element, Cairo's primary numeric type) and values can be of any type V.

Using a Felt252Dict is ideal because it provides near-constant time complexity, O(1), for average-case lookups. This is far more efficient and scalable than using a long series of if-else statements or a large match block, especially if the mapping were to become much larger.

Provability and Determinism

Every operation in Cairo is deterministic. This means that for a given input, the output will always be the same. This is a fundamental requirement for building smart contracts and provable programs. The Scrabble Score algorithm is inherently deterministic, making it a perfect fit for Cairo's execution model. By writing this logic in Cairo, you are practicing how to structure code that can be proven correct, a skill essential for the Starknet ecosystem.


How to Implement the Scrabble Score Logic in Cairo

Let's break down the implementation into logical steps. We will construct a helper function to create our letter-value map and a main function to perform the scoring calculation. This separation of concerns makes the code cleaner and more reusable.

The overall flow of our algorithm can be visualized as follows:

    ● Start
    │
    ▼
  ┌──────────────────┐
  │ Get Input Word   │
  │ (ByteArray)      │
  └────────┬─────────┘
           │
           ▼
  ┌──────────────────┐
  │ Initialize       │
  │ score = 0        │
  └────────┬─────────┘
           │
           ▼
    ◆ Loop Over Each Character
   ╱         ╲
  Yes         No ───┐
  │                 │
  ▼                 │
┌──────────────────┐  │
│ Convert to       │  │
│ Lowercase        │  │
└────────┬─────────┘  │
         │            │
         ▼            │
┌──────────────────┐  │
│ Lookup Value in  │  │
│ Dictionary       │  │
└────────┬─────────┘  │
         │            │
         ▼            │
┌──────────────────┐  │
│ Add Value to     │  │
│ Total Score      │  │
└────────┬─────────┘  │
         │            │
         └────────────┘
                      │
                      ▼
                 ┌───────────┐
                 │ Return    │
                 │ Total Score│
                 └───────────┘
                      │
                      ▼
                   ● End

Step 1: Creating the Letter-Value Dictionary

First, we need a function that builds and returns our Felt252Dict. This function encapsulates the data setup, keeping it separate from the scoring logic.

use core::dict::Felt252Dict;

// Helper function to create the dictionary of letter values.
fn dictionary() -> Felt252Dict<u16> {
    // 1. Initialize an empty Felt252Dict that maps felt252 keys to u16 values.
    let mut values: Felt252Dict<u16> = Default::default();

    // 2. Populate the dictionary with 1-point letters.
    values.insert('a', 1);
    values.insert('e', 1);
    values.insert('i', 1);
    values.insert('o', 1);
    values.insert('u', 1);
    values.insert('l', 1);
    values.insert('n', 1);
    values.insert('r', 1);
    values.insert('s', 1);
    values.insert('t', 1);

    // 3. Populate with 2-point letters.
    values.insert('d', 2);
    values.insert('g', 2);

    // 4. Populate with 3-point letters.
    values.insert('b', 3);
    values.insert('c', 3);
    values.insert('m', 3);
    values.insert('p', 3);

    // 5. Populate with 4-point letters.
    values.insert('f', 4);
    values.insert('h', 4);
    values.insert('v', 4);
    values.insert('w', 4);
    values.insert('y', 4);

    // 6. Populate with 5-point letters.
    values.insert('k', 5);

    // 7. Populate with 8-point letters.
    values.insert('j', 8);
    values.insert('x', 8);

    // 8. Populate with 10-point letters.
    values.insert('q', 10);
    values.insert('z', 10);

    // 9. Return the fully populated dictionary.
    values
}

Code Breakdown: `dictionary()` function

  • use core::dict::Felt252Dict;: This line imports the necessary Felt252Dict type from Cairo's core library.
  • fn dictionary() -> Felt252Dict<u16>: This defines a function named dictionary that takes no arguments and returns a Felt252Dict where values are of type u16.
  • let mut values: Felt252Dict<u16> = Default::default();: We declare a new mutable variable values. We use Default::default() to create a new, empty instance of the dictionary.
  • values.insert('a', 1);: We use the insert method to add key-value pairs. The key is a character literal like 'a', which Cairo automatically converts into a felt252. The value is the score, a u16 literal.
  • values: The last expression in a Cairo function without a semicolon is implicitly returned. Here, we return the populated dictionary.

Step 2: Implementing the Main Scoring Function

Now we create the main score function that takes the word, uses the dictionary, and calculates the total score. This function demonstrates iteration, character manipulation, and dictionary lookups.

use core::dict::Felt252Dict;
use core::byte_array::ByteArray;
use core::prelude::Default;
use core::str_trait::IntoString;

// Assuming the dictionary() function from above is available.

// Main function to calculate the Scrabble score of a word.
pub fn score(word: ByteArray) -> u16 {
    // 1. Get the populated letter-value dictionary.
    let mut values = dictionary();
    
    // 2. Initialize the total score to zero.
    let mut score: u16 = 0;
    
    // 3. Initialize a loop counter.
    let mut i = 0;
    
    // 4. Loop through the ByteArray character by character.
    while let Option::Some(char) = word.at(i) {
        // 5. Convert the character to lowercase to ensure case-insensitivity.
        let lower_char = lowercase(char);
        
        // 6. Look up the character's value in the dictionary.
        // The .into() converts the char to a felt252 for the key.
        // .get() returns 0 if the key is not found (e.g., for punctuation).
        let char_score = values.get(lower_char.into());
        
        // 7. Add the character's score to the total.
        score += char_score;
        
        // 8. Increment the counter to move to the next character.
        i += 1;
    }
    
    // 9. Return the final calculated score.
    score
}

// Helper function to convert a character to lowercase.
// Note: This is a simplified version for ASCII characters.
fn lowercase(char: u8) -> u8 {
    if char >= 'A' && char <= 'Z' {
        char + 32
    } else {
        char
    }
}

Detailed Code Walkthrough: `score()` function

  1. Get Dictionary: let mut values = dictionary(); calls our helper function to get the pre-filled map of letter values. We make it mutable in case we needed to modify it, though in this specific implementation it's not strictly necessary.
  2. Initialize Score: let mut score: u16 = 0; creates a mutable variable to accumulate the score. We explicitly type it as u16.
  3. Initialize Counter: let mut i = 0; sets up an index for our manual loop over the ByteArray.
  4. The Loop: while let Option::Some(char) = word.at(i) is a very common and powerful pattern in Cairo and Rust.
    • word.at(i) attempts to access the byte (character) at index i.
    • This method doesn't return the byte directly. Instead, it returns an Option<u8>. This is a safety feature: if the index i is out of bounds, it returns Option::None instead of crashing. If the index is valid, it returns Option::Some(value).
    • The while let construct continues the loop as long as word.at(i) returns a Some variant. It simultaneously unwraps the value and assigns it to the char variable. When word.at(i) returns None (we've reached the end of the word), the loop terminates gracefully.
  5. Lowercase Conversion: let lower_char = lowercase(char); calls a helper function to handle case-insensitivity. Our simple lowercase function checks if a character is within the ASCII range for uppercase letters ('A' through 'Z') and adds 32 to convert it to its lowercase equivalent. Otherwise, it returns the character unchanged.
  6. Dictionary Lookup: let char_score = values.get(lower_char.into()); is the core lookup logic.
    • lower_char.into(): The get method on Felt252Dict expects a felt252 key. The .into() trait conversion handles turning our u8 character into a felt252.
    • values.get(...): This method looks for the key in the dictionary. If the key exists, it returns its associated value (the score). If the key does not exist (e.g., for a space, hyphen, or other non-alphabetic character), it gracefully returns the default value for the type, which is 0 for numeric types like u16. This elegantly handles invalid characters without extra code.
  7. Accumulate Score: score += char_score; adds the score of the current character to our running total.
  8. Increment Counter: i += 1; advances our index to process the next character in the next loop iteration.
  9. Return Score: Finally, score is returned after the loop completes.

Alternative Approaches and Optimizations

The solution using Felt252Dict is highly idiomatic and efficient in Cairo. However, for the sake of understanding the language's capabilities, it's useful to consider alternatives. The most common alternative in many languages would be a large conditional block, like a match statement.

Using a `match` Statement

A match statement can also solve this problem by directly checking the value of each character. While functional, this approach has significant drawbacks in terms of readability and maintainability for a mapping of this size.

fn get_char_value_match(char: u8) -> u16 {
    match char {
        'a' | 'e' | 'i' | 'o' | 'u' | 'l' | 'n' | 'r' | 's' | 't' => 1,
        'd' | 'g' => 2,
        'b' | 'c' | 'm' | 'p' => 3,
        'f' | 'h' | 'v' | 'w' | 'y' => 4,
        'k' => 5,
        'j' | 'x' => 8,
        'q' | 'z' => 10,
        _ => 0, // Default case for any other character
    }
}

You would then call this function inside the loop instead of `values.get()`. This works, but it tightly couples the data (the letter values) with the logic, making it harder to change values later.

Here is a visualization comparing the two logical flows:

  ┌───────────────────────────┐      ┌──────────────────────────┐
  │   Felt252Dict Approach    │      │    match Statement Approach  │
  └───────────────────────────┘      └──────────────────────────┘
               │                                  │
               ▼                                  ▼
        ┌─────────────┐                      ┌─────────────┐
        │ Get `char`  │                      │ Get `char`  │
        └──────┬──────┘                      └──────┬──────┘
               │                                  │
               ▼                                  ▼
      Hash(char) ⟶ [Dictionary] ⟶ Value      ◆ Is `char` 'a' or 'e'...?
                                           ╱           ╲
                                          Yes           No
                                          │              │
                                          ▼              ▼
                                        [Value=1]     ◆ Is `char` 'd' or 'g'...?
                                                       ╱           ╲
                                                      Yes           No
                                                      │              │
                                                      ▼              ▼
                                                    [Value=2]      ...and so on

Pros and Cons Analysis

Choosing the right data structure is a critical skill. Here’s a comparison to help you decide when to use each pattern.

Feature Felt252Dict Approach match Statement Approach
Readability Excellent. The data (dictionary) is completely separate from the processing logic (loop). Poor for large mappings. The function becomes a giant, hard-to-read block of code.
Maintainability High. To change a letter's value, you only need to edit one line in the dictionary() function. Low. Changing values requires finding the right line within the complex match logic. Adding new letters is cumbersome.
Performance Generally O(1) average time complexity for lookups, making it very fast and scalable. Can be O(n) in the worst case, as the machine code might have to perform a sequence of comparisons.
Flexibility Very high. The dictionary could be loaded from storage or passed as an argument, allowing for dynamic scoring rules. Very low. The scoring rules are hardcoded directly into the program logic.

For this problem, and for most key-value mapping tasks, the Felt252Dict is unequivocally the superior choice. It leads to code that is cleaner, more efficient, and far easier to maintain.


Real-World Applications of This Pattern

While calculating a Scrabble score might seem like a simple academic exercise from the kodikra learning path, the underlying pattern of mapping identifiers to data is one of the most common in all of software engineering, including smart contract development.

  • Configuration Management: Imagine a smart contract that needs to look up configuration values, such as fee rates for different types of transactions. A dictionary is a perfect way to store and retrieve these settings.
  • Token Whitelists or Blacklists: In DeFi, you might use a dictionary-like structure (e.g., a mapping) to store a list of addresses that are permitted (or denied) to interact with a contract. The key would be the address, and the value could be a simple boolean.
  • State Machines: When building a system that can be in various states (e.g., an order that is 'Pending', 'Shipped', 'Delivered'), you can use a dictionary to map state identifiers to functions or data associated with that state.
  • Data Parsers: When parsing data from an external source, you often need to translate tokens or codes into meaningful values. A dictionary is the ideal tool for this kind of translation.

Mastering this simple pattern in Cairo prepares you for tackling these more complex, real-world challenges on Starknet.


Frequently Asked Questions (FAQ)

1. What exactly is a Felt252Dict in Cairo?
A Felt252Dict<V> is Cairo's built-in hash map or dictionary data structure. It stores key-value pairs where the key is always a felt252 (field element) and the value can be of any type V. It's highly optimized for efficient data retrieval based on a key.

2. Why is it necessary to convert characters to lowercase?
The rules of Scrabble are case-insensitive, meaning 'A' and 'a' have the same score. By converting every character to a consistent case (lowercase) before the dictionary lookup, we ensure our program correctly scores words like "Queen" or "JaVa" without needing separate dictionary entries for uppercase letters.

3. What does word.at(i) return and why use the while let pattern?
The word.at(i) method safely accesses an element in a ByteArray. It returns an Option<u8> enum, which can be either Some(value) if the index is valid or None if the index is out of bounds. The while let Option::Some(char) = ... pattern is an elegant and safe way to loop until None is returned, avoiding both panics from out-of-bounds access and the need for manual boundary checks.

4. How would I handle non-alphabetic characters if they should not be ignored?
In our current implementation, the values.get() method returns 0 for any character not in the dictionary, effectively ignoring them. If you needed to handle them differently, for instance, by returning an error, you could use the dict_get syscall, which returns an Option. You could then match on the result and handle the None case explicitly, perhaps by panicking or returning an error code.

5. What is a ByteArray and how does it relate to strings?
A ByteArray is a dynamic array of bytes (u8). It is Cairo's current idiomatic way to represent strings of arbitrary length, as each character can be encoded as a sequence of UTF-8 bytes. It provides a flexible way to handle text data in smart contracts.

6. Could I use a different integer type for the score, like u32 or u64?
Yes, you absolutely could. We chose u16 (0 to 65,535) because it's more than sufficient for any realistic Scrabble score and uses less storage than larger integer types. Using the smallest appropriate data type is good practice for gas efficiency in smart contracts, though for this specific off-chain calculation, the difference is negligible.

7. Is the `lowercase` helper function robust enough for all text?
The provided lowercase function is simplified and only works for the English alphabet (ASCII characters). A production-grade implementation would need a more comprehensive library to correctly handle the full range of Unicode characters and their case variations. However, for the constraints of this specific kodikra module, it is perfectly adequate.

Conclusion: From Game Logic to Cairo Mastery

We have successfully implemented a robust and efficient Scrabble score calculator in Cairo. This journey took us through several core concepts essential for any Cairo developer: leveraging the Felt252Dict for efficient key-value mapping, safely iterating over a ByteArray using the while let pattern, and the importance of separating data from logic for clean, maintainable code.

This seemingly simple problem from the kodikra.com curriculum serves as a powerful lesson. It demonstrates how Cairo's design principles—safety, explicitness, and efficiency—guide you toward writing better code. The skills you've honed here are directly transferable to building complex, provable applications on Starknet. By mastering these fundamentals, you are building a solid foundation for your future as a Cairo developer.

Ready to apply these concepts to more advanced challenges? Explore our complete Cairo 3 Learning Roadmap to continue your journey. For a deeper dive into the language's features, check out our comprehensive Cairo resources and become a true expert.

Disclaimer: The code and explanations in this article are based on Cairo v2.6.x and the Starknet ecosystem as of its writing. The Cairo language is under active development, and syntax or library functions may change in future versions.


Published by Kodikra — Your trusted Cairo learning resource.