Rust for Beginners: Build a Word Game · lab 05 of 7

[ free ] [ enum · match ] [ ~35 min if you have programmed · ~1.8 h if you have not ]
05

Score every letter

[ preview ] The lesson is free to read — building it needs a free account.

You'll learn: how to invent a type of your own — one with exactly the values your game needs and no others — and how match makes the compiler check that you handled every one. This is the lab where your game starts to look like the game: a feedback row under every guess that isn't the word.

"Not it" is not an answer

Your game answers a wrong guess with Not it. — true, and useless. The whole pleasure of a word-guessing game is that a wrong guess teaches you something: this letter is right where it stands, that one is in the word somewhere else, that one is a dead end. Guess slate when the secret is crane and the game should say, letter by letter:

s l a t e
. . ^ . ^

One mark under each letter, three marks in all:

  • ^ — the letter is in the word, in exactly that place.
  • ~ — the letter is in the word, somewhere else.
  • . — the letter is not in the word at all.

slate against crane happens to earn no ~. Guess heart and all three show up:

h e a r t
. ~ ^ ~ .

Next guess, you know more than you did.

So here is the lab's question. Each letter of a guess earns one of three results — what type holds a three-way answer?

bool is too small

You know bool, but it holds two values and you need three. You could number them — 0 for a miss, 1 for a near, 2 for a hit — but then nothing stops a 7 sneaking in, and a name misspelled as "naer" still runs and quietly does the wrong thing, and you find out at the worst possible moment.

What you want to tell the computer is: there are exactly three results, here are their names, and nothing else is ever one. Rust lets you say precisely that.

enum — a type you invent, with the values you name

pub enum LetterScore {
    Hit,
    Near,
    Miss,
}

This declares a new type, LetterScore, as real as bool or String. Hit, Near and Miss are its three variants — the complete list of values a LetterScore can ever be. Not 7. Not "naer". There is no fourth; the compiler will not let one exist. (The keyword is short for "enumeration" — a type whose values you can list out.)

You write a value with the type's name in front, joined by :: — the same "look inside" mark you have been using since String::new():

let first = LetterScore::Miss;

:: reads as inside: inside LetterScore, the value Miss.

match — every case, or it doesn't compile

To use a LetterScore you ask which of the three it is. Rust's tool for that is match, and it is the other half of why enums are worth having.

Here it is on a smaller enum than yours — two variants, not three, and nothing this game has ever heard of. The shape is what you are reading; the answer stays yours to write in step 4:

enum Weather {
    Rain,
    Sun,
}

fn kit(sky: Weather) -> char {
    match sky {
        Weather::Rain => 'u',
        Weather::Sun => 'h',
    }
}

Two words for the parts:

  • arm — one line inside the braces: what to match on the left of the =>, the answer on the right.
  • pattern — the compiler's word for that left side, the value score is compared against.

Rust tries the arms top to bottom; the first one that fits wins, and its answer is the whole match's answer. It is an expression, like if/else in lab 02, and here it is kit's whole body.

Now the part that makes match more than a tidy if/else chain. Delete the Sun arm and compile — the compiler refuses:

error[E0004]: non-exhaustive patterns: `Weather::Sun` not covered

It knows the type has two values, sees you handled one, and names the one you forgot. Handle both and it stops complaining. The same holds for your three: and if the game ever grows a fourth score, every match over LetterScore stops compiling until you say what the new score should do. A forgotten case found at compile time instead of by a confused player: that is the whole trade, and it is why this course never writes a catch-all _ arm in a match over its own enum. A _ says "whatever else shows up, do this" — and silences that protection forever. (The compiler's own advice on that error offers "a wildcard pattern" — that is a _. Don't take it here; add the arm it names instead.)

Notice, too, what kit returns: a char, in single quotes — 'u' is one character, "u" is a string containing one. Lab 03 gave you chars, out of .chars(); here you write one yourself.

Step 4 asks you for mark, which is this shape with one more arm: three variants, three answers, and the same refusal if you leave one out.

The line above the enum: derive

One line sits above the enum in your task:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]

Copy it exactly. derive asks the compiler to write boring code for you; here is what each name buys:

  • Debug — lets a failing test print a LetterScore, so you can read what you got.
  • PartialEq — lets == compare two of them; without it assert_eq! on your enum does not even compile.
  • Eq — always travels with PartialEq. Write both.
  • Clone — lets vec![LetterScore::Miss; 5] stamp out five copies.
  • Copy — lets you lift one score out of a row — mark(scores[position]) — without moving it.

A list of scores: Vec<LetterScore>

A five-letter guess earns five results, in order — a job for Vec, Rust's list that grows. The angle brackets say what it holds: Vec<LetterScore> is a list of letter scores, exactly as Vec<char> is a list of characters. You build one from empty, pushing as you go:

let mut results = Vec::new();
results.push(LetterScore::Hit);

And when you need "five misses" in one go — a test will — vec![LetterScore::Miss; 5] stamps out five copies of one value.

The scoring rule

Each letter of the guess, at each position, earns:

  • Hit — the secret has this same letter at this same position;
  • Near — not a Hit, but the letter appears somewhere in the secret;
  • Miss — the letter is nowhere in the secret.

"Somewhere in the secret" is one method call: secret.contains(letter) — true if the letter appears anywhere in the string. The position question needs the secret's letter at a position: lab 04 gave you half the tool, indexing, and the other half is new — secret.chars().collect() gathers the letters into a Vec<char> you can index. (The project hands you that line.)

Walking a word with .chars() hands you each letter but not its position, so keep count beside the loop: a mut number that starts at 0 and goes up by one as the last thing each lap does. += 1 is add one and keep the name — the used += 1 your game loop has run since lab 03:

let mut so_far = 0;
for c in word.chars() {
    println!("{c} is letter {so_far}");
    so_far += 1;
}

With word set to "tea" that prints t is letter 0, e is letter 1, a is letter 2. Where the count goes up matters: at the end of the lap, so the body sees the position of the letter it is holding, not the next one's.

Asking those three questions in order is lab 02's if/else grown a middle branch. else if asks its question only when the line above answered no, so the checks run top to bottom and the first yes wins:

if same_position {
    // Hit
} else if in_the_word {
    // Near
} else {
    // Miss
}

Choosing which score to make is an if; asking which score you were handed is a match.

The flaw you are shipping on purpose

Score eerie against the secret speed with the rule above, and here is what comes back:

e e r i e
~ ~ . . ~

Three es marked Near. But speed has only two es. The simple rule asks each letter "are you in the word at all?" — and all three es truthfully answer yes, though they are all pointing at the same two es in speed. Two es cannot be evidence for three: the row over-promises, and a player reading it thinks there are three es to place.

So what does this course do about it? For now, nothing — and we say so out loud. The rule above is genuinely how you would write a first scorer, and the flaw only appears when a guess repeats a letter — which most guesses don't. Your test suite will pin the flawed behaviour: a test that writes today's answer down, so it cannot change without a test going red. That test is simple_scoring_overcounts_duplicates, it asserts the three Nears, and it makes the flaw documented, fenced, and impossible to forget.

To fix it properly you need to ask: is there another e in the secret that nothing has claimed yet — and where? That question can come back empty, and Rust has a type for an answer that might not exist. It is the next lab's whole subject, and this bug is why you'll want it.

Shipping a known, documented flaw and fixing it properly later is not a compromise of engineering — it is engineering.

That's the lesson — free to read. Create a free account to build it: the brief, the grading table and the hint ladder are on the Lab tab.

Create a free account →

Free to start · no credit card · Rust for Beginners is free

The brief, the grading table and the hint ladder live here.

Create a free account to unlock this lab.

Create a free account →

Free to start · no credit card · Rust for Beginners is free

Chat with Guru about the check that is failing you — the written hint ladder on the Lab tab is free and unlimited either way.

Create a free account to unlock this lab.

Create a free account →

Free to start · no credit card · Rust for Beginners is free

Your submitted diff lands here with comments on the exact lines.

Create a free account to unlock this lab.

Create a free account →

Free to start · no credit card · Rust for Beginners is free