Chapter 2: Programming a Guessing Game in Rust
This chapter provides a hands-on introduction to the Rust programming language by building a complete, interactive console guessing game. Through this project, core Rust concepts are introduced in a practical context: variable mutability, standard input/output handling, external crate management via Cargo, error handling with Result and pattern matching, type conversion and variable shadowing, looping constructs, and terminal output styling.
1. Project Initialization and Workflow
Rust uses Cargo as its official build system and package manager. Starting a new binary project generates standard boilerplate files and directory structures required for compilation and dependency management.
# Generate a new binary application directory
cargo new guessing_game
# Navigate into the project directory
cd guessing_game
Generated Project Structure
To compile and run the initial program:
cargo run
2. Setting Up Variables and Reading Standard Input
The first iteration of the game prompts the user for a guess, captures that input from the terminal, and prints the captured value back out.
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
Key Language Concepts
let mut guess: In Rust, variables are immutable by default. To allow a variable's value to change, themutkeyword must be explicitly declared (let mut).String::new(): Generates a new, growable, UTF-8 encoded empty instance of aString.- The
::syntax indicates thatnewis an associated function of theStringtype (equivalent to a static method in other languages).
- The
io::stdin(): Returns a handle to the standard input stream of the terminal process.&mut guess: Passes a mutable reference ofguessintoread_line.- References (
&) allow different parts of code to access the same piece of data in memory without copying it or transferring ownership. References are also immutable by default, so&mutis required to allowread_lineto write data into the buffer.
- References (
- String Placeholders (
{}): Theprintln!macro replaces{}with the value of the corresponding argument provided after the format string.
3. Error Handling Fundamentals: The Result Enum
I/O operations in Rust do not throw runtime exceptions. Instead, potential failures are encoded directly into the type system using the Result enumeration.
The expect Method
Calling .expect("Failed to read line") acts directly on the returned Result:
- If the variant is
Ok(value):expectunwraps and returns the underlying value. - If the variant is
Err(error): The program crashes (panics) and prints the provided error message along with debugging context.
Note: Omitting
expector explicit error handling will trigger a compiler warning, as Rust enforces explicit acknowledgment of fallible operations.
4. Managing External Dependencies via Cargo.toml
Rust's standard library does not include a built-in random number generator. To generate a secret number, the external rand crate is imported via the project manifest.
Updating Cargo.toml
[dependencies]
rand = "0.8.3"
colored = "2.0.0"
Running cargo build fetches, compiles, and locks the exact versions of the specified crates alongside their sub-dependencies.
5. Generating Secret Numbers with the rand Crate
To use functionality from external crates, specific traits and modules must be brought into scope using use statements.
use rand::Rng;
fn main() {
// thread_rng() yields a local thread-specific random number generator
// gen_range() generates numbers in a specified range [low, high)
let secret_number = rand::thread_rng().gen_range(1..101);
println!("The secret number is: {}", secret_number);
}
use rand::Rng;: Brings theRngtrait into scope. Traits define methods that types can implement; thegen_rangemethod requiresRngto be in scope to be callable on the generator.- Range Expression (
1..101): Defines an upper-exclusive range from 1 to 100 inclusive.
6. Comparing Values and Pattern Matching
Comparing values requires evaluating relationship states: less than, greater than, or equal to. Rust models this using the std::cmp::Ordering enumeration.
The Ordering Enum Variants
Evaluating Conditions with match
A match expression inspects a value against a series of pattern arms and executes the code associated with the first matching pattern:
use std::cmp::Ordering;
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
7. Type Conversion and Variable Shadowing
Input captured from the terminal is stored as a string (e.g., "50\n"). Comparing this string directly to an integer (secret_number) fails compilation because Rust enforces strict static typing.
let guess: u32 = guess.trim().parse().expect("Please type a number!");
Mechanisms Applied
trim(): Removes leading and trailing whitespace, including newline characters (\nor\r\n) produced when the user presses Enter.parse(): Parses a string into another data type. Becauseparse()can infer multiple numeric types, explicit type annotation (let guess: u32) is required.- Variable Shadowing: Rust allows re-declaring an existing variable name using
let. This pattern avoids forcing artificial names (such asguess_strandguess_int) when converting data types within the same scope.
8. Looping and Flow Control
To permit multiple attempts, execution is wrapped inside a loop block. The loop continues indefinitely until explicitly interrupted with break.
loop {
// Prompt user, read input, and parse
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break; // Terminate the infinite loop
}
}
}
9. Robust Error Handling (Eliminating Panics)
Crashing on non-numeric input is unacceptable in production environments. By replacing .expect() with a match expression on parse(), the program gracefully handles invalid input and advances to the next iteration.
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue, // '_' is a wildcard pattern catching all errors
};
- If
parse()returnsOk(num), the integer is extracted and bound toguess. - If
parse()returnsErr(_),continueskips the rest of the loop block and starts the next iteration immediately.
10. Complete Application Implementation
Below is the complete, refined implementation of the guessing game, featuring colored terminal output via the colored crate (Colorize trait).
use colored::*;
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
loop {
println!("Please input 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(num) => num,
Err(_) => {
println!("{}", "Please enter a valid number!".yellow());
continue;
}
};
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("{}", "Too small!".red()),
Ordering::Greater => println!("{}", "Too big!".red()),
Ordering::Equal => {
println!("{}", "You win!".green());
break;
}
}
}
}
11. Execution Flow Diagram
## Summary and Key Takeaways
- Immutability by Default: Variables defined with
letcannot be modified unless explicitly declared withlet mut. - References and Borrowing: Memory can be accessed safely without taking ownership using references (
&). Modifying the target data requires a mutable reference (&mut). - Associated Functions: Functions scoped to a type rather than a specific instance use the
Type::function()syntax (e.g.,String::new()). - Explicit Result Handling: Functions prone to failure return the
Resultenum (OkorErr). Rust requires developers to handle errors either via methods like.expect()or structurally viamatch. - Cargo Dependency Management: External libraries (crates) are declared in
Cargo.toml.Cargo.lockensures deterministic, reproducible builds across environments. - Variable Shadowing: Re-using an existing variable identifier with
letallows type transformations in-place without generating redundant variable names. - Exhaustive Matching: The
matchcontrol flow operator enforces handling every potential variant of anenum(such asOrderingorResult), eliminating unhandled branch errors at compile time.