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

[ free ] [ lists · indexing · % ] [ ~20 min if you have programmed · ~60 if you have not ]
04

A different word every game

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

You'll learn: how Rust holds many values under one name — the array, the slice &[&str] your functions will take, and reaching a word by its position. One % sign turns any number into a position that is always on the list, and the same number always lands on the same word.

You already know the secret word

Your game plays by its own rules now: it counts turns, refuses junk, and can be lost. But you wrote let secret = "crane"; with your own hands. You cannot be surprised by a word you typed. For the game to surprise its own author, the secret has to come from somewhere bigger than one line: a list.

A list with a fixed length: the array, and the slice that borrows it

The simplest list in Rust is the array — a row of values, all the same type, whose length is fixed when you write it:

let mini = ["apple", "berry", "crane"];

Three &strs in a row. You reach one by its position — its index — in square brackets, and positions start at zero:

mini[0]   // "apple"
mini[1]   // "berry"
mini[2]   // "crane"

The consequence that matters today: a list of three has indexes 0, 1, 2 — and no index 3. Reach for a position worked out while the program runs, past the end —

let wanted = mini.len() + 4;   // 7, worked out while the program runs
mini[wanted]

— and the program stops on the spot with a message that names both numbers:

index out of bounds: the len is 3 but the index is 7

Rust calls that a panic — a deliberate stop at the line that went wrong, instead of quietly reading someone else's memory. Write mini[3] literally and it never reaches a run at all: the compiler sees the answer from where it stands and refuses, with error: this operation will panic at runtime. You will make both impossible in a moment, with math.

Your functions will not take an array directly, though — an array's length is part of its type, and you do not want a pick that only works on lists of exactly 64. The type your functions take is the slice:

words: &[&str]

Read it as a borrowed view of some &strs in a row — however many there are. An array is lent as a slice by writing & in front of it:

let view: &[&str] = &mini;   // the same three words, seen as a slice

The test list you are about to write is one line of that shape, with the array spelled out in place of mini — the project prints it. The course's own word lists are already slices, so they go in as they are. A slice knows its own length, words.len() — the fact everything below stands on.

The array's sibling is Vec, the list that can grow. The word list never changes size, which is why it ships as a fixed list instead; you build your first Vec next lab.

% — the wheel that always lands on the list

Here is the lab's one genuinely clever line. You want puzzle number 7 — or 7_000_003, and Rust lets you space out a long number with _ — to pick a word from a 64-word list, every number landing somewhere on the list, the same number always landing on the same word.

The % operator gives the remainder after division. 7 % 3 is 1, because 7 divided by 3 is 2 with 1 left over. Two facts make it perfect here:

  • the remainder of dividing by len is always 0 up to len - 1 — exactly the valid indexes, so the panic above becomes impossible by construction, as long as the list has at least one word in it;
  • it is not random. 41 % 3 is 2 today, tomorrow, and on your friend's machine.

So, on the three-word mini above:

7 % mini.len()          // 1 — and mini[7 % mini.len()] is "berry"

Any number, wrapped by % mini.len(), comes out as 0, 1 or 2: around the list like a wheel, 0, 1, 2, 0, 1, 2, …. pick is that wheel on the list it is handed, and the project prints its one line, because two marks in it are new. The same answer every time sounds like the opposite of what a guessing game wants, but look at what it buys: tests can know the answer (pick of puzzle 1 on a 3-word list is the second word, assertable), and two players who type cargo run -- 42 race the same word. The dice live in main.rs, which turns the clock into a puzzle number when you don't supply one. The library always gives the same answer; the luck lives in main.rs.

as — two kinds of number, one bridge

Why (puzzle as usize)? The puzzle number arrives as a u64 — a 64-bit unsigned integer, big enough for a clock reading. But Rust indexes lists with usize — the machine's own size-of-things type. They are different types, and Rust never converts numbers behind your back; mixing them is a compile error, not a rounding surprise. Try puzzle % words.len() and the compiler answers with three errors — one for each place the two types meet. The one that names the problem is:

error[E0277]: cannot calculate the remainder of `u64` divided by `usize`

as is the explicit bridge: puzzle as usize says treat this number as that type; I know what I'm doing.

Walking a list

The for loop from lab 03 walks a list exactly as it walked characters — one lap per entry, the loop's name holding the current one:

for word in &mini {
    println!("{word}");
}

Three laps, three lines printed: apple, berry, crane. The & lends the array as a slice, so this is the same walk your function makes over the slice it is handed. is_known is this walk with a question inside it; the project says which question, and names the lab where you wrote the shape. One wrinkle — a borrowed list hands you each entry borrowed too, so comparing an entry with the guess needs one extra mark. Nothing in this course derives that mark, so the project prints that one line.

The word lists themselves

Open game/lantern/src/wordlist.rs; it shipped finished and you never edit it. Two constants, two standards:

  • WORDS — sixty-four common five-letter words: the secrets, the words the game can pick. Short and curated on purpose, because a secret has to be a word worth guessing toward.
  • ACCEPTED — every secret, plus over a thousand more common five-letter words: the guess dictionary. A guess does not have to be a possible secret. It only has to be a real word. A game that refuses ghost feels broken to the friend you hand it to — so the list a guess is checked against is deliberately much bigger than the list a secret is drawn from, which is why every function you write takes its list as an argument.

A shipped test, the_list_is_sound, has proved both lists sound since your first cargo test — five lowercase letters per word, no word twice, every secret also an accepted guess — measured from the lists themselves, not promised in a comment.

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