Guessing Game Part 1
Building a very small Rust application.
Guessing Game Part 1
We're going to build on "Hello Rust" to write a small guessing game program.
You're not expected to understand all the details of all the code, but rather start getting familiar with the language and with building applications.
Let's eat the cake 🍰 and then we'll learn the recipe👨🍳.
Tip: Follow along in your terminal or PowerShell window.
Learning objectives:
By the end of this module you should be able to:
- Use basic
cargocommands to create projects and compile rust code - Add external dependencies (crates) to a project
- Recognize some useful syntax like Rust's
Resulttype with.expect() - Recognize and fix some common Rust compilation errors
Keep Practicing with the Terminal
- This is Part 1 where we use the terminal
- In Part 2, we will start using VSCode which integrates
- code editor
- terminal window
- compiler hints
- AI assistance
Guessing game demo
Compiling review and reference
Option 1: Compile directly
- put the content in file
hello.rs - command line:
- navigate to this folder
rustc hello.rs- run
./helloorhello.exe
Option 2: Use Cargo
- create a project:
cargo new PROJECT-NAME - main file will be
PROJECT-NAME/src/main.rs - to build and run:
cargo run - the machine code will be in :
./target/debug/PROJECT-NAME
Different ways to run Cargo
cargo runcompiles, runs, and saves the binary/executable in/target/debugcargo buildcompiles but does not runcargo checkchecks if it compiles (fastest)cargo run --releasecreates (slowly) "fully optimized" binary in/target/release
Back to the guessing game
In MacOS terminal or Windows PowerShell, go to the folder you created to hold all your projects:
cd ~/projects
Let's use cargo to create a project:
cargo new guessing-game
Replace the contents of src/main.rs with:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
// This is all technically one line of code
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
And then:
cargo run
Question: Program doesn't do much? How can we improve it?
More on variables
Let's take a look at this variable assignment:
#![allow(unused)] fn main() { let mut guess = String::new(); }
As we saw in the earlier module, we assign a variable with let as in
#![allow(unused)] fn main() { let count = 5; }
But by default Rust variables are immutable.
Definition:
im·mu·ta·ble
adjective
unchanging over time or unable to be changed
"an immutable fact"
Try executing the following code cell.
fn main() { let count = 5; count = 7; }
Rust compiler errors are pretty descriptive!
error[E0384]: cannot assign twice to immutable variable `count`
--> src/main.rs:4:1
|
3 | let count = 5;
| ----- first assignment to `count`
4 | count = 7;
| ^^^^^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
3 | let mut count = 5;
| +++
For more information about this error, try `rustc --explain E0384`.
error: could not compile `playground` (bin "playground") due to 1 previous error
It often even tells you how to correct the error with the mut keyword to make
the variable mutable.
#![allow(unused)] fn main() { let mut count = 5; count = 7; }
Question: Why might it be helpful to have variables be immutable by default?
.expect() - a tricky concept
We'll go into all of this more later, but:
read_line()returns aResulttype which has two variants -OkandErrOkmeans the operation succeeded, and returns the successful valueErrmeans something went wrong, and it returns the string you passed to.expect()
More in a few future module.
More on macros!
- A macro is code that writes other code for you / expands BEFORE it compiles.
- They end with ! like println!, vec!, or panic!
For example, println!("Hello"); roughly expands into
#![allow(unused)] fn main() { use std::io::{self, Write}; io::stdout().write_all(b"Hello\n").unwrap(); }
while println!("Name: {}, Age: {}", name, age); expands into
#![allow(unused)] fn main() { use std::io::{self, Write}; io::stdout().write_fmt(format_args!("Name: {}, Age: {}\n", name, age)).unwrap(); }
Rust Crates
In Rust, the collection files in a project form a "crate".
You can have:
- binary or application crate, that you can execute directly, or a
- library crate, which you can use in your application
Rust makes it super easy to publish and use crates.
See crates.io.
Using crates: generate a random number
We want to add a random number, so we need a way of generating them.
Rust doesn't have a random number generator in its standard library
so we will use a crate called rand.
We can do that with the command:
cargo add rand
which will produce an output like...
Output
% cargo add rand
Updating crates.io index
Adding rand v0.9.2 to dependencies
Features:
+ alloc
+ os_rng
+ small_rng
+ std
+ std_rng
+ thread_rng
- log
- nightly
- serde
- simd_support
- unbiased
Updating crates.io index
Locking 17 packages to latest Rust 1.85.1 compatible versions
Adding cfg-if v1.0.3
Adding getrandom v0.3.3
Adding libc v0.2.175
Adding ppv-lite86 v0.2.21
Adding proc-macro2 v1.0.101
Adding quote v1.0.40
Adding r-efi v5.3.0
Adding rand v0.9.2
Adding rand_chacha v0.9.0
Adding rand_core v0.9.3
Adding syn v2.0.106
Adding unicode-ident v1.0.19
Adding wasi v0.14.5+wasi-0.2.4
Adding wasip2 v1.0.0+wasi-0.2.4
Adding wit-bindgen v0.45.1
Adding zerocopy v0.8.27
Adding zerocopy-derive v0.8.27
Take a look at Cargo.toml now.
cat Cargo.toml
[package]
name = "guessing-game-part1"
version = "0.1.0"
edition = "2024"
[dependencies]
rand = "=0.8.5"
Note that the version number is captured.
Also take a look at Cargo.lock.
It's kind of like pip freeze or conda env export in that it
fully specifies your environment down to the package versions.
Generate Random Number
So now that we've specified that we will use the rand
crate, we add to our main.rs:
#![allow(unused)] fn main() { use rand::Rng; }
after the use std::io, and add right after fn main() {
#![allow(unused)] fn main() { let secret_number = rand::rng().random_range(1..=100); println!("The secret number is: {secret_number}"); }
Run your program. Whaddayathink so far?
Let's Check Guess
Obviously, we better compare the guess to the "secret number".
Add the following code to the end of your main function.
#![allow(unused)] fn main() { if guess == secret_number { println!("You win!"); } else { println!("You lose!"); } }
And run your program again. 🤔
In-Class Activity: Compiler Error Hints!
This activity is designed to teaching you to to not fear compiler errors and to show you that Rust's error messages are actually quite helpful once you learn to read them!
Please do NOT use VSCode yet! Open your files in nano, TextEdit / Notepad or another plain text editor.
Instructions
The code asks the user for a series of integers, one at a time, then counts the number, sum and average.
But there are four syntax errors in the code.
Working in pairs, fix the syntax errors based on the compiler error messages.
Put a comment (using double backslashes, e.g. \\ comment) either on the line
before or at the end of the stating what you changed to fix the error.
Paste the corrected code into Gradescope.
I'll give you a 2 minute warning to wrap up in gradescope and then we'll review the errors.
Again Please do NOT use VSCode yet! It ruins the fun
Setup Instructions
Go to your projects folder and create a new Rust project.
cd ~/projects # or whatever your main projects folder is called
cargo new compiler-errors
cd compiler-errors
cargo add rand
# quick test of the default project
cargo run
You should see "Hello World!" without any errors.
Starter Code (src/main.rs)
Replace the code in main.rs with the following code.
use std::io::{self, Write}; fn main() { println!("Enter integers, one per line. Empty line to finish.") let nums: Vec<i32> = Vec::new() loop { print!("> "); io::stdout().flush().unwrap(); let mut input = String::new(); if io::stdin().read_line(&mut input).is_err() { return; } let trimmed = input.trim(); if trimmed.is_empty(): break; match trimmed.parse::<i32>() { Ok(n) => nums.push(n), Err(_) => println!("Please enter a valid integer."), } } if nums.is_empty() { println!("No numbers entered."); } else { let sum: i32 = nums.iter().sum(); let avg = sum as f64 / nums.len() as f64; println!("Count = {nums.len()}, Sum = {sum}, Average = {avg:.2}"); } }
- Compile the code
- Read the compiler output, starting from the top
- Fix the error
- Repeat...
List of Errors
What did you find?
- error 1:
- error 2:
- error 3:
- error 4:
Recap
- Variables in Rust are immutable by default - we need to explicitly mark them as
mutto make them mutable - The
letkeyword is used for variable declaration and initialization in Rust - Rust has strong error handling with
Resulttypes that haveOkandErrvariants - The
.expect()method is used to handle potential errors by unwrapping theResultor panicking with a message - Basic I/O in Rust uses the
std::iomodule for reading from stdin and writing to stdout