Building a number guessing game

You have learnt enough about Rust, now it's time to build your first Rust project. In this tutorial, we're going to build a simple number guessing game.

Here's how it works, the program will generate a random number between 1 and 100. The user will have to guess the number. If the user's guess is higher than the random number, the program will display "Too high!". If the user's guess is lower, the program will display "Too low!". If the user guesses the number correctly, the program will display "You win!".

The user will have a limited number of attempts to guess the number. If the user runs out of attempts, the program will display "You lose!".

Let's get started!

Learn Rust by Practice

Master Rust through hands-on coding exercises and real-world examples.

Setting up the project

First things first, let's set up the project. Create a new Rust project by running the following command:

cargo new number-guessing-game

Cargo new Cargo new

Navigate to the project directory, and open it in your favorite code editor:

cd number-guessing-game
 
code . # This will open the project in Visual Studio Code

Cargo automatically creates a folder with the name of the project and initializes a new Rust project inside it. The project structure should look like this:

.
└── number-guessing-game/
    ├── src/
    │   └── main.rs
    ├── .gitignore
    └── Cargo.toml

When you open your editor, Rust will automatically create a Cargo.lock file which is used to lock the dependencies of your project to specific versions.

.
└── number-guessing-game/
    ├── src/
    │   └── main.rs
    ├── .gitignore
    ├── Cargo.lock
    └── Cargo.toml

Writing the code

Now that we have our project set up, let's start writing the code.

Since this project is only a CLI project and we will not have a UI, we need to show the user some text to interact with the program and then we need to read the inputs from the user. For that matter we will need to use the Rust standard library.

The Rust standard library

The Rust standard library is the core of the Rust programming language. It provides a rich set of APIs for working with strings, files, networking, and more. The standard library is divided into modules, each of which provides a set of related functionality.

For this project, we will use the std::io module for the CLI interaction and the std::rand module to generate random numbers.

In order to import some code from the standard library (or any other external crate), we use the use keyword followed by the path to the module we want to import.

Let's start by importing the io module from the standard library, add this to the top of your main.rs file:

use std::io;
 
fn main() {
    println!("Number guessing game");
}

Generating a random number

We have explained functions in the previous chapters, now we have to create a function that generates a random number between 1 and 100.

In order to generate a random number, we will need to use another crate, but this one is external and we'll need to download it from crates.io.

Luckily Cargo provides a very convenient way to manage dependencies in Rust projects. We can add a dependency to our project by either adding it to the Cargo.toml file, or by running the cargo add <crate-name> command.

We will use the command line to add the rand crate to our project:

cargo add rand

Cargo add rand Cargo add rand

Cargo will automatically add the latest version and update the Cargo.toml file with the new dependency:

[package]
name = "number-guessing-game"
version = "0.1.0"
edition = "2021"
 
[dependencies]
rand = "0.8.5"

Now that we have the rand crate added to our project, we can use it to create our generate_random_number() function.

use rand::Rng;
 
fn generate_random_number() -> u32 {
  rand::thread_rng().gen_range(1..=100)
}

In this function, we use the rand::thread_rng() function to get a random number generator, and then call the gen_range() method on it to generate a random number between 1 and 100.

The gen_range() method accepts a range as an argument, you have seen a range before in the loops chapter when we were using it in a for loop.

This is the same thing, we are using the 1..=100 range to generate a random number between 1 and 100. The = means that the range is inclusive, so the number 100 is included in the range.

We can then use the function in the main() function to generate a random number.

fn main() {
    println!("Number guessing game");
 
    let random_number = generate_random_number();
 
    println!("Random number: {}", random_number);
}

Random number Random number

Reading user input

In order to read the user input we now need to use the std::io we imported earlier. We will create a function that reads the user input and returns it.

fn read_user_input() -> u32 {
    let mut user_input = String::new();
 
    io::stdin()
        .read_line(&mut user_input)
        .expect("Failed to read line");
 
    user_input.trim().parse().expect("Please type a number!")
}

In the function above, we are creating a mutable variable user_input of type String, at first this variable will be empty, but then it will be given to the read_line() method as a &mut which is a mutable reference to the variable.

We will cover references later, but all you need to know about references for now is that they allow you to get access to data in memory without taking ownership of it. The & symbol is used to create a reference to a variable, and the mut keyword is used to make it mutable. Together &mut means a mutable reference.

The read_line() method will mutate the value to the value that the user types in the console. The expect() method is used to handle errors, if the read_line() method fails, it will make the program panic and display the message "Failed to read line".

Panicking in Rust makes the program stop immediately and display an error message.

After that we are using the trim() method to remove any whitespace from the user input, and then we are using the parse() method to convert the String to a u32 (unsigned 32-bit integer).

You might be wondering, how does Rust know which data type should it parse the string to? There are many possible types that the string can be parsed to like i32, f32, f64, etc.

The answer is the explicit return type of the function. When we write -> u32 after the function name, we are telling Rust that this function will return a value of type u32. So when we call the parse() method, Rust will automatically know that we want to parse the string to a u32.

We can then use this function in the main() function to read the user input.

fn main() {
    println!("Number guessing game");
 
    let random_number = generate_random_number();
 
    let user_input = read_user_input();
 
    println!("You guessed: {}", user_input);
    println!("The random number is: {}", random_number);
}

User input User input

Great! Now we have the random number generated and the user input read, but the problem is that the user input is only being read once, we need to read the user input multiple times until either the user hits their guess limit or they guess the number correctly.

To do that, we can use a loop:

fn main() {
    println!("Number guessing game");
 
    let random_number = generate_random_number();
 
    loop {
        let user_input = read_user_input();
 
        println!("You guessed: {}", user_input);
    }
}

Great! Now we can read the user input as many times as we want, but we need to add some logic to compare the user input with the random number and display the appropriate message.

Comparing the user input with the random number

Now we need to compare the user input with the random generated number and print a message based on the comparison. To do that, let's create a new function called compare_numbers().

fn compare_numbers(user_input: u32, random_number: u32) -> String {
    if user_input > random_number {
        return String::from("Too high!");
    } else if user_input < random_number {
        return String::from("Too low!");
    } else {
        return String::from("You win!");
    }
}

In this function we are using control flow to compare the user input with the random number. If the user input is greater than the random number, we return a String with the message Too high!, if it's less, we return Too low!, and if it's equal, we return You win!.

Let's add this to the main() function:

fn main() {
    println!("Number guessing game");
 
    let random_number = generate_random_number();
 
    loop {
        let user_input = read_user_input();
 
        let text = compare_numbers(user_input, random_number);
 
        println!("{}", text);
    }
}

Guessing loop Guessing loop

Now, the program prints the right message based on the user input, but we need to add some more logic to handle the case when the user guesses the number correctly or when the user runs out of attempts.

Handling the game logic

Let's first add the limit to the number of tries a user can have. We will add a counter to the main() function and increment it every time the user makes a guess.

fn main() {
    println!("Number guessing game");
 
    let random_number = generate_random_number();
    let mut attempts = 0;
 
    loop {
        let user_input = read_user_input();
 
        let text = compare_numbers(user_input, random_number);
 
        println!("{}", text);
 
        attempts += 1;
 
        if attempts == 5 {
            println!("You lose! The number was: {}", random_number);
            break;
        }
    }
}

Here, we have added a few lines, one to create a mutable variable attempts and set it to 0, and then we increment it by 1 every time the user makes a guess.

After that, we added another if expression to check if the user has hit the limit of 5 attempts, if they have, we print "You lose!" and the random number, and then we break out of the loop.

Since the maximum number of attempts is a constant, we can actually create a global variable for it using the const keyword.

const MAX_ATTEMPTS: u32 = 5;
 
fn main() {
    ...
    if attempts == MAX_ATTEMPTS {
        println!("You lose! The number was: {}", random_number);
        break;
    }
    ...
}

Let's run the game and see how it works.

Max attempts Max attempts

Great! It works exactly as expected. The user has a limited number of attempts to guess the number, and if they run out of attempts, the program displays "You lose!" and reveals the random number.

Handling the win condition

Now we need to handle the win condition. If the user guesses the number correctly, we need to print "You win!" and break out of the loop.

fn main() {
    println!("Number guessing game");
 
    let random_number = generate_random_number();
    let mut attempts = 0;
 
    loop {
        let user_input = read_user_input();
 
        let text = compare_numbers(user_input, random_number);
 
        println!("{}", text);
 
        attempts += 1;
 
        if user_input == random_number {
            break;
        }
 
        if attempts == MAX_ATTEMPTS {
            println!("You lose! The number was: {}", random_number);
            break;
        }
    }
}

Win condition Win condition

As you can see, the program will exit automatically whenever the user guesses the number correctly.

Congratulations on building your first project with Rust! You have learned how to generate random numbers, read user input, and handle game logic. You can now expand on this project by adding more features, you can add a scoring system or give the user the ability to restart the game.

Here's the GitHub repository for the project, you can look at the code, run it, and even contribute to it.

Number Guessing Game


In the next lesson, we're going to learn about Ownership in Rust, the most unique feature of the Rust programming language.