🐍 Rust Snake Game Overview: What You’ll Learn and Build

This rust snake game overview introduces you to the goals, skills, and real-world value of building a terminal-based snake game using Rust in 2025.
Table of Contents
🧠 What Is the Rust Snake Game?
The Rust Snake Game is a beginner-friendly coding project where you recreate the classic “Snake” game using the Rust programming language — all within your terminal. Unlike modern GUI games, this one runs directly in your command-line interface using crossterm
, giving you low-level control over input and output.
While the gameplay itself is simple, the process of building this game will expose you to powerful real-world skills: handling user input, working with game loops, drawing graphics in a terminal, and writing maintainable Rust code with enums, vectors, and logic structures.
🎯 Why This Game Is the Perfect First Project
If you’re new to Rust—or even programming in general—building a small but complete project like this is one of the most effective ways to transform theoretical knowledge into real, applied skill. Instead of passively consuming tutorials, you’ll actively build, design, test, and finish a fully functioning game from scratch. That’s what makes this Rust Snake Game overview more than just a how-to guide—it’s a practical roadmap to thinking like a developer and solving problems like one.
Before you dive in, we recommend starting with these two short Rust simulations to warm up your logic and modular thinking:
- ☕️ Rust Coffee Vending Machine Simulator: Learn control flow, match statements, and
struct
design in a hands-on, beginner-friendly way. - 🗺️ Rust MUD Game Map System: Explore text-based movement in a grid-based world—skills that directly translate to Snake logic.
These foundational projects are quick, fun, and directly relevant to what you’re about to build here.
💡 Why You Should Build This Game
✅ Finishable in a weekend – The scope is just right: challenging enough to learn, small enough to complete.
✅ Teaches real-time logic – You’ll implement a real game loop, input handling, and frame-based updates.
✅ Covers key Rust concepts – Enums, match expressions, VecDeque
, modules, and error handling are all built in.
✅ Hands-on practice – You’ll learn through doing: testing, debugging, refactoring, and polishing.
✅ No frameworks required – It runs directly in your terminal using lightweight crates like crossterm
.
✅ Expandable to GUI – Once you’re done, you can take the same logic and upgrade it to a graphical version using macroquad
or ggez
.
✅ Portfolio-worthy – You’ll walk away with a clean, modular Rust project you can proudly share on GitHub or your resume.
✅ Actually fun – Seeing your snake slither across the screen in a game you built yourself is satisfying in a way no tutorial ever is.
🧱 What You’ll Build, Exactly
- A grid-based snake that grows when it eats
- Keyboard-controlled movement (arrow keys)
- Randomly spawning food
- Game-over logic when hitting walls or self
- Terminal rendering using
crossterm
- Queue-based movement system using
VecDeque
- Clean, modular codebase ready for expansion
📌 Technologies Used
- Language: Rust (2021 edition or later)
- Crates:
🧭 Next Steps
In the next section, we’ll discuss why building this snake game in Rust is more than a fun exercise—it’s a valuable foundation for serious Rust development.
Stay tuned for Section 2: Why Build a Snake Game in Rust?
🎮 Why Build a Snake Game in Rust? A Beginner-Friendly Project
Wondering why build snake game rust projects like this one? Learn how a terminal-based snake game can teach you real-world Rust skills, fast and effectively.
💡 Why This Simple Game Is Surprisingly Powerful
It might look like just a fun little arcade game, but building a Snake Game in Rust is one of the most effective ways to truly learn the language.
Rather than memorizing syntax or blindly following exercises, you’ll be applying Rust to solve real problems: user input, movement logic, collisions, and more.
So, why build a snake game in Rust? Because it teaches you how to think like a systems programmer—while building something enjoyable and visual.
🚀 5 Reasons to Build This Snake Game in Rust
1. It Teaches Real-Time Logic
Game development forces you to think about time, state, and changes per second. You’ll build a live game loop and respond to player input in real time.
2. You Learn State Management
Keeping track of the snake’s body, direction, food, and collisions teaches you to manage complex state transitions using data structures like VecDeque
.
3. You Use Rust for Something Real
Instead of toy problems or isolated code snippets, this project gives you practical experience with Rust’s syntax, ownership model, and enums.
4. You Experience the Terminal Like a Pro
Using the crossterm
crate, you’ll learn how to draw and clear screens, move cursors, and control user interactions—all in the terminal, without a GUI.
5. It’s Small But Complete
Unlike massive open-ended projects, a Snake Game is easy to finish, but complex enough to teach core concepts like rendering, loops, and event handling.
🧠 What You’ll Take Away
By the end of this project, you’ll be able to:
- Build a real-time terminal app from scratch
- Use Rust crates and external libraries confidently
- Structure and modularize your code for readability and reuse
- Understand how systems programming applies to interactive apps
✅ Summary
The Snake Game isn’t just retro—it’s a rite of passage for developers learning any language. And in Rust, it becomes a powerful mini-lab where you master performance, safety, and design patterns in one go.
So, why build snake game rust tutorials like this one?
Because they actually make you a better, faster, and more confident developer.
🔜 Up Next
Now that you know why this project is worth your time, let’s move on to the next step:
Section 3: Core Concepts and Learning Goals

🧠 Core Concepts and Learning Goals for Rust Snake Game
This section breaks down the rust game development basics you’ll learn through this project—like real-time input, data structures, and clean code architecture.
🎓 What Will You Learn from This Project?
The Rust Snake Game is more than just a nostalgic remake. It’s a learning playground designed to give you hands-on experience with some of the most important concepts in Rust game development basics.
Whether you’re new to Rust or programming in general, this project walks you through real development practices: logic, structure, and code that runs live in the terminal.
🧱 Core Concepts Covered
Here’s a breakdown of the technical and conceptual skills you’ll develop:
1. Game Loop Logic
Learn how games update continuously by using a loop that redraws the screen and listens for player input. This is the foundation of any interactive application.
2. State Management
Track and manage game state, including the snake’s body, direction, score, and food. You’ll use Rust’s vectors and enums to structure your data clearly.
3. Real-Time Input Handling
Capture keyboard input without blocking program flow using the crossterm
library. You’ll implement responsive controls using non-blocking I/O.
4. Coordinate-Based Movement
Handle movement in a 2D grid by updating x/y positions per frame. You’ll apply math and conditional logic to move your snake naturally.
5. Collision Detection
Implement simple yet powerful algorithms to detect when the snake hits a wall, eats food, or crashes into itself.
6. Randomness
Use the rand
crate to generate random positions for food and keep the gameplay dynamic and unpredictable.
7. Clean Code Architecture
You’ll begin with a single file, then learn how to modularize your game logic into reusable, testable pieces. This is essential for scaling and readability.
🛠️ Tools and Libraries You’ll Use
Tool | Purpose |
---|---|
Rust | The main programming language |
crossterm | Terminal screen control, input, and rendering |
rand | Random number generation for food placement |
VecDeque | Efficient snake body movement using queue behavior |
🚀 Learning Goals Recap
By completing this Rust Snake Game, you’ll walk away with:
- A working knowledge of Rust game development basics
- The ability to build interactive terminal applications
- Confidence using third-party crates
- Practical skills in real-time logic and design
- A structured approach to organizing and scaling Rust code
🔜 Coming Up Next
Now that we’ve outlined what you’ll learn, it’s time to get started. In the next section, we’ll walk through your Rust project setup step by step.
→ Section 4: Rust Project Setup for Your Terminal Snake Game
🔧 Rust Project Setup for Your Terminal Snake Game
Follow this rust project setup guide to create your terminal-based snake game from scratch. Learn how to initialize a Rust project and install essential crates.
📁 Step 1: Create a New Rust Project
Open your terminal and run the following command to create a new Rust project:
cargo new snake_game
cd snake_game
This creates a new folder called snake_game
with the following basic structure:
bashsnake_game/
├── Cargo.toml # Project configuration and dependencies
└── src/
└── main.rs # Your Rust code starts here
You now have a working Rust project scaffold. Let’s make it game-ready.
🧩 Step 2: Add Dependencies to Cargo.toml
Open the Cargo.toml
file and under [dependencies]
, add these libraries:
[dependencies]
crossterm = "0.26"
rand = "0.8"
✅ What These Crates Do:
crossterm
: A powerful library that lets you control the terminal—move the cursor, detect keypresses, and clear/redraw the screen.rand
: A standard crate for generating random numbers, used for placing food in the game.
You can explore them on crates.io:
After saving the file, download the dependencies:
cargo build
📦 Step 3: Understand Your Project Structure
Your game logic will be written in main.rs
at first, but eventually, you may break it into modules for readability.
Here’s a preview of how you might structure it later:
src/
├── main.rs # Entry point, runs game loop
├── game.rs # Handles game state and logic
├── snake.rs # Controls snake movement and growth
├── input.rs # Processes keyboard input
└── ui.rs # Handles rendering to terminal
We’ll start simple, then grow toward this modular structure in later sections.
🧪 Step 4: Test Your Setup with a Simple Print
Just to verify everything is set up correctly, replace the contents of main.rs
with this:
fn main() {
println!("Rust Snake Game project is ready to build!");
}
Then run:
cargo run
You should see:
Rust Snake Game project is ready to build!
If you do, your setup is complete.
✅ Summary
You’ve just finished the rust project setup required to build your terminal snake game.
Now you have:
- A working Rust environment
- External crates installed
- A structured foundation to build on
🔜 Next Up
Let’s move from preparation to action. In the next section, we’ll design the structure of the game itself: game state, components, and how the logic will flow.
→ Section 5: Rust Game Structure Planning
🧩 Rust Game Structure Planning: Designing Your Snake Engine
In this section, you’ll learn rust game structure planning to break your Snake Game into logical components like game loop, input, snake logic, and rendering.
🎮 Why Structure Matters in Game Development
Before writing any logic, it’s important to ask:
“What parts will this game need, and how will they talk to each other?”
Good structure:
- Makes your code readable and modular
- Allows easier testing and debugging
- Makes adding features (scoreboard, GUI, restart button) easier later on
That’s why rust game structure planning is a crucial early step—especially if you’re aiming for clean, maintainable code.
🧠 Major Components of the Snake Game
Here’s a breakdown of the core components we’ll implement:
Component | Description |
---|---|
main.rs | The entry point: initializes and runs the game loop |
game | Handles overall game state: snake, food, game over |
snake | Controls movement, growth, and collision logic |
input | Reads and processes keyboard events |
ui | Responsible for rendering to the terminal using crossterm |
We’ll start in a single file, but this is the structure we’re aiming to modularize into.
🔄 How the Game Will Work – Step-by-Step Flow
flowchart TD
Start --> Setup[Initialize Game]
Setup --> Loop[Game Loop]
Loop --> Input[Check Keyboard Input]
Input --> Update[Update Snake State]
Update --> Collision[Check Collision]
Collision --> Draw[Render to Terminal]
Draw --> Loop
Collision --> GameOver[Game Over?]
GameOver -->|Yes| End
GameOver -->|No| Loop
💬 Explanation of Each Step
- Setup: Initialize snake position, direction, and food.
- Input: Listen for keypresses (arrow keys, ‘q’ to quit).
- Update: Move the snake’s head and update body accordingly.
- Collision: Check if the snake has hit a wall or itself.
- Draw: Redraw the entire game screen in the terminal.
🧱 Planned Data Structures
Name | Type | Used For |
---|---|---|
VecDeque<(u16, u16)> | Snake body segments | |
Direction enum | Current movement direction | |
(u16, u16) | Food position | |
bool flag | Game over state |
📁 Folder and File Planning (Future Modularization)
src/
├── main.rs # Runs the game loop
├── game.rs # Game state, food generation, game over
├── snake.rs # Movement and collision
├── input.rs # Keyboard input handling
└── ui.rs # Drawing to terminal
We’ll begin by implementing everything in main.rs
, and as the logic becomes more complex, we’ll extract each part into its own module.
✅ Summary
You now understand the full architecture of your terminal-based Snake Game. By following this rust game structure planning guide, you’re setting yourself up for:
- Easier debugging
- Cleaner code
- Long-term maintainability
- Smooth transition to GUI if desired later
🔜 Next Up
Now it’s time to bring your game loop to life.
→ Section 6: Creating the Rust Game Loop
🔄 Section 6: Creating the Rust Game Loop
Learn how to build the core rust game loop that powers your terminal snake game. This loop handles real-time updates, keyboard input, movement, and drawing.
🧠 What Is a Game Loop?
A game loop is a continuously running cycle that updates the game state and renders the screen in real time. Almost every game engine — from Unity to custom terminal games — relies on this core logic.
In this Snake Game, our game loop will:
- Check for keyboard input
- Update the snake’s position
- Check for collisions or food
- Redraw the terminal screen
- Repeat at a consistent frame rate (e.g. every 100ms)
⚙️ Step-by-Step: Building the Rust Game Loop
✅ Step 1: Import Required Crates
Open main.rs
and import the necessary libraries:
use std::{
collections::VecDeque,
io::{stdout, Write},
thread,
time::Duration,
};
use crossterm::{
cursor,
event::{self, Event, KeyCode},
execute,
terminal::{self, ClearType},
};
use rand::Rng;
These imports allow us to:
- Control the terminal (
crossterm
) - Handle user input
- Pause execution between frames (
thread::sleep
) - Generate random numbers for food (
rand
)
✅ Step 2: Define Game Constants
const WIDTH: u16 = 20;
const HEIGHT: u16 = 10;
We define a small 20×10 terminal grid where our snake will move.
✅ Step 3: Create Direction Enum
This will allow us to change snake direction based on input.
#[derive(Clone, Copy, PartialEq)]
enum Direction {
Up,
Down,
Left,
Right,
}
✅ Step 4: Initialize Game State
Inside main()
:
fn main() {
let mut stdout = stdout();
terminal::enable_raw_mode().unwrap(); // read input directly
execute!(stdout, terminal::Clear(ClearType::All), cursor::Hide).unwrap();
// Snake body stored in queue
let mut snake: VecDeque<(u16, u16)> = VecDeque::new();
snake.push_front((5, 5));
let mut dir = Direction::Right;
// Food at a random position
let mut food = (10, 5);
let mut rng = rand::thread_rng();
✅ Step 5: Build the Game Loop
Here comes the heart of the program:
loop {
// --- INPUT ---
if event::poll(Duration::from_millis(100)).unwrap() {
if let Event::Key(key) = event::read().unwrap() {
dir = match key.code {
KeyCode::Up if dir != Direction::Down => Direction::Up,
KeyCode::Down if dir != Direction::Up => Direction::Down,
KeyCode::Left if dir != Direction::Right => Direction::Left,
KeyCode::Right if dir != Direction::Left => Direction::Right,
KeyCode::Char('q') => break, // quit
_ => dir,
};
}
}
// --- LOGIC ---
let (head_x, head_y) = *snake.front().unwrap();
let new_head = match dir {
Direction::Up => (head_x, head_y.saturating_sub(1)),
Direction::Down => (head_x, head_y + 1),
Direction::Left => (head_x.saturating_sub(1), head_y),
Direction::Right => (head_x + 1, head_y),
};
// Game over check
if snake.contains(&new_head)
|| new_head.0 == 0 || new_head.1 == 0
|| new_head.0 >= WIDTH || new_head.1 >= HEIGHT {
break;
}
// Move the snake
snake.push_front(new_head);
if new_head == food {
food = (
rng.gen_range(1..WIDTH - 1),
rng.gen_range(1..HEIGHT - 1),
);
} else {
snake.pop_back();
}
// --- RENDERING ---
execute!(stdout, cursor::MoveTo(0, 0)).unwrap();
for y in 0..HEIGHT {
for x in 0..WIDTH {
if snake.contains(&(x, y)) {
print!("O");
} else if (x, y) == food {
print!("X");
} else {
print!(" ");
}
}
println!();
}
stdout.flush().unwrap();
}
execute!(stdout, cursor::Show).unwrap();
terminal::disable_raw_mode().unwrap();
println!("Game Over!");
}
🗂 Breakdown of Game Loop Stages
Phase | Code Block | What It Does |
---|---|---|
Input | event::poll → match key.code | Reads arrow keys & updates direction |
Update | new_head → snake.push_front | Moves snake in the chosen direction |
Game Logic | if new_head == food / snake.contains(&new_head) | Grows snake or ends game |
Rendering | execute!(stdout, cursor::MoveTo) + loop | Redraws the entire grid in the terminal |
🧪 Test It!
cargo run
Use arrow keys to move. Eat the “X” food, avoid crashing. Press q
to quit.
✅ Summary
Your rust game loop is now fully working! You’ve built:
- A real-time, responsive game loop
- Input detection using
crossterm
- Dynamic snake logic that updates every frame
This is the engine that powers everything else in your game.
🔜 Next Up
Now that the loop is alive, let’s focus on how the snake moves and how to manage its body in a structured way.
→ Section 7: Implementing Snake Movement with Direction Control
🐍 Section 7: Implementing Snake Movement with Direction Control
In this rust snake movement section, you’ll modularize movement logic into a clean
Snake
struct, apply direction controls, and prepare for unit testing and debugging.
🧩 Step 1: Create snake.rs
Module
Create a new file inside src/
named snake.rs
and paste the following code.
use std::collections::VecDeque;
/// Direction enum defines which way the snake is currently moving.
/// It will be used to calculate the next head position.
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
/// Snake struct holds the entire body and direction of the snake.
/// The body is a VecDeque for fast insertion/removal from both ends.
#[derive(Debug)]
pub struct Snake {
pub body: VecDeque<(u16, u16)>, // Queue of (x, y) coordinates
pub direction: Direction, // Current direction of the snake
}
impl Snake {
/// Creates a new snake starting at the given coordinates.
pub fn new(start_x: u16, start_y: u16) -> Self {
let mut body = VecDeque::new();
body.push_front((start_x, start_y)); // Snake starts with 1 segment
Snake {
body,
direction: Direction::Right, // Default initial direction
}
}
/// Changes direction unless the new direction is directly opposite.
/// Prevents the snake from reversing into itself.
pub fn change_direction(&mut self, new_dir: Direction) {
if self.direction == Direction::Up && new_dir != Direction::Down
|| self.direction == Direction::Down && new_dir != Direction::Up
|| self.direction == Direction::Left && new_dir != Direction::Right
|| self.direction == Direction::Right && new_dir != Direction::Left
{
self.direction = new_dir;
}
}
/// Calculates the next coordinate where the head will move.
/// Does not actually move the snake yet.
pub fn next_head_position(&self) -> (u16, u16) {
let (x, y) = self.body.front().unwrap(); // Current head position
match self.direction {
Direction::Up => (*x, y.saturating_sub(1)),
Direction::Down => (*x, y + 1),
Direction::Left => (x.saturating_sub(1), *y),
Direction::Right => (x + 1, *y),
}
}
/// Moves the snake forward by adding a new head.
/// If `grow` is false, the tail is removed to keep the same length.
pub fn move_forward(&mut self, grow: bool) {
let new_head = self.next_head_position();
self.body.push_front(new_head);
if !grow {
self.body.pop_back(); // Remove tail if not growing
}
}
/// Checks whether the snake's head has collided with its own body.
pub fn is_self_collision(&self) -> bool {
let head = self.body.front().unwrap();
self.body.iter().skip(1).any(|&segment| segment == *head)
}
/// Returns the current head position.
pub fn head(&self) -> (u16, u16) {
*self.body.front().unwrap()
}
/// Checks whether a given position is part of the snake's body.
pub fn contains(&self, pos: &(u16, u16)) -> bool {
self.body.contains(pos)
}
}
🧪 Step 2: Add Unit Tests
At the bottom of snake.rs
, add tests to validate your logic:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_snake_moves_right_by_default() {
let mut snake = Snake::new(5, 5);
snake.move_forward(false);
assert_eq!(snake.head(), (6, 5)); // moves right by default
}
#[test]
fn test_snake_grows_when_eating() {
let mut snake = Snake::new(5, 5);
snake.move_forward(true); // simulate eating
assert_eq!(snake.body.len(), 2); // grows in length
}
#[test]
fn test_invalid_reverse_direction() {
let mut snake = Snake::new(5, 5);
snake.change_direction(Direction::Left); // Opposite of Right
assert_eq!(snake.direction, Direction::Right); // Should stay the same
}
#[test]
fn test_self_collision_detection() {
let mut snake = Snake::new(5, 5);
snake.move_forward(true);
snake.change_direction(Direction::Down);
snake.move_forward(true);
snake.change_direction(Direction::Left);
snake.move_forward(true);
snake.change_direction(Direction::Up);
snake.move_forward(true); // Collides with self
assert!(snake.is_self_collision());
}
}
Run the tests with:
cargo test
🧩 Step 3: Use the Module in main.rs
In main.rs
, enable the new module:
mod snake;
use snake::{Snake, Direction};
Replace the original snake VecDeque
and movement logic with:
let mut snake = Snake::new(5, 5);
Handle input like this:
if let Event::Key(key) = event::read().unwrap() {
match key.code {
KeyCode::Up => snake.change_direction(Direction::Up),
KeyCode::Down => snake.change_direction(Direction::Down),
KeyCode::Left => snake.change_direction(Direction::Left),
KeyCode::Right => snake.change_direction(Direction::Right),
KeyCode::Char('q') => break,
_ => {}
}
}
Update logic using the new Snake
methods:
let next_head = snake.next_head_position();
let ate_food = next_head == food;
if snake.contains(&next_head)
|| next_head.0 == 0 || next_head.1 == 0
|| next_head.0 >= WIDTH || next_head.1 >= HEIGHT {
break;
}
snake.move_forward(ate_food);
if ate_food {
food = (
rng.gen_range(1..WIDTH - 1),
rng.gen_range(1..HEIGHT - 1),
);
}
🐞 Optional: Add Debugging Output
You can print the state of the snake during gameplay to help debug:
eprintln!("Snake head: {:?}", snake.head());
eprintln!("Snake direction: {:?}", snake.direction);
Use eprintln!
so it doesn’t interfere with the main game output in the terminal.
✅ Summary
With this section complete, you’ve:
- Modularized the snake logic into a separate file
- Created a robust
Snake
struct with direction, growth, and collision logic - Added unit tests to validate movement and collisions
- Enabled debug output to help trace bugs in real time
This makes your snake movement logic:
- Cleaner 🧼
- Testable 🧪
- Expandable 🔧
- Reusable ♻️
🔜 Up Next
In the next section, you’ll implement dynamic, random food generation using the rand
crate.
→ Section 8: Generating Food with Randomness
🍎 Section 8: Generating Food with Randomness
In this section, you’ll learn how to implement rust snake food generation using randomness. You’ll place food at random positions and avoid overlaps with the snake.
🎯 What You’ll Learn
- How to use the
rand
crate in Rust - How to generate random
(x, y)
positions inside the terminal grid - How to ensure the food doesn’t appear on top of the snake
- How to modularize this logic into a clean function
🧩 Step 1: Make Sure rand
Is Added to Cargo.toml
In your Cargo.toml
:
[dependencies]
rand = "0.8"
This crate allows us to generate random numbers using a modern, flexible API.
🔄 Step 2: Create a Food Generation Function
We’ll add this function in main.rs
for now, but eventually it can be moved into its own module like game.rs
or world.rs
.
use rand::Rng;
/// Generate a random (x, y) position for the food.
/// Ensures that the food is not spawned on top of the snake.
fn generate_food(width: u16, height: u16, snake: &Snake) -> (u16, u16) {
let mut rng = rand::thread_rng();
loop {
// Random values within game boundary
let x = rng.gen_range(1..width - 1);
let y = rng.gen_range(1..height - 1);
let pos = (x, y);
// If the generated position is NOT on the snake, return it
if !snake.contains(&pos) {
return pos;
}
// If it overlaps, try again (loop)
}
}
📌 Why We Use a loop
Random values may occasionally fall on top of the snake. To prevent this, we generate a value and test if it’s valid, repeating until we find a free spot.
🧪 Optional Test Simulation
You can verify it works by adding this in your game loop temporarily:
eprintln!("New food at: {:?}", food);
This will print where new food is generated every time the snake eats.
🧠 How It Integrates with the Game Loop
Inside your main game loop, when the snake eats food:
if ate_food {
food = generate_food(WIDTH, HEIGHT, &snake);
}
This replaces the previous random logic with a clean, reusable function that avoids bugs like overlapping food.
🛠️ Example Before vs After
❌ Old Code (Unsafe Overlap Possible):
food = (
rng.gen_range(1..WIDTH - 1),
rng.gen_range(1..HEIGHT - 1),
);
✅ New Code (Safe & Modular):
food = generate_food(WIDTH, HEIGHT, &snake);
🧼 Optional Refactoring Idea
If you want to move food logic into its own module later:
src/
├── food.rs // add this
Inside food.rs
:
use rand::Rng;
use crate::snake::Snake;
pub fn generate(width: u16, height: u16, snake: &Snake) -> (u16, u16) {
let mut rng = rand::thread_rng();
loop {
let x = rng.gen_range(1..width - 1);
let y = rng.gen_range(1..height - 1);
if !snake.contains(&(x, y)) {
return (x, y);
}
}
}
Then in main.rs
:
mod food;
use food::generate as generate_food;
✅ Summary
You now have a reliable, reusable function that:
- Generates food randomly
- Prevents overlapping with the snake
- Fits cleanly into your game loop
- Can be easily moved to a module
This completes your rust snake food generation logic!
🔜 Up Next
Now that the snake and food work together properly, it’s time to handle collision detection more formally and implement the game over logic.
→ Section 9: Detecting Collisions in Snake Game
💥 Section 9: Detecting Collisions in Snake Game
In this section, you’ll implement rust collision detection to determine when the snake hits a wall or itself. This logic will trigger the game over condition.
🎯 What You’ll Learn
- How to detect if the snake has hit the walls
- How to detect if the snake has collided with itself
- How to cleanly separate collision logic from the main loop
- How to write a reusable
is_game_over()
function
📦 Step 1: Add Collision Logic Inside Snake Module (Optional)
If you’d like, add this inside snake.rs
:
/// Checks if the snake's head has collided with the game boundaries
pub fn is_wall_collision(&self, width: u16, height: u16) -> bool {
let (x, y) = self.head();
x == 0 || y == 0 || x >= width || y >= height
}
You already have this method:
pub fn is_self_collision(&self) -> bool
These two functions together form the complete collision detection system.
🧩 Step 2: Use Collision Logic in Main Game Loop
In main.rs
, right after calculating the snake’s next position, check for collisions:
let next_head = snake.next_head_position();
if snake.contains(&next_head) {
break; // Self-collision
}
if next_head.0 == 0 || next_head.1 == 0 || next_head.0 >= WIDTH || next_head.1 >= HEIGHT {
break; // Wall collision
}
Or, if using the methods from the snake module:
snake.move_forward(ate_food);
if snake.is_self_collision() || snake.is_wall_collision(WIDTH, HEIGHT) {
break;
}
This will exit the loop and trigger the game over state.
🐞 Optional Debugging Output
To verify that collisions are being detected correctly:
eprintln!("Head position: {:?}", snake.head());
eprintln!("Collision with self? {}", snake.is_self_collision());
eprintln!("Collision with wall? {}", snake.is_wall_collision(WIDTH, HEIGHT));
This helps track down bugs during development.
🧪 Optional Unit Test for Self-Collision
In snake.rs
, you can already test is_self_collision()
like this:
#[test]
fn test_self_collision_detection() {
let mut snake = Snake::new(5, 5);
snake.move_forward(true); // (6,5)
snake.change_direction(Direction::Down);
snake.move_forward(true); // (6,6)
snake.change_direction(Direction::Left);
snake.move_forward(true); // (5,6)
snake.change_direction(Direction::Up);
snake.move_forward(true); // (5,5) - back to original
assert!(snake.is_self_collision());
}
This validates that self-collision is working as intended.
✅ Summary
Your snake game can now detect when:
- The snake hits the wall
- The snake hits itself
- It’s time to end the game loop
You’ve successfully implemented rust collision detection using clean, reusable logic inside your modules.
🔜 Up Next
Now that we know when the game should end, it’s time to display a game over message and optionally let the player restart or exit.
→ Section 10: Rendering to Terminal with Crossterm
⌨️ Section 11: Handling Keyboard Input in Rust Snake Game
This section teaches how to implement rust keyboard input using crossterm to control the snake’s direction in real time without blocking the game loop.
🎯 What You’ll Learn
- How to read arrow keys and key events using
crossterm
- How to handle non-blocking (async) input so the game loop keeps running
- How to safely change direction using your
Snake
module - How to modularize input logic into a function or module
🧩 Step 1: Enable crossterm
for Input
You should already have this in your Cargo.toml
:
[dependencies]
crossterm = "0.26"
Now, in your main.rs
or input.rs
, import the necessary modules:
use crossterm::event::{self, Event, KeyCode};
🔄 Step 2: Add Non-blocking Input Handling to the Game Loop
In your main game loop, use event::poll()
with a timeout to avoid blocking.
if event::poll(std::time::Duration::from_millis(100)).unwrap() {
if let Event::Key(key_event) = event::read().unwrap() {
match key_event.code {
KeyCode::Up => snake.change_direction(Direction::Up),
KeyCode::Down => snake.change_direction(Direction::Down),
KeyCode::Left => snake.change_direction(Direction::Left),
KeyCode::Right => snake.change_direction(Direction::Right),
KeyCode::Char('q') => break, // quit the game
_ => {} // ignore other keys
}
}
}
✅ Why Use poll()
The poll()
method checks if any input is available within the given time (non-blocking).
Without it, event::read()
would freeze your game until a key is pressed.
🧼 Optional Refactor: Move to input.rs
Create a new file input.rs
:
use crossterm::event::{self, Event, KeyCode};
use crate::snake::{Snake, Direction};
pub fn handle_input(snake: &mut Snake) -> bool {
if event::poll(std::time::Duration::from_millis(100)).unwrap() {
if let Event::Key(key_event) = event::read().unwrap() {
match key_event.code {
KeyCode::Up => snake.change_direction(Direction::Up),
KeyCode::Down => snake.change_direction(Direction::Down),
KeyCode::Left => snake.change_direction(Direction::Left),
KeyCode::Right => snake.change_direction(Direction::Right),
KeyCode::Char('q') => return true, // quit
_ => {}
}
}
}
false
}
Then in main.rs
:
mod input;
use input::handle_input;
if handle_input(&mut snake) {
break; // Quit if 'q' is pressed
}
🧪 Optional Debugging Tip
You can print input events to track what’s happening:
eprintln!("Pressed: {:?}", key_event.code);
This is helpful to confirm which key codes crossterm
is detecting in your terminal.
🔐 Safety Check: Prevent Reverse Movement
Already handled in your snake.change_direction()
logic, this ensures:
// From snake.rs
if self.direction == Direction::Up && new_dir != Direction::Down
The snake can’t instantly reverse into itself.
✅ Summary
In this section, you’ve added real-time, non-blocking input handling using crossterm
.
You now have:
- Full arrow key support
- A clean, modular input function
- A responsive game loop that doesn’t freeze waiting for input
You’ve mastered rust keyboard input — one of the trickiest parts of CLI games!
🔜 Up Next
Next, we’ll refactor the snake logic with VecDeque
, which you already partially did, but now we’ll organize and finalize that part more cleanly.
→ Section 12: Refactoring with VecDeque: A Smarter Snake in Rust
🧠 Section 12: Refactoring with VecDeque – A Smarter Snake in Rust
This section shows how to refactor your rust snake game using VecDeque to optimize snake movement, growth, and tail handling in a clean and efficient way.
🎯 What You’ll Learn
- Why
VecDeque
is better suited thanVec
for snake-like movement - How to structure snake movement using front and back operations
- How
VecDeque
helps manage real-time head/tail updates - How to cleanly refactor your
Snake
struct
🧩 Why Use VecDeque
?
VecDeque
is a double-ended queue that lets you efficiently:
- Add items to the front (head)
- Remove items from the back (tail)
Perfect for a snake, which grows from the front and shrinks from the tail as it moves.
Compare:
Action | Vec | VecDeque |
---|---|---|
Add to front | ❌ O(n) slow | ✅ O(1) fast |
Remove from back | ✅ O(1) | ✅ O(1) |
🧱 Refactored Snake Struct (In snake.rs
)
If you’ve already added this earlier, great!
Here’s the final version, explained with detailed comments:
use std::collections::VecDeque;
/// Enum for the current direction of the snake
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
/// The Snake struct stores the body and movement direction
#[derive(Debug)]
pub struct Snake {
pub body: VecDeque<(u16, u16)>, // The snake’s segments (x, y)
pub direction: Direction,
}
impl Snake {
/// Initializes a new snake at a starting position
pub fn new(start_x: u16, start_y: u16) -> Self {
let mut body = VecDeque::new();
body.push_front((start_x, start_y)); // Place the initial head
Snake {
body,
direction: Direction::Right, // Default direction
}
}
/// Update the movement direction, rejecting reverse movement
pub fn change_direction(&mut self, new_dir: Direction) {
if self.direction == Direction::Up && new_dir != Direction::Down
|| self.direction == Direction::Down && new_dir != Direction::Up
|| self.direction == Direction::Left && new_dir != Direction::Right
|| self.direction == Direction::Right && new_dir != Direction::Left
{
self.direction = new_dir;
}
}
/// Calculates the next head position based on direction
pub fn next_head_position(&self) -> (u16, u16) {
let (x, y) = self.body.front().unwrap();
match self.direction {
Direction::Up => (*x, y.saturating_sub(1)),
Direction::Down => (*x, y + 1),
Direction::Left => (x.saturating_sub(1), *y),
Direction::Right => (x + 1, *y),
}
}
/// Move the snake forward, optionally growing
pub fn move_forward(&mut self, grow: bool) {
let new_head = self.next_head_position();
self.body.push_front(new_head); // Add new head
if !grow {
self.body.pop_back(); // Remove tail if not eating
}
}
/// Check if the head has collided with the body
pub fn is_self_collision(&self) -> bool {
let head = self.body.front().unwrap();
self.body.iter().skip(1).any(|&segment| segment == *head)
}
/// Check if head is outside game bounds
pub fn is_wall_collision(&self, width: u16, height: u16) -> bool {
let (x, y) = self.head();
x == 0 || y == 0 || x >= width || y >= height
}
/// Get current head position
pub fn head(&self) -> (u16, u16) {
*self.body.front().unwrap()
}
/// Check if a given position is part of the body
pub fn contains(&self, pos: &(u16, u16)) -> bool {
self.body.contains(pos)
}
}
🔁 Integration in main.rs
In your game loop:
let next_head = snake.next_head_position();
let ate_food = next_head == food;
snake.move_forward(ate_food);
if snake.is_self_collision() || snake.is_wall_collision(WIDTH, HEIGHT) {
break;
}
🧪 Test It
Run this inside tests
or at the bottom of snake.rs
:
#[test]
fn test_snake_vecdeque_movement() {
let mut snake = Snake::new(5, 5);
snake.move_forward(false); // should go right
assert_eq!(snake.head(), (6, 5));
}
Run it:
cargo test
✅ Summary
With this refactor, your snake movement is now:
- Efficient: O(1) for both head and tail
- Realistic: Grows only when eating food
- Maintainable: Clean separation of logic
You’ve used VecDeque to build a smarter, faster, and modular Snake in Rust.
🔜 Up Next
Let’s implement the game over screen and restart logic, and give players a way to exit cleanly.
→ Section 13: Implementing Game Over and Restart Feature
☠️ Section 13: Implementing Game Over and Restart Feature
This section shows how to implement rust snake game over logic with an option to restart or quit. You’ll display a message, pause the loop, and handle user input after game ends.
🎯 What You’ll Learn
- How to detect when the game is over
- How to display a game over message in the terminal
- How to wait for user input (
r
to restart,q
to quit) - How to reset the game state cleanly for a fresh start
🧩 Step 1: Detect Game Over Condition
This should already be working from previous sections:
if snake.is_self_collision() || snake.is_wall_collision(WIDTH, HEIGHT) {
break; // Game over
}
After this, the main loop exits, but we want to give the user a message and a choice.
💬 Step 2: Display a Game Over Message
Below the loop (outside loop { ... }
), add this code:
use crossterm::{style::Print, cursor::MoveTo};
execute!(
stdout,
cursor::MoveTo(0, HEIGHT + 1),
Print("Game Over! Press R to Restart or Q to Quit.")
).unwrap();
This will print the message just below the game grid.
⏳ Step 3: Wait for R or Q Input
After the message, wait for player input:
loop {
if event::poll(Duration::from_millis(500)).unwrap() {
if let Event::Key(key_event) = event::read().unwrap() {
match key_event.code {
KeyCode::Char('r') => {
// restart logic goes here
break; // restart
}
KeyCode::Char('q') => {
// exit cleanly
execute!(stdout, cursor::Show).unwrap();
terminal::disable_raw_mode().unwrap();
return; // end the program
}
_ => {}
}
}
}
}
🔄 Step 4: Wrap Game Loop in a run_game()
Function
To allow restarting, move your game logic into a function like this:
fn run_game() {
let mut stdout = stdout();
terminal::enable_raw_mode().unwrap();
execute!(stdout, terminal::Clear(ClearType::All), cursor::Hide).unwrap();
let mut snake = Snake::new(5, 5);
let mut food = generate_food(WIDTH, HEIGHT, &snake);
let mut rng = rand::thread_rng();
loop {
// input + movement + drawing as before...
// break on game over
}
// show game over message
// wait for 'r' or 'q'
}
Then in main()
:
fn main() {
loop {
run_game(); // if 'r' is pressed, we loop and restart
// if 'q' is pressed inside run_game(), it will return and exit
break;
}
}
Or return a flag:
fn run_game() -> bool {
// return true if user wants to restart
}
Then:
fn main() {
while run_game() {}
}
🧼 Optional Cleanup on Exit
Always restore terminal state at the end:
execute!(stdout, cursor::Show).unwrap();
terminal::disable_raw_mode().unwrap();
✅ Summary
You now have a complete game over and restart system:
- Players see a clear message when they lose
- They can press
r
to restart orq
to quit - The game resets without relaunching the program
This creates a polished rust snake game over logic flow that feels complete and user-friendly.
🔜 Up Next
Now it’s time to organize your project into clean, scalable files using modules.
→ Section 14: Rust Project Modularization: Clean Code for Snake Game
🧼 Section 14: Rust Project Modularization – Clean Code for Snake Game
In this section, you’ll organize your rust snake game into modules like
snake.rs
,input.rs
, andgame.rs
to improve readability, maintainability, and scalability.
🎯 What You’ll Learn
- Why and how to modularize Rust projects
- How to split logic into multiple
.rs
files - How to use
mod
,pub
, anduse
for cross-module access - How to reduce clutter in
main.rs
🧩 Why Modularize?
As your project grows, having everything in main.rs
becomes:
- Hard to read
- Difficult to debug
- Impossible to reuse
By breaking the code into logical modules, you make it easier to:
✅ Read
✅ Reuse
✅ Test
✅ Expand (e.g., add GUI, sound, scoring)
📁 Step 1: Plan Your Folder Structure
Here’s the structure we’ll aim for:
src/
├── main.rs # Game entry point & loop
├── snake.rs # Snake struct, movement, collision
├── input.rs # Keyboard input
├── food.rs # Food generation logic
└── game.rs # Game loop, restart logic, game over
You can start by creating these files manually, or using terminal:
touch src/snake.rs src/input.rs src/food.rs src/game.rs
🧠 Step 2: Create Modules and Declare in main.rs
In main.rs
, at the top:
mod snake;
mod input;
mod food;
mod game;
use crate::game::run_game;
📦 Step 3: Move Logic to Each File
✅ snake.rs
Holds the Snake
struct, Direction
enum, and movement logic
(Already covered in Section 7 & 12)
✅ input.rs
use crossterm::event::{self, Event, KeyCode};
use crate::snake::{Snake, Direction};
pub fn handle_input(snake: &mut Snake) -> bool {
if event::poll(std::time::Duration::from_millis(100)).unwrap() {
if let Event::Key(key_event) = event::read().unwrap() {
match key_event.code {
KeyCode::Up => snake.change_direction(Direction::Up),
KeyCode::Down => snake.change_direction(Direction::Down),
KeyCode::Left => snake.change_direction(Direction::Left),
KeyCode::Right => snake.change_direction(Direction::Right),
KeyCode::Char('q') => return true,
_ => {}
}
}
}
false
}
✅ food.rs
use rand::Rng;
use crate::snake::Snake;
pub fn generate_food(width: u16, height: u16, snake: &Snake) -> (u16, u16) {
let mut rng = rand::thread_rng();
loop {
let x = rng.gen_range(1..width - 1);
let y = rng.gen_range(1..height - 1);
let pos = (x, y);
if !snake.contains(&pos) {
return pos;
}
}
}
✅ game.rs
This will hold the entire game loop and game over logic:
use crossterm::{
cursor::{Hide, MoveTo, Show},
event::{self, Event, KeyCode},
execute,
style::Print,
terminal::{self, ClearType},
};
use std::{
io::{stdout, Write},
thread,
time::Duration,
};
use crate::{food::generate_food, input::handle_input, snake::{Snake, Direction}};
const WIDTH: u16 = 20;
const HEIGHT: u16 = 10;
pub fn run_game() -> bool {
let mut stdout = stdout();
terminal::enable_raw_mode().unwrap();
execute!(stdout, ClearType::All, Hide).unwrap();
let mut snake = Snake::new(5, 5);
let mut food = generate_food(WIDTH, HEIGHT, &snake);
loop {
if handle_input(&mut snake) {
break;
}
let next_head = snake.next_head_position();
let ate = next_head == food;
snake.move_forward(ate);
if snake.is_self_collision() || snake.is_wall_collision(WIDTH, HEIGHT) {
break;
}
if ate {
food = generate_food(WIDTH, HEIGHT, &snake);
}
execute!(stdout, MoveTo(0, 0)).unwrap();
for y in 0..HEIGHT {
for x in 0..WIDTH {
if snake.contains(&(x, y)) {
print!("O");
} else if (x, y) == food {
print!("X");
} else {
print!(" ");
}
}
println!();
}
stdout.flush().unwrap();
thread::sleep(Duration::from_millis(100));
}
execute!(
stdout,
MoveTo(0, HEIGHT + 1),
Print("Game Over! Press R to Restart or Q to Quit.")
).unwrap();
loop {
if event::poll(Duration::from_millis(500)).unwrap() {
if let Event::Key(key_event) = event::read().unwrap() {
match key_event.code {
KeyCode::Char('r') => return true,
KeyCode::Char('q') => {
execute!(stdout, Show).unwrap();
terminal::disable_raw_mode().unwrap();
return false;
}
_ => {}
}
}
}
}
}
🏁 Final main.rs
mod snake;
mod input;
mod food;
mod game;
fn main() {
while game::run_game() {}
}
✅ Summary
You now have a fully modularized Rust Snake Game!
This structure gives you:
- 📦 Clean files and clear responsibilities
- 🧪 Easier testing
- 🔁 Easy restarts
- 🚀 Room to grow (scoreboard, settings, GUI, etc.)
You’ve mastered rust project modularization and are ready for final polish!
🔜 Up Next
Next, let’s finish strong with visuals, UI tweaks, and little features that make the terminal experience feel great.
→ Section 15: Polishing the Terminal Snake Game (UI + Final Touches)
🎨 Section 15: Polishing the Terminal Snake Game – UI + Final Touches
In this section, you’ll improve the polish of your rust terminal snake game by adding borders, messages, screen clearing, and other UI tweaks for a better experience.
🎯 What You’ll Learn
- How to draw borders around the game area
- How to display the score (based on snake length)
- How to clear and re-render frames more smoothly
- How to add finishing touches for professional polish
📏 Step 1: Add Borders Around the Game Grid
Update the drawing logic inside your game loop in game.rs
:
execute!(stdout, MoveTo(0, 0)).unwrap();
for y in 0..=HEIGHT {
for x in 0..=WIDTH {
let ch = if x == 0 || x == WIDTH || y == 0 || y == HEIGHT {
"#" // Draw walls
} else if snake.contains(&(x, y)) {
"O" // Snake body
} else if (x, y) == food {
"X" // Food
} else {
" " // Empty space
};
print!("{}", ch);
}
println!();
}
✅ Result:
#
represents bordersO
represents snakeX
represents food
🧮 Step 2: Add a Score Display
Right below the game board, show the current score:
let score = snake.body.len() - 1;
execute!(
stdout,
MoveTo(0, HEIGHT + 1),
Print(format!("Score: {}", score))
).unwrap();
You can also track the highest score using a mutable variable.
🧼 Step 3: Clean Terminal Frames
Before rendering each new frame, clear the screen fully:
use crossterm::terminal::Clear;
execute!(stdout, Clear(ClearType::All)).unwrap();
Then continue with MoveTo(0, 0)
and your drawing loop.
This removes flickering and leftover characters.
🛠️ Step 4: Optional UI Enhancements
You can also:
- Add color with
crossterm::style::Color
- Print a live control guide:
WASD / Arrow Keys: Move | Q: Quit
- Center text using
MoveTo(x, y)
dynamically
Example: Highlight food in red
use crossterm::style::{Color, SetForegroundColor, ResetColor};
execute!(stdout, SetForegroundColor(Color::Red)).unwrap();
print!("X");
execute!(stdout, ResetColor).unwrap();
🧪 UX Tip: Faster Restart
After game over, show:
Game Over! Your score: 12
Press R to Restart or Q to Quit.
And allow immediate keypress (already handled in Section 13).
✅ Summary
You’ve now given your terminal snake game:
- 📏 Clean borders
- 🧮 Score tracking
- 🌈 Visual distinction
- 🖼️ Frame clearing for smooth animations
These subtle details transform a “working project” into a “playable game.”
You’ve completed the UI polish phase of your Rust Snake Game!
🔜 Final Section
In the last section, we’ll look at how to transition this terminal game into a GUI version using engines like ggez
or macroquad
.
→ Section 16: Expanding to GUI – Rust Snake Game with ggez or macroquad
🖥️ Section 16: Expanding to GUI – Rust Snake Game with ggez or macroquad
In this final section, you’ll learn how to transition your Rust Snake Game into a graphical window using game engines like ggez and macroquad for visual upgrades.
🎯 What You’ll Learn
- The pros and cons of two popular Rust game engines:
ggez
andmacroquad
- How to prepare your terminal game logic for GUI conversion
- How to draw snake, food, and background using pixels or shapes
- How to wire keyboard input and a render loop in a GUI engine
🆚 ggez vs macroquad – Which Should You Choose?
Feature | ggez | macroquad |
---|---|---|
Style | SDL2-like, low-level | Web-friendly, minimalist |
Platform | Native (desktop only) | Native + WASM (runs in browser) |
Language feel | Similar to other 2D engines | Similar to PICO-8 or Love2D |
Rendering | Sprite, shapes, canvas | Built-in draw functions |
Input Handling | KeyEvent system | is_key_down() polling |
Learning curve | Steeper | Beginner friendly |
We’ll show an outline using macroquad
since it’s easier to get started, and doesn’t require event loops or manual canvas control.
🚀 Step-by-Step Migration Plan (Using macroquad
)
📦 Step 1: Add macroquad
to Cargo.toml
[dependencies]
macroquad = "0.3"
🧠 Step 2: Update main file
Replace main.rs
contents with a macroquad
-compatible version.
use macroquad::prelude::*;
use std::collections::VecDeque;
const GRID_SIZE: f32 = 20.0;
const WIDTH: i32 = 20;
const HEIGHT: i32 = 20;
#[derive(Clone, Copy, PartialEq)]
enum Direction {
Up,
Down,
Left,
Right,
}
struct Snake {
body: VecDeque<(i32, i32)>,
dir: Direction,
}
impl Snake {
fn new() -> Self {
let mut body = VecDeque::new();
body.push_back((10, 10));
Snake {
body,
dir: Direction::Right,
}
}
fn head(&self) -> (i32, i32) {
*self.body.front().unwrap()
}
fn move_forward(&mut self, grow: bool) {
let (x, y) = self.head();
let new_head = match self.dir {
Direction::Up => (x, y - 1),
Direction::Down => (x, y + 1),
Direction::Left => (x - 1, y),
Direction::Right => (x + 1, y),
};
self.body.push_front(new_head);
if !grow {
self.body.pop_back();
}
}
fn change_dir(&mut self, new_dir: Direction) {
if (self.dir == Direction::Up && new_dir != Direction::Down)
|| (self.dir == Direction::Down && new_dir != Direction::Up)
|| (self.dir == Direction::Left && new_dir != Direction::Right)
|| (self.dir == Direction::Right && new_dir != Direction::Left)
{
self.dir = new_dir;
}
}
fn draw(&self) {
for (x, y) in &self.body {
draw_rectangle(
*x as f32 * GRID_SIZE,
*y as f32 * GRID_SIZE,
GRID_SIZE,
GRID_SIZE,
GREEN,
);
}
}
fn is_collision(&self) -> bool {
let head = self.head();
self.body.iter().skip(1).any(|&p| p == head)
}
}
#[macroquad::main("Snake Game GUI")]
async fn main() {
let mut snake = Snake::new();
let mut food = (5, 5);
let mut frame_timer = 0.0;
loop {
clear_background(BLACK);
// Handle input
if is_key_pressed(KeyCode::Up) {
snake.change_dir(Direction::Up);
} else if is_key_pressed(KeyCode::Down) {
snake.change_dir(Direction::Down);
} else if is_key_pressed(KeyCode::Left) {
snake.change_dir(Direction::Left);
} else if is_key_pressed(KeyCode::Right) {
snake.change_dir(Direction::Right);
}
// Move snake every 0.15 sec
frame_timer += get_frame_time();
if frame_timer > 0.15 {
let grow = snake.head() == food;
snake.move_forward(grow);
if grow {
use rand::Rng;
let mut rng = rand::thread_rng();
food = (rng.gen_range(0..WIDTH), rng.gen_range(0..HEIGHT));
}
if snake.is_collision() {
break;
}
frame_timer = 0.0;
}
// Draw food
draw_rectangle(
food.0 as f32 * GRID_SIZE,
food.1 as f32 * GRID_SIZE,
GRID_SIZE,
GRID_SIZE,
RED,
);
// Draw snake
snake.draw();
next_frame().await;
}
}
🧪 What You Just Did
- Replaced terminal drawing with
draw_rectangle()
- Replaced input handling with
is_key_pressed()
- Added smooth 60 FPS animation using
next_frame().await
✅ Summary
You’ve now transitioned your CLI-based Snake Game to a full GUI Snake Game in Rust, complete with:
- A visible snake and food grid
- Real-time rendering at 60 FPS
- Smooth keyboard controls
- Room for animations, sounds, menus, etc.
You’ve completed the final milestone of this project 🎉
🧱 What’s Next?
- 🧪 Add scoring, restart, pause
- 🔊 Add sound with
macroquad::audio
- 🧵 Add menus or skins
- 🌍 Export as WASM to run in browsers
🏁 Congratulations!
You’ve built a complete, modular, testable, and expandable Snake Game in Rust —
from raw terminal interface to polished GUI.
This is no longer a beginner exercise — it’s a fully fledged mini-game engine that you can now share, tweak, or even deploy on the web.
🧠 Rust Snake Game – FAQ
1. What is the Rust Snake Game and why should I build it?
The Rust Snake Game is a terminal-based version of the classic Snake game built using the Rust programming language. It’s a perfect beginner project for learning Rust fundamentals like game loops, input handling, and data structures.
2. Do I need prior Rust experience to make this Snake Game?
Not necessarily. This project starts from scratch and explains every step, making it beginner-friendly—even if you’re new to Rust or game development.
3. Why is VecDeque used instead of Vec for the snake body?
VecDeque offers efficient O(1) operations at both ends, which is ideal for snake movement where you frequently add to the front (head) and remove from the back (tail).
4. How does collision detection work in the Rust Snake Game?
Collision is detected by checking if the snake’s head overlaps with the wall boundaries or its own body. If either occurs, the game ends.
5. How do I handle keyboard input in the Rust terminal environment?
We use the crossterm
crate to detect key presses asynchronously, allowing real-time control of the snake using arrow keys or WASD.
6. How can I generate food randomly without overlapping the snake?
Using the rand
crate, a random position is generated and checked against the snake’s current body. If it overlaps, a new position is generated until it’s valid.
7. Can I restart the game after a game over?
Yes. The game includes restart logic: after a game over, the player can press R
to restart or Q
to quit. The game resets cleanly using a loop.
8. How can I transition this Rust Snake Game to a GUI version?
You can use macroquad
or ggez
to render the game in a graphical window. Logic remains mostly the same, but drawing and input handling switch to graphical APIs.
9. Is this Snake Game project suitable for my programming portfolio?
Absolutely. It demonstrates understanding of Rust, real-time logic, data structures, modularization, testing, and UI—making it an impressive project to showcase.
10. How do I organize my code using modules in Rust?
The project is modularized into files like snake.rs
, input.rs
, food.rs
, and game.rs
. This improves readability, testing, and future feature expansion.
11. Can I publish this Snake Game online as a playable version?
Yes, especially if you convert it to a GUI using macroquad
, which supports WASM. You can compile it for the web and host it as a playable Rust browser game.
🔗 External References for Rust Snake Game
- RustSnake – Terminal-Based Snake Game in Rust
- A self-contained Snake game implemented in Rust without external dependencies.Crates
- GitHub Repository: https://github.com/flo-at/rustsnake
- Reddit Discussion: https://www.reddit.com/r/rust/comments/z1g9rd/rustsnake_a_terminalbased_snake_game_written_in/
- SLMT/rust-snake
- A simple Snake game written in Rust, showcasing basic game mechanics.Medium+3Scott Logic+3Reddit+3
- GitHub Repository: https://github.com/SLMT/rust-snake
- Let’s Build Snake with Rust – Scott Logic Blog
- A step-by-step tutorial on implementing the classic Snake game using Rust.Scott Logic
- Blog Post: https://blog.scottlogic.com/2020/10/08/lets-build-snake-with-rust.html
- Tutorial: Snake Game in Rust (Part 1/2) by Eleftheria Batsou
- A beginner-friendly guide to building a Snake game in Rust using the Piston game engine.유튜브
- Medium Article: https://eleftheriabatsou.medium.com/tutorial-snake-game-in-rust-part-1-2-6aa48bcc2aaa
- Creating a Snake Clone in Rust with Bevy – Marcus’ Blog
- An in-depth tutorial on creating a Snake game using the Bevy game engine in Rust.Medium
- Blog Post: https://mbuffett.com/posts/bevy-snake-tutorial/
- Rusty Projects 03— Snake Game Model by Yen
- A project showcasing a Snake game model built with Bevy, featuring unique design elements.
- Medium Article: https://medium.com/rustaceans/rusty-projects-03-snake-game-model-3efa0e3969aa
- Rustlang Project: Snake Game – YouTube Tutorial
- A video tutorial demonstrating the development of a Snake game in Rust using the Piston game engine.
- YouTube Video: https://www.youtube.com/watch?v=DnT_7M7L7vo