Project: Guessing Game
This is your first real project. The computer picks a secret number between 1 and 100. You guess. It tells you “too small” or “too big” until you get it right, and then it tells you how many tries you needed.
It’s a small program, but it uses a lot of what you’ve learned so far: variables, match, loops, and Result. We’ll build it one step at a time, and every step will run on its own.
What you’ll build
Section titled “What you’ll build”When you’re done, a game on your own computer will look like this:
Guess the number! (1 to 100)Please type your guess:50Too big!Please type your guess:helloThat's not a whole number. Try again!Please type your guess:25Too small!Please type your guess:37You got it in 3 attempts!Notice two friendly details. When the player types hello, the game doesn’t crash. It asks again. And that bad input doesn’t count as an attempt.
Step 1: Create the project
Section titled “Step 1: Create the project”Open a terminal and make a new Cargo project:
cargo new guessing_gamecd guessing_gameAll the code on this page goes in src/main.rs. Run it any time with:
cargo runIf you haven’t set up Rust yet, see Get Started first.
Step 2: Read a guess from the keyboard
Section titled “Step 2: Read a guess from the keyboard”To read what the player types, use std::io::stdin() and its read_line method:
use std::io;
fn main() { println!("Please type your guess:");
let mut guess = String::new(); io::stdin() .read_line(&mut guess) .expect("Failed to read line");
println!("You guessed: {guess}");}This program waits for you to type something and press Enter, so run it with cargo run on your own machine. (The Rust Playground has no keyboard input, which is why there’s no “Try it” button here.)
A few things are going on:
String::new()makes an empty, growable String to hold the text.read_line(&mut guess)adds the typed line onto the end ofguess. It needs&mutbecause it changes the string. See Borrowing if&mutis new to you.read_linereturns aResult, because reading input can fail..expect(...)says “if this fails, stop the program with this message”. That’s fine for now; we’ll handle it more gently later.
One surprise: the text includes the Enter key. If you type 42, guess holds "42\n". Keep that in mind for the next step.
Step 3: Turn the text into a number
Section titled “Step 3: Turn the text into a number”The player typed text, but we want a number we can compare. .trim() removes the spaces and the newline at both ends. Then .parse() tries to turn the text into a number.
Parsing can fail. "seven" is not a number. So parse() returns a Result, and we use match to handle both cases:
fn main() { // Pretend the player typed these lines (each ends with Enter). let inputs = ["42\n", " 7 \n", "seven\n", "-3\n"];
for input in inputs { match input.trim().parse::<u32>() { Ok(num) => println!("{:?} -> the number {num}", input), Err(e) => println!("{:?} -> not a valid guess ({e})", input), } }}Output:
"42\n" -> the number 42" 7 \n" -> the number 7"seven\n" -> not a valid guess (invalid digit found in string)"-3\n" -> not a valid guess (invalid digit found in string)parse::<u32>() tells Rust which type to parse into. u32 is an unsigned (never negative) whole number, which is why -3 is rejected. That’s what we want: the secret is always between 1 and 100.
In the game, we write the same idea like this:
let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => { println!("That's not a whole number. Try again!"); continue; }};Here the type comes from let guess: u32, so we don’t need ::<u32>. If parsing works, the match gives back the number. If it fails, we print a kind message and continue to ask again. (continue only works inside a loop, which we’ll add in Step 5.)
Why not just use .expect()? Because then a typo would crash the whole game. Handling the Err case is how you make a program that’s pleasant to use.
Step 4: Compare the guess with the secret
Section titled “Step 4: Compare the guess with the secret”Every number has a .cmp() method. It compares two values and returns an std::cmp::Ordering, an enum with three variants: Less, Greater, and Equal. A match on it reads almost like English:
use std::cmp::Ordering;
fn main() { let secret: u32 = 42;
for guess in [10, 42, 99] { match guess.cmp(&secret) { Ordering::Less => println!("{guess}: Too small!"), Ordering::Greater => println!("{guess}: Too big!"), Ordering::Equal => println!("{guess}: You win!"), } }}Output:
10: Too small!42: You win!99: Too big!You could write the same thing with if guess < secret ... else if .... The match version is nice because the compiler checks that you handled all three cases. Forget one and it won’t compile.
Step 5: Keep asking until the guess is right
Section titled “Step 5: Keep asking until the guess is right”Now we wrap everything in a loop. It runs forever until we break out of it. We also keep a counter, attempts, and only add 1 when the guess was a real number.
In the real game, input comes from the keyboard. To make a version that runs in the Playground, we use a fixed secret and a fixed list of “typed” lines. inputs.next() gives us the next line, or None when the list runs out:
use std::cmp::Ordering;
fn main() { let secret: u32 = 37; // Pretend the player typed these, one per line. let mut inputs = ["50", "hello", "25", "37"].into_iter(); let mut attempts = 0;
println!("Guess the number! (1 to 100)");
loop { let input = match inputs.next() { Some(line) => line, None => { println!("No more guesses. The number was {secret}."); break; } }; println!("> {input}");
let guess: u32 = match input.trim().parse() { Ok(num) => num, Err(_) => { println!("That's not a whole number. Try again!"); continue; } };
attempts += 1;
match guess.cmp(&secret) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You got it in {attempts} attempts!"); break; } } }}Output:
Guess the number! (1 to 100)> 50Too big!> helloThat's not a whole number. Try again!> 25Too small!> 37You got it in 3 attempts!Try changing the secret or the list of guesses and run it again. Put the right answer first, or leave it out, and watch what happens.
The None case is important. It’s the Playground version of “the player has no more input”, and the real game needs the same idea (see Step 7).
Step 6: Pick a random secret number
Section titled “Step 6: Pick a random secret number”A game with the same secret every time isn’t much fun. In a real project, you’d use the rand crate, which is the standard choice for random numbers in Rust. The Random Numbers lesson shows how to use it. You add it with:
cargo add randFor this project we’ll stay with the standard library only, so you don’t need any setup. The trick: ask the clock for the current time, take the nanoseconds part (a number from 0 to 999,999,999 that changes constantly), and squeeze it into the range 1 to 100 with % (remainder):
use std::time::{SystemTime, UNIX_EPOCH};
fn secret_number() -> u32 { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("the clock is set before 1970") .subsec_nanos(); nanos % 100 + 1}
fn main() { let secret = secret_number(); let in_range = (1..=100).contains(&secret); println!("Picked a secret number. Is it between 1 and 100? {in_range}");}Output:
Picked a secret number. Is it between 1 and 100? truenanos % 100 is always 0 to 99, so adding 1 gives 1 to 100. We print only whether it’s in range, so the output is the same every time. Print secret itself if you want to peek.
Be honest with yourself about what this is: it’s “unpredictable enough for a game”, not real randomness. Never use a trick like this for passwords, security tokens, or anything where someone might try to guess the value. For those, use a proper crate.
Step 7: The complete game
Section titled “Step 7: The complete game”Here’s everything together. This is the version to put in src/main.rs and play with cargo run.
One new thing: we match on the result of read_line instead of calling .expect(). read_line returns Ok(0) when there’s no more input, for example if the player presses Ctrl+D (Ctrl+Z then Enter on Windows), or if input is piped in from a file. Without that check, the loop would spin forever on empty input.
use std::cmp::Ordering;use std::io;use std::time::{SystemTime, UNIX_EPOCH};
fn secret_number() -> u32 { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("the clock is set before 1970") .subsec_nanos(); nanos % 100 + 1}
fn main() { println!("Guess the number! (1 to 100)");
let secret = secret_number(); let mut attempts = 0;
loop { println!("Please type your guess:");
let mut guess = String::new(); match io::stdin().read_line(&mut guess) { Ok(0) => { // End of input: nothing more to read. println!("No more input. The number was {secret}. Bye!"); break; } Ok(_) => {} Err(e) => { println!("Could not read your input: {e}"); break; } }
let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => { println!("That's not a whole number. Try again!"); continue; } };
attempts += 1;
match guess.cmp(&secret) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You got it in {attempts} attempts!"); break; } } }}To try it in the browser instead, use the Step 5 version, which plays a fixed list of guesses. 🦀
Common mistakes
Section titled “Common mistakes”Forgetting mut on the string. read_line needs to change the string, so it must be mutable:
use std::io;
fn main() { let guess = String::new(); io::stdin().read_line(&mut guess).expect("Failed to read line");}error[E0596]: cannot borrow `guess` as mutable, as it is not declared as mutable --> src/main.rs:5:27 |5 | io::stdin().read_line(&mut guess).expect("Failed to read line"); | ^^^^^^^^^^ cannot borrow as mutable |help: consider changing this to be mutable |4 | let mut guess = String::new(); | +++The fix is exactly what the compiler suggests: let mut guess = String::new();.
Comparing text with a number. If you forget to parse, guess is still a String, and you can’t compare a String with a u32:
use std::cmp::Ordering;
fn main() { let secret: u32 = 42; let guess = String::from("50");
match guess.cmp(&secret) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => println!("You win!"), }}error[E0308]: mismatched types --> src/main.rs:7:21 |7 | match guess.cmp(&secret) { | --- ^^^^^^^ expected `&String`, found `&u32` | | | arguments to this method are incorrectRust never converts between text and numbers for you. Parse first, then compare.
Forgetting .trim(). This one compiles, but every guess is rejected, because the newline is not a digit:
fn main() { let typed = "42\n"; println!("{:?}", typed.parse::<u32>()); println!("{:?}", typed.trim().parse::<u32>());}Output:
Err(ParseIntError { kind: InvalidDigit })Ok(42)Ideas to extend it
Section titled “Ideas to extend it”Once your game works, try these on your own. Each one is small:
- Check the range. If the guess is 0 or above 100, say “Please pick a number from 1 to 100” and don’t count it.
- Limit the attempts. Give the player 7 tries. If they run out, show the secret and end the game.
- Play again. When the player wins, ask “Play again? (y/n)” and start a new round with a new secret.
- Keep a best score. Remember the fewest attempts across rounds and print it at the end.
- Give warmer hints. Say “very close!” when the guess is within 3 of the secret.
guess.abs_diff(secret)gives the distance between two numbers. - Use the real
randcrate. Runcargo add randand follow its documentation on docs.rs to replacesecret_number(). - Move logic into functions. Write
fn check_guess(guess: u32, secret: u32) -> Orderingand add tests for it.
io::stdin().read_line(&mut s)reads a line, including the newline, and returns aResult.Ok(0)means there’s no more input..trim().parse()turns text into a number. It returns aResult, somatchon it to handle bad input kindly.a.cmp(&b)returnsOrdering::Less,Greater, orEqual, andmatchmakes sure you handle all three.loopwithcontinueandbreakkeeps the game going until the player wins.- The clock trick is fine for a game, but real projects use the
randcrate.
Quick check
Section titled “Quick check”Quick check
1. The player types 42 and presses Enter. What does the string from read_line contain?
2. Why does the game use match on parse() instead of .expect()?
3. What does read_line returning Ok(0) mean?
4. Which values can guess.cmp(&secret) return?
Score: 0 / 4
Read more
Section titled “Read more”- Programming a Guessing Game — The Rust Book: the Book’s own version of this project. It uses the
randcrate and explains each line in detail. Stdin::read_line— std docs: why the text is appended to yourString, why it keeps the newline, and why it returnsOk(0)at the end of input.str::parse— std docs: howparsepicks the target type from your annotation, and what the error looks like when the text isn’t a number.std::cmp::Ordering— std docs: theLess/Equal/Greaterenum thatcmpreturns, plus helpers likeis_ltandreverse.- The
randcrate — docs.rs: the real way to pick random numbers, once you’re ready to add your first crate.