Programming a Guessing Game
Read original on doc.rust-lang.orgLet’s jump into Rust by working through a hands-on project together! This
chapter introduces you to a few common Rust concepts by showing you how to use
them in a real program. You’ll learn about let, match, methods, associated
functions, external crates, and more!
In this project, you’ll implement a classic beginner programming problem: a guessing game. Here’s how it works: the program will generate a random integer between 1 and 100. It will then prompt the player to enter a guess. After a guess is entered, the program will indicate whether the guess is too low or too high. If the guess is correct, the game will print a congratulatory message and exit.
Setting Up a New Project
To set up a new project, go to your projects directory and use Cargo to create a new project:
$ cargo new guessing_game
$ cd guessing_game
Look at the generated Cargo.toml file:
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
[dependencies]
And the generated src/main.rs:
fn main() {
println!("Hello, world!");
}
Let’s compile and run this “Hello, world!” program:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Hello, world!
Processing a Guess
The first part of the guessing game program will ask for user input, process that input, and check that the input is in the expected form. To start, we’ll allow the player to input a guess. Enter the code in Listing 2-1 into src/main.rs.
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}");
}
Listing 2-1: Code that gets a guess from the user and prints it
Let’s go over this code in detail.
Importing the io Library
To obtain user input and then print the result as output, we need to bring the
io input/output library into scope. The io library comes from the standard
library, known as std:
use std::io;
By default, Rust has a set of items defined in the standard library that it brings into the scope of every program. This set is called the prelude, and you can see everything in it in the standard library documentation.
If a type you want to use isn’t in the prelude, you have to bring that type
into scope explicitly with a use statement.
Storing Values with Variables
Next, we create a variable to store the user input:
let mut guess = String::new();
In Rust, variables are immutable by default, meaning once we give the variable
a value, the value won’t change. To make a variable mutable, we add mut
before the variable name.
String::new() is a function that returns a new instance of a String.
String is a string type provided by the standard library that is a growable,
UTF-8 encoded bit of text.
The :: syntax in the ::new line indicates that new is an associated
function of the String type. An associated function is a function that’s
implemented on a type, in this case String.
Receiving User Input
We call stdin() from the io module to handle user input:
io::stdin()
.read_line(&mut guess)
The stdin function returns an instance of std::io::Stdin, which is a type
that represents a handle to the standard input for your terminal.
Next, .read_line(&mut guess) calls the read_line method on the standard
input handle to get input from the user. We’re passing &mut guess as the
argument to read_line to tell it what string to store the user input in.
The & indicates that this argument is a reference, which gives you a way to
let multiple parts of your code access one piece of data without needing to
copy that data into memory multiple times. References are immutable by default,
so we need to write &mut guess rather than &guess to make it mutable.
Handling Potential Failure with Result
The read_line method returns a Result value. Result is an enumeration,
often called an enum, which is a type that can be in one of multiple possible
states. We call each possible state a variant.
The purpose of Result types is to encode error-handling information.
Result’s variants are Ok and Err. The Ok variant indicates the
operation was successful, and inside Ok is the successfully generated value.
The Err variant means the operation failed, and Err contains information
about how or why the operation failed.
.expect("Failed to read line");
If this instance of Result is an Err value, expect will cause the program
to crash and display the message that you passed as an argument.
Printing Values with println! Placeholders
Let’s look at the output line:
println!("You guessed: {guess}");
This line prints the string that now contains the user’s input. The {}
set of curly brackets is a placeholder: think of {} as little crab pincers
that hold a value in place.
Testing the First Part
Let’s test the first part of the guessing game:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6
Generating a Secret Number
Next, we need to generate a secret number that the user will try to guess. The secret number should be different every time so the game is fun to play more than once. We’ll use a random number between 1 and 100 so the game isn’t too difficult.
Rust doesn’t yet include random number functionality in its standard library.
However, the Rust team does provide a rand crate with said functionality.
Using a Crate to Get More Functionality
Remember that a crate is a collection of Rust source code files. The project
we’ve been building is a binary crate, which is an executable. The rand
crate is a library crate, which contains code that is intended to be used in
other programs and can’t be executed on its own.
Before we can write code that uses rand, we need to modify the Cargo.toml
file to include the rand crate as a dependency:
[dependencies]
rand = "0.8.5"
Now, without changing any of the code, let’s build the project:
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
Downloaded libc v0.2.127
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.53s
Cargo fetches the latest versions of everything that dependency needs from the registry, which is a copy of data from Crates.io.
Generating a Random Number
Let’s update src/main.rs 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!("The secret number is: {secret_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}");
}
Listing 2-3: Adding code to generate a random number
First we add the line use rand::Rng;. The Rng trait defines methods that
random number generators implement, and this trait must be in scope for us to
use those methods.
We call the rand::thread_rng function that gives us the particular random
number generator we’re going to use: one that is local to the current thread
of execution and is seeded by the operating system. Then we call the
gen_range method on the random number generator. This method is defined by
the Rng trait that we brought into scope with the use rand::Rng; statement.
The gen_range method takes a range expression as an argument and generates a
random number in the range. The kind of range expression we’re using here takes
the form start..=end and is inclusive on the lower and upper bounds, so we
need to specify 1..=100 to request a number between 1 and 100.
Let’s run the program a couple of times:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.53s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5
You should get different random numbers, and they should all be numbers between 1 and 100.
Comparing the Guess to the Secret Number
Now that we have user input and a random number, we can compare them. That step is shown in Listing 2-4:
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
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 type 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!"),
}
}
Listing 2-4: Handling the possible return values of comparing two numbers
First we add another use statement, bringing a type called std::cmp::Ordering
into scope from the standard library. The Ordering type is another enum and
has the variants Less, Greater, and Equal. These are the three outcomes
that are possible when you compare two values.
Then we add five new lines at the bottom that use the Ordering type. The
cmp method compares two values and can be called on anything that can be
compared. It takes a reference to whatever you want to compare with.
We use a match expression to decide what to do next based on which variant of
Ordering was returned from the call to cmp with the values in guess and
secret_number.
But wait—we also need to convert the guess from a String to a number type
so we can compare it to secret_number:
let guess: u32 = guess.trim().parse().expect("Please type a number!");
We create a variable named guess. But wait, doesn’t the program already have
a variable named guess? It does, but helpfully Rust allows us to shadow the
previous value of guess with a new one. Shadowing lets us reuse the guess
variable name rather than forcing us to create two unique variables.
We bind this new variable to the expression guess.trim().parse(). The guess
in the expression refers to the original guess variable that contained the
input as a string. The trim method on a String instance will eliminate any
whitespace at the beginning and end, including the newline character. The
parse method on strings converts a string to another type. Here, we use it
to convert from a string to a number.
Let’s run the program now:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!
Allowing Multiple Guesses with Looping
The loop keyword creates an infinite loop. We’ll add a loop to give users
more chances at guessing the number:
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
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 type 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!"),
}
}
}
The user can now guess multiple times, but the loop never ends!
Quitting After a Correct Guess
Let’s program the game to quit when the user wins by adding a break statement:
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
Adding the break line after You win! makes the program exit the loop when
the user guesses the secret number correctly.
Handling Invalid Input
To further refine the game’s behavior, rather than crashing the program when the user inputs a non-number, let’s make the game ignore a non-number so the user can continue guessing:
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
We switch from an expect call to a match expression to move from crashing
on an error to handling the error. Remember that parse returns a Result
type, and Result is an enum that has the variants Ok and Err.
If parse is able to successfully turn the string into a number, it will
return an Ok value that contains the resulting number. That Ok value will
match the first arm’s pattern, and the match expression will just return the
num value.
If parse is not able to turn the string into a number, it will return an
Err value. The Err value does not match the Ok(num) pattern but does
match the Err(_) pattern. The underscore, _, is a catchall value. The
continue tells the program to go to the next iteration of the loop and ask
for another guess.
The Complete Game
Here’s the final, working code for the guessing game:
use std::cmp::Ordering;
use std::io;
use rand::Rng;
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;
}
}
}
}
Listing 2-6: Complete guessing game code
Let’s play:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!
Summary
This project was a hands-on way to introduce you to many new Rust concepts:
let, match, functions, the use of external crates, and more. In the next
few chapters, you’ll learn about these concepts in more detail. Chapter 3
covers concepts that most programming languages have, such as variables, data
types, and functions, and shows how to use them in Rust. Chapter 4 explores
ownership, a feature that makes Rust different from other languages. Chapter 5
discusses structs and method syntax, and Chapter 6 explains how enums work.