← thecodex.expert · The Codex Family of Knowledge
Tier 0 · Absolute Beginner · Rust Project

Number Guessing Game

Your very first program with a memory and a decision: the computer picks a secret number, and you guess until you get it. You will learn how a program asks a question, checks an answer, and repeats.

🧠 Teaches how to think spoonfed, every age Last verified:

1 The Problem

We want a tiny game: the computer thinks of a number between 1 and 100, and the player keeps guessing. After each guess the computer says “too high” or “too low” until the player gets it right. Simple — but it teaches the three things every program does: ask, decide, and repeat.

Where this shows up in real life: every time an app checks your password, validates a form, or keeps asking until you give a valid answer, it is doing exactly this — take input, compare it to something, and loop until the condition is met. Learn it here in 15 lines, and you have learned the heartbeat of almost every program.

2 How to Think About It

Before writing any code, picture the game as a loop with a decision inside it. You do not need to know Rust yet — you just need to know the shape of what happens:

The plan — in plain English
1. Pick a secret number once, at the start. → 2. Ask the player to guess. → 3. Compare the guess to the secret: lower, higher, or equal? → 4. If not equal, go back to step 2. If equal, celebrate and stop. That is the whole program. Everything below is just saying this in Rust.

Too low

Too high

Correct!

Computer picks a secret number 1-100

Ask the player to guess

Is the guess right?

Say 'too low'

Say 'too high'

Show how many guesses it took

Game over

3 The Build — explained part by part

Here is the complete game, split into a testable play_game function and a tiny main that wires it to real stdin/stdout. Read each part’s plain-English note below it.

Rustsrc/main.rs
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hasher};
use std::io::{self, BufRead, Write};

/// Rust's standard library deliberately ships no random number generator —
/// that job belongs to the `rand` crate in real projects. `RandomState`,
/// normally used to seed HashMap's hasher against denial-of-service attacks,
/// is reseeded from the OS's random source every time `RandomState::new()`
/// runs, so hashing nothing with a fresh one gives an unpredictable u64 with
/// zero external dependencies — good enough for a guessing game.
fn random_u64() -> u64 {
    RandomState::new().build_hasher().finish()
}

/// Picks a whole number in `[min, max]` (inclusive on both ends).
fn random_range(min: u32, max: u32) -> u32 {
    min + (random_u64() % (max - min + 1) as u64) as u32
}

/// Plays one full game against a fixed `secret`, reading guesses from
/// `input` and writing prompts and feedback to `output`. Kept separate from
/// `main` so it can be tested without touching real stdin/stdout.
fn play_game<R: BufRead, W: Write>(secret: u32, input: &mut R, output: &mut W) -> u32 {
    let mut guesses = 0u32;
    loop {
        write!(output, "Guess a number between 1 and 100: ").ok();
        output.flush().ok();

        let mut line = String::new();
        if input.read_line(&mut line).unwrap_or(0) == 0 {
            writeln!(output, "No more input — giving up.").ok();
            return guesses;
        }

        let guess: u32 = match line.trim().parse() {
            Ok(n) => n,
            Err(_) => {
                writeln!(output, "That's not a whole number — try again.").ok();
                continue;
            }
        };
        guesses += 1;

        if guess < secret {
            writeln!(output, "Too low.").ok();
        } else if guess > secret {
            writeln!(output, "Too high.").ok();
        } else {
            writeln!(output, "Correct! You got it in {guesses} guesses.").ok();
            return guesses;
        }
    }
}

fn main() {
    let secret = random_range(1, 100);
    let stdin = io::stdin();
    let mut input = stdin.lock();
    let mut output = io::stdout();
    play_game(secret, &mut input, &mut output);
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Cursor;

    #[test]
    fn finds_the_secret_in_one_guess() {
        let mut input = Cursor::new(b"42\n".to_vec());
        let mut output = Vec::new();
        let guesses = play_game(42, &mut input, &mut output);
        assert_eq!(guesses, 1);
        assert!(String::from_utf8(output).unwrap().contains("Correct!"));
    }

    #[test]
    fn narrows_down_with_too_low_and_too_high() {
        let mut input = Cursor::new(b"10\n90\n50\n".to_vec());
        let mut output = Vec::new();
        let guesses = play_game(50, &mut input, &mut output);
        assert_eq!(guesses, 3);
        let text = String::from_utf8(output).unwrap();
        assert!(text.contains("Too low."));
        assert!(text.contains("Too high."));
    }

    #[test]
    fn ignores_input_that_is_not_a_number() {
        let mut input = Cursor::new(b"banana\n7\n".to_vec());
        let mut output = Vec::new();
        let guesses = play_game(7, &mut input, &mut output);
        assert_eq!(guesses, 1);
        assert!(String::from_utf8(output).unwrap().contains("not a whole number"));
    }

    #[test]
    fn random_range_stays_within_bounds() {
        for _ in 0..500 {
            let n = random_range(1, 100);
            assert!((1..=100).contains(&n));
        }
    }
}
⚠ No in-browser playground here
Rust compiles to a real binary, so unlike the Python version of this project there is no editor above you can run in the browser. Copy the code below and run it on your own machine — it takes seconds once Rust (via rustup) is installed.
What each part does — in plain words
RandomState::new().build_hasher().finish() — Rust’s standard library deliberately ships no random number generator; that job belongs to the rand crate in real projects (cargo add rand). This build environment cannot reach crates.io, so this project borrows a well-known trick: RandomState, the type HashMap normally uses to seed its hasher against denial-of-service attacks, reseeds itself from the OS’s random source every time you call RandomState::new() — hashing nothing with a fresh one gives an unpredictable number with zero external dependencies. Good enough for a guessing game; a real project should still reach for rand.

fn play_game<R: BufRead, W: Write>(...) — the whole game logic takes its input and output as generic parameters instead of calling stdin()/stdout() directly. That is what lets the tests below feed it fake input and capture its output without touching a real terminal.

loop { ... } — Rust’s dedicated infinite-loop keyword, used here instead of while true because it signals intent directly: this loop is deliberately unbounded until a return inside it ends the function.

line.trim().parse() — read_line keeps the trailing newline, so trim() removes it first; .parse() then tries to turn the text into a u32, returning a Result that the match right after it handles explicitly — there is no exception to accidentally ignore.

if guess < secret / > secret / else — the decision, identical in shape to every other language’s version of this game.
Common mistakes — and how to avoid them
✗ Calling random_range(1, 100) inside the loop instead of once before it — the secret would change every guess and the game could never be won.
✓ Pick the secret once, before play_game starts looping.
✗ Writing if guess = secret by mistake instead of ==.
✓ Rust will not even compile this one — assignment is not an expression that produces a bool here, so the compiler catches the bug for you at build time, the same protection Go gives you.
✗ Calling .parse() without checking the Result, or reaching for .unwrap() — a non-numeric guess would crash the whole program instead of printing a friendly message.
✓ Match on the Result explicitly, as play_game does, and continue the loop on a parse error.

4 Test & Prove Each Part

How do we know the game works without playing it by hand? We write small tests — each one checks one rule and proves it. Rust’s built-in #[cfg(test)] module and cargo test are the standard way to do this, no extra install required.

Guessing the secret on the first try counts as 1 guess
The game correctly narrows down with a mix of too-low and too-high guesses
Non-numeric input is rejected with a friendly message, not a crash
The random secret always lands between 1 and 100
Rustsrc/main.rs (tests module)
#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Cursor;

    #[test]
    fn finds_the_secret_in_one_guess() {
        let mut input = Cursor::new(b"42\n".to_vec());
        let mut output = Vec::new();
        let guesses = play_game(42, &mut input, &mut output);
        assert_eq!(guesses, 1);
        assert!(String::from_utf8(output).unwrap().contains("Correct!"));
    }

    #[test]
    fn narrows_down_with_too_low_and_too_high() {
        let mut input = Cursor::new(b"10\n90\n50\n".to_vec());
        let mut output = Vec::new();
        let guesses = play_game(50, &mut input, &mut output);
        assert_eq!(guesses, 3);
        let text = String::from_utf8(output).unwrap();
        assert!(text.contains("Too low."));
        assert!(text.contains("Too high."));
    }

    #[test]
    fn ignores_input_that_is_not_a_number() {
        let mut input = Cursor::new(b"banana\n7\n".to_vec());
        let mut output = Vec::new();
        let guesses = play_game(7, &mut input, &mut output);
        assert_eq!(guesses, 1);
        assert!(String::from_utf8(output).unwrap().contains("not a whole number"));
    }

    #[test]
    fn random_range_stays_within_bounds() {
        for _ in 0..500 {
            let n = random_range(1, 100);
            assert!((1..=100).contains(&n));
        }
    }
}

Run them with cargo test. Each test is an ordinary function marked #[test] inside a mod tests block in the same file — Rust keeps unit tests right beside the code they test rather than in a separate file. A std::io::Cursor stands in for real stdin, which is exactly what the generic play_game<R, W> signature was designed to make possible.

5 The Interface

Even a tiny program has an interface — the way a person interacts with it. Here is its contract, documented plainly, the same way a professional would describe any tool.

INPUTYour guessa whole number 1–100, typed by the player
What it expects
A number like 42, typed and then Enter pressed.
OUTPUTFeedbackone of three replies
What it returns
"Too low."
"Too high."
"Correct! You got it in N guesses."

6 Run It & Automate It

Save the code as src/main.rs inside a Cargo project's src/ folder and run it with cargo run — Cargo compiles and executes in one step while you are experimenting, then cargo build --release gives you an optimized binary once you are done.

Run it locally
cargo run
Then type a number, press Enter, and follow the “too high / too low” hints until you win.

A CI tool like Jenkins runs cargo test automatically whenever the code changes — every line below has a plain explanation.

What you should see when it works
Terminala real run
Guess a number between 1 and 100: 50
Too high.
Guess a number between 1 and 100: 25
Too low.
Guess a number between 1 and 100: 37
Too high.
Guess a number between 1 and 100: 31
Correct! You got it in 4 guesses.
If it breaks — how to fix it
🚨 error[E0308]: mismatched types
Usually means a function expected a u32 and got something else, like a &str straight from input. Convert explicitly with .parse() and handle the Result.
🚨 The game seems to ignore my guess or always says the same thing.
Check that you are comparing guess to secret and not to a copy that never updates — and that the loop actually returns or breaks on a correct guess.
🚨 cargo: command not found
Rust was not installed, or your shell has not picked up its PATH yet. Reinstall via rustup and open a new terminal.
GroovyJenkinsfile
// Jenkinsfile — runs the tests automatically every time the code changes.
pipeline {
    agent any                                  // run on any available machine

    stages {
        stage('Get the code') {
            steps { checkout scm }             // download the latest code
        }
        stage('Set up Rust') {
            steps {
                sh 'rustc --version'                // confirm Rust is installed
                sh 'cargo build'                     // compile, downloading any crates
            }
        }
        stage('Run the tests') {
            steps {
                sh 'cargo clippy -- -D warnings'     // catch obvious mistakes before running
                sh 'cargo test'                       // run every test, show each result
            }
        }
    }

    post {
        success { echo 'All tests passed.' }
        failure { echo 'A test failed — look above.' }
    }
}
🎯 Try this next — make it yours
  1. Limit the attempts. Give the player only 7 guesses. (Teaches: counting down, and an early return on failure.)
  2. Add difficulty levels. Let the player choose a range like 1–1000. (Teaches: passing parameters into random_range.)
  3. Use the real rand crate. If you have network access, replace random_range with rand::rng().random_range(1..=100). (Teaches: adding a dependency with cargo add.)
What you learned
You learned Rust’s one dedicated infinite-loop keyword, Result-based input parsing with no exceptions to forget, and a genuinely useful pattern — writing core logic against generic Read/Write parameters so it can be tested without touching a real terminal. You also saw why Rust keeps randomness out of the standard library. Related: Control Flow, The Result Type.