Building a Guessing Game in Rust: A Beginner's Journey

Yo, what’s good? Let’s dive into Rust with a dope guessing game project that’s perfect for beginners. This isn’t just about coding a game—it’s about grokking Rust’s core concepts like variables, loops, error handling, external crates, ownership, borrowing, and pattern matching. By the end, you’ll have a working game and a solid grasp of Rust’s fundamentals. Let’s get to it, bro!
What’s the Guessing Game?
The guessing game is a classic:
The program picks a random number between 1 and 100.
You guess a number, and it tells you if it’s too high, too low, or correct.
Hit the right number, and it yells “You win!” and stops.
We’ll build this step-by-step, exploring Rust’s let
, match
, loops, crates, and more advanced concepts like ownership and borrowing. Here’s the final code we’re aiming for:
use rand;
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Welcome to Guessing Game");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
println!("Please enter 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(_) => continue
};
println!("you guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too Small"),
Ordering::Equal => {
println!("You win");
break;
},
Ordering::Greater => println!("Too Big")
}
}
}
Setting Up the Project
Kick things off with Cargo, Rust’s build tool. Open your terminal in your projects directory and run:
$ cargo new guessing_game --bin
$ cd guessing_game
The --bin
flag creates an executable project. You’ll get a Cargo.toml
file for configuration and a src/main.rs
file with:
fn main() {
println!("Hello, world!");
}
Run it with cargo run
to confirm it works:
$ cargo run
Compiling guessing_game v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 1.50 secs
Running `target/debug/guessing_game`
Hello, world!
Sweet, let’s transform this into our game.
Step 1: Capturing User Input
First, we need to grab the user’s guess and display it. Replace src/main.rs
with:
use std::io;
fn main() {
println!("Welcome to Guessing Game");
println!("Please enter your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("you guessed: {}", guess);
}
Breaking It Down
Importing
std::io
:use std::io;
pulls in the input/output library for reading user input.Prompting:
println!("Welcome to Guessing Game");
andprintln!("Please enter your guess.");
set the vibe with a welcome message and prompt.Variables and Mutability:
let mut guess = String::new();
creates a mutable variableguess
as an emptyString
. Rust variables are immutable by default, somut
lets us modify it.String::new()
is an associated function that creates a new, empty string.Reading Input:
io::stdin().read_line(&mut guess)
reads a line from the keyboard intoguess
. The&mut guess
is a mutable reference, lettingread_line
update the string..expect("Failed to read line")
crashes if reading fails, with an error message.Printing the Guess:
println!("you guessed: {}", guess);
uses{}
as a placeholder to show the input.
Run it:
Welcome to Guessing Game
Please enter your guess.
6
you guessed: 6
Concept: Ownership and Borrowing
Rust’s ownership system is a game-changer. Every value has a single owner, and when the owner goes out of scope, the value is dropped (memory is freed). In let mut guess = String::new();
, guess
owns the String
. When we pass &mut guess
to read_line
, we’re borrowing it as a mutable reference, allowing read_line
to modify it without taking ownership. This avoids copying the string, which is efficient and safe. Rust’s rules ensure references don’t outlive the data they point to, preventing bugs like dangling pointers.
Step 2: Generating a Random Number
We need a random secret number. Rust’s standard library doesn’t do random numbers, but the rand
crate does. Add it to Cargo.toml
:
[dependencies]
rand = "0.8.5"
Use version 0.8.5
for modern Rust compatibility. Cargo’s Semantic Versioning means it’ll grab >=0.8.5
but <0.9.0
. Run cargo build
:
$ cargo build
Updating crates.io index
Downloading crates ...
Compiling rand v0.8.5
Compiling guessing_game v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 1.50 secs
Cargo creates a Cargo.lock
file to lock dependency versions for consistent builds. Update src/main.rs
:
use rand;
use std::io;
use rand::Rng;
fn main() {
println!("Welcome to Guessing Game");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {}", secret_number);
println!("Please enter your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("you guessed: {}", guess);
}
What’s New?
Using
rand
:use rand;
anduse rand::Rng;
import therand
crate and itsRng
trait for random number methods.Random Number:
rand::thread_rng().gen_range(1..=100)
generates a random number from 1 to 100 (inclusive).thread_rng
provides a thread-local random number generator, seeded by the OS.Debug Output:
println!("The secret number is: {}", secret_number);
shows the number for testing (we’ll remove it later).
Run it a few times with cargo run
to see different random numbers.
Concept: Crates and Modules
The rand
crate is a library crate, unlike our binary crate (the executable). Crates are Rust’s way of packaging code, and use
statements bring modules into scope. rand::Rng
is a trait (like an interface) defining random number methods. Rust’s module system organizes code, and ::
navigates it (e.g., rand::thread_rng
). You can explore rand
’s docs with cargo doc --open
.
Step 3: Comparing the Guess to the Secret Number
Let’s compare the guess to the secret number. Update src/main.rs
:
use rand;
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Welcome to Guessing Game");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {}", secret_number);
println!("Please enter 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(_) => continue
};
println!("you guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too Small"),
Ordering::Equal => {
println!("You win");
break;
},
Ordering::Greater => println!("Too Big")
}
}
Key Changes
Importing
Ordering
:use std::cmp::Ordering;
brings in theOrdering
enum (Less
,Equal
,Greater
) for comparisons.Parsing Input:
let guess: u32 = match guess.trim().parse() { ... };
converts theString
input to au32
.trim()
removes whitespace (like newlines), andparse()
tries to make a number. Thematch
handles errors:Ok(num)
returns the number,Err(_)
loops back.Comparing:
guess.cmp(&secret_number)
returns anOrdering
variant. Thematch
expression prints “Too Small”, “Too Big”, or “You win” and breaks on a correct guess.
Concept: Pattern Matching with match
The match
expression is Rust’s superpower for pattern matching. It’s like a switch statement on steroids, ensuring you handle all possible cases (exhaustive matching). In match guess.cmp(&secret_number)
, we match against Ordering
variants, running different code for each. Similarly, match guess.trim().parse()
handles Result
variants (Ok
or Err
). Rust enforces that all variants are covered, preventing bugs.
Why Parse?
secret_number
is a u32
, but guess
is a String
. Rust’s type system demands matching types for cmp
. Without parse
, you’d get:
error[E0308]: mismatched types
--> src/main.rs: expected `std::string::String`, found `u32`
The match
with continue
ensures invalid inputs don’t crash the game.
Concept: Error Handling with Result
The parse()
method returns a Result
enum: Ok(value)
for success, Err(error)
for failure. We use match
to handle both cases gracefully, unlike .expect()
, which crashes on errors. Result
is Rust’s way of encoding errors explicitly, making code robust. Similarly, read_line
returns a Result
, but we use .expect()
for simplicity here.
Run it:
Welcome to Guessing Game
The secret number is: 58
Please enter your guess.
76
you guessed: 76
Too Big
Step 4: Allowing Multiple Guesses
Your code already has a loop
for multiple guesses. The loop
keyword creates an infinite loop, prompting for guesses until break
(on a correct guess) or continue
(on invalid input) changes the flow. Test it:
Welcome to Guessing Game
The secret number is: 61
Please enter your guess.
10
you guessed: 10
Too Small
Please enter your guess.
foo
Please enter your guess.
61
you guessed: 61
You win
Concept: Control Flow with Loops
Rust’s loop
is a simple way to create an infinite loop, perfect for games. break
exits the loop, and continue
skips to the next iteration. Rust also has while
and for
loops, but loop
is ideal here for its clarity. Control flow in Rust is explicit, making it easy to reason about program behavior.
Step 5: Final Touches
Remove the println!("The secret number is: {}", secret_number);
to keep the game fair. Here’s the final code:
use rand;
use std::io;
use std::cmp::Ordering;
use rand::Rng;
fn main() {
println!("Welcome to Guessing Game");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
println!("Please enter 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(_) => continue
};
println!("you guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too Small"),
Ordering::Equal => {
println!("You win");
break;
},
Ordering::Greater => println!("Too Big")
}
}
}
Step 6: Bonus Concept - Exploring Option
While our game uses Result
for error handling, Rust’s Option
enum is another key concept. Option
represents a value that might or might not exist: Some(value)
or None
. Imagine extending the game to read the range from user input. You might parse a string into an Option<u32>
:
let range: Option<u32> = "100".parse().ok();
match range {
Some(num) => println!("Valid range: {}", num),
None => println!("Invalid range"),
}
Unlike Result
, which handles errors, Option
handles absence. You could use Option
to make the game’s range configurable, falling back to 100 if the input is invalid.
Conclusion
Boom! You’ve built a slick guessing game and learned a ton of Rust:
Project Setup: Using Cargo for projects and dependencies.
Variables:
let
,mut
, andString
.Input/Output:
std::io
for user interaction.Crates: Adding
rand
for random numbers.Pattern Matching:
match
forOrdering
andResult
.Error Handling: Using
Result
to handle parsing errors.Ownership and Borrowing: Safe memory management with references.
Control Flow: Loops with
loop
,break
, andcontinue
.Option: A peek at handling optional values.
Rust’s ownership, type safety, and explicit error handling make it a beast for reliable code.
Subscribe to my newsletter
Read articles from Aviral directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
