You'll learn: how to declare your own type — a struct for the game's
state, an impl block of methods, and the /// comments that say what each
one promises. Six labs of parts become one Game, and only two new decisions
get made: the sentence a win prints, and what turns_left answers when there
is nothing left. By the end main is a loop and the game is finished —
playable, and yours to hand to someone.
What main.rs is juggling
Look at what main.rs is carrying by now. The secret, in one variable. The
guesses so far, in a Vec you added last lab. The guess limit, in a
constant. And the knowledge of how they combine — when the game is won, when
it is over, what the board looks like — is spread across function calls that
main strings together by hand.
Every piece works and every piece is tested. What is missing is a name for the thing they make together. "A game in progress" is one idea — this secret, these guesses, this limit — and nothing in your code says so.
struct: data that belongs together, named
A struct bundles named values into one new type:
pub struct Game {
pub secret: String,
pub guesses: Vec<String>,
pub max_guesses: usize,
}
Each line inside is a field: a name, a colon, a type. Where a tuple
bundles by position — .0, .1 — a struct bundles by name, and for three
fields that will travel together for the rest of the program, names are
worth having. Note the types are all ones you know: a struct is not new
kinds of data, it is old data with one name over it.
You make one with a struct literal — the type's name and a value for every field:
let game = Game {
secret: "crane".to_string(),
guesses: Vec::new(),
max_guesses: 6,
};
Leave a field out and rustc lists what is missing; there is no such thing as
a half-built Game. Reach the parts with a dot: game.secret,
game.guesses, game.max_guesses.
The fields are pub, like everything else this course builds — main.rs
reads game.secret to name the word when a player leaves early, and this
course hides nothing.
impl: what a Game can be asked
Data alone is a filing cabinet. An impl block ("implementation") attaches
functions to the type:
impl Game {
pub fn is_won(&self) -> bool {
for guess in &self.guesses {
if guess == &self.secret {
return true;
}
}
false
}
}
A function inside impl Game whose first parameter is &self is called a
method. self is the particular Game the method was asked about, and
the & in &self is the same lending you met in lab 01: the method reads
the game, it does not take it away. Inside, self.secret and self.guesses
reach the fields.
You call a method with the dot: game.is_won(). You never pass self
yourself — game on the left of the dot is self, which is why
game.is_won() reads like a question put to the game. Inside another method
of the same block, that call is written self.is_won(): same dot, and the
game asking itself.
is_won is above as a worked example, not as the answer: the project asks for
yours first, from a shape you already own.
Methods that change the game need a different first parameter: &mut self.
Recording a guess alters self.guesses, so:
pub fn guess(&mut self, word: &str) {
self.guesses.push(word.to_string());
}
The mut discipline from lab 01 follows the type all the way up: a method
that mutates needs &mut self, and calling it needs the variable to be
let mut game, or rustc refuses — measured on this course's toolchain:
error[E0596]: cannot borrow `game` as mutable, as it is not declared as mutable
|
5 | game.guess("slate");
| ^^^^ cannot borrow as mutable
Reading a struct's methods tells you which ones can change it — not a convention, the compiler enforces it.
:: for the type, . for the value
One function in the block has no self at all:
pub fn new(secret: String, max_guesses: usize) -> Game {
Game {
secret,
guesses: Vec::new(),
max_guesses,
}
}
With no self, this is not a method — there is no particular game yet; its
job is to make one. It is called an associated function: a function
that belongs to the type rather than to a value of it, and you call it
through the type's name with a double colon: Game::new(secret, 6). You
have used this spelling all course — String::new() since lab 01's banner,
Vec::new() since lab 05's scorer — and now you know what it meant.
The rule: :: reaches through a type's name, . reaches through a
value. Mix them up and rustc says so precisely:
error[E0599]: no method named `new` found for struct `Game` in the current scope
|
5 | let again = game.new("slate".to_string(), 6);
| ^^^ this is an associated function, not a method
Two smaller things in new, both spellings you will now see everywhere:
- Field shorthand — when a variable and a field share a name,
secret,alone meanssecret: secret. String, not&str—newtakes the secret by value: the game owns its secret from now on, so the caller hands the value over rather than lending it.
One more spelling you will meet: inside impl you may write Self instead
of the type's own name. This course spells Game out — read Self as "the
type this block is about".
Why every method is only a few lines
Here is board, the biggest method in the finished game:
pub fn board(&self) -> String {
let mut out = String::new();
for guess in &self.guesses {
let scores = score_guess_exact(&self.secret, guess);
out.push_str(&render_score(guess, &scores));
out.push('\n');
}
out.push_str(&tracker_line(&self.secret, &self.guesses));
out.push('\n');
if !self.is_over() {
out.push_str(&turn_line(self.turns_used() + 1, self.max_guesses));
out.push('\n');
}
out
}
Read the names: score_guess_exact — lab 06. render_score — lab 05.
tracker_line — lab 06. turn_line — lab 03. The methods that do the
visible work are a few lines that call functions you already wrote and
tested: outcome_line picks between a format! and lab 03's
reveal_line. The rest ask one small question each about the three fields.
Read the arguments as closely as the names: score_guess_exact is handed the
secret before the guess, and swapping the two makes a plausible-looking wrong
row over a lab 06 that is perfectly green.
This is deliberate, and it is the design lesson the course ends on: the
functions know how, the struct knows what together. If you find yourself
writing new game logic inside Game, stop — it belongs in a module with its
own tests, and the method should call it. A type whose methods are thin is a
type you can trust by reading it — which is why the struct arrives last,
when every method checks against a function you built yourself.
///: writing the promise, not the mechanism
You have met doc comments already — the three you read above banner.rs's
constants in lab 01, and the one you typed above every function you wrote in
labs 01 and 02. This lab you write your own, on a type. Three slashes,
directly above the item:
/// One game in progress: what the player is hunting, what they have tried,
/// and how many tries they get.
pub struct Game {
A // comment talks to whoever is editing this file. A /// comment
documents the item below it for whoever is using it — editors show it when
you hover a name, and cargo doc builds it into browsable pages. The craft
is in what you write: not what the code does, but what the caller may rely
on. "True when the latest guess found the word" is a
promise; "loops over guesses" is a restatement. You write one for Game, its
fields and its methods, and the difference between those two sentences is the
skill being practised.
When the game is finished, the project brief hands you on to the next course on this path: Rust for Systems: Read a SQLite File.