Rust Tutorial 2: Let's make a Guessing Game!

khair_al_anam

Khair Alanam

Posted on September 21, 2023

Rust Tutorial 2: Let's make a Guessing Game!

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


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:
cargo new <Name of the project>
Enter fullscreen mode Exit fullscreen mode
  • Let's name our project "guessing_game" and run:
cargo new guessing_game
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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


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:

fn main() {
    let a = 2;
}
Enter fullscreen mode Exit fullscreen mode

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:

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);
}
Enter fullscreen mode Exit fullscreen mode

To execute the above program, run the command:

cargo run
Enter fullscreen mode Exit fullscreen mode

You will get the output:

Rust output

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:

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);
}
Enter fullscreen mode Exit fullscreen mode

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:

let hello: String = String::new("Hello World!");
Enter fullscreen mode Exit fullscreen mode

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


Control flow statements

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

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

fn main() {
    loop {
        println!("Hello world");
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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

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

cargo build
Enter fullscreen mode Exit fullscreen mode

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

Rust immutable variable error

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:

let mut a: i32 = 0;
Enter fullscreen mode Exit fullscreen mode

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.

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
}
Enter fullscreen mode Exit fullscreen mode

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!

fn main() {
    let secret_number: i32 = 34;
}
Enter fullscreen mode Exit fullscreen mode

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.

use std::io;

fn main() {
    let secret_number: i32 = 34;
}
Enter fullscreen mode Exit fullscreen mode

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

use std::io;

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

    let user_input: String = String::new();
}
Enter fullscreen mode Exit fullscreen mode

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:

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

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

    io::stdin().read_line(&user_input).expect("Some Input Error.");
}
Enter fullscreen mode Exit fullscreen mode

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.

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.");
}
Enter fullscreen mode Exit fullscreen mode

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:
rand = "0.8.5"
Enter fullscreen mode Exit fullscreen mode

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

Cargo.toml file

  • 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.

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.");
}
Enter fullscreen mode Exit fullscreen mode

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:

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.");
}
Enter fullscreen mode Exit fullscreen mode

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:

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

Now our current code will look like this:

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!");
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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

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:

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!");
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Guessing Game output


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

đź’– đź’Ş đź™… đźš©
khair_al_anam
Khair Alanam

Posted on September 21, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related