Building Terminal Snake in Rust: A Systems Programming Journey
After completing The Rust Book through Chapter 8, I decided to build something practical to internalize ownership, borrowing, and structs. The result: a fully-functional terminal Snake game using crossterm.
Architecture Overview
The game is built around four core structs:
#[derive(Clone, Copy, PartialEq)]
struct Position { x: u16, y: u16 }
enum Direction { Up, Down, Right, Left }
struct Snake {
body: Vec<Position>,
direction: Direction,
}
struct Game {
snake: Snake,
food: Position,
running: bool,
score: i32
}
Key Implementation Patterns
1. Non-Blocking Input Handling
The game loop runs at 80ms intervals, but we need to check for keyboard input without blocking:
if event::poll(Duration::from_millis(0))? {
if let Event::Key(key_event) = event::read()? {
// Process arrow keys
}
}
This pattern is crucial for game loops where the snake moves continuously even without input.
2. Snake Movement & Growth
Movement is implemented by inserting a new head and removing the tail:
snake.body.insert(0, new_head_position);
snake.body.pop(); // Remove tail (unless growing)
When food is eaten, we skip the pop(), naturally growing the snake.
3. Efficient Rendering with Buffering
Instead of flushing every draw command (O(n) terminal updates), we batch them:
for pos in &snake.body {
stdout.queue(cursor::MoveTo(pos.x, pos.y))?
.queue(style::PrintStyledContent("██".yellow()))?;
}
stdout.flush()?; // Single flush for entire frame
This eliminates flicker and improves performance.
4. Collision Detection Order Matters
A subtle bug: checking body collision after eating food caused false positives. The fix:
check_wall_collision()?;
check_body_collision()?; // ← Check BEFORE eating food
check_food_collision()?;
What I Learned
Ownership & Borrowing in Practice:
- Passing
&mut Gamevs&Gamebased on whether functions modify state - Using
.clone()strategically forPosition(cheap Copy type) - Managing mutable references across function boundaries
Pattern Matching Power:
Direction changes with validation (can't reverse into yourself):
direction = match key_event.code {
KeyCode::Up if !matches!(snake.direction, Direction::Down) => Direction::Up,
// ...
}
Memory Safety:
Zero unsafe code, no memory leaks, no buffer overflows. Rust's compiler enforced correct resource management throughout.
Technical Stack
- crossterm - Cross-platform terminal manipulation
- rand - Random food spawning
- Raw mode terminal with alternate screen buffers
What's Next?
This project was a stepping stone toward my goal of becoming an ML Systems Engineer. Next up: building production Rust backends with Axum for async ML inference serving.
🔗 GitHub: https://github.com/Halloloid/RustySnake
What game would you build to learn a new language? Drop your thoughts below!

Top comments (0)