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

[ free ] [ struct · impl ] [ ~35 min if you have programmed · ~1.8 h if you have not ]
07

Put the whole game in one box

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

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 means secret: secret.
  • String, not &strnew takes 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.

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