The guessing game
A real working program. Input, a dependency, match, and loops in one sitting.
A program is the fastest way to make a language concrete. This chapter writes one: a number-guessing CLI. The user picks a number, the program tells them higher or lower, and the loop ends when they win. About forty lines of code, but it touches input, output, an external crate, pattern matching, and control flow. Everything you need to read a real Rust file by the end.
This is throwaway code on purpose. Some choices here (specifically .expect() on errors) would be wrong in a library. Chapter 9 fixes that. For a one-off prototype that runs in a terminal, the looseness is fine, and that distinction is worth internalizing now.
Start a new project
cargo new guessing-game
cd guessing-gameReplace src/main.rs with this, then we will walk through it line by line.
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess a number between 1 and 100.");
let secret = rand::thread_rng().gen_range(1..=100);
loop {
println!("Your guess:");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(n) => n,
Err(_) => {
println!("Please type a number.");
continue;
}
};
match guess.cmp(&secret) {
Ordering::Less => println!("Too small."),
Ordering::Greater => println!("Too big."),
Ordering::Equal => {
println!("You got it.");
break;
}
}
}
}Run it once before reading further:
cargo runThe first run will fail because rand is not yet a dependency. Cargo prints the fix. Let us add it deliberately.
Adding a dependency
Open Cargo.toml and add rand under [dependencies]:
[package]
name = "guessing-game"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = "0.8"Run cargo build once. Cargo downloads rand and its transitive dependencies, compiles them, and caches the artifacts. The Cargo.lock file pins the exact versions; commit it for binaries, leave it out of .gitignore for libraries (which is the opposite of what most ecosystems do, but it is the convention).
You can also add a dependency from the command line:
cargo add randEither way, the dependency is now usable via use rand::... at the top of any file in the crate.
Reading input
The standard library handles I/O. std::io::stdin() returns a handle to standard input. .read_line(&mut guess) reads one line, appending it to a String you pass in.
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("failed to read line");Three things to notice:
let mut. Variables are immutable by default.mutis opt-in. The compiler will tell you if you try to mutate without it.&mut guess. A mutable reference.read_lineborrowsguessexclusively to write into it, then returns the borrow..expect(...).read_linereturnsResult<usize, io::Error>..expect()unwraps the success case or panics with the message on failure.
For a CLI prototype where stdin failing means the user is doing something weird and you do not care, .expect() is fine. In a library you publish to the world, it would be unacceptable. Chapter 9 covers ? and proper error propagation. Hold the thought.
Parsing the input
guess is a String that ends with a newline character. We want a number. Two steps: trim, then parse.
let guess: u32 = match guess.trim().parse() {
Ok(n) => n,
Err(_) => {
println!("Please type a number.");
continue;
}
};This block deserves a careful look:
- Shadowing. The new
let guessrebinds the same name to a different type. The oldStringis dropped at the end of this block. Rust lets you do this on purpose; it is cleaner than inventingguess_string,guess_trimmed,guess_int. - Type annotation.
: u32tells the compiler what type to parse to. Without it,.parse()would not know the target type. Try removing it; the error message is excellent. matchonResult.parse()returnsResult<u32, ParseIntError>.Ok(n) => nextracts the integer.Err(_)ignores the specific error (the_says "I do not care about the value") and asks the loop to start over.
continue jumps to the next iteration of the enclosing loop. break ends it. Both work inside nested loops and can be labeled if you need to target an outer one.
Comparing with Ordering
u32::cmp compares two integers and returns an Ordering: an enum with three variants, Less, Greater, and Equal. Match exhaustively:
match guess.cmp(&secret) {
Ordering::Less => println!("Too small."),
Ordering::Greater => println!("Too big."),
Ordering::Equal => {
println!("You got it.");
break;
}
}Three things matter here:
- Exhaustiveness. The compiler checks that every variant is handled. Add a fourth variant to
Ordering(you cannot, it is sealed, but pretend) and thismatchwould fail to compile. That property scales: when you add a new error variant later, the compiler points to every place you have to handle it. - No fallthrough. Each arm is independent. No
breakkeyword formatcharms. - Match is an expression. Each arm can return a value. We use it for side effects here, but
let result = match ... { ... }is the idiomatic way to compute branched values.
Variables, mutability, shadowing
Notice the two different guess bindings. The first is let mut guess = String::new(), which we mutate via read_line. The second is let guess: u32 = match ..., which is immutable and a different type.
This is shadowing, and it is preferred over reusing a single mut variable across types. When you reach for let mut, ask yourself: would shadowing produce a clearer, immutable rebinding instead? The answer is often yes.
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("...");
// later, reassigning the same name to a different type would fail
let parsed: u32 = guess.trim().parse().expect("not a number");
println!("{}", parsed);let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("...");
let guess: u32 = guess.trim().parse().expect("not a number");
println!("{}", guess);The right side reuses the name across types via shadowing. The left side invents a second name for no reason. In larger code, the second name leaks into the rest of the function and creates ambiguity.
Loops and termination
loop { ... } is the unconditional loop. while and for exist for the cases where the condition is more natural at the top.
for i in 0..10 { ... } // 0, 1, ..., 9
for c in "rust".chars() { ... }
while condition() { ... }In the guessing game, loop is correct because there is no natural condition at the top: we only know we are done after comparing. The break inside the Ordering::Equal arm is the exit.