DaleSchool

Your First Program: Number Guessing Game

Beginner20min

Learning Objectives

  • Combine all the syntax learned so far to build a complete program
  • Create and build a new project with Cargo
  • Add an external crate (rand) to Cargo.toml
  • Read and process user input

Installing Rust on Your Computer

Up until now, we've been running code in the Rust Playground. Starting with this module, we're going to build programs directly on your computer!

Run this command in your terminal to install Rust:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

On Windows, download the installer from rustup.rs.

Once the installation is done, let's verify it worked:

rustc --version
cargo --version

If you see version numbers, you're all set!

Cargo — Rust's All-in-One Tool

Cargo is Rust's build system and package manager. It handles project creation, building, running, and managing external libraries — all in one tool.

Let's create a new project:

cargo new guessing_game
cd guessing_game

This creates the following folder structure:

guessing_game/
├── Cargo.toml    # Project configuration file
└── src/
    └── main.rs   # Source code file

Open src/main.rs and you'll see it already has a "Hello, world!" program. Try running it with cargo run!

cargo run

Working Code: Number Guessing Game

Now let's build the actual game. First, we need to add the rand crate to generate random numbers. A crate is a library made by someone else.

cargo add rand

This command automatically adds rand to your Cargo.toml.

Paste the code below into src/main.rs and run it with cargo run!

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

fn main() {
    println!("Number Guessing Game!");
    let secret = rand::thread_rng().gen_range(1..=100);

    loop {
        println!("Enter a number (1-100):");

        let mut guess = String::new();
        io::stdin().read_line(&mut guess).unwrap();

        let guess: i32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                println!("Please enter a valid number!");
                continue;
            }
        };

        match guess.cmp(&secret) {
            Ordering::Less => println!("Too low!"),
            Ordering::Greater => println!("Too high!"),
            Ordering::Equal => {
                println!("You got it!");
                break;
            }
        }
    }
}

When you run it, you'll be prompted to enter a number. After each guess, it tells you whether the answer is higher or lower, and it keeps going until you get it right!

Line by Line

That's a fair amount of code, right? Let's break it down piece by piece.

Step 1: Imports

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

use is the keyword for bringing in functionality from elsewhere.

  • std::io — Input/output functionality
  • std::cmp::Ordering — The result of comparing two values (Less, Greater, Equal)
  • rand::Rng — Random number generation (from the crate we added)

Step 2: Generate a Random Number

let secret = rand::thread_rng().gen_range(1..=100);

This generates a random number between 1 and 100 and stores it in secret. The 1..=100 syntax is the same range syntax we learned in the for loop module!

Step 3: Read User Input

let mut guess = String::new();
io::stdin().read_line(&mut guess).unwrap();

String::new() creates an empty string, and read_line() stores what the user types into it. unwrap() means "if there's an error, just crash the program." We'll learn proper error handling later.

Step 4: Convert String to Number

let guess: i32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => {
        println!("Please enter a valid number!");
        continue;
    }
};

What the user typed is a string. To compare it with a number, we need to convert it to i32.

  • trim() — Removes the newline character at the end of the input
  • parse() — Attempts to convert the string to a number
  • match — Branches based on the result (success is Ok, failure is Err)

Notice how we declare guess again with let using the same name? That's shadowing, which we learned in Module 02! We're replacing the string guess with a numeric guess.

Step 5: Compare

match guess.cmp(&secret) {
    Ordering::Less => println!("Too low!"),
    Ordering::Greater => println!("Too high!"),
    Ordering::Equal => {
        println!("You got it!");
        break;
    }
}

cmp() compares two values and returns Less, Greater, or Equal. We use match to print the appropriate message for each case, and break out of the loop when the guess is correct.

"Why?" — If You've Made It This Far, You've Got the Rust Basics Down!

This program uses everything we've learned so far:

  • Variables and mutability: let, let mut
  • Types: i32, String
  • Function calls: println!(), read_line(), parse()
  • Branching: match (a more powerful alternative to if!)
  • Loops: loop + break, continue
  • Ranges: 1..=100

It might look complex at first, but when you break it down, it's just a combination of things you already know. In Phase 2, we'll explore concepts unique to Rust — ownership, structs, error handling, and more. Stay tuned!

Deep Dive

Cargo.toml — Exploring the project configuration file

Open Cargo.toml and you'll see something like this:

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"

[dependencies]
rand = "0.8"
  • [package] — Project name, version, and Rust edition
  • [dependencies] — List of external crates. Running cargo add rand automatically adds entries here

Here are some frequently used Cargo commands worth knowing:

| Command | What it does | | ----------------- | ------------------------------------------ | | cargo new name | Create a new project | | cargo run | Build + run (most used during development) | | cargo build | Build only (doesn't run) | | cargo check | Quickly check if the code compiles | | cargo add crate | Add an external crate |

match — More powerful than if

In this game, match appears twice. match is similar to if/else if, but it requires you to handle every possible case.

fn main() {
    let number = 3;

    match number {
        1 => println!("one"),
        2 => println!("two"),
        3 => println!("three"),
        _ => println!("something else"), // all other cases
    }
}

_ means "everything else." We'll cover match in much more detail in Phase 2.

If you try to parse() something that isn't a number, you'll get an error. In our game, we handle this with match.

But what if we had used unwrap() instead?

// This will crash the program if the input isn't a number!
let guess: i32 = guess.trim().parse().unwrap();

If you type "abc", you'll see this error:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value:
ParseIntError { kind: InvalidDigit }'

What this means: "You tried to convert something that isn't a number into a number." Using match instead of unwrap() lets you handle errors gracefully.

Mini Project: Upgrade the Game

Once the basic game is working, try adding these features one at a time!

  1. Count attempts: Display how many guesses it took. Hint: Create a counter with a mut variable.
  2. Range validation: If the user enters a number outside 1-100, print "Please enter a number between 1 and 100!"
  3. Difficulty selection: Let the player choose the range before the game starts. (e.g., 1-10, 1-100, 1-1000)

Q1. What is the Cargo command to create a new project?

  • A) cargo init project
  • B) cargo new project
  • C) cargo create project
  • D) cargo start project

Q2. What does guess.trim().parse() do in the code below?

let guess: i32 = guess.trim().parse().unwrap();
  • A) Converts the string to uppercase
  • B) Removes whitespace and converts to a number
  • C) Reverses the string
  • D) Copies the string

Q3. What does _ mean in a match expression?

  • A) It causes an error
  • B) It does nothing
  • C) It handles all remaining cases
  • D) It stops the loop