Rust is a powerful and modern programming language that offers safety and performance, making it a great choice for a wide range of applications. Here we'll take a hands-on approach to learning Rust by creating a simple guessing game. Through this project, we'll explore several common Rust concepts, such as `let`, `match`, methods, associated functions, and even external crates. This is a perfect starting point for beginners to get a taste of Rust and its syntax.
The Guessing Game
The guessing game we're going to create is a classic programming problem often used to teach the fundamentals of a new language. Here's how it works: the program generates a random integer between 1 and 100, and then it prompts the player to enter a guess. After the player's guess, the program will inform whether the guess is too low or too high. If the guess is correct, the game will print a congratulatory message and exit.
Setting Up a New Project
To get started, let's create a new Rust project for our guessing game. If you haven't already set up a projects directory, you can follow these steps to create one:
- Open your terminal or command prompt.
- Navigate to the directory where you want to create your Rust projects, or create a new directory for this purpose.
- Run the following commands to create a new project named "guessing_game" and change to its directory:
$ cargo new guessing_game
$ cd guessing_game
The `cargo new` command creates a new Rust project with the specified name ("guessing_game" in this case), and the `cd` command changes the current directory to the project's directory.
Project Structure
After running the above commands, you'll find a couple of important files and directories in your new project:
Cargo.toml
The `Cargo.toml` file is the configuration file for your Rust project. It specifies the project's name, version, and other metadata. Here's what it might look like:
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
# You can specify dependencies here, but for now, we don't have any.
[dependencies]
src/main.rs
The `src/main.rs` file is where you'll be writing all the code for your guessing game. By default, it contains a simple "Hello, world!" program:
fn main() {
println!("Hello, world!");
}
Running the Initial Program
Before we start implementing the guessing game, let's compile and run the initial "Hello, world!" program to ensure that our Rust environment is set up correctly. In your terminal, navigate to the project directory (where `Cargo.toml` and `src` directory are located), and run the following command:
$ cargo run
This command compiles and runs your Rust program. You should see the following output:
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Hello, world!
The `cargo run` command is convenient for quickly testing your code during development. It compiles and runs your program in a single step.
Now that we have our Rust environment set up and running, we can start implementing the guessing game in `src/main.rs`.
Processing a Guess in Rust: The Guessing Game
The first part of the guessing game program in Rust involves obtaining user input, processing it, and checking that the input is in the expected form.
To get started, we need to import the input/output (io) library from the standard library (std). This library allows us to interact with the user.
use std::io;
In Rust, a predefined set of items from the standard library is available by default, known as the prelude. These items are automatically in scope for every Rust program. When we need to use something outside of the prelude, like the io library, we explicitly bring it into scope with a `use` statement.
The entry point for any Rust program is the `main` function:
fn main() {
// ...
}
In the `main` function, the `println!` macro is used to print messages to the screen, providing information about the game and requesting user input:
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
}
Now, let's move on to handling user input. To store the user's guess, we create a mutable variable named `guess`:
let mut guess = String::new();
In Rust, variables are immutable by default, which means their values cannot be changed once assigned. To make a variable mutable, we use the `mut` keyword. This is important because we want to change the value of `guess` as the user enters their guess.
The `String::new()` function creates a new, empty instance of a `String`. The `String` type is provided by the standard library and represents a growable, UTF-8 encoded text.
The line `io::stdin().read_line(&mut guess)` is where we receive user input. It reads a line from the standard input (keyboard) and stores it in the `guess` variable:
Sure, here is a Rust snippet of the code you provided:
use std::io;
.read_line(&mut guess)
.expect("Failed to read line");
The `io::stdin()` call returns a `std::io::Stdin` instance, representing the standard input handle.
The `.read_line(&mut guess)` part calls the `read_line` method on the standard input handle. It reads user input and appends it to the `guess` string. The `&mut guess` argument is a mutable reference to the `guess` string, allowing the method to modify its content.
To handle potential errors while reading input, we use the `.expect("Failed to read line")` method. If an error occurs, this method will cause the program to crash and display the provided error message. Error handling is a crucial aspect of Rust programming, and more advanced error handling techniques are covered in later chapters.
If you don't use `.expect`, you will receive a warning when compiling the code, reminding you to handle potential errors properly:
warning: unused `Result` that must be used
To suppress this warning, you can add error-handling code, but for this simple guessing game, we choose to crash the program if an error occurs.
Finally, we print the user's guess using the `println!` macro and placeholders:
println!("You guessed: {}", guess);
The `{guess}` placeholder is replaced with the value of the `guess` variable. You can also use placeholders for expressions in the format string, as demonstrated earlier.
To test the first part of the guessing game, you can run it using `cargo run`:
$ cargo run
Guess the number!
Please input your guess.
6
You guessed: 6
The Importance of a Secret Number
A guessing game is only as exciting as the secret number that the player is trying to guess. To keep the game interesting for multiple rounds, it's essential to generate a new secret number each time. In our example, we will use random numbers between 1 and 100 to make the game challenging yet enjoyable.
Using the Rand Crate
Rust doesn't include random number generation functionality in its standard library. However, the Rust community provides various crates that extend Rust's capabilities. The rand crate is one such library that offers random number generation.
Adding rand as a Dependency
To use the rand crate in your Rust project, you need to add it as a dependency in your `Cargo.toml` file. The `Cargo.toml` file is where you manage external dependencies for your project. Open the `Cargo.toml` file and add the following line below the `[dependencies]` section:
[dependencies]
rand = "0.8.5"
This line tells Cargo to include the rand crate in your project, specifically version 0.8.5. It's essential to specify the version to ensure compatibility with your code. The semantic version specifier `"0.8.5"` means any version that is at least 0.8.5 but less than 0.9.0. This ensures that your code remains compatible with the examples in this tutorial.
Building the Project
After adding the rand crate as a dependency, you need to build your project using Cargo. Open your terminal and navigate to the project directory, then run:
$ cargo build
Cargo will automatically download the rand crate and any other dependencies needed to build your project. This process ensures that your project compiles successfully with the added crate.
Ensuring Reproducible Builds
Cargo has a mechanism for ensuring that your project's build is reproducible. When you build your project for the first time, Cargo creates a `Cargo.lock` file. This file contains a list of the versions of all dependencies, ensuring that your project remains consistent. This is crucial for reproducible builds and is often checked into source control along with your code.
If you need to update a crate in the future, you can use the `cargo update` command. Cargo will ignore the `Cargo.lock` file and figure out the latest versions that fit your specifications in the `Cargo.toml`. It will then update the `Cargo.lock` file accordingly.
Generating a Random Number
With the rand crate added as a dependency and the project successfully built, you can now use it to generate a random number for your guessing game. Open your `src/main.rs` file and make the following changes:
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);
}
Here's what these changes do:
- We import the `rand::Rng` trait to access the random number generation methods.
- We use `rand::thread_rng()` to get a random number generator specific to the current thread.
- We call `gen_range(1..=100)` to generate a random number between 1 and 100 (inclusive).
This sets up the secret number that the player needs to guess.
Running the Program
Now that your program is updated to generate a secret number, you can run it to test the functionality. Open your terminal and navigate to the project directory, then run:
$ cargo run
You should see the program printing a message to the console, displaying the secret number, and prompting you for a guess. The secret number will change every time you run the program, making the game unpredictable and exciting.
Comparing the Guess to the Secret Number
In the previous part of the Rust guessing game tutorial, we covered the basics of user input and generating a random number. Now, it's time to compare the user's guess with the secret number to determine if they've won.
The code for comparing the user's guess to the secret number is located in the `main` function of the Rust program. Let's break it down step by step.
Importing the Necessary Libraries
At the beginning of the Rust program, we add several `use` statements to bring in external libraries, namely `rand::Rng` and `std::cmp::Ordering`. These libraries are essential for generating random numbers and performing comparisons.
use rand::Rng;
use std::cmp::Ordering;
Comparing the Guess and Secret Number
Inside the `main` function, after obtaining the user's input, we use the `match` statement to compare the user's guess with the secret number. The `cmp` method compares two values and returns an `Ordering` enum, which can have one of three possible variants: `Less`, `Greater`, or `Equal`.
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
- If the user's guess is less than the secret number, it prints "Too small!".
- If the guess is greater than the secret number, it prints "Too big!".
- If the guess is equal to the secret number, it prints "You win!".
The `match` statement is a powerful feature in Rust that helps handle different cases or outcomes systematically. It evaluates the given value and executes the code block associated with the first matching arm. In our example, it will stop once a match is found, ensuring that only one message is printed.
Handling Type Mismatch Error
Before we proceed, it's important to address the type mismatch error that occurs in the initial code. The error arises because the user's guess is read as a `String`, while the secret number is an integer.
error[E0308]: mismatched types
--> src/main.rs:22:21
|
22 | match guess.cmp(&secret_number) {
| --- ^^^^^^^^^^^^^^ expected struct `String`, found integer
| |
| arguments to this function are incorrect
|
= note: expected reference `&String`
found reference `&{integer}`
To resolve this issue, we need to convert the user's input (a `String`) into an integer type (`u32`) that can be compared to the secret number.
let guess: u32 = guess.trim().parse().expect("Please type a number!");
- `guess.trim()` removes any leading or trailing whitespace from the user's input, which is important for accurate parsing.
- `parse()` attempts to convert the trimmed string into a numerical type.
- The type annotation `u32` specifies that we want the result to be an unsigned 32-bit integer.
- The `expect` method handles potential errors. If the conversion fails, it will crash the game and display the specified error message.
Allowing Multiple Guesses with Looping in Rust
In the previous part of our journey in creating a simple guessing game in Rust, we learned how to generate a secret number and give the user one chance to guess it. However, a single attempt might not be enough for some players, and that's where loops come into play. Here we will explore how to use a loop to provide users with multiple guesses and improve the game's overall functionality.
The Infinite Loop
In Rust, you can create an infinite loop using the `loop` keyword. By placing all the relevant code inside a loop, you can allow users to make guesses until they either win or decide to quit the game. Here's a snippet of what your code might look like:
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
println!("Please input your guess.");
// Read user input and handle it.
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
}
As you can see, everything from the guess input prompt onwards is now enclosed within a loop. When the user enters a guess, the program evaluates it and provides feedback. However, this introduces a new problem - the game seems impossible to quit.
Allowing the User to Quit
While users can always interrupt the program using the keyboard shortcut `ctrl-c`, there is a more user-friendly way to exit the game. We can leverage the error handling behavior related to parsing numbers to allow the user to quit gracefully.
If the user enters a non-number input, the program will crash with an error. We can use this to our advantage and let the user quit the game. Here's an example:
$ cargo run
Compiling guessing_game v0.1.0
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/main.rs:28:47
Typing "quit" will quit the game. However, it's worth noting that any non-numeric input will also cause the program to exit. This isn't ideal; we want the game to stop only when the correct number is guessed.
Quitting After a Correct Guess
To address this, we can program the game to quit when the user guesses the correct number. We achieve this by adding a `break` statement within the `Ordering::Equal` arm of our match expression. This `break` statement will exit the loop and, consequently, the program itself.
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
This change ensures that the game ends gracefully when the user correctly guesses the secret number.
Handling Invalid Input:
To further enhance the game's behavior, we can modify it to ignore non-numeric input rather than crashing the program. We make this adjustment by altering the line where the user's input is converted from a `String` to a `u32`. Here's how you can do it:
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}");
In this code, we switch from using `expect` to a `match` expression, which allows us to handle errors gracefully. The `parse` method returns a `Result` type, which is an enum with two variants: `Ok` and `Err`. Using the `match` expression, we can respond differently to each variant.
If `parse` successfully converts the user's input into a number, it returns an `Ok` value containing the numeric result. We match this `Ok` variant and use the parsed number for further processing.
If `parse` encounters an error, it returns an `Err` value, which contains information about the error. In this case, we use the `Err(_)` pattern, where the underscore `_` is a catchall value that matches all `Err` values, regardless of their content. By using `continue`, we instruct the program to ignore the error and proceed to the next iteration of the loop, allowing the user to make another guess.
With these changes, your guessing game is now more robust and user-friendly. Players can make multiple guesses until they either win or choose to quit. Invalid input no longer crashes the program, providing a smoother gaming experience. To further improve the game, you can consider adding features like keeping track of the number of attempts and giving hints to the player. Congratulations on successfully building your Rust guessing game!