7.9Adding a dependency: random numbers
This chapter's project is a guessing game, and a guessing game needs a secret number that's different every run. That one requirement is going to teach you two things: how computers fake randomness, and how to use code you didn't write, which may quietly be the more important lesson in this chapter.
Act one: computers can't do random
A computer is a deterministic machine; same program, same inputs, same result, every time (that's most of what we like about them). Dice and coin flips get their unpredictability from physics. Your CPU has no dice. What it can do is simulate the look of randomness with arithmetic.
A pseudo-random number generator (PRNG) is an algorithm that keeps a number as internal state and, each time you ask, scrambles that state thoroughly and serves you a piece of the result. Here's a tiny, working one, built from nothing but lesson 4.4's wrapping arithmetic:
fn main() {
let mut state: u32 = 7; // the seed
for _ in 0..8 {
state = state.wrapping_mul(747796405).wrapping_add(2891336453);
print!("{} ", state % 100);
}
println!();
}92 97 10 7 84 29 18 75
(The _ is lesson 7.3's wildcard doing loop duty: we want eight repetitions and don't care which one is which.) Multiply by a big odd constant, add another, let the result wrap, repeat: the output hops around with no obvious rhyme. But run the program again and you'll get exactly the same eight numbers, because of course you will; it's arithmetic.
The starting value of the state is called the seed, and it's the only source of variety a PRNG has. Same seed, same sequence, forever (change the 7 above to an 8 and you get 1 6 3 80 41 46 91 64, also forever). So programs that want different numbers each run seed the generator with something that differs each run, typically entropy the operating system collects. And programs that want reproducible randomness, like a game with shareable level codes or a test you can re-run, seed with a fixed value on purpose. Same trick, both directions.
Real PRNGs differ from our toy in quality, not kind: bigger state, better scrambling, statistical guarantees, and immunity to the toy's flaws (low bits repeating in patterns, some values arriving too often). Writing a good one is research-grade work, which is precisely why you shouldn't: this is the textbook moment for someone else's code.
Act two: crates.io and your first dependency
Rust's standard library deliberately doesn't include a random number generator. That sounds like an omission until you remember what's behind it. Back in lesson 0.5, you learned that shareable units of Rust code are called crates, that the community's crates live at crates.io, and that declaring a dependency makes Cargo do all the fetching and compiling; that lesson promised you'd "do it for real in lesson 7.9, adding random numbers to your first game." Welcome to lesson 7.9. The de facto standard crate for randomness is rand.
From inside your project's folder, one command:
$ cargo add rand
Updating crates.io index
Adding rand v0.10.1 to dependencies
That's it. (The real output is a little chattier: a list of rand's optional "features" with plus and minus signs, and a line about locking package versions. Both are Cargo being thorough; neither needs your attention yet.) Open Cargo.toml and the [dependencies] section that's sat empty since lesson 0.8 (which predicted this moment too) has its first entry:
[dependencies]
rand = "0.10.1"
You could have typed that line by hand (cargo add is a convenience, not magic), and on the next build, Cargo does the rest: downloads rand and the few crates it depends on, compiles them once, and links them into your program. The first build after adding a dependency is noticeably chattier and slower; after that, the compiled results are reused and builds are quick again.
What does the version number promise?
"0.10.1" is a semantic version: the convention that version numbers encode compatibility. Cargo reads it as "0.10.1 or any newer release that won't break my code" (0.10.2 yes, 0.11 no; a breaking change must bump the leftmost nonzero part of the version, here the 10). This is how thousands of strangers' codebases update each other safely. The first build also writes a Cargo.lock file recording the exact versions used, so a teammate (or you, next year) building the same project gets byte-identical dependencies. You'll never edit the lockfile; just know it's why builds are reproducible.
Rolling dice
With rand added, the dice roll is one line:
fn main() {
let roll = rand::random_range(1..=6);
println!("You rolled: {roll}");
}You rolled: 4
(Your roll will differ. That's the feature.) The function takes a range, lesson 7.5's syntax in a new home, and returns a uniformly distributed value from it: every face of the die equally likely, none of our toy generator's biases. Behind the scenes, it uses a generator that the operating system seeded for you at startup.
rand::random_range is the convenience form, perfect for a roll here and there. When you're generating lots of numbers, ask for the generator once and reuse it:
use rand::prelude::*;
fn main() {
let mut rng = rand::rng();
for _ in 0..10 {
print!("{} ", rng.random_range(1..=6));
}
println!();
}5 1 5 6 1 2 2 2 5 4
rand::rng() hands you the generator itself; the use rand::prelude::*; line imports the crate's commonly-needed names, including the one that puts the .random_range(...) method on it (chapter 16 explains how methods can arrive via imports; until then, the prelude line is the recipe). And notice mut: act one is hiding in that keyword. Every call scrambles the generator's internal state, and Rust makes the mutation visible in the type system, even when the state belongs to a library.
Older tutorials look different
If you search the web for Rust randomness, you'll find rand::thread_rng().gen_range(...) everywhere, including, as of this writing, the official Rust Book's guessing game chapter. Those names are from rand 0.8 and were renamed in 2025 (thread_rng → rng, gen_range → random_range). If a tutorial shows the old names, it predates the rename; the concepts translate directly.
Reproducible randomness
For the same-seed-same-sequence trick with a real generator: StdRng::seed_from_u64(42) (with the prelude imported) gives you a generator you seed yourself, and then .random_range(...) works as usual. Every run produces the same sequence (seed 42, five d6 rolls: 1 4 2 4 6, every time), which is exactly what you want for debugging a randomness-dependent bug, and exactly what you don't want for a guessing game. Note the reproducibility is per-version: rand reserves the right to change StdRng's algorithm across releases.
Quiz time
Question #1
Your friend runs the toy PRNG program from act one on their machine and gets 92 97 10 7 84 29 18 75, the same numbers as the lesson. Should they file a bug report?
Show solution
No; this is a PRNG behaving exactly as defined. The sequence is pure arithmetic on the seed, and the seed is hardcoded to 7, so every machine on Earth produces those numbers. If they want different output per run, they need a different seed per run (which is what rand arranges automatically), not better luck.
Question #2
Without running it, what can you say for certain about this program's two output lines?
use rand::prelude::*;
fn main() {
let mut a = StdRng::seed_from_u64(99);
let mut b = StdRng::seed_from_u64(99);
println!("{} {} {}", a.random_range(1..=100), a.random_range(1..=100), a.random_range(1..=100));
println!("{} {} {}", b.random_range(1..=100), b.random_range(1..=100), b.random_range(1..=100));
}Show solution
The two lines are identical (and each contains three numbers between 1 and 100). You can't predict which numbers without running it, but a and b are independent generators started from the same seed, so they produce the same sequence in lockstep. (With seed 99 on rand 0.10, both lines read 99 64 76, and your machine will agree; the 99 leading is pure coincidence.) One generator asked six times would not print two matching lines; each call advances the shared state.
Question #3
Write a program that rolls a six-sided die 100 times and reports how many sixes came up. (For loop, one reused generator, a counter. Run it a few times; the answer should hover near 100/6 ≈ 17.)
Show solution
use rand::prelude::*;
fn main() {
let mut rng = rand::rng();
let mut sixes = 0;
for _ in 0..100 {
if rng.random_range(1..=6) == 6 {
sixes += 1;
}
}
println!("rolled {sixes} sixes out of 100");
}rolled 14 sixes out of 100
(One sample run; yours will wobble around 17.) If you got exactly 17 every time, you'd be right to get suspicious; randomness that's too tidy isn't random. A result far outside roughly 8 to 27, run after run, would also be suspicious, but that's a statistics course's territory.
You now have everything the project needs: loops, match, input, parsing, and a secret number. Next lesson, the course stops doing examples and builds a program.