You'll learn: how to make the computer repeat something — a fixed number of times, or until something happens — and how to stop, skip, and count while it does. By the end of this lab your game has real stakes: six guesses, and then it's over.
Your game takes one guess and answers it. Nobody loses a one-guess game, and a game you cannot lose has no tension. What is missing is repetition — ask, answer, ask again, up to six times, stopping early if the player wins. Rust has three ways to say "do this again"; this lab needs two of them, and uses each where it fits best.
for — do something once per item
The workhorse. A for loop runs its body once for each item in a group of
values — the letters in a word, the numbers in a count — and hands you the item
each time around. Programmers call such a group a collection; lab 04 builds
one from scratch.
for c in guess.chars() {
println!("one letter: {c}");
}
Read it aloud: for each character c in the guess's characters, run the
block. If guess is "crane", the body runs five times, and c is 'c',
then 'r', then 'a', 'n', 'e'.
Two new things are hiding in those three lines.
First, .chars(). A &str is text, and Rust will not let you loop over text
without saying what a "step" is — one character? one byte? The compiler makes
you choose. Try to write for c in guess and the compiler stops you with:
error[E0277]: `&str` is not an iterator
|
2 | for c in guess {
| ^^^^^ `&str` is not an iterator; try calling `.chars()` or `.bytes()`
Read the line the carets point at — the compiler names both fixes. For this
game the answer is always .chars(): we care about letters.
Second, c has a type you never wrote: char. A char is one character, and
it is written with single quotes — 'a' is a char, "a" is a string with
one character in it. They are different types and Rust will not mix them up
for you. Characters come with useful questions built in, and this lab needs
exactly one:
c.is_ascii_lowercase() // true for 'a' through 'z', false for everything else
return works from inside a loop
Here is the shape you will write today. To answer "is every character a lowercase letter?", walk the characters and bail out the moment one fails:
pub fn is_letters(guess: &str) -> bool {
for c in guess.chars() {
if !c.is_ascii_lowercase() {
return false;
}
}
true
}
return false; does not mean "stop the loop" — it means "stop the whole
function, the answer is false". The loop dies with it. And if the loop
finishes without ever hitting that return, every character passed the test,
so the last line of the function answers true.
That last line has no return and no semicolon, on purpose: from lab 01, the
last expression of a function is its answer. You could also write return true; there — it means the same thing. This course uses return only for
leaving early, and a bare last line for the function's answer.
One edge to know about: if guess is empty, the loop body never runs — with no
characters, nothing can fail the test, so the function answers true. That
sounds like a bug, but it is not one here: the length check from lab 02 already
refuses an empty guess, and this lab wires the two checks together so each one
only has one job.
&& — both must be true
A guess the game accepts has the right length and contains only letters. Rust
spells "and" as &&, and both sides of it are yes-or-no questions. Two more
questions you can ask of numbers, both new today: a > b (is a bigger?) and
a >= b (is a bigger, or the same?). Joined with &&:
turn > 0 && max_guesses >= turn
Read it: the turn has started, and the limit still covers it. The whole
expression is true only when both sides are. Today's step 3 joins two
questions this way — and you wrote both of them yourself.
The turn counter needs >= on its own, too: six guesses allowed and six used
means nothing is left, so the limit itself counts as over.
&& has a twin, ||, Rust's or: true when either side is. Nothing in this
lab needs it — lab 07's finished game is where you will write one.
Using what lab 02 built: use crate::
is_right_length lives in guess.rs — a different file from the one you are
writing today. Inside your library, other modules are reached through
crate, which means this library, from the top:
use crate::guess::is_right_length;
Read it right to left: the function is_right_length, inside the module
guess, inside crate — your own library. After that line, the function is
usable by its short name for the rest of the file. (In main.rs you have been
writing use lantern::... instead — same idea, but from outside the library
you name it like any other crate, and from inside you say crate.)
Numbers can be a collection too: ranges
Sometimes you don't have a collection — you have a count. "Draw five underscores" has no list to walk. Rust writes a count as a range:
for _ in 0..3 {
println!("tick");
}
That prints tick three times. 0..3 is the numbers from 0 up to but not
including 3 — 0, 1, 2: three numbers, three laps. The count can be a name
as easily as a literal: 0..word_len is the numbers from 0 up to but not
including word_len, and for a 5-letter word that is 0, 1, 2, 3, 4 — five
numbers, so the body runs five times. The name _ (an underscore, fittingly)
is how you tell Rust I know the loop hands me a value each time; I'm not going
to use it. Name it i instead and the compiler warns you about an unused
variable — _ is the polite way to say "on purpose".
If you have written C, Java or JavaScript, your fingers may type
for (i = 0; i < n; i++). Rust has no such form: it is always
for x in 0..n or for c in guess.chars(). This is the one mistake where the
compiler is unusually little help — typed from habit, it says only:
error: expected one of `)`, `,`, `@`, `if`, or `|`, found `=`
No error code, no help: line — if you see it pointing at a for line, the
fix is rewriting the line in Rust's shape.
while — repeat until something changes
A for loop knows in advance how many times it will run. The game's main loop
does not: it ends when the player wins or runs out of turns, and you cannot
know which turn that will be. That is a while loop:
while !out_of_turns(used, max_guesses) {
// one whole turn happens here, and somewhere in it, `used` changes
}
while checks its condition before every lap — one lap is one run through the
body: still true, run the body again; false, walk away. The one rule a while
loop depends on is that something in the body must be able to change the
condition — here, taking a turn adds one to used. Forget that and the loop
runs forever; your terminal appears to hang, and Ctrl+C is how you get out.
(Rust has a third loop, spelled loop { }, which repeats until a break;
stops it. break works in any loop — you will type one in this lab's shell
code, and that is all this course needs it for.)
continue — skip the rest of this lap
Inside any loop, continue; means stop this lap here and start the next one.
It is exactly what an invalid guess needs: don't count it, don't score it, go
back and ask again.
if !is_valid(&guess, word_len) {
println!("{word_len} letters, a-z only. Try again.");
continue;
}
// only a valid guess ever reaches this line
This is a small kindness with a big effect on how the game feels: a typo costs the player nothing. The turn counter only moves for guesses the game accepted.