Make a Back-End Number Guessing Game with Rust. poster image

Make a Back-End Number Guessing Game with Rust.

By James Sinkala.

Published on , updated on | 11 mins Read

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:

Run this script on the terminal!
# 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:

Run this script on the terminal!
$ 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. Start by printing on the console information about the game and instructions on how to play it.

Add the following lines of code to the main.rs file:

Copy this code!
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. Update the main.rs file to reflect the following.

Copy this code!
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.");
}

Let's get the first user input needed in the game.

Add this to your code!
    // continue from previous code block
    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.

Rust's let statement.

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 values are assigned 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.

</div>
Variable bindings in Rust.

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's Result types.

Rust has a number of types named Result in it's standard library and their purpose is to encode error handling information, the resulting values from these types 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 ensure that we have a valid number and not crash our program on user input we'll bind a default number on _lower_limit. And we can achieve that by with the help of 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.

Add this to your code!
    // continue from previous code block
    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.

Add this before the main() function!
// add this at the top
extern crate rand;

use rand::Rng;

Add this to your code!
    // continue from previous code block
    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 provide the number bound in 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.

Add this to your code!
    // continue from previous code block
    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.

Add this before the main() function!
// add this at the top
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.

The source code for this game is available in the following git repository.

The source code for this game is available in the following git repository.

A terminal number guessing game

James Sinkala

James is a Full Stack developer who loves sharing his knowledge with others in the form of technical writing.