BlogHome
Make a Back-End Number Guessing Game with Rust

Make a Back-End Number Guessing Game with Rust

Mar 1, 2021Updated on Mar 14, 2022

I started learning Rust a while back and in good faith I've decided to share the experience with the community.

This is a game for Rust beginner and pros alike but more so to the beginners as it provides a good starting point to practice and understand some few Rust concepts. I'm a Rust beginner so if I get it then you can too.

The Game

In this game the player gets to fill in a lower inclusive limit and and a higher exclusive limit of a range of numbers where the number to be guessed will be based on. The game should create a random secret_number which the player will be trying to guess afterwards.

Before creating the game you'll need to install Rust into your system.

To simplify setting up Rust projects we'll need the support of Cargo. So what's Cargo?

Cargo

Cargo is the Rust package manager. It downloads and compiles Rust package's dependencies, compiles packages, makes distributable packages and uploads them to crates.io, the Rust community’s package registry. As far as this game is concerned we'll use Cargo to manage our project, download and compile the project dependencies, in this case rand a crate we'll need to generate the game's secret_number. Well, a 'crate' is simply a Rust package.

Install Cargo so that you can be able to use it in the game. Read The Cargo Book if you need to learn more about it.

Start a New Cargo Package

After installation of both Rust and Cargo, open up your terminal and run the following command inside your projects folder:

# create new guessing-game package
$ cargo new guessing-game --bin

# then switch into the guessing-game directory
$ cd guessing-game

The above command will generate the Cargo configuration file, a src directory and a main.rs file inside it. The --bin flag that we pass on the cargo new command, tells Cargo that we intend to create a direct executable file as opposed to a library.

The resulting directory structure.

.
├── Cargo.toml
└── src
    └── main.rs

A breakdown of the project's template.

Cargo.toml: This is a Cargo configuration file that contains all the metadata that Cargo needs to compile the package. All the dependencies go below [dependencies]. These are the details inside the file:

[package]
name = "guessing-game"
version = "0.1.0"
authors = ["Your Name [email protected]>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

src/main.rs file: Cargo expects the Rust package's source files to be in the src directory leaving the package's root directory for README's, licence information and all other files not related to a package's code. The main.rs file is the package entry file containing the main() function:

fn main() {
    println!("Hello, world!");
}

fn: is used to declare functions in Rust, followed by the function's name. The main() function is the beginning of every Rust program. println!(): is a Rust macro that's used to print things to the terminal.

To see what this code results to run the following code on the package's root:

$ cargo run

# results to - Hello, world!

If everything was set up correctly you should see the - "Hello, world!" output on the terminal

Creating The Game

Let's proceed with creating our game. Add the following lines of code to the main.rs file:

use std::io;

fn main(){
    println!("'Guess The Number' Game!");
    println!("In this game you'll be guessing the secret number between a range of numbers, the lower being inclusive while the higher being exclusive. To start provide the lower and higher limits of the range of numbers.");
}

Since the game involves getting user input, we'll use the io prelude from Rust's standard library to handle this. Next We'll print on the console information about the game and instructions on how to play it.

    let mut _lower_limit = String::new();
    
    println!("Choose the lower limit (number > 0): ");

    io::stdin().read_line(&mut _lower_limit)
    .expect("Failed to read value");

    let _lower_limit: u32 = match _lower_limit.trim().parse() {
        Ok(num) => num,
        Err(_) => 1,
    };

In the above code, we are binding the variable _lower_limit to an empty string on the first line. The let statement in Rust enables us to bind patterns (left side of the let statement) to a value (right side of a let statement) as opposed to most languages where we assign values to a variable name on the left side, this means we can have a let statement like

  let (a, b, c) = (19, 16, 31);

which will result to a being 19, b being 16 and c being 31.

In Rust variable bindings are immutable, that is after we have created a variable binding let foo = bar; we can not change the value of foo unless we declare that the binding we are intending to create should be mutable by adding "mut" resulting to let mut foo = bar;. The String::new() above as provided by Rust's standard library provides an empty String type value to bind to _lower_limit.

Then we proceed to taking input from the terminal by using the .read_line() method called on the Stdin() handle which we get from the io::stdin() function. The read_line() method puts what is typed into the &mut _lower_limit String that we pass into it and returns an io::Result. Rust has a number of types name Result types in it's standard library and their purpose is to encode error handling information, also the types resulting values have methods defined on them.

For our io::Result type the expect() method which takes the value it's called on crashes the program displaying the message that's passed into it in case the value is unsuccessful. Without calling the expect() method the program will compile displaying a warning in case of an error and likely resulting in unwanted results.

In Rust we can shadow a previously assigned variable and this is helpful in a case where we probably need to change the type of a variable without the need to binding it into a new varible. This is demonstrated above where we initially binded _lower_limit as a String let mut _lower_limit = String::new(); then proceeded to shadowing it as an unsigned 32 bit integer with let _lower_limit: u32.

To make sure we bind the varible to a number since we'll need numbers to make a range of numbers (who thought?) we call on the initially String bound variable _lower_limit _lower_limit.trim().parse() where the trim() method trims the white spaces at the start and end of the String then call on it the parse() method which parses Strings into some kind of number where in this case we hint to it the type of number we want the bunding to be, which is an unsigned 32 bit integer - let _lower_limit: u32 = .

To make sure we have a valid number and not crash our program on the contrary we'll' bind a default number on _lower_limit if that's the case. And we achieve that by using a match statement.

    let _lower_limit: u32 = match _lower_limit.trim().parse() {
        Ok(num) => num,
        Err(_) => 1,
    };

If a number is returned after _lower_limit.trim().parse() we return that with the Ok(num) => num and in case of anything else we bind _lower_limit to our default number 1 instead of crashing the program.

We replicate the same process above for the _higher_limit.

    let mut _higher_limit = String::new();
    
    println!("Choose the higher limit (number > _lower_limit + 1): ");

    io::stdin().read_line(&mut _higher_limit)
    .expect("Failed to read value");

    let _higher_limit: u32 = match _higher_limit.trim().parse() {
        Ok(num) => num,
        Err(_) => 101,
    };

Next we generate our random number that is between the two values provided above. We do that by first adding the rand crate we talked about before in the Cargo.toml file under dependencies.

[dependencies]
rand = "0.8.4"

Run the Cargo check command.

cargo check

Cargo will download the needed dependencies and give us a status output.

We then intergrate the external crate by adding it at the start of our code and using it's rand::Rng trait.

extern crate rand;

use rand::Rng;
    let secret_number = rand::thread_rng().gen_range(_lower_limit.._higher_limit);

We use the rand::thread_rng() method which copies the random number generator local to the thread of execution our program is in and we call the gen_range() method provided by the rand::Rng trait which takes a range of numbers (numeric range created as start..end in Rust) and returns a number that is lower bound inclusive but exclusive on the upper bound.

We then proceed to the part where the player guesses the secret number. On this part the game loops the instructions until the player gets the correct number, that is in case the player doesn't guess the secret number we give them another opportunity to guess it again. On the process we bind the count of times they guessed the number to the variable _moves_made to be used later.

    let mut _moves_made: u32 = 0;

    loop {
        println!("Input guess: ");

        let mut _guess = String::new();

        io::stdin()
            .read_line(&mut _guess)
            .expect("Failed to read input!!!");

        let _guess: u32 = match _guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed the number: {}", _guess);

        _moves_made += 1;

        match _guess.cmp(&secret_number) {
            Ordering::Less  => println!("Too small!!"),
            Ordering::Greater  => println!("Too big!!"),
            Ordering::Equal  => {
                let total_moves = _higher_limit - _lower_limit;
                let total_moves_float = total_moves as f64;
                let moves_made_float = _moves_made as f64;
                let efficiency: f64 = ((total_moves_float - moves_made_float) / total_moves_float) * 100.00;
                println!("YOU HAVE WON!! \n Guesses Made: {}\n Possible Guesses: {}\n Efficiency: {}%" , _moves_made, total_moves, efficiency);
                break;
            }
        }
    }

To compare the numbers, in this case the player's input and the secret number we need to add another type into scope, the - std::cmp::Ordering;. We add the following at the start of our code.

use std::cmp::Ordering;

Since we need to compare two numbers, we call the cmp() method which compares the thing it's called upon to the refference of the thing one wants to compared it to var1.comp(&refferenceToVar2) returning the Ordering type. We use the match statement to compare the player's guess to the secret number and check the type of Ordering that is returned. If it is Ordering::Equal then we display the desired output and break the loop ending the game otherwise we notify the player whether the guess is above or below the secret number. This last part is specifically added to reduce the difficulty of the game, eliminating it kicks the game's difficulty up a notch.

Hopefully this is just the start on sharing what I learn and create on my Rust adventures, probably some of you will get to join in. Go ahead and conquer the terminal with Rust.