1. Overview
Rust has been gaining attention recently due to its unique combination of performance, safety, and modern programming features. Its strict ownership model eliminates common memory issues like null pointer dereferencing and data races, providing a secure environment for developers. At the same time, its expressive syntax and focus on developer productivity make it a strong contender for systems programming. Its growing ecosystem and community continue to expand its capabilities into areas like web development and embedded systems, ensuring a confident and secure learning experience for developers.
When diving into Rust, a language lauded for its speed and safety, I wanted to start with a fun and educational project. Inspired by my experience with C and C++, I decided to learn Rust by implementing a classic snake game - a project that not only provides a fun learning experience but also reinforces an understanding of programming fundamentals. This journey has introduced me to Rust’s unique features and reinforced my understanding of programming fundamentals, engaging and motivating me to learn more.
As I embarked on this journey, I also wanted to understand how LLMs can assist in grasping Rust in terms of syntax, layout, flow, and execution aspects. These models played a crucial role in helping me debug and resolve issues, providing the necessary support and guidance in my learning process. For this, I used the following different models. Except for GPT 4o, I also want to run the others on my home GPU cluster - again, to help experiment with different aspects of inference.
- Phi 4
- Llama 3.3 70b Instruct
- Qwen 2.5B Coder 32B
- DeepSeek R1 Distill Llama 8B
- GPT 4o
1.1. LLM Performance Comparison
Model | Code Generation | Error Explanation | Documentation | Overall Experience |
---|---|---|---|---|
Phi 4 | ✔️✔️✔️ | ✔️✔️ | ✔️✔️ | Good for simpler tasks |
Llama 3.3 70b | ✔️✔️✔️✔️ | ✔️✔️✔️ | ✔️✔️✔️ | Strong but inconsistent |
Qwen 2.5B Coder 32B | ✔️✔️✔️✔️ | ✔️✔️✔️ | ✔️✔️✔️✔️ | Excellent for code examples |
DeepSeek R1 Distill 8B | ✔️✔️ | ✔️✔️ | ✔️✔️ | Limited understanding of Rust |
GPT 4o | ✔️✔️✔️✔️✔️ | ✔️✔️✔️✔️✔️ | ✔️✔️✔️✔️✔️ | Comprehensive assistance throughout |
Of course, it is not an apples-to-apples comparison. Each started well but then quickly went downhill for various reasons. The only awesome one I finally ended up using all the way through was GPT4o.
1.2. Why Rust?
Rust is designed to be a systems-level language with modern features. It eliminates common pitfalls like memory safety issues without compromising performance. As someone familiar with C and C++, I think this seems like a natural progression.
2. The Snake Game Project
/^\\/^\\
/ o o \\
( ^ )
\\_______/
| |
| |
The nostalgic, classic snake game provides a perfect playground for learning Rust’s syntax, constructs, and libraries. For the game, I started with the basic game but then soon added other things - I wanted to give the player a choice on the size of the play area (measured in number of characters as the rendering is in ASCII) and speed.
A crate in Rust is a collection of related code akin to a library in C++ or a module in Python. The project imports the following crates for different functionalities needed:
crossterm
: For terminal UI and input handling.rand
: For generating random numbers (e.g., food placement).
Let us walk through some key implementation parts and draw parallels to C and C++ for easier comprehension.
3. Key Concepts in the Code
3.1 Structs: Rust’s Version of struct
in C
Rust’s struct
is similar to struct
in C and C++ but with additional safety and functionality. Here’s how we define a Point
struct to represent 2D coordinates:
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
Parallel in C/C++:
struct Point {
int x;
int y;
};
In Rust, #[derive(...)]
automatically implements traits like Debug
(for debugging), Clone
(for copying), and PartialEq
(for comparisons).
3.2 Vectors Vec<T>
): Dynamic Arrays
Rust uses Vec<T>
for dynamically-sized arrays, similar to std::vector
in C++.
let mut snake = vec![Point { x: width / 2, y: height / 2 }];
Parallel in C++:
std::vector<Point> snake = { Point{width / 2, height / 2} };
- Unlike C++, Rust ensures safety with bounds checking and ownership rules.
3.3 Ownership and Borrowing
Rust’s ownership model ensures memory safety without a garbage collector. When working with snake
, you own or borrow the data.
Example:
let mut new_head = snake.last().unwrap().clone();
Here, .clone()
creates a deep copy of the last element to avoid ownership issues.
Parallel in C++: Copying would be explicit but lacks ownership enforcement:
Point new_head = snake.back();
3.4 Enums and Pattern Matching
Rust’s pattern matching with match
is more expressive than a switch
statement in C++.
match key_event.code {
KeyCode::Up if direction.y == 0 => next_direction = Point { x: 0, y: -1 },
KeyCode::Down if direction.y == 0 => next_direction = Point { x: 0, y: 1 },
_ => {}
}
Parallel in C++:
A switch
with additional if
conditions might approximate this but would lack the same level of elegance.
3.5 Error Handling
Rust avoids exceptions by using the Result
type for error handling, relying on explicit and predictable control flow to manage errors. This differs from other languages and ensures errors are handled explicitly rather than relying on potentially disruptive exception mechanisms, which can lead to unwieldy codebases in large systems.
fn main() -> crossterm::Result<()> {
terminal::enable_raw_mode()?;
...
terminal::disable_raw_mode()?;
Ok(())
}
The ?
operator propagates errors automatically, akin to C++’s std::optional
or manual error handling:
if (!enable_raw_mode()) return -1;
3.6 Functional Features
Rust’s iterators and closures make code concise and expressive. For instance, generating random food coordinates:
let new_food = Point {
x: rng.gen_range(1..width - 1),
y: rng.gen_range(1..height - 1),
};
Parallel in C++:
Point new_food = { rand() % (width - 2) + 1, rand() % (height - 2) + 1 };
Rust’s range syntax and rand
crate simplify random number generation.
4. Code
The complete source code for this project is available on GitHub: https://github.com/bahree/rustysnake
The game loop handles input, updates the snake’s position, checks for collisions, and renders the game state. The crossterm
library provides a simple way to handle keyboard input and terminal rendering.
loop {
// Handle input
if crossterm::event::poll(Duration::from_millis(game_speed))? {
if let Event::Key(key_event) = event::read()? {
match key_event.code {
KeyCode::Char('q') => break,
KeyCode::Up if direction.y == 0 => direction = Point { x: 0, y: -1 },
KeyCode::Down if direction.y == 0 => direction = Point { x: 0, y: 1 },
KeyCode::Left if direction.x == 0 => direction = Point { x: -1, y: 0 },
KeyCode::Right if direction.x == 0 => direction = Point { x: 1, y: 0 },
_ => {}
}
}
}
// Update snake position
let new_head = Point {
x: (snake[0].x + direction.x + width) % width,
y: (snake[0].y + direction.y + height) % height,
};
// Check collision with self
if snake.contains(&new_head) {
break;
}
snake.insert(0, new_head);
// Check if snake ate food
if snake[0] == food {
// Generate new food
loop {
let new_food = Point {
x: rng.gen_range(0..width),
y: rng.gen_range(0..height),
};
if !snake.contains(&new_food) {
food = new_food;
break;
}
}
} else {
snake.pop();
}
// ...
}
Feel free to clone, fork, or contribute to the repository!
4.1 Running the Game
To run this on Windows:
- Install Rust with
rustup
. - Add dependencies to
Cargo.toml
:[dependencies] crossterm = "0.27" rand = "0.8"
- Build and run the project:
cargo run
- Compile the project:
cargo build --release
4.2 Controls
Key | Action |
---|---|
Arrow Keys | Move the snake |
+ | Increase game speed |
- | Decrease game speed |
Spacebar | Pause/Resume the game |
q | Quit the game |
4.3 Gameplay
- Select the boundary size and difficulty from the menu.
- Use arrow keys to move the snake.
- Eat the red food (
■
) to grow your snake and increase your score. - Avoid hitting the walls (
#
) or yourself!
5. Learnings
I encountered a few things while learning Rust through this snake game project. These are called out below. As with anything else, where things are different, one must forget what one knows and learn the new way of doing things.
- Ownership model: Coming from C/C++, adjusting to Rust’s strict borrowing rules required a shift in thinking
- Terminal UI limitations: Working with crossterm’s terminal interface meant designing around ASCII graphics
- Game loop timing: Balancing responsiveness with performance required careful tuning
- Error propagation: Learning to use the
?
operator andResult
types effectively took practice
Understanding these differences was part of the learning process and helped solidify some of my understanding of Rust’s principles.
5.1 Reflecting on Rust
Building this game helped me appreciate Rust’s features:
- Memory Safety: Rust eliminates segmentation faults common in C and C++.
- Expressive Syntax: Iterators, pattern matching, and traits make Rust elegant and powerful.
- Community and Ecosystem: Libraries like
crossterm
andrand
accelerate development.
5.2 Acknowledgment
Throughout this journey, I used the assistance of LLMs to clarify Rust concepts and draw parallels to languages I already know. This collaborative approach made learning smoother and deepened my understanding by offering different perspectives. The combination of hands-on learning and real-time assistance has been invaluable.
6 Final Thoughts
Rust might feel different initially if you’re from a C or C++ background, but its safety guarantees and modern features make it worthwhile. Building a snake game is just one of many ways to start.
What will you build next? 📎