Programming a Guessing Game
This chapter demonstrates core Rust concepts through a practical project: a CLI guessing game. Unlike Node.js scripts, this compiles to a self-contained binary with no runtime dependencies.
Key concepts covered:
- Variables and mutability (
let
vslet mut
) - Pattern matching (
match
expressions) - Error handling (
Result
type vs exceptions) - External dependencies (Cargo vs npm)
- Type system (compile-time safety vs runtime checks)
Project Setup
$ cargo new guessing_game
$ cd guessing_game
Generated structure matches standard Rust conventions:
# Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
[dependencies]
Input Handling
Unlike Node.js’s readline
or process.stdin
, Rust uses explicit imports and error handling:
use std::io; fn main() { println!("Guess the number!"); println!("Please input your guess."); let mut guess = String::new(); // Mutable variable (explicit) io::stdin() .read_line(&mut guess) // Borrows mutable reference .expect("Failed to read line"); // Error handling (no try/catch) println!("You guessed: {guess}"); }
Key differences from TypeScript/Node.js:
- Explicit mutability:
let mut
vslet
(immutable by default) - Borrowing:
&mut guess
passes a mutable reference, not the value - No exceptions:
Result
type forces explicit error handling - No garbage collection: Memory managed through ownership system
External Dependencies
Add the rand
crate to Cargo.toml
:
[dependencies]
rand = "0.8.5"
vs npm/package.json:
- Dependencies compile into the binary (no
node_modules
at runtime) - Semantic versioning with automatic compatibility resolution
Cargo.lock
ensures reproducible builds (like package-lock.json)
$ cargo build # Downloads, compiles, and links dependencies
Random Number Generation
use rand::Rng; use std::io; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1..=100); // Unlike Math.random(), this is cryptographically secure by default println!("Please input your guess."); let mut guess = String::new(); io::stdin().read_line(&mut guess).expect("Failed to read line"); println!("You guessed: {guess}"); println!("The secret number was: {secret_number}"); }
Pattern Matching and Comparison
Rust’s match
is more powerful than TypeScript’s switch
:
use rand::Rng; use std::cmp::Ordering; use std::io; fn main() { let secret_number = rand::thread_rng().gen_range(1..=100); println!("Please input your guess."); let mut guess = String::new(); io::stdin().read_line(&mut guess).expect("Failed to read line"); // Type conversion with error handling let guess: u32 = guess.trim().parse().expect("Please type a number!"); println!("You guessed: {guess}"); // Pattern matching (exhaustive) match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => println!("You win!"), } }
Key concepts:
- Type conversion:
parse()
returnsResult<T, E>
, notT | undefined
- Exhaustive matching: Compiler ensures all cases are handled
- No type coercion: Must explicitly convert
String
tou32
Game Loop with Robust Error Handling
use rand::Rng; use std::cmp::Ordering; use std::io; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1..=100); loop { // Infinite loop (like while(true) but more idiomatic) println!("Please input your guess."); let mut guess = String::new(); io::stdin().read_line(&mut guess).expect("Failed to read line"); // Graceful error handling instead of throwing exceptions let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => { println!("Please type a number!"); continue; // Skip to next iteration } }; println!("You guessed: {guess}"); match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You win!"); break; // Exit loop } } } }
Final Implementation
use rand::Rng; use std::cmp::Ordering; use std::io; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1..=100); loop { println!("Please input your guess."); let mut guess = String::new(); io::stdin() .read_line(&mut guess) .expect("Failed to read line"); let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; println!("You guessed: {guess}"); match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You win!"); break; } } } }
Key Rust Concepts Demonstrated
Memory Safety:
- No null pointer dereferences (no
null
orundefined
) - Borrowing prevents data races at compile time
- No manual memory management needed
Type System:
- Static typing with inference (like TypeScript but stricter)
Result<T, E>
for error handling (instead of exceptions)- Pattern matching ensures exhaustive case handling
Performance:
- Zero-cost abstractions (no runtime overhead)
- Compiled binary runs without runtime environment
- No garbage collection pauses
Error Handling:
- Explicit error handling with
Result
andOption
types match
expressions for control flow- Compile-time guarantees about error handling
This approach differs significantly from TypeScript/Node.js patterns and demonstrates Rust’s emphasis on safety, performance, and explicit error handling.