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
scoreis 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 aLetterScore, so you can read what you got.PartialEq— lets==compare two of them; without itassert_eq!on your enum does not even compile.Eq— always travels withPartialEq. Write both.Clone— letsvec![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.