Chapter 2. Programming my guessing Game

Note

This notes come from Official Rust Documentation

Guessing game's code


Here we are going to explain each part of this code, trying to be clear in each part of this.

Code


use std::io;

fn main() {
    println!("Guess the number!");

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

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Explanation of the first part. Printing the user input


Quote

If a type you want to use isn’t in the prelude, you have to bring that type into scope explicitly with a use statement. Using the std::io library provides you with a number of useful features, including the ability to accept user input.

Here, we will pass to see variables and how to store their values.

Storing values in variables

This is interesting, this is a new feature of Rust, here we are creating a variable, but there is another example of how to create/set a variable in Rust:

let apples = 5;

Here we create a variable called apples with value of 5. By default in Rust, variables are immutable, meaning that once we set to the variable a value, this value won't change. This will be seen in more detail in Chapter 3 Variables and Mutability in Rust.

let apples = 5; // immutable
let mut bannanas = 3; //mutable
Note

Comments in rust are with this: //

We can say that this complete line creates a new empty space for a string:

let mut guess = String::new();

Receiving User input

Quote

If we hadn’t imported the io module with use std::io; at the beginning of the program, we could still use the function by writing this function call as std::io::stdin. The stdin function returns an instance of [std::io::Stdin](file:///home/alan/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/share/doc/rust/html/std/io/struct.Stdin.html), which is a type that represents a handle to the standard input for your terminal.

In the part of .read_line() we call read_line method on the standard input handle to get the input from user. We passed &mut guess which is the string that will be modified taking the value from user input.

Quote

The full job of read_line is to take whatever the user types into standard input and append that into a string (without overwriting its contents), so we therefore pass that string as an argument. The string argument needs to be mutable so that the method can change the string’s content.

The part of the string as a param, &mut guess, we put &, in this way, we are calling a reference, we are talking about more references in Chapter 4. Understanding Ownership.

this argument is a reference, which gives you a way to let multiple parts of your code access one piece of data without needing to copy that data into memory multiple times.

This feature is one of the most important in Rust language, an advantage over other programming languages.

Handling Potential failure

Now we are going to see this line of code:

.expect("Failed to read line");

This line is part of the second line of code that we are seeing here. The entire line is this:

io::stdin().read_line(&mut guess).expect("Failed to read line");

This last line is in charge to prevent fails while the user enters the guess, if something is wrong, in the command line you will see Failed to read line.

Quote

As mentioned earlier, read_line puts whatever the user enters into the string we pass to it, but it also returns a Result value. [Result](file:///home/alan/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/share/doc/rust/html/std/result/enum.Result.html) is an [enumeration](file:///home/alan/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/share/doc/rust/html/book/ch06-00-enums.html), often called an enum, which is a type that can be in one of multiple possible states. We call each possible state a variant.

We will see more about enums in Chapter 6. Enums and Pattern Matching. Result has two possible type of variants, Ok and Err, Ok indicates that operation was successful, and Err means that failed, with some information about that. If the result type is an Ok, it will return the value that it holds and just show it.

If you do not use expect, Rust will warn you that you are missing a error handler method. In Chapter 9. Error Handling in Rust, we will see it.

Printing Values

Using println!, we are going to print the value of guess, to do this, we can use {} inside of "":

println!("You guessed: {guess}");

The {} works as a placeholder. When you want to print a variable, inside of that placeholder must be the name of your variable. Also, when you want printing a complete expression, for example, a sum, you can put empty brackets {} and after that, you have to put a coma and then you put the expression to be showed:

let x = 5;
let y = 10;

println!("x = {x} and y + 2 = {}", y + 2);

At this point, we have done the first part of our guessing game.

Second part. Generating a random number


The number will change each time that the game starts.

Increasing functionality with crates.

Rust does not have a rand library yet, but Rust team has provided a rand crate

Quote

Remember that a crate is a collection of Rust source code files. The project we’ve been building is a binary crate, which is an executable. The rand crate is a library crate, which contains code that is intended to be used in other programs and can’t be executed on its own.

Here is where cargo does its work, with cargo we can include dependencies from crates. To do that, we have to modify cargo.toml in dependencies section:

[dependencies]
rand = "0.8.5"

Everything that follows a heading, it's part of that section, until another section begins. In this case, we are going setting some crates in [dependencies] heading, where all external dependencies will be set. Here we set an specific version for rand. Cargo understands semantic versiong (also called SemVer). In this case "0.8.5" is a shorthand of ^0.8.5, this means that the version will be at least 0.8.5, below 0.9.0.

Quote

Cargo considers these versions to have public APIs compatible with version 0.8.5, and this specification ensures that you’ll get the latest patch release that will still compile with the code in this chapter. Any version 0.9.0 or greater is not guaranteed to have the same API as what the following examples use.

Now we have to re-build our project:

cargo build

This will fetch all dependencies specified in cargo.toml

Quote

You may see different version numbers (but they will all be compatible with the code, thanks to SemVer!) and different lines (depending on the operating system), and the lines may be in a different order.

When we include a external dependency, Cargo fetches the latest versions of everything that dependency needs from Crates.io is where people in Rust ecosystem post their open source projects for others to use.

The way that cargo handles changes is amazing. If you make trivial changes in your main.rs and compile/build your project, cargo only show the lines of compiling and finishing the compilation of guessing game, this is because Cargo knows that you already have the requests dependencies and it does not have to re build your project.

Ensuring Reproducible Builds

Rust ensures that you can rebuild the same artifact every time you or anyone else build your code. Cargo only will use the versions that you specified in Cargo.toml file.

In an specific scenario, we will suppose that rand release a new version 0.8.6 where a bunch of bugs have been fixed, but if you update, the code could broke. To revert this kind of situations, Cargo makes a file called Cargo.lock where you will find information about the versions that you used in the first build of your code.

Quote

When you build your project in the future, Cargo will see that the Cargo.lock file exists and will use the versions specified there rather than doing all the work of figuring out versions again.

In simple words, your project will remain at 0.8.5 until you specify in Cargo.toml.

Updating a crate to get a new version.

Cargo provides you the command update, this will ignore cargo.lock and will figure out all the latest versions that fit with your cargo.toml. Then, cargo will rewrite cargo.lock. Otherwise, cargo will only look for versions greater than 0.8.5 but less than 0.9.0.

Quote

If the rand crate has released the two new versions 0.8.6 and 0.999.0, you would see the following if you ran cargo update:

cargo update
Updating crates.io index
Locking 1 package to latest Rust 1.85.0 compatible version
Updating rand v0.8.5 -> v0.8.6 (available: v0.999.0)

At this moment, you will be able to see in your cargo.lock that the new version is 0.8.6. To use a version in the 0.999.x series you should have to specify it in cargo.toml:

[dependencies]
rand = "0.999.0"

Next time you update cargo, it will look for those requirements for the new version series that you specified in cargo.toml

Generating a random number

We will import:

use std::io;
use rand::Rng;

fn main() {
    println!("Guess the number!");
    
	let secret_number = rand::thread_rng().gen_range(1..=100);

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

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {guess}");
}

This library rand comes with Rng, it is a trait, this defines methods that random number generators use. We will see what is a trait later.

We will think about this line

let secret_number = rand::thread_rng().gen_range(1..=100)

The first part rand::thread_rng() is a function that gives us the random number generator, one that is local to the current thread of execution and is seeded by the operating system. The we call gen_range between 1 and 100: 1..=100, and this will generate a random number between 1 and 100.

Comparing guessed number with random number

Convert input to number

Note

First we notice that we are using the same name to this "new" value guess, but guess has already used in the empty string value.

This capability is called shadowing. If you are replacing or changing the type of a value, you can use the same name to replace that instead of forcing us to create, for example guess_str and guess

let mut guess = String::new();
// ...
let guess: u32 = guess.trim().parse().expect("Please type a number!");

trim removes any white space at the beginning and at the end.

trim method also deletes \n or \r\n.

After that we must to convert guess from string to u32 value. parse method convert the string guess to another type of data. We need to convert that to a numeric type.

To specify the conversion, we use : u32 after let guess:

let guess: u32 = ...

parse works only with characters that can be converted into numbers. So to prevent erros, in the same way that we read the value, we will use a expect method to prevent failures.

Match and cmp

We will import this. Ordering is an enum type. Its variants are three. These variants are possible outcomes when you compare two values.

use std::cmp::Ordering;

Comparing guessed number and define the level of proximity.

match guess.cmp(&secret_number) {
	Ordering::Less => println!("Too small!"),
	Ordering::Greater => println!("Too big!"),
	Ordering::Equal => println!("You win!"),
}

The method cmp can be called for any type that can be compared:

match guess.cmp(&secret_number) ...

Then return a variant of Ordering. We use match to return a value based on wich Ordering variant the code is taking:

{
	// Variant to take => Action to do
	Ordering::Less => println!("Too small!"),
	// ... 
}

Loop

This is one of the bucles available to use in Rust. loop is an infinite loop:

use std::io;

use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!");
    
	let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
	    println!("Please input your guess.");
	
	    let mut guess = String::new();
	    io::stdin()
	        .read_line(&mut guess)
	        .expect("Failed to read line");
	        
	        
	    let guess: u32 = guess.trim().parse().expect("Please type a number!");
	    
	    match guess.cmp(&secret_number) {
			Ordering::Less => println!("Too small!"),
			Ordering::Greater => println!("Too big!"),
			Ordering::Equal => {
				println!("You win!");
				break;
			},
		}
		
	    println!("You guessed: {guess}");
    }
}

This will give more chances to the user guessing the number.

Also here we use break in the moment user guess the random number.

Handling invalid input

        // --snip--

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

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

        println!("You guessed: {guess}");

        // --snip--

As well know, parse returns a Result type, which is an enum type with error and ok variants:

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

Here also we are using match expression instead of expect. If string is not able to be converted, it will match with the second arm Err which has and underscore (_), this means that catch_all value.

Complete code


use std::io;

use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!");
    
	let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
	    println!("Please input your guess.");
	
	    let mut guess = String::new();
	    io::stdin()
	        .read_line(&mut guess)
	        .expect("Failed to read line");
	        
	        
	    let guess: u32 = match guess.trim().parse() {
          Ok(num) => num,
          Err(_) => continue,
      };
	    
	    match guess.cmp(&secret_number) {
			Ordering::Less => println!("Too small!"),
			Ordering::Greater => println!("Too big!"),
			Ordering::Equal => {
				println!("You win!");
				break;
			},
		}
		
	    println!("You guessed: {guess}");
    }
}