DEV Community

Cover image for Terminal Snake in Rust: A Systems Programming Journey
CH Amrut Prasad Patro
CH Amrut Prasad Patro

Posted on

Terminal Snake in Rust: A Systems Programming Journey

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
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()?;
Enter fullscreen mode Exit fullscreen mode

What I Learned

Ownership & Borrowing in Practice:

  • Passing &mut Game vs &Game based on whether functions modify state
  • Using .clone() strategically for Position (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,
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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)