Rust Tutorial 2: Let's make a Guessing Game!
Tags: programming, rust, tutorial
Published: 2023-09-21 07:07:36

_Reading time: 20 minutes_

Welcome back to the Rust Tutorial series!

In this tutorial, I will be programming a simple guessing game! On the way, we will learn basic concepts like variables, loops, control statements, etc.

As promised in the previous tutorial, we will use Rust's built-in tool called Cargo to make our game.

Also, I will be referring to some chapters from the official Rust documentation to get the basics done. However, I will be building projects in each tutorial based on the concepts learned.

So let's get started!

![Rust programmer humor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2jrmz2qd5o6rclno5vvy.png)

---

## Building with Cargo

To the web developers out there, Cargo is basically npm in Rust. It's a package manager as well as a build tool.

* To create our guessing game project, we have to run the below command:

``` shell
cargo new <Name of the project>
```

* Let's name our project "guessing_game" and run:

``` shell
cargo new guessing_game
```

* Now open the `guessing_game` folder in VS Code.

* On opening the folder, you will notice that the folder structure is dead simple. It's just a `src` folder with a `main.rs` file and a `Cargo.toml` file.

```
src
-> main.rs

Cargo.toml
```

The `main.rs` file just contains a simple "Hello World" program. Cargo.toml is basically package.json in web development projects. It just lists out any dependencies and packages used in the project.

![Content of Cargo.toml file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rshr2yovl62tfzyvzq80.png)

---

## Variables in Rust

### `let` and `const`

Like JavaScript, we use the `let` keyword to declare a Rust variable. There is also a `const` keyword, like JavaScript, but you will be using `let` keyword for 99.9% of the time.

Example:

```rust
fn main() {
    let a = 2;
}
```

Let's declare some more variables and print them in our terminal. But how do you print them?

Rust uses formatting using `{}` to print the variables. For example:

```rust
fn main() {
    let a = 2;
    let b = 3;
    let c = 4;

    println!("a is {}", a);
    println!("b is {}", b);
    println!("c is {}", c);
    println!("a, b, c are {}, {} and {} respectively", a, b, c);
}
```

To execute the above program, run the command:

``` shell
cargo run
```

You will get the output:

![Rust output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ue852w1gyuhrrrrkjl5s.png)

You will also notice that after running the `cargo run` command, we got extra files in our directory:
* `Cargo.lock`: It's more or less similar to package-lock.json in web development projects.
* Then, we have the `target` folder where the executable file is stored. If you open the `debug` folder in the `target` folder, you will see a lot of files related to the project build files plus the executable file.

In the end, you don't have to worry about anything inside the `target` folder. All you need to focus on is everything inside the `src` folder.

---

### Using different data and datatypes

Rust is a strongly typed static programming language. It means that once the variable has data of some datatype like numbers, strings, etc., that variable can only store data of that datatype.

In Rust, it's essential to declare the variable along with its datatype. However, Rust can infer the datatype from the data you've given for the variable.

Let's get to know the different datatypes:

``` rust
fn main() {
    // integers

    let a: u8 = 2;                                // numbers from 0 - 255
    let b: i8 = -34;                              // numbers from -128 - 127
    let c: u16 = 1000;                            // numbers from 0 - 65535
    let d: i16 = -20000;                          // numbers from -32768 - 32767
    let e: u32 = 1000000;                         // numbers from 0 - 4294967295
    let f: i32 = -5000000;                        // numbers from -2147483648 - 2147483647
    let g: u64 = 10000000000;                     // numbers from 0 - 18446744073709551615
    let h: i64 = -80000000000;                    // numbers from -9223372036854775808 - 9223372036854775807
    let i: u128 = 1000000000000000000000000;      // numbers from 0 - 340282366920938463463374607431768211455
    let j: i128 = -9000000000000000000000000;     // numbers from -170141183460469231731687303715884105728 - 170141183460469231731687303715884105727

    // floating-points
    let x: f32 = 3.14;                            // 32-bit floating-point
    let y: f64 = 2.71828182845;                   // 64-bit floating-point

    // boolean types
    let is_true: bool = true;                     // Boolean type (true)
    let is_false: bool = false;                   // Boolean type (false)

    // single character type
    let ch: char = 'A';                           // Character type

    // strings
    let str1: &str = "Hello, world!";             // String slice
    let str2: String = String::from("Hello");     // String object

    println!("u8: {}", a);
    println!("i8: {}", b);
    println!("u16: {}", c);
    println!("i16: {}", d);
    println!("u32: {}", e);
    println!("i32: {}", f);
    println!("u64: {}", g);
    println!("i64: {}", h);
    println!("u128: {}", i);
    println!("i128: {}", j);

    println!("f32: {}", x);
    println!("f64: {}", y);

    println!("bool (true): {}", is_true);
    println!("bool (false): {}", is_false);

    println!("char: {}", ch);

    println!("&str: {}", str1);
    println!("String: {}", str2);
}
```

---

#### Integers

There are mainly two types of integer data types.
1. Unsigned Integers (Can represent only positive integers including 0)
2. Signed Integers (Can represent both negative and positive integers)

Unsigned integer datatypes include: `u8`, `u16`, `u32`, `u64`, and `u128`.
Signed integer datatypes include: `i8`, `i16`, `i32`, `i64`, and `i128`.

I am not going to explain each data type from integers. But I will explain `u8` and `i8`, while the rest can be inferred from these two.

Variables with the `u8` type imply that such variables can store 2^8 = 256 unique positive integers from 0 to 255. Whereas for `i8`, you can represent 256 unique integers like `u8`, but we have negative integers too. Variables of the `i8` type can store values from -128 to 127.

Similarly, `u16` and `i16` can represent 2^16 = 65536 unique integers, 2^32 = 4294967296 unique integers for `u32` and `i32`, and so on.

So, you may be wondering, which datatype should I choose for integers?

The answer is `i32`. Most of the time, when dealing with integers, they would be in the range of `i32`. Even Rust considers the default integer type as `i32` when inferring from integer data.

---

#### Floating-points

There are only two datatypes; `f32` and `f64`. You can represent any decimal number using floating points.

As for which datatype to use, use `f64`. It's also the default floating-point datatype.

---

#### Booleans

Booleans represent the values `true` and `false` and are represented by the `bool` keyword.

---

#### Character types

The `char` datatype represents any single character enclosed in single or double quotation marks. For example, `'a'`, `'w'`, `"1"`, `"?"`, etc. Variables with a `char` type can store exactly one character and not more or less than that. Storing more or less than one character for the `char` type will throw an error.

---

#### Strings

This can get a bit tricky because both `&str` and `String` are related to each other and very similar. I will differentiate them here:

|        &str        |        String        |
| :-- | :-- |
| Cannot change size | Can change size      |
| Used for read-only operations | Used for both read and write operations|
| Variables of this type are string slices of some existing string. | Variables of this type are actually string objects and can be manipulated by any string operations. |

Anyways, which one to use? Just use `String`. But remember, unlike other datatypes, it's important to declare a string using `String::new()` like the code snippet below along with the `String` datatype:

``` rust
let hello: String = String::new("Hello World!");
```

Here, `String` in `String::new()` is a library, and the `new()` is the function to declare a string object. As for the double colons `::`, it's similar to double-colon notation in C++.


![Rust meme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h48s0cvzs7b5f2cfdo0d.jpg)

---

### Control flow statements

The if-else condition statements in Rust are just as identical as those of other programming languages.

``` rust
fn main() {
    let a: i32 = 15;

    if a == 10 {
        println!("It's 10");
    } else if a == 20 {
        println!("It's 20");
    } else if a == 15 {
        println!("It's 15");
    } else {
        println!("Some number");
    }
}
```

Notice that when mentioning conditions, you don't need to enclose the condition in brackets `()` similar to Python.

---

### Loops

Surprisingly, there are five types of loops in Rust!
1. `loop` expression
2. `while` expression
3. `while let` expression
4. `for` expression
5. Labeled block expression

We will know these loops in the coming tutorials while building some projects. But for this guessing game, let's learn the `loop` expression.

As the name suggests, a `loop` expression is a block that loops the execution of the statements inside the `loop` indefinitely.

``` rust
fn main() {
    loop {
        println!("Hello world");
    }
}
```

That's the syntax. Look how simple it is! This program will print "Hello world" for eternity :)

You can break out of the loop by using a `break` statement.

Now for practice, let's write the code given below:

``` rust
fn main() {
    let a: i32 = 0;
    loop {
        if a == 5 {
            break;
        }
        println!("Hello world");
        a += 1;
    }
}
```

Now, try to compile without executing the code by running the below command:

``` shell
cargo build
```

When you run the command, you will get the below error:

![Rust immutable variable error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s5xl9l7fopd2jjg4siec.png)

This is because Rust variables are immutable by default, despite using the `let` keyword. To make it mutable, use the `mut` keyword before the variable declaration like this:

``` rust
let mut a: i32 = 0;
```

Now you can run the code using `cargo run`, and you'll see "Hello world" printed five times.

A quick tip: You can change the value of the variable without the `mut` keyword just by re-declaring the same variable with the `let` keyword and then assigning the new value. This concept is called "Shadowing" in Rust.

You can also use the same variable to assign a new value with a different datatype by shadowing. We will see this concept when we are making our guessing game.

``` rust
fn main() {
    let a: i32 = 4; // 'a' is of integer type
    println!("{}", a); // prints 4

    let a: f64 = 4.5; // 'a' becomes floating point here
    println!("{}", a); // prints 4.5
}
```

---

## Let's make the Guessing Game!

So, let's get started!

First off, we need a secret number for our guess. Let's declare one!

``` rust
fn main() {
    let secret_number: i32 = 34;
}
```

Now, we need user input to guess. But how?

For that, we need to use a library called `std::io`. We can import any library in Rust using the `use` keyword.

``` rust
use std::io;

fn main() {
    let secret_number: i32 = 34;
}
```

To get the user input, we need a variable to store it first. So let's declare a variable called `user_input`.


``` rust
use std::io;

fn main() {
    let secret_number: i32 = 34;

    let user_input: String = String::new();
}
```

Wait, why is the user input a string and not a number?

Well, it's because, by default, all the inputs coming from the user are stored as strings, including numbers. Hence, we declare a string variable. But there's a way to convert a string input to an input of type integer or float as long as the string is a number.

Now, here's the confusing part to get the input from the user:

After you have declared a string variable `user_input`, go to the next line.
* Then, type `io` to use the `std::io` library.
* Then, attach the `stdin()` function using double colon notation so that Rust can access the input device.
* Then, attach the `read_line()` function using simple dot notation to read the input from the user.
* The `read_line()` accepts the "reference" of the variable in which you are going to store the user input, i.e., `user_input`. The reference of the variable can be taken using the `&` notation. As for why we are using `&`, it's part of the memory management in Rust which I will cover in a later tutorial. For now, just follow the tutorial.
* And finally, it could be possible that some error could come due to some erroneous input. Therefore, we have to finally attach one more function called the `expect()` function to handle such errors.
* `expect()` function accepts a string parameter to print some error message.

After all this confusing stuff, your final code should look like this:

``` rust
fn main() {
    let secret_number: i32 = 34;

    let user_input: String = String::new();

    io::stdin().read_line(&user_input).expect("Some Input Error.");
}
```

It's confusing but will become easier once it's used frequently.

Also, if you are using VS Code, you may get an error in the third line stating that it has to use the `mut` keyword. That's because when it comes to inputs, they have to be mutable. So you must use the `mut` keyword in both the declaration and the read_line function.

``` rust
fn main() {
    let secret_number: i32 = 34;

    let mut user_input: String = String::new();

    io::stdin().read_line(&mut user_input).expect("Some Input Error.");
}
```

---

### Let's use randomness!

Notice that our secret number is set to 34. But, we want a random number every time we start playing a new game. To generate random numbers, we have to import a new library called `rand`.

However, this library is not built into Rust and has to be installed in our project.

To install `rand`:
* Open `Cargo.toml`.
* Under the `[dependencies]` tab, type this:

``` toml
rand = "0.8.5"
```

Basically, we are adding a new dependency called `rand` which as of writing now, is currently in version 0.8.5.

![Cargo.toml file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/38tt1syvpvyntbj6y8hp.png)

* Then finally, run the `cargo build` command.

After this, you will have `rand` installed.

Next, let's import `rand` using the `use` keyword. Plus, we will be specifically importing the `Rng` trait to generate random numbers.

What are traits? They are basically abstract classes in Rust. I will discuss these in a later tutorial.

``` rust
use std::io;
use rand::Rng;

fn main() {
    let secret_number: i32 = 34;

    println!("Guess the number between 1 and 100: ")
    let mut user_input: String = String::new();

    io::stdin().read_line(&mut user_input).expect("Some Input Error.");
}
```

Now, let's remove the `secret_number` value and replace it with a random number. To do that, we will have to type a long line just like we did to get the user input:

* Type `rand` in the `secret_number` expression.
* Then using double colon notation, attach the `thread_rng()` function.
* Then using dot notation, attach the `gen_range()` function.
* `gen_range` accepts a range of values in which a random number can be selected.

Before I finish adding the code for random number generation, it's important to know that Rust has a weird but unique way of declaring ranges of numbers.

---

In Python, you may have used the `range()` function to generate a range of numbers. Something like `range(1, 101)` to generate all the numbers between 1 and 101 (101 is not included).

But in Rust, you use this unique "double dot" notation to generate a range of numbers.
For example, to generate numbers between 1 to 10. You have to write like this: `1..11`.
The number at the right end is exclusive, hence you will have numbers from 1 upto and including 10.

For 1 to 100, it would be `1..101`.

But let's say you want to include the right end as well. For example, you want a range of numbers between 1 to 6 with 6 included. Then you can write the range as `1..=6`.

For 1 to 100, you can write it as `1..=100`.

Now, let's pick up where we have left off.

---

So remember that our `gen_range()` function needs a range of numbers as a parameter. And we need the user to guess a number between 1 to 100. So, we will have `1..101` or `1..=100` as our range.

So the final code for our random number generation looks like this:

``` rust
use std::io;
use rand::Rng;

fn main() {
    let secret_number: i32 = rand::thread_rng().gen_range(1..101);

    println!("Guess the number between 1 and 100: ");
    let mut user_input: String = String::new();

    io::stdin().read_line(&mut user_input).expect("Some Input Error.");
}
```

---

### Comparing the input guess and the secret number

Now it's time to see if the user input and the secret number are the same. To do that, we can set some "if" conditions to see if the given input is greater than, less than, or equal to the secret number.

Here's the code for the conditional block:

``` rust
if user_input < secret_number {
    println!("Too small!");
} else if user_input > secret_number {
    println!("Too big!");
} else {
    println!("You guessed it!");
}
```

Now our current code will look like this:

``` rust
use std::io;
use rand::Rng;

fn main() {
    let secret_number: i32 = rand::thread_rng().gen_range(1..101);

    println!("Guess the number between 1 and 100: ");
    let mut user_input: String = String::new();

    io::stdin().read_line(&mut user_input).expect("Some Input Error.");

    if user_input < secret_number {
        println!("Too small!");
    } else if user_input > secret_number {
        println!("Too big!");
    } else {
        println!("You guessed it!");
    }
}
```

Now, you will probably get the error regarding the mismatched types in the conditional block. That's because the `user_input` is a string while the `secret_number` is an integer.

To fix that, we will have to parse our `user_input` into an integer. Here's how:

``` rust
let user_input: i32 = user_input.trim().parse().expect("Not an integer");
```
Let me explain:

Here, we re-declare the `user_input` variable to be of integer type by the concept of shadowing. Then to get rid of any trailing spaces, we use the `trim()` function. Then we use the `parse()` function to parse the input (which is of string type) to any datatype we want to convert to, which is `i32` in this case. Then in case of any error, we use the `expect()` function with a custom error message.

Our code will now look like this:

``` rust
use std::io;
use rand::Rng;

fn main() {
    let secret_number: i32 = rand::thread_rng().gen_range(1..101);

    println!("Guess the number between 1 and 100: ");
    let mut user_input: String = String::new();

    io::stdin().read_line(&mut user_input).expect("Some Input Error.");

    let user_input: i32 = user_input.trim().parse().expect("Not an integer");

    if user_input < secret_number {
        println!("Too small!");
    } else if user_input > secret_number {
        println!("Too big!");
    } else {
        println!("You guessed it!");
    }
}
```

You can now run this program and play the guessing game!

However, you will notice that the game only plays once and then stops.

So, we need to run this program until we guess the secret number.

---

### Loop the program

All we need to do here is to add a `loop` expression with a condition to break out of the loop. The loop has to break once we guess the number right.

Just wrap the guessing logic under a `loop` expression and then modify the `else` clause to include a `break` statement.

Here's the final code:

``` rust
use std::io;
use rand::Rng;

fn main() {
    let secret_number: i32 = rand::thread_rng().gen_range(1..101);

    loop {
        println!("Guess the number between 1 and 100: ");
        let mut user_input: String = String::new();

        io::stdin().read_line(&mut user_input).expect("Some Input Error.");

        let user_input: i32 = user_input.trim().parse().expect("Not an integer");

        if user_input < secret_number {
            println!("Too small!");
        } else if user_input > secret_number {
            println!("Too big!");
        } else {
            println!("You guessed it! It's {}!", secret_number);
            break;
        }
    }
}
```

Now run the code using `cargo run` and you are good to go!


![Guessing Game output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pcsi2prn2nwg60p5gita.png)

---

Congratulations! You have made a really solid program that covers the major Rust basics including variables, datatypes, user input, loops, etc.

For the next tutorial, we will be improving this guessing game to handle errors as well as learn some new concepts on the way. I could cover this here but since this tutorial is getting long, I think it's better to keep it as a separate short tutorial.

Anyways, I hope you have learned a lot in this tutorial!

Have an awesome day ahead!

GitHub Repo: https://github.com/khairalanam/rust-guessing-game

If you like whatever I write here, follow me on Devto and check out my socials:

LinkedIn: https://www.linkedin.com/in/khair-alanam-b27b69221/
Twitter: https://www.twitter.com/khair_alanam
GitHub: https://github.com/khairalanam