Learning Rust 🦀: 05- Your first Rust game: "Guess The Number"
Tags: learning, rust, programming, development
Published: 2023-06-16 17:52:13

Yes! You have read it right; we are going to write our first game using just the basic knowledge covered in the last 4 articles. So let's begin.

>#### ⚠️ Remember!
You can find all the code snippets for this series in its [accompanying repo](https://github.com/FadyGrAb/rust-learning-series)  
If you don't want to install Rust locally, you can play with all the code of this series in the official [Rust Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021) that can be found on its official page. But for this one, you need a `cargo` installation on your machine (or a VM).
⚠️⚠️ The articles in this series are loosely following the contents of "The Rust Programming Language, 2nd Edition" by Steve Klabnik and Carol Nichols in a way that reflects my understanding from a Python developer's perspective.

⭐ I try to publish a new article every week (maybe more if the Rust gods 🙌 are generous 😁) so stay tuned 😉. I'll be posting "new articles updates" on my [LinkedIn](https://www.linkedin.com/in/fady-abdelmassih) and [Twitter](https://twitter.com/fadyGrAb).

## Table of Contents:
- [The game description and requirements](#the-game-description-and-requirements)
- [Create a new project](#create-a-new-project)
- [Processing user input](#processing-user-input)
- [Using external crates](#using-external-crates)
- [Generate a secret number](#generate-a-secret-number)
- [Guess and secret number comparison](#guess-and-secret-number-comparison)
- [Run the game continuously](#run-the-game-continuously)
- [Handling invalid input and properly quit the game](#handling-invalid-input-and-properly-quit-the-game)
- [Finish up and release the game](#finish-up-and-release-the-game)

### The game description and requirements:
"Guess the Number" is a simple guessing game where the game generates a `secret number` and the user tries to `guess` it.  
The game should tell the user if his/her guess is `too big` or `too small` and the user wins and the game quit if he/she guesses the secret number.  
User input should be checked for invalid non-numeric values and warn the user about that.  
The number of the users tries should be tracked and shown when the user wins.
The user can quit the game by typing `quit`.
The game should run continuously until either the user wins or quits. 

### Create a new project:
Like we've learned before, type the following to create a new `guess_the_number` project and change directory (cd) into it:
```bash
cargo new guess_the_number
cd guess_the_number
```
I'd like you to have a look at `cargo.toml` file that was generated for you with the new project:
```toml
# cargo.toml
[package]
name = "guess_the_number"
version = "0.1.0"
edition = "2021"

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

[dependencies]
```
For now, let's just say that `cargo.toml` serves as a place to describe your app and to list its dependencies (none for now). Think of it like the Python's `requirements.txt` that's used with `pip` like this `pip install -r requiremts.txt` but with more details.  
And like always, `cargo` has generated a "Hello, World" main function for us to start with.

### Processing user input:
Our first order of business is to get what the user enters from stdin. Type the following:
```rust
use std::io;

fn main() {
    println!("Welcome to: GUESS THE NUMBER game!");
    println!("Please input your guess ...");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Unable to parse input!");

    println!("Your guess is {guess}")
}
```
This is the first time in the series that we "import libraries" from the app. We do that with the `use` keyword. Here we imported the `io` library from the standard library called `std`.
```rust
use std::io;
```
Next, we need some place to hold the user's guess. Therefore, we have defined the `guess` variable:
```rust
let mut guess = String::new();
```
The `mut` keyword is not new but the `String::new()` is. Because we don't know what the user will write so `guess` can't be a string literal (remember? String literals are hardcoded in the code and known at compile time). We instead use the `String` type and it associate `new` function which creates an "empty" place in memory that can have unknown size.  
>⚠️ more on String type in the next article 😉

After that, we use the `io` library that we've imported and call its `stdin()` function that returns a "handle" to the StdIn that has a `read_line` function that reads the StdIn. What's interesting is what we pass to the `read_line` function, we pass a "mutable reference" to the `guess` variable so the function can change its value (mutable) but doesn't take ownership over it (reference).  
>🦀 New Rust Terminology: "_ownership_" is basically what makes Rust memory safety. I'll discuss it in detail in the next article but for now know that variables can't switch scopes back and forth in Rust by default. Here is the "main" scope and "read_line" function scope.

The last new part in this piece of code is the `.expect("Unable to parse input!")`. This is here because `read_line` doesn't return a value, it returns a `Result` enum (enumeration) which in this case has two variants (think of variants as values), `Ok` and `Err`. By default, if `Result` is `Ok`, the value that the user has entered is returned. Else if `Result` is `Err`, the program "panics" showing the defined error message.  
Go ahead and type `cargo run` and validate that what you enter is printed out.


### Using external crates:
Now we need a "random" secret number for the user to guess. This time we will need and external "crate" called `rand`.  
>🦀 New Rust Terminology: "_crate_" is Python's packages equivalent for Rust. Visit [crates.io](crates.io) to see all Rust's available crates.

To use crates in your apps, you list them in the `cargo.toml` file we talked about earlier.
```toml
[package]
name = "guess_the_number"
version = "0.1.0"
edition = "2021"

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

[dependencies]
rand = "0.8.5"
```
We will discuss Rust dependencies in a lot more detail in later articles. For now, know that you use `cargo.toml` to list your dependencies. Run your app now, this time you will see the following "crates" being downloaded and installed:

![dependencies](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fzn1wtr0vfd3oyuwxk7b.png) 
`cargo` knew your dependencies for `cargo.toml` and if you run your app again, you will notice that nothing is downloaded or installed again. Another thing worth noting is the `cargo.lock` file which resides in the project's root directory. `cargo` create this file to "lock" dependencies version to ensure consistent builds. If `cargo` finds `cargo.lock`, then it's used for dependencies listing instead of `cargo.toml`.

### Generate a secret number:
Now we use the `rand` crate that we listed in `cargo.toml` like we did with the `io` library earlier. Our code becomes this:
```rust
use rand::Rng;
use std::io;

fn main() {
    println!("Welcome to: GUESS THE NUMBER game!");

    // Generate secret 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("Unable to parse input!");

    println!("Your guess is {guess}");
}
```
The details of how the `rand` crate and its components work aren't important right now. All you need to know is it's used to generate a random number from 1 to 100 (inclusive) as denoted by the "=" before the high end in the range expression `1..=100`.  
Run the code and you will see a different secret number each time.

### Guess and secret number comparison:
We have the user's `guess` and the `secret_number` now, we need to compare them. In order to do that, we will import yet another standard library component called `Ordering` which is an `enum` with variants `Less`, `Greater` and `Equal` and we will get a glimpse at Rust's "pattern matching" using the `match` keyword.
But in Rust in order to compare variables, they must be of the same type. Till now in our code, `guess` is of `String` type and `secret_number` is of `i32` implicitly (default integer type) therefore any comparison between the two will result in the app panicing. In order to remedy that, we will have to make some changes with how we parse the `guess` variable:
```rust
use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Welcome to: GUESS THE NUMBER game!");

    // Generate secret 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("Unable to parse input!");

    // Shadowing
    let guess: u32 = guess
        .trim()
        .parse()
        .expect("Please input a number between 1 and 100");

    println!("Your guess is {guess}");

    // Pattern matching
    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small"),
        Ordering::Greater => println!("Too big"),
        Ordering::Equal => println!("You win!"),
    }
}
```
You will notice that we re-defined `guess` again. But this time as a `u32` type. This is called "Shadowing" in Rust.
>🦀 New Rust Terminology: "_shadowing_" is when you redefine variables inside the same scope in Rust. You can change the variable's type and mutability using shadowing. The old value is destroyed thought!

Now `guess` is `u32` and `secret_number` is infered by the Rust compiler as `u32` too and we can compare them (this is allowed in Rust). We used the `match` keyword to do pattern matching as we pass a reference to `secret_number` (Rememeber the `read_line` function and ownership?) to the `cmp` function associate on `u32` type then the rest is self-explanatory 😁.  
Run the code and try to get the three prints in the `match` block.

### Run the game continuously:
As you may have noticed, the app quits at any entered value and the game doesn't continue requesting the user's guess if he/she has given the wrong guess. To fix that, we will use `loop` and "break" the loop if the user has guessed right. Also, it's now a good time to track the user's tries count:
```rust
use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Welcome to: GUESS THE NUMBER game!");

    // Generate secret number
    let secret_number = rand::thread_rng().gen_range(1..=100);
    println!("The secret number is {secret_number}");

    let mut tries = 0;

    loop {
        println!("Please input your guess ...");

        let mut guess = String::new();
        tries += 1;

        io::stdin()
            .read_line(&mut guess)
            .expect("Unable to parse input!");

        // Shadowing
        let guess: u32 = guess
            .trim()
            .parse()
            .expect("Please input a number between 1 and 100");

        println!("Your guess is {guess}");

        // Pattern matching
        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small"),
            Ordering::Greater => println!("Too big"),
            Ordering::Equal => {
                println!("You win! Took you {tries} tries to guess the secret number!");
                break;
            }
        }
    }
}
```
Run the code now and validate that the app quits if the user has guessed right and the number of tries is shown.

### Handling invalid input and properly quit the game:
The game is now almost complete. We just have to do some user experience enhancements like handling invalid user input (non-numeric) and introduce a quitting mechanism if the user typed `quit`.
```rust
use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Welcome to: GUESS THE NUMBER game!");

    // Generate secret number
    let secret_number = rand::thread_rng().gen_range(1..=100);
    println!("The secret number is {secret_number}");

    let mut tries = 0;

    loop {
        println!("Please input your guess ...");

        let mut guess = String::new();
        tries += 1;

        io::stdin()
            .read_line(&mut guess)
            .expect("Unable to parse input!");

        // If the user input "quit", the game quits.
        if guess.trim().to_lowercase() == "quit" {
            break;
        }

        // Shadowing
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                println!("Please input a number between 1 and 100");
                continue;
            }
        };

        println!("Your guess is {guess}");

        // Pattern matching
        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small"),
            Ordering::Greater => println!("Too big"),
            Ordering::Equal => {
                println!("You win! Took you {tries} tries to guess the secret number!");
                break;
            }
        }
    }
}
```
For the `quit` part, we are using a simple `if` to check on `guess` after reading the stdin line. And we've modified the shadowing section of `guess` to use pattern matching too. If the `Result` enum returns `Ok`, return the number. Else if it returns, `Err`, print a warning message and continue the loop.

### Finish up and release the game:
To finish up, we will just delete the line where the secret number  is printed so the complete code will be:
```rust
use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Welcome to: GUESS THE NUMBER game!");

    // Generate secret number
    let secret_number = rand::thread_rng().gen_range(1..=100);

    let mut tries = 0;

    loop {
        println!("Please input your guess ...");

        let mut guess = String::new();
        tries += 1;

        io::stdin()
            .read_line(&mut guess)
            .expect("Unable to parse input!");

        // If the user input "quit", the game quits.
        if guess.trim().to_lowercase() == "quit" {
            break;
        }

        // Shadowing
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                println!("Please input a number between 1 and 100");
                continue;
            }
        };

        println!("Your guess is {guess}");

        // Pattern matching
        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small"),
            Ordering::Greater => println!("Too big"),
            Ordering::Equal => {
                println!("You win! Took you {tries} tries to guess the secret number!");
                break;
            }
        }
    }
}
```
Up until now, we type `cargo build` to build our app. This builds the app relatively quickly but forgoes some optimizations that would make the binary run faster. We can see the generated binary in `target/debug`.  
For release builds, we should type `cargo build --release`. You will notice that the build stage takes longer than usual (the optimizations we talked about) and now we will have the release binary in `target/release`.
>⚠️ The `--release` flag works also with `cargo run`

Go ahead and type the following:
```bash
cargo build --release
```
Then run the game directly without `cargo` as follows:
```bash
# Linux
./target/release/guess_the_number
```
And the game will run smoothly 😎. Try to guess the number from the first try!  
The next article, I'll start to discuss Rust specific features and characteristics namely "ownership". See you then 👋