Overview & Philosophy
Rust is a systems programming language built on three pillars: performance, reliability, and productivity. It competes with C and C++ in raw speed while eliminating entire classes of bugs at compile time.
No garbage collector. No runtime overhead. Compiled to native machine code. Zero-cost abstractions means you pay only for what you use.
The ownership system guarantees memory safety and thread safety without a GC. If it compiles, it almost certainly won't segfault or race.
Expressive type system, pattern matching, great tooling (Cargo), and error messages that actually help you fix the problem.
WebAssembly, OS kernels, game engines, CLI tools, networking, embedded systems, and anything that used to need C or C++.
How Rust differs from languages you know
| Concept | C / C++ | Python / JS / Java | Rust |
|---|---|---|---|
| Memory management | Manual malloc/free | Garbage collector | Ownership rules (compile-time) |
| Null pointers | Yes — undefined behavior | Yes — NullPointerException | No null; use Option<T> |
| Runtime errors | Segfaults, UB | Exceptions at runtime | Most caught at compile time |
| Concurrency safety | Data races possible | GIL / runtime checks | Compile-time thread safety |
| Abstractions | Often have cost | Runtime cost | Zero-cost (erased at compile time) |
| Package manager | None standard | pip / npm | Cargo (built-in, excellent) |
Installation & Tooling
Rust ships with the rustup toolchain manager, the rustc compiler, and Cargo — its build system and package manager. All three install together in one command.
Install (all platforms)
# macOS / Linux — run in terminal:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Windows — download rustup-init.exe from rustup.rs
# Verify installation:
rustc --version
cargo --version
Essential Cargo Commands
| Command | What it does |
|---|---|
cargo new my_project | Create a new binary project |
cargo new --lib my_lib | Create a library crate |
cargo build | Compile (debug mode) |
cargo build --release | Compile with full optimizations |
cargo run | Build and execute |
cargo test | Run all tests |
cargo check | Type-check without producing a binary (fast!) |
cargo add serde | Add a dependency from crates.io |
cargo doc --open | Build and open documentation |
cargo fmt | Auto-format your code |
cargo clippy | Lint for common mistakes and anti-patterns |
Project Structure
my_project/
├── Cargo.toml ← project manifest (dependencies, metadata)
├── Cargo.lock ← locked dependency versions (commit this!)
└── src/
├── main.rs ← entry point for binaries
└── lib.rs ← root of a library crate
Syntax Quick-Reference
A dense cheat-sheet — every construct you'll use daily, side by side with the equivalent in C/Python to make the comparison concrete.
Comments
// Single-line comment
/* Multi-line comment */
/// Doc comment (generates HTML docs for the item below)
/// Supports **Markdown**.
fn my_function() {}
Variables & Mutability
let x = 5; // immutable by default — like const in JS
let mut y = 10; // mutable — you must be explicit
const MAX: u32 = 100_000; // constant — type annotation required
// Shadowing — re-declare a name in same scope
let x = x + 1; // x is now 6, still immutable
let x = x.to_string(); // can even change type via shadowing
Primitive Types
| Category | Types | Notes |
|---|---|---|
| Signed integers | i8 i16 i32 i64 i128 isize | i32 is default |
| Unsigned integers | u8 u16 u32 u64 u128 usize | usize for indexing |
| Floats | f32 f64 | f64 is default |
| Bool | bool | true / false |
| Character | char | Unicode scalar, 4 bytes, 'A' |
| Tuple | (i32, f64, bool) | Fixed length, mixed types |
| Array | [i32; 5] | Fixed length, same type, stack-allocated |
Type Annotations & Casting
let n: i64 = 42;
let f: f32 = 3.14;
let b: bool = true;
let c: char = '🦀';
// Explicit casting with `as`
let x: i32 = 5;
let y: f64 = x as f64; // Rust never implicitly casts
Operators
| Category | Operators |
|---|---|
| Arithmetic | + - * / % |
| Comparison | == != < > <= >= |
| Logical | && || ! |
| Bitwise | & | ^ << >> ! |
| Assignment | = += -= *= /= |
| Reference | & &mut * (borrow / dereference) |
| Range | 0..5 (exclusive), 0..=5 (inclusive) |
Strings
// &str — string slice (borrowed, immutable view into string data)
let s1: &str = "Hello, world!";
// String — heap-allocated, growable
let mut s2: String = String::from("Hello");
s2.push_str(", world!");
s2.push('!'); // push a single char
// String formatting
let name = "Rustacean";
let greeting = format!("Hello, {}!", name);
// Common methods
s1.len() // byte length
s1.is_empty() // true if ""
s1.contains("lo") // substring check
s1.to_uppercase()
s1.trim() // strip whitespace
s1.split(' ') // returns an Iterator
Types & Variables
Rust uses static, strong typing with powerful inference. Once a variable has a type, it never changes — but shadowing lets you rebind a name to a new value of a different type.
Tuples
let tup: (i32, f64, bool) = (42, 3.14, true);
// Access by index
let x = tup.0; // 42
// Destructuring
let (a, b, c) = tup;
println!("a={} b={} c={}", a, b, c);
Arrays
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 10]; // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
arr[0] // first element (panics if out of bounds)
arr.len() // 5
&arr[1..3] // slice reference: [2, 3]
Type Inference in Action
// Rust infers types from context
let v = Vec::new(); // ❌ error — can't infer T yet
let v: Vec<i32> = Vec::new(); // ✅ annotate explicitly
let v = Vec::<i32>::new(); // ✅ turbofish syntax
let mut v = Vec::new();
v.push(1_i32); // ✅ inferred from first push
Control Flow
Rust's control flow features are expression-oriented — most constructs return values. This is different from C/Java where they are purely statements.
if / else if / else
let n = 7;
// if is an expression — can return a value
let label = if n % 2 == 0 { "even" } else { "odd" };
if n < 0 {
println!("negative");
} else if n == 0 {
println!("zero");
} else {
println!("positive");
}
Loops
// loop — unconditional, like while(true)
let mut count = 0;
let result = loop {
count += 1;
if count == 10 { break count * 2; } // break with a value!
};
// while loop
let mut i = 0;
while i < 5 {
print!("{} ", i);
i += 1;
}
// for loop — iterate over a range or collection
for n in 0..5 { // 0, 1, 2, 3, 4
print!("{} ", n);
}
for n in 0..=5 { // 0, 1, 2, 3, 4, 5
print!("{} ", n);
}
// Iterate over a collection
let fruits = ["apple", "banana", "cherry"];
for fruit in fruits.iter() {
println!("I like {}", fruit);
}
// Loop with index using enumerate()
for (i, fruit) in fruits.iter().enumerate() {
println!("{}: {}", i, fruit);
}
// Loop labels — break outer loops
'outer: for x in 0..5 {
for y in 0..5 {
if x + y == 6 { break 'outer; }
}
}
Functions
Functions in Rust use the fn keyword. The last expression in a function body is its return value — no return keyword needed (though you can use it for early returns).
// Basic function
fn greet(name: &str) {
println!("Hello, {}!", name);
}
// Function with return type
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon = return this expression
}
// Explicit return (for early exit)
fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 { return 0.0; }
a / b
}
// Multiple return values via tuple
fn min_max(v: &[i32]) -> (i32, i32) {
let mut min = v[0];
let mut max = v[0];
for &n in &v[1..] {
if n < min { min = n; }
if n > max { max = n; }
}
(min, max)
}
let (lo, hi) = min_max(&[3, 1, 9, 2, 7]); // lo=1, hi=9
; and don't return a value. Expressions evaluate to a value. Adding a ; to an expression turns it into a statement. This is why function bodies end without a semicolon on the return value.
Ownership
Ownership is Rust's most unique feature — it's how memory safety is achieved at compile time with no garbage collector. Master this and you master Rust.
The Three Rules
Every value in Rust has exactly one owner — a variable that "owns" the data.
There can only be one owner at a time. You can't have two variables simultaneously owning the same heap data.
When the owner goes out of scope, the value is automatically dropped (memory freed). No GC needed.
Move Semantics
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED into s2. s1 is no longer valid.
// println!("{}", s1); ❌ compile error: value borrowed after move
println!("{}", s2); // ✅ s2 owns the string now
// Primitive types (i32, f64, bool, char) implement Copy
// — they are copied rather than moved.
let x = 5;
let y = x; // x is COPIED, not moved. Both x and y are valid.
println!("{} {}", x, y); // ✅ fine
Clone — Explicit Deep Copy
let s1 = String::from("hello");
let s2 = s1.clone(); // deep copy — both are valid but independent
println!("{} and {}", s1, s2); // ✅
Ownership and Functions
fn takes_ownership(s: String) { // s is moved in
println!("{}", s);
} // s is dropped here — memory freed
fn gives_ownership() -> String {
String::from("mine now") // moved to the caller
}
let my_str = String::from("hello");
takes_ownership(my_str); // my_str is MOVED, invalid after this
let new_str = gives_ownership(); // new_str owns the returned String
Borrowing & References
Instead of transferring ownership, you can borrow a value by creating a reference. References let you use a value without taking ownership of it.
Immutable References
fn calculate_length(s: &String) -> usize {
s.len() // we borrow s — we can read but not modify it
} // s goes out of scope but is NOT dropped (we don't own it)
let s = String::from("hello");
let len = calculate_length(&s); // & creates a reference
println!("'{}' has {} bytes", s, len); // s still valid!
Mutable References
fn append_world(s: &mut String) {
s.push_str(", world!");
}
let mut s = String::from("hello");
append_world(&mut s);
println!("{}", s); // "hello, world!"
The Borrowing Rules
- You may have any number of immutable references at one time.
- OR you may have exactly one mutable reference — but not both simultaneously.
- References must always be valid (no dangling pointers).
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
// let r3 = &mut s; ❌ Cannot borrow as mutable while immutably borrowed
println!("{} {}", r1, r2); // r1 and r2 scope ends here
let r3 = &mut s; // ✅ now fine — r1, r2 are no longer in use
r3.push_str(" world");
Slices — References to Contiguous Data
let s = String::from("hello world");
let hello: &str = &s[0..5]; // string slice
let world: &str = &s[6..]; // to end
let arr = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[1..3]; // [2, 3] — array slice
Lifetimes
Lifetimes tell the compiler how long references are valid. In most cases the compiler infers them (lifetime elision). You only write explicit lifetimes when the compiler needs help relating multiple references.
// Without lifetime annotations, this would be ambiguous:
// Which reference lives longer — x or y?
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// 'a means: the returned reference lives as long as the shorter of x and y.
// Lifetimes in structs holding references
struct Excerpt<'a> {
part: &'a str,
}
let text = String::from("Call me Ishmael. Some years ago...");
let first_sentence = text.split('.').next().unwrap();
let e = Excerpt { part: first_sentence };
// e.part cannot outlive `text`
Structs
Structs group related fields together. They're Rust's primary mechanism for creating custom data types — similar to classes, but without inheritance.
Defining and Using Structs
// Define a struct
struct Player {
name: String,
health: u32,
level: u32,
is_alive: bool,
}
// Instantiate
let mut hero = Player {
name: String::from("Ferris"),
health: 100,
level: 1,
is_alive: true,
};
// Access and modify fields
hero.health -= 20;
println!("{} has {} HP", hero.name, hero.health);
// Struct update syntax (like spread in JS)
let hero2 = Player {
name: String::from("Ferris Jr."),
level: 5,
..hero // copy remaining fields from hero
};
Methods with impl
impl Player {
// Associated function (constructor pattern)
fn new(name: &str) -> Self {
Player {
name: name.to_string(),
health: 100,
level: 1,
is_alive: true,
}
}
// Method (takes self by reference)
fn describe(&self) {
println!("[Lv{}] {} — {} HP", self.level, self.name, self.health);
}
// Mutable method (takes self by mutable reference)
fn take_damage(&mut self, dmg: u32) {
self.health = self.health.saturating_sub(dmg);
if self.health == 0 { self.is_alive = false; }
}
// Consuming method (takes ownership)
fn retire(self) -> String {
format!("{} has retired!", self.name)
}
}
let mut p = Player::new("Ferris");
p.describe();
p.take_damage(30);
p.describe();
Tuple Structs and Unit Structs
// Tuple struct — named tuple type
struct Color(u8, u8, u8);
let red = Color(255, 0, 0);
println!("R={}", red.0);
// Unit struct — no fields, used for traits
struct AlwaysReady;
let _ = AlwaysReady;
Enums & Option<T>
Rust's enums are far more powerful than C enums — each variant can hold different types and amounts of data. They're algebraic data types.
Basic Enums
enum Direction { North, South, East, West }
let dir = Direction::North;
match dir {
Direction::North => println!("Going north!"),
Direction::South => println!("Going south!"),
_ => println!("Going somewhere else"),
}
Rich Enums — Variants with Data
enum Message {
Quit, // no data
Move { x: i32, y: i32 }, // named fields
Write(String), // single String
ChangeColor(u8, u8, u8), // three u8s
}
fn handle(msg: Message) {
match msg {
Message::Quit => println!("Quitting"),
Message::Move { x, y } => println!("Move ({},{})", x, y),
Message::Write(text) => println!("Write: {}", text),
Message::ChangeColor(r, g, b) => println!("Color ({},{},{})", r, g, b),
}
}
Option<T> — Replacing null
// Option is defined as: enum Option<T> { Some(T), None }
let some_number: Option<i32> = Some(42);
let no_number: Option<i32> = None;
// Using Option values
if let Some(n) = some_number {
println!("Got: {}", n);
}
let value = some_number.unwrap_or(0); // 42
let value = no_number.unwrap_or(0); // 0
let value = some_number.unwrap_or_else(|| compute_default());
let doubled = some_number.map(|n| n * 2); // Some(84)
Pattern Matching
match is one of Rust's crown jewels. It's exhaustive — the compiler forces you to handle every case — and can destructure complex data in one line.
match
let x: i32 = 7;
match x {
1 => println!("one"),
2 | 3 => println!("two or three"),
4..=6 => println!("four to six"),
n if n % 2 == 0 => println!("{} is even", n), // guard
n => println!("other: {}", n), // catch-all binding
}
// match on a struct
struct Point { x: i32, y: i32 }
let p = Point { x: 0, y: 7 };
match p {
Point { x: 0, y } => println!("on y-axis at {}", y),
Point { x, y: 0 } => println!("on x-axis at {}", x),
Point { x, y } => println!("at ({}, {})", x, y),
}
if let — Single-arm match
let config = Some("dark_mode");
// Verbose match:
match config {
Some(val) => println!("Setting: {}", val),
None => {},
}
// Concise if let:
if let Some(val) = config {
println!("Setting: {}", val);
}
while let
let mut stack = Vec::new();
stack.push(1); stack.push(2); stack.push(3);
while let Some(top) = stack.pop() {
println!("{}", top); // prints 3, 2, 1
}
Traits
Traits define shared behavior — they're similar to interfaces in Java/Go, or abstract base classes in Python. They're the foundation of Rust's polymorphism.
// Define a trait
trait Describable {
fn describe(&self) -> String;
// Default method — can be overridden
fn print_description(&self) {
println!("{}", self.describe());
}
}
struct Car { make: String, year: u32 }
struct Bike { brand: String }
impl Describable for Car {
fn describe(&self) -> String {
format!("{} ({})", self.make, self.year)
}
}
impl Describable for Bike {
fn describe(&self) -> String {
format!("Bike: {}", self.brand)
}
}
// Trait bounds — accept any type implementing Describable
fn print_item(item: &impl Describable) { // impl Trait syntax
item.print_description();
}
// Generic form (equivalent)
fn print_item<T: Describable>(item: &T) {
item.print_description();
}
Important Standard Traits
| Trait | Purpose | Note |
|---|---|---|
Display | println!("{}", x) | Human-readable output |
Debug | println!("{:?}", x) | Debugging output — can derive |
Clone | x.clone() | Explicit deep copy — can derive |
Copy | Implicit copy on assignment | Only for stack types — can derive |
PartialEq | == operator | Can derive |
Ord / PartialOrd | Ordering / comparison | Can derive |
Default | T::default() | Zero-value constructor |
Iterator | Iteration protocol | Implement next() |
// Deriving common traits automatically
#[derive(Debug, Clone, PartialEq)]
struct Point { x: f64, y: f64 }
let p = Point { x: 1.0, y: 2.0 };
println!("{:?}", p); // Point { x: 1.0, y: 2.0 }
let p2 = p.clone();
println!("{}", p == p2); // true
Generics
Generics let you write code that works with many different types while keeping all type-safety. Rust generics are monomorphized — the compiler creates specialized versions for each type used, so there's zero runtime cost.
// Generic function
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest { largest = item; }
}
largest
}
println!("{}", largest(&[34, 50, 25, 100])); // 100
println!("{}", largest(&['y', 'm', 'a'])); // y
// Generic struct
struct Pair<T> { first: T, second: T }
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.first >= self.second {
println!("first is larger: {}", self.first);
} else {
println!("second is larger: {}", self.second);
}
}
}
Error Handling
Rust has no exceptions. Instead it uses the Result<T, E> type for recoverable errors and panic! for unrecoverable ones. This forces you to think about every error path.
Result<T, E>
// Result is: enum Result<T, E> { Ok(T), Err(E) }
use std::num::ParseIntError;
fn parse_and_double(s: &str) -> Result<i32, ParseIntError> {
let n: i32 = s.parse()?; // ? propagates the error if Err
Ok(n * 2)
}
match parse_and_double("21") {
Ok(n) => println!("Result: {}", n), // 42
Err(e) => println!("Error: {}", e),
}
The ? Operator
use std::fs;
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
// ? unwraps Ok or returns Err early — equivalent to:
// match result { Ok(v) => v, Err(e) => return Err(e) }
let content = fs::read_to_string("username.txt")?;
Ok(content.trim().to_string())
}
Common Result Methods
let r: Result<i32, &str> = Ok(42);
r.unwrap() // get value or panic
r.unwrap_or(0) // get value or default
r.unwrap_or_else(|e| 0) // get value or compute default
r.expect("Failed to get n") // unwrap with custom panic message
r.is_ok() // true
r.is_err() // false
r.map(|n| n * 2) // Ok(84) — transform Ok value
r.map_err(|e| e.len()) // transform Err value
Custom Error Types
use std::fmt;
#[derive(Debug)]
enum AppError {
IoError(std::io::Error),
ParseError(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::IoError(e) => write!(f, "IO error: {}", e),
AppError::ParseError(s) => write!(f, "Parse error: {}", s),
}
}
}
Collections
The standard library provides three key heap-allocated collections: Vec<T>, HashMap<K,V>, and HashSet<T>.
Vec<T> — Dynamic Array
let mut v: Vec<i32> = Vec::new();
let mut v = vec![1, 2, 3]; // macro shorthand
v.push(4);
v.pop(); // returns Option<T>
v.insert(1, 10); // insert at index 1
v.remove(1); // remove at index 1
v.len();
v.is_empty();
v.contains(&3);
v.sort();
v.sort_by(|a, b| b.cmp(a)); // reverse sort
v.iter().sumi32>() // sum all elements
// Safe access
match v.get(10) {
Some(val) => println!("{}", val),
None => println!("out of bounds"),
}
HashMap<K, V>
use std::collections::HashMap;
let mut scores: HashMap<String, u32> = HashMap::new();
scores.insert(String::from("Alice"), 100);
scores.insert(String::from("Bob"), 85);
// Only insert if key doesn't exist
scores.entry(String::from("Alice")).or_insert(50); // no-op
scores.entry(String::from("Carol")).or_insert(75); // inserts 75
// Access
if let Some(score) = scores.get("Alice") {
println!("Alice: {}", score);
}
// Iterate
for (name, score) in &scores {
println!("{}: {}", name, score);
}
// Word frequency counter pattern
let text = "hello world hello rust hello";
let mut freq: HashMap<&str, u32> = HashMap::new();
for word in text.split_whitespace() {
let count = freq.entry(word).or_insert(0);
*count += 1; // dereference to modify
}
// freq: {"hello": 3, "world": 1, "rust": 1}
Closures & Iterators
Closures are anonymous functions that can capture their environment. The iterator pattern in Rust is lazy and composable — chains of iterator adapters produce no intermediate allocations.
Closures
// Basic closure syntax
let add = |a, b| a + b;
println!("{}", add(3, 4)); // 7
// Closures capture their environment
let threshold = 5;
let is_big = |n| n > threshold; // captures threshold
println!("{}", is_big(10)); // true
// move closure — takes ownership of captured values
let name = String::from("world");
let greeting = move || println!("Hello, {}!", name);
greeting(); // name is moved into the closure
Iterator Adapter Chain
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result: Vec<i32> = numbers
.iter()
.filter(|&&n| n % 2 == 0) // keep evens: [2,4,6,8,10]
.map(|&n| n * n) // square: [4,16,36,64,100]
.take(3) // first 3: [4,16,36]
.collect(); // materialize into Vec
// Common terminal operations:
let sum: i32 = numbers.iter().sum();
let product: i32 = numbers.iter().product();
let max = numbers.iter().max(); // Option<&i32>
let count = numbers.iter().count();
// find / any / all
let first_even = numbers.iter().find(|&&n| n % 2 == 0);
let has_big = numbers.iter().any(|&n| n > 5);
let all_pos = numbers.iter().all(|&n| n > 0);
// flat_map, zip, chain, enumerate
let words = vec!["hello world", "foo bar"];
let all_words: Vec<&str> = words.iter()
.flat_map(|s| s.split(' '))
.collect(); // ["hello", "world", "foo", "bar"]
Modules & Crates
Rust organizes code into modules (namespaces within a crate) and crates (compilation units/packages). Everything is private by default.
// src/lib.rs or src/main.rs
mod animals {
// pub = visible outside the module
pub struct Dog {
pub name: String,
age: u32, // private field
}
impl Dog {
pub fn new(name: &str, age: u32) -> Dog {
Dog { name: name.to_string(), age }
}
pub fn bark(&self) { println!("Woof! I'm {}", self.name); }
}
mod internals { // nested module
pub fn helper() {}
use super::*; // access parent module
}
}
// Using the module
use animals::Dog; // bring into scope
let d = Dog::new("Rex", 3);
d.bark();
// External crates — add to Cargo.toml first:
// [dependencies]
// rand = "0.8"
use rand::Rng;
let n: u32 = rand::thread_rng().gen_range(1..=100);
File-based modules
// src/main.rs
mod utils; // loads from src/utils.rs OR src/utils/mod.rs
use utils::some_function;
Hello, World!
The obligatory first program. Even this simple example reveals several Rust fundamentals worth understanding.
main.rsfn main() {
println!("Hello, world!");
}
What's happening here?
fn main()— the program entry point. Every Rust binary must have exactly one.println!— note the!: this is a macro, not a function. Macros are identified by the exclamation mark and are expanded at compile time.- Statements end with
;. - No return type on
mainmeans it returns()— the unit type, Rust's equivalent ofvoid.
Format String Variants
let name = "Ferris";
let age = 3;
println!("Hello, {}! You are {} years old.", name, age);
println!("Hello, {name}! You are {age} years old."); // named args
println!("{:?}", (name, age)); // debug format
println!("{:#?}", (name, age)); // pretty debug
println!("{:>10}", name); // right-align in 10 chars
println!("{:05}", age); // zero-pad: "00003"
println!("{:.2}", 3.14159); // 2 decimal places: "3.14"
eprintln!("Error: something bad"); // print to stderr
Beginner Project: Adventure RPG Engine
This single-file program is a text-based mini RPG that exercises ownership, structs, enums, traits, pattern matching, closures, iterators, error handling, and collections — all in a cohesive, runnable project.
Structs & impl blocks · Enums with data · Traits (Display, Debug, custom) · Pattern matching · Vec and HashMap · Closures & iterators · Option and Result · Loops · Format strings · rand crate usage · Modules-in-miniature
Setup
Create the project
cargo new rust_rpg
cd rust_rpg
Add the rand dependency in Cargo.toml
[dependencies]
rand = "0.8"
Replace src/main.rs with the full program below
Run it
cargo run
Full Source — src/main.rs
main.rs// ═══════════════════════════════════════════════════════════
// Rust RPG Engine — A comprehensive beginner Rust project
// ═══════════════════════════════════════════════════════════
use rand::Rng;
use std::collections::HashMap;
use std::fmt;
// ── SECTION 1: ENUMS WITH DATA ───────────────────────────
// Rust enums can carry different data per variant — this is
// what makes them "algebraic data types".
#[derive(Debug, Clone, PartialEq)]
enum Element { Fire, Ice, Lightning, Physical }
#[derive(Debug, Clone)]
enum Item {
Potion { heal: u32 }, // named fields
Weapon { name: String, damage: u32, element: Element },
KeyItem(String), // tuple variant
}
// Implement Display for our Item enum
impl fmt::Display for Item {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Item::Potion { heal }
=> write!(f, "Potion (+{} HP)", heal),
Item::Weapon { name, damage, element }
=> write!(f, "{} [{:?}] ({}dmg)", name, element, damage),
Item::KeyItem(s)
=> write!(f, "★ {}", s),
}
}
}
// ── SECTION 2: TRAIT DEFINITION ─────────────────────────
// Traits define shared behavior. Any type can implement
// this trait to participate in combat.
trait Combatant {
fn name(&self) -> &str;
fn health(&self) -> u32;
fn max_health(&self) -> u32;
fn attack_power(&self) -> u32;
fn take_damage(&mut self, amount: u32);
fn is_alive(&self) -> bool { self.health() > 0 }
// Default method using other trait methods
fn health_bar(&self) -> String {
let ratio = self.health() as f32 / self.max_health() as f32;
let filled = (ratio * 20.0) as usize;
let empty = 20.saturating_sub(filled);
format!("[{}{}] {}/{}",
"█".repeat(filled),
"░".repeat(empty),
self.health(), self.max_health())
}
}
// ── SECTION 3: STRUCTS AND impl BLOCKS ──────────────────
// Hero struct — owns heap data (String, Vec, HashMap)
#[derive(Debug)]
struct Hero {
name: String,
hp: u32,
max_hp: u32,
level: u32,
xp: u32,
gold: u32,
base_atk: u32,
inventory: Vec<Item>,
equipped: Option<Item>, // Option = might have a weapon
kill_log: HashMap<String, u32>, // tracks kills per monster type
}
impl Hero {
// Associated function (constructor) — no `self`
fn new(name: &str) -> Hero {
Hero {
name: name.to_string(),
hp: 100, max_hp: 100,
level: 1, xp: 0,
gold: 50, base_atk: 10,
inventory: vec![
Item::Potion { heal: 30 },
Item::Potion { heal: 30 },
],
equipped: None,
kill_log: HashMap::new(),
}
}
// Heal the hero — saturating arithmetic prevents overflow
fn heal(&mut self, amount: u32) {
self.hp = (self.hp + amount).min(self.max_hp);
println!(" ✨ {} healed for {} HP! ({})",
self.name, amount, self.health_bar());
}
// Try to use a potion from inventory
// Returns Result to demonstrate error handling
fn use_potion(&mut self) -> Result<(), String> {
// find_position of a Potion in inventory using iterator
let pos = self.inventory
.iter()
.position(|item| matches!(item, Item::Potion { .. }));
match pos {
None => Err("No potions in inventory!".to_string()),
Some(i) => {
if let Item::Potion { heal } = self.inventory.remove(i) {
self.heal(heal);
}
Ok(())
}
}
}
// Equip a weapon from inventory by name
fn equip_weapon(&mut self, weapon_name: &str) -> Result<(), String> {
let pos = self.inventory
.iter()
.position(|item| matches!(item,
Item::Weapon { name, .. } if name == weapon_name));
match pos {
None => Err(format!("No weapon named '{}' in inventory", weapon_name)),
Some(i) => {
let weapon = self.inventory.remove(i);
println!(" ⚔ Equipped: {}", weapon);
self.equipped = Some(weapon);
Ok(())
}
}
}
// Gain experience and level up if threshold reached
fn gain_xp(&mut self, amount: u32) {
self.xp += amount;
let xp_needed = self.level * 100;
if self.xp >= xp_needed {
self.xp -= xp_needed;
self.level += 1;
self.max_hp += 20;
self.hp = self.max_hp;
self.base_atk += 5;
println!(" 🌟 LEVEL UP! {} is now level {}! HP restored.",
self.name, self.level);
}
}
// Record a kill in our HashMap
fn record_kill(&mut self, monster: &str) {
let count = self.kill_log.entry(monster.to_string()).or_insert(0);
*count += 1;
}
// Print kill log using iterator methods
fn print_kill_log(&self) {
println!("\n 📜 Kill Log:");
if self.kill_log.is_empty() {
println!(" (none yet)");
return;
}
// Sort entries by kill count descending — closures + iterators
let mut entries: Vec<(&String, &u32)> = self.kill_log.iter().collect();
entries.sort_by(|a, b| b.1.cmp(a.1));
for (monster, count) in &entries {
println!(" {:<20} ×{}", monster, count);
}
let total: u32 = self.kill_log.values().sum();
println!(" Total kills: {}", total);
}
// Show full status screen
fn status(&self) {
println!("\n╔═══════════ HERO STATUS ═══════════╗");
println!("║ Name: {}", self.name);
println!("║ Level: {} XP: {}/{}",
self.level, self.xp, self.level * 100);
println!("║ HP: {}", self.health_bar());
println!("║ ATK: {}", self.attack_power());
println!("║ Gold: {} gp", self.gold);
println!("║ Weapon:{}",
self.equipped.as_ref()
.map(|w| format!(" {}", w))
.unwrap_or_else(|| " (none)".to_string()));
// Iterator: count potions with filter
let potion_count = self.inventory.iter()
.filter(|i| matches!(i, Item::Potion { .. }))
.count();
println!("║ Bag: {} item(s), {} potion(s)",
self.inventory.len(), potion_count);
println!("╚════════════════════════════════════╝");
}
}
// Implement the Combatant trait for Hero
impl Combatant for Hero {
fn name(&self) -> &str { &self.name }
fn health(&self) -> u32 { self.hp }
fn max_health(&self) -> u32 { self.max_hp }
fn attack_power(&self) -> u32 {
// Base attack + equipped weapon bonus, using Option::map
let weapon_bonus = self.equipped.as_ref()
.and_then(|item| {
if let Item::Weapon { damage, .. } = item {
Some(*damage)
} else { None }
})
.unwrap_or(0);
self.base_atk + weapon_bonus
}
fn take_damage(&mut self, amount: u32) {
self.hp = self.hp.saturating_sub(amount);
}
}
// ── SECTION 4: MONSTER STRUCT ────────────────────────────
#[derive(Debug, Clone)]
struct Monster {
name: String,
hp: u32,
max_hp: u32,
attack: u32,
xp_reward: u32,
gold_drop: u32,
element: Element,
loot: Option<Item>, // Some monsters drop loot
}
impl Combatant for Monster {
fn name(&self) -> &str { &self.name }
fn health(&self) -> u32 { self.hp }
fn max_health(&self) -> u32 { self.max_hp }
fn attack_power(&self) -> u32 { self.attack }
fn take_damage(&mut self, amount: u32) {
self.hp = self.hp.saturating_sub(amount);
}
}
// ── SECTION 5: MONSTER CATALOG ───────────────────────────
// A function returning a Vec of all available monsters.
// Demonstrates struct initialization and Vec construction.
fn monster_catalog() -> Vec<Monster> {
vec![
Monster {
name: "Goblin Scout".into(),
hp: 30, max_hp: 30,
attack: 8,
xp_reward: 40, gold_drop: 15,
element: Element::Physical,
loot: None,
},
Monster {
name: "Fire Imp".into(),
hp: 45, max_hp: 45,
attack: 14,
xp_reward: 65, gold_drop: 20,
element: Element::Fire,
loot: Some(Item::Weapon {
name: "Ember Blade".into(),
damage: 18,
element: Element::Fire,
}),
},
Monster {
name: "Ice Wraith".into(),
hp: 60, max_hp: 60,
attack: 11,
xp_reward: 80, gold_drop: 30,
element: Element::Ice,
loot: Some(Item::Potion { heal: 50 }),
},
Monster {
name: "Storm Dragon".into(),
hp: 120, max_hp: 120,
attack: 22,
xp_reward: 200, gold_drop: 100,
element: Element::Lightning,
loot: Some(Item::KeyItem("Dragon's Heart".into())),
},
]
}
// ── SECTION 6: COMBAT ENGINE ─────────────────────────────
// Generic over any type that implements Combatant.
// Returns true if the attacker wins (target dies).
fn combat_round<A: Combatant, D: Combatant>(
attacker: &A,
defender: &mut D,
rng: &mut impl rand::Rng,
) -> bool {
// 10% miss chance using random number
if rng.gen_bool(0.1) {
println!(" ✗ {} missed!", attacker.name());
return false;
}
// Damage variance: base ± 20%
let base = attacker.attack_power();
let damage = rng.gen_range((base * 80 / 100)..=(base * 120 / 100));
defender.take_damage(damage);
println!(" ⚔ {} hits {} for {} damage! {}",
attacker.name(), defender.name(), damage,
defender.health_bar());
!defender.is_alive()
}
// Full battle loop
fn battle(hero: &mut Hero, monster: &mut Monster,
rng: &mut impl rand::Rng) -> bool {
println!("\n🗡 BATTLE: {} vs {}!", hero.name(), monster.name());
println!(" Monster HP: {}", monster.health_bar());
let mut turn = 0;
loop {
turn += 1;
println!("\n — Turn {} —", turn);
// Hero attacks first each turn
if combat_round(hero, monster, rng) {
println!(" 💀 {} was defeated!", monster.name());
break;
}
// Monster counterattack
// NOTE: combat_round borrows hero as mutable — monster as immutable
let monster_atk = monster.attack_power();
let monster_name = monster.name().to_string();
if rng.gen_bool(0.1) {
println!(" ✗ {} missed!", monster_name);
} else {
let dmg = rng.gen_range(
(monster_atk * 80 / 100)..=(monster_atk * 120 / 100));
hero.take_damage(dmg);
println!(" ⚔ {} hits {} for {} damage! {}",
monster_name, hero.name(), dmg, hero.health_bar());
}
if !hero.is_alive() {
println!(" 💀 {} has fallen!", hero.name());
return false; // hero lost
}
// Hero auto-uses potion if HP below 30%
let hp_pct = hero.hp as f32 / hero.max_hp as f32;
if hp_pct < 0.3 {
match hero.use_potion() {
Ok(()) => {},
Err(e) => println!(" ⚠ {}", e),
}
}
}
true // hero won
}
// ── SECTION 7: MAIN GAME LOOP ────────────────────────────
fn main() {
let mut rng = rand::thread_rng();
println!("╔══════════════════════════════════════╗");
println!("║ ⚔ RUST RPG ENGINE ⚔ ║");
println!("║ A Comprehensive Rust Demo Program ║");
println!("╚══════════════════════════════════════╝\n");
// Struct construction via associated function
let mut hero = Hero::new("Ferris");
println!("Welcome, {}! Your adventure begins...", hero.name());
// Get the monster catalog (Vec<Monster>)
let catalog = monster_catalog();
// Print available enemies using iterator + formatting
println!("\n📖 You will face these enemies:");
for (i, m) in catalog.iter().enumerate() {
println!(" {}. {} [{:?}] — {} HP",
i + 1, m.name, m.element, m.max_hp);
}
// Show starting status
hero.status();
// Fight each monster in sequence — clone so we own the instance
for template in &catalog {
let mut monster = template.clone();
let hero_won = battle(&mut hero, &mut monster, &mut rng);
if !hero_won {
println!("\n💀 GAME OVER — {} was defeated.", hero.name());
hero.print_kill_log();
return;
}
// Victory — award loot using pattern matching on Option
hero.gain_xp(monster.xp_reward);
hero.gold += monster.gold_drop;
println!(" 🏆 Earned {} XP and {} gold!",
monster.xp_reward, monster.gold_drop);
// Pattern match on Option<Item>
if let Some(item) = monster.loot.take() {
println!(" 🎁 Loot dropped: {}", item);
match &item {
Item::Weapon { name, .. } => {
println!(" Auto-equipping {}...", name);
hero.inventory.push(item);
let wname = name.clone();
match hero.equip_weapon(&wname) {
Ok(()) => {},
Err(e) => println!(" ⚠ {}", e),
}
}
_ => { hero.inventory.push(item); }
}
}
hero.record_kill(&monster.name);
hero.status();
}
// ── VICTORY SCREEN ──────────────────────────────────────
println!("\n\n🎉 VICTORY! {} has conquered all enemies!", hero.name());
println!("═══════════════════════════════════════════");
hero.print_kill_log();
// Final stats with iterator methods
let total_kills: u32 = hero.kill_log.values().sum();
let total_monsters = catalog.len();
let pct = total_kills as f64 / total_monsters as f64 * 100.0;
println!("\n Final level: {}", hero.level);
println!(" Final gold: {} gp", hero.gold);
println!(" Completion: {:.1}%", pct);
// Demonstrate closures in inventory summary
println!("\n Final Inventory:");
if hero.inventory.is_empty() {
println!(" (empty)");
} else {
hero.inventory.iter().for_each(|i| println!(" • {}", i));
}
println!("\n Thanks for playing Rust RPG!");
println!(" Written in 🦀 Rust.");
}
Expected Output
Concepts Demonstrated — Cross-Reference
| Rust Concept | Where in the Program |
|---|---|
| Structs + impl | Hero, Monster + all their methods |
| Enums with data | Item, Element variants |
| Trait definition | trait Combatant |
| Trait implementation | impl Combatant for Hero/Monster |
| Default trait methods | health_bar(), is_alive() |
| Generics | fn combat_round<A: Combatant, D: Combatant> |
| Ownership + move | monster.loot.take(), function args |
Borrowing (&, &mut) | Every function parameter in combat |
| Option<T> | equipped, loot, find_position returns |
| Result<T, E> | use_potion(), equip_weapon() |
| Pattern matching | match on Item, Option, Result |
| if let | Loot handling, potion usage |
| Vec<T> | inventory, catalog |
| HashMap<K,V> | kill_log |
| Closures | filter, map, sort_by, for_each |
| Iterators | iter(), position(), filter(), sum(), enumerate() |
| Derive macros | #[derive(Debug, Clone, PartialEq)] |
| Display trait | impl fmt::Display for Item |
| Saturating arithmetic | saturating_sub(), .min() |
| External crates | rand crate for RNG |
| Format strings | format!, println! with {:?}, {:.1} |
| loop / for / break | Combat loop, monster iteration |