You'll learn: what a variable is, what a function is, and the small ritual of proving your code works with a test. Those three things are the floor every later lab stands on — and by the end of this one, your terminal will print your game's opening screen.
What we are building
When the finished game starts, the first thing a player sees is the banner:
== lantern ==
Guess the 5-letter word. You have 6 tries.
Two lines. The first carries the game's name. The second states the rules. This lab builds both — and everything you need to build them is on this page.
A variable is a name for a value
In Rust you give a value a name with let:
let word_len = 5;
Read it aloud: let word_len be 5. From that line on, writing word_len
means 5. The name is yours to choose; Rust's habit is lowercase words joined
with underscores, like word_len and max_guesses.
One rule to meet now rather than mid-lab: a Rust variable does not change
unless you say so. After let word_len = 5;, trying to set word_len to 6
is an error — not a warning, an error. If you want a value you can change, you
say so up front with mut (short for mutable — changeable):
let mut screen = String::new();
Now screen may change, and the compiler holds you to it: mut values may
change, plain let values may not. That is strict, and it is also a gift:
every plain let is a promise that the value is still what it was made as.
A function is a small machine
A function takes values in, does one job, and hands a value back. Here is the shape, using the very function you will write:
pub fn rules_line(word_len: usize, max_guesses: usize) -> String {
// the body goes here
}
Read it piece by piece, because every part means one thing:
fn— function. The keyword that starts every function.pub— public. Other files may use this function. Without it, the function is private to its own file.main.rs— the small file that actually runs the game — reaches into this file from outside, so this course marks its functionspub.rules_line— the function's name, chosen by us, same naming habit as variables.(word_len: usize, max_guesses: usize)— the values it takes in, called arguments. Each one is a name, a colon, and a type — the kind of value it is.usizeis Rust's type for a whole number that counts things: a length, a number of tries. Rust asks you to write the types where values go in and come out, so the compiler can check every call against them.-> String— the arrow says what comes back. This function returns aString: text.{ … }— the body: the lines that do the work.
One more Rust habit to meet here, because it looks like magic until it is
explained: the last line of a body, without a semicolon, is the value the
function returns. A body that ends in screen hands back whatever screen
holds.
Text: String and format!
Rust's everyday type for text you build and own is String. Two ways to make
one, and one way to grow it — this lab uses all three:
let max_guesses = 6;
let empty = String::new(); // "" — nothing yet, ready to grow
let line = format!("You have {max_guesses} tries."); // built from a template
format! is a template machine: inside the quotes, a name in curly braces —
{max_guesses} — is replaced by that variable's value. It is how you turn the
number 6 into the text "You have 6 tries." without gluing pieces by hand.
The third way is growing a String piece by piece. A String made with
String::new() starts empty; push_str adds text to its end:
let mut screen = String::new();
screen.push_str("== ");
That screen.push_str(…) shape — a value, a dot, an action — is called
calling a method: push_str is something every String knows how to do.
And because pushing changes screen, it only works on a let mut — which is
the one place in this lab that needs mut.
Constants: values that are part of the design
Some values are not variables at all — they are decisions. This game's word
length is five. Its guess count is six. Rust spells a decision with const:
pub const WORD_LEN: usize = 5;
A const has a name in capitals, always has its type written, and never
changes. Your starter file ships three of them — the game's name, the word
length, the guess count — already written, so you can see the shape before you
write your own functions beneath them.
One crate, many files: pub mod
Your game's code lives in game/lantern/src/, one file per lab. But Rust does
not scan folders: a file is not part of your program until a pub mod line
names it. The file src/lib.rs is the table of contents, and
pub mod banner;
in it means: there is a file called banner.rs beside me; it is a part of
this crate; other code may use it. (The word module means one named box of
code — for now, one file.) Until that line exists, the compiler never opens
banner.rs at all — you could write anything in there and no error would
appear, which is worth knowing before it confuses you.
There is a second spelling, pub mod banner { … }, which holds the module's
code right there between the braces instead of in its own file. You will meet
that form at the bottom of this very lab — the test block is a module written
inline. Same idea both times: a named box of code. One names a file; the other
holds its contents directly.
The test ritual
A test is a function that calls your function and checks the answer. That
is the entire idea. Rust has it built in: mark a function with #[test], and
cargo test will find it, run it, and print a line saying whether it passed.
The checking is done by two built-in tools:
assert_eq!(left, right)— these two values must be equal. If they are, nothing happens and the test passes. If not, the test fails and both values are printed, so you can see exactly how they differ.assert!(condition)— this must be true. Used when there is no pair to compare, only a yes-or-no question.
Tests live at the bottom of the file they test, inside a block the project has you type out. Here is what each of its lines means:
#[cfg(test)]— compile the next item only when testing. Your finished game does not carry its tests around; this line is what leaves them out of the real build.mod tests { … }— the inline module form from the section above: a named box holding the tests.use super::*;— the first line inside, every time.supermeans the file around this box, and the*means everything in it. A module is a fresh scope — without this line, the tests cannot seerules_lineorGAME_NAMEat all, and the compiler will say it cannot find them.
When you run cargo test, every test prints its own line — the test's name,
three dots, ok or FAILED. Step 5 of the project shows you the four lines
your own run prints.
When Rust says no
You are going to see error messages this lab — on purpose. So here is the fact that makes them bearable:
Not compiling is the normal state of writing Rust. People who write Rust every day see errors dozens of times an hour. The error is not a grade. It is the compiler telling you, precisely, what it needs next.
A Rust error names the file and line, points an arrow at the exact spot, says
what it expected and what it found — and often writes the fix out for you under
a help: heading. When one appears: read all of it, bottom included, before
changing anything.