Published on

Chapter 2: Programming Guessing Game

11 min read

Authors
banner

Programming a Guessing Game 🦀

There's something magical about writing your first real program in a new language. In Rust, we'll build a classic guessing game that introduces you to essential concepts while creating something genuinely fun. This project will teach you about input/output, variables, control flow, and error handling—all the building blocks you need for Rust programming.

Our game is simple: the program generates a random number between 1 and 100, and you try to guess it. After each guess, the program tells you whether your guess was too high or too low. When you guess correctly, the game congratulates you and exits.

Setting Up the Project

Let's start by creating a new Rust project:

cargo new guessing_game
cd guessing_game

Open src/main.rs and you'll see the familiar "Hello, world!" program. We'll replace this with our guessing game code.

Processing User Input

First, let's handle getting input from the user:

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}");
}

This code introduces several important concepts. The use std::io; line brings the input/output library into scope. In Rust, the standard library has many useful features, but only a small subset is brought into scope automatically through the prelude.

We create a mutable variable called guess to store user input. The String::new() function creates a new, empty string. The :: syntax indicates that new is an associated function of the String type—similar to a static method in other languages.

The stdin().read_line(&mut guess) call gets input from the user and appends it to our string. The & indicates that this argument is a reference, allowing multiple parts of your code to access the same data without copying it. References are immutable by default, so we write &mut guess rather than &guess to make it mutable.

Handling Errors

The read_line method returns a Result type, which is Rust's way of encoding error handling information. Result is an enum with two variants: Ok and Err. If read_line succeeds, it returns Ok containing the number of bytes read. If it fails, it returns Err containing error information.

The expect method handles the Result. If it's an Err value, expect will crash the program and display the message you provide. If it's Ok, expect returns the value that Ok holds.

Let's test our program:

cargo run
$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

Perfect! We're successfully reading and displaying user input.

Generating a Random Number

Now we need to generate a random number. Rust's standard library doesn't include random number functionality, so we'll use the rand crate. First, update your Cargo.toml file:

[dependencies]
rand = "0.8.5"

When you run cargo build, Cargo will download and compile the rand crate and its dependencies. Cargo makes dependency management effortless—you simply declare what you need, and Cargo handles the rest.

Now let's update our code to generate a random number:

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}");
    println!("The secret number was: {secret_number}");
}

We add use rand::Rng; to bring the Rng trait into scope. Traits define methods that types can implement—we'll need this trait to use the random number generator methods.

The rand::thread_rng() function gives us a random number generator that's local to the current thread and seeded by the operating system. The gen_range method generates a random number in the specified range. The syntax 1..=100 means "from 1 to 100, inclusive."

Comparing Guess and Secret Number

Now we need to compare the user's guess with our secret number:

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

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");

    let guess: u32 = guess.trim().parse().expect("Please enter a number!");

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

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

We bring std::cmp::Ordering into scope. Ordering is an enum with variants Less, Greater, and Equal—the three possible outcomes when comparing two values.

The crucial line is let guess: u32 = guess.trim().parse().expect("Please enter a number!");. This demonstrates Rust's concept of shadowing—we're creating a new variable named guess that shadows the previous one. This pattern is common when you want to convert a value from one type to another.

The trim method removes whitespace from the beginning and end of the string, including the newline character that read_line adds. The parse method converts the string to a number. We specify the type as u32 (unsigned 32-bit integer) to match our secret number's type.

The match expression compares our guess with the secret number using the cmp method, which returns an Ordering variant. The match expression then executes the code associated with the matching pattern.

Adding the Game Loop

Our game currently only allows one guess. Let's add a loop to allow multiple attempts:

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

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 enter a number!");

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

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

The loop keyword creates an infinite loop. We add a break statement when the user guesses correctly, which exits the loop and ends the program.

Handling Invalid Input

Our current program crashes if the user enters non-numeric input. Let's make it more robust by handling this gracefully:

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

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,
        };

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

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

Instead of using expect to crash on invalid input, we use a match expression to handle the Result returned by parse. If parsing succeeds (Ok), we use the number. If it fails (Err), we use continue to skip to the next iteration of the loop and ask for another guess.

The underscore _ in Err(_) is a catchall pattern that matches any error value, regardless of the specific error information.

The Complete Game

Let's test our finished game:

cargo run
$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
50
You guessed: 50
Too big!
Please input your guess.
25
You guessed: 25
Too small!
Please input your guess.
37
You guessed: 37
Too big!
Please input your guess.
31
You guessed: 31
You win!

Perfect! Our guessing game is complete and handles all the scenarios we designed for.

What You've Learned

Through building this simple game, you've encountered many fundamental Rust concepts:

Variables and Mutability: You created both immutable bindings (secret_number) and mutable ones (guess), and used shadowing to convert between types.

Functions and Methods: You called associated functions like String::new() and methods like .trim() and .parse().

Control Flow: You used match expressions for pattern matching and loop for repetition, with break and continue for flow control.

Error Handling: You worked with Result types and learned to handle both expected and unexpected errors gracefully.

External Crates: You added a dependency and used functionality from the rand crate.

Types: You worked with strings, numbers, and learned about type conversion and inference.

This guessing game demonstrates how Rust encourages you to think about edge cases and handle errors explicitly. The compiler guided you toward writing robust code that handles invalid input gracefully rather than crashing.

As you continue learning Rust, you'll find that these patterns—explicit error handling, pattern matching, and careful thinking about data flow—are central to writing reliable Rust programs. The habits you've developed in this simple game will serve you well as you tackle more complex projects.

Š 2025 President-XD