- Published on
Chapter 2: Programming Guessing Game
11 min read
- Authors
- Name
- Mohsin Mukhtiar
- @justmohsin_

Table of Contents
Programming a Guessing Game đŚ
There's something magical about writing your first real program in a new language. In Rust, we'll build a classic guessing game that introduces you to essential concepts while creating something genuinely fun. This project will teach you about input/output, variables, control flow, and error handlingâall the building blocks you need for Rust programming.
Our game is simple: the program generates a random number between 1 and 100, and you try to guess it. After each guess, the program tells you whether your guess was too high or too low. When you guess correctly, the game congratulates you and exits.
Setting Up the Project
Let's start by creating a new Rust project:
cargo new guessing_game
cd guessing_game
Open src/main.rs
and you'll see the familiar "Hello, world!" program. We'll replace this with our guessing game code.
Processing User Input
First, let's handle getting input from the user:
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}");
}
This code introduces several important concepts. The use std::io;
line brings the input/output library into scope. In Rust, the standard library has many useful features, but only a small subset is brought into scope automatically through the prelude.
We create a mutable variable called guess
to store user input. The String::new()
function creates a new, empty string. The ::
syntax indicates that new
is an associated function of the String
typeâsimilar to a static method in other languages.
The stdin().read_line(&mut guess)
call gets input from the user and appends it to our string. The &
indicates that this argument is a reference, allowing multiple parts of your code to access the same data without copying it. References are immutable by default, so we write &mut guess
rather than &guess
to make it mutable.
Handling Errors
The read_line
method returns a Result
type, which is Rust's way of encoding error handling information. Result
is an enum with two variants: Ok
and Err
. If read_line
succeeds, it returns Ok
containing the number of bytes read. If it fails, it returns Err
containing error information.
The expect
method handles the Result
. If it's an Err
value, expect
will crash the program and display the message you provide. If it's Ok
, expect
returns the value that Ok
holds.
Let's test our program:
cargo run
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6
Perfect! We're successfully reading and displaying user input.
Generating a Random Number
Now we need to generate a random number. Rust's standard library doesn't include random number functionality, so we'll use the rand
crate. First, update your Cargo.toml
file:
[dependencies]
rand = "0.8.5"
When you run cargo build
, Cargo will download and compile the rand
crate and its dependencies. Cargo makes dependency management effortlessâyou simply declare what you need, and Cargo handles the rest.
Now let's update our code to generate a random number:
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
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}");
println!("The secret number was: {secret_number}");
}
We add use rand::Rng;
to bring the Rng
trait into scope. Traits define methods that types can implementâwe'll need this trait to use the random number generator methods.
The rand::thread_rng()
function gives us a random number generator that's local to the current thread and seeded by the operating system. The gen_range
method generates a random number in the specified range. The syntax 1..=100
means "from 1 to 100, inclusive."
Comparing Guess and Secret Number
Now we need to compare the user's guess with our secret number:
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..=100);
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please enter a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
We bring std::cmp::Ordering
into scope. Ordering
is an enum with variants Less
, Greater
, and Equal
âthe three possible outcomes when comparing two values.
The crucial line is let guess: u32 = guess.trim().parse().expect("Please enter a number!");
. This demonstrates Rust's concept of shadowingâwe're creating a new variable named guess
that shadows the previous one. This pattern is common when you want to convert a value from one type to another.
The trim
method removes whitespace from the beginning and end of the string, including the newline character that read_line
adds. The parse
method converts the string to a number. We specify the type as u32
(unsigned 32-bit integer) to match our secret number's type.
The match
expression compares our guess with the secret number using the cmp
method, which returns an Ordering
variant. The match
expression then executes the code associated with the matching pattern.
Adding the Game Loop
Our game currently only allows one guess. Let's add a loop to allow multiple attempts:
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..=100);
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 = guess.trim().parse().expect("Please enter a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
The loop
keyword creates an infinite loop. We add a break
statement when the user guesses correctly, which exits the loop and ends the program.
Handling Invalid Input
Our current program crashes if the user enters non-numeric input. Let's make it more robust by handling this gracefully:
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..=100);
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(_) => continue,
};
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Instead of using expect
to crash on invalid input, we use a match
expression to handle the Result
returned by parse
. If parsing succeeds (Ok
), we use the number. If it fails (Err
), we use continue
to skip to the next iteration of the loop and ask for another guess.
The underscore _
in Err(_)
is a catchall pattern that matches any error value, regardless of the specific error information.
The Complete Game
Let's test our finished game:
cargo run
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
50
You guessed: 50
Too big!
Please input your guess.
25
You guessed: 25
Too small!
Please input your guess.
37
You guessed: 37
Too big!
Please input your guess.
31
You guessed: 31
You win!
Perfect! Our guessing game is complete and handles all the scenarios we designed for.
What You've Learned
Through building this simple game, you've encountered many fundamental Rust concepts:
Variables and Mutability: You created both immutable bindings (secret_number
) and mutable ones (guess
), and used shadowing to convert between types.
Functions and Methods: You called associated functions like String::new()
and methods like .trim()
and .parse()
.
Control Flow: You used match
expressions for pattern matching and loop
for repetition, with break
and continue
for flow control.
Error Handling: You worked with Result
types and learned to handle both expected and unexpected errors gracefully.
External Crates: You added a dependency and used functionality from the rand
crate.
Types: You worked with strings, numbers, and learned about type conversion and inference.
This guessing game demonstrates how Rust encourages you to think about edge cases and handle errors explicitly. The compiler guided you toward writing robust code that handles invalid input gracefully rather than crashing.
As you continue learning Rust, you'll find that these patternsâexplicit error handling, pattern matching, and careful thinking about data flowâare central to writing reliable Rust programs. The habits you've developed in this simple game will serve you well as you tackle more complex projects.