---
title: "Jogo Snake no Terminal em Rust"
url: "https://rustlang.com.br/projetos/snake-game/"
markdown_url: "https://rustlang.com.br/projetos/snake-game.MD"
description: "Construa o clássico jogo Snake no terminal com Rust usando crossterm: game loop, controle por teclado, colisão, pontuação e velocidade."
date: "2026-02-24"
author: "Equipe Rust Brasil"
---

# Jogo Snake no Terminal em Rust

Construa o clássico jogo Snake no terminal com Rust usando crossterm: game loop, controle por teclado, colisão, pontuação e velocidade.


Neste projeto vamos construir o clássico **jogo Snake (cobrinha)** diretamente no terminal usando Rust e a crate `crossterm` para manipulação de terminal. A cobra se move continuamente pela tela, o jogador controla a direção com as setas do teclado, e o objetivo é comer as frutas que aparecem aleatoriamente sem colidir consigo mesma ou com as paredes. A cada fruta consumida, a cobra cresce e a velocidade aumenta.

Este é um projeto excelente para aprender sobre game loops, manipulação de terminal em modo raw, entrada de teclado não-bloqueante e estruturas de dados como `VecDeque`, que é perfeita para representar o corpo da cobra.

## O Que Vamos Construir

Um jogo Snake completo com as seguintes funcionalidades:

- Cobra que se move continuamente em uma direção
- Controle por setas do teclado (cima, baixo, esquerda, direita)
- Frutas que aparecem em posições aleatórias
- Crescimento da cobra ao comer frutas
- Detecção de colisão com paredes e com o próprio corpo
- Pontuação exibida em tempo real
- Velocidade crescente conforme a pontuação aumenta
- Tela de game over com opção de reiniciar

## Estrutura do Projeto

```
snake-game/
├── Cargo.toml
└── src/
    └── main.rs
```

## Configurando o Projeto

Crie o projeto com o Cargo:

```bash
cargo new snake-game
cd snake-game
```

Edite o `Cargo.toml` com as dependências necessárias:

```toml
[package]
name = "snake-game"
version = "0.1.0"
edition = "2021"

[dependencies]
crossterm = "0.28"
rand = "0.8"
```

Usamos `crossterm` para controle do terminal (modo raw, posicionamento do cursor, leitura de teclas) e `rand` para gerar posições aleatórias das frutas.

## Passo 1: Definindo os Tipos e Estruturas

Vamos começar definindo as estruturas fundamentais do jogo: posição, direção, cobra e estado do jogo.

```rust
use crossterm::{
    cursor, event::{self, Event, KeyCode, KeyEvent},
    execute, queue,
    style::{self, Stylize},
    terminal::{self, ClearType},
};
use rand::Rng;
use std::collections::VecDeque;
use std::io::{self, Write};
use std::time::{Duration, Instant};

/// Posição (x, y) no tabuleiro
#[derive(Clone, Copy, PartialEq)]
struct Posicao {
    x: u16,
    y: u16,
}

/// Direção de movimento da cobra
#[derive(Clone, Copy, PartialEq)]
enum Direcao {
    Cima,
    Baixo,
    Esquerda,
    Direita,
}

impl Direcao {
    /// Verifica se duas direções são opostas (para impedir inversão)
    fn eh_oposta(&self, outra: &Direcao) -> bool {
        matches!(
            (self, outra),
            (Direcao::Cima, Direcao::Baixo)
                | (Direcao::Baixo, Direcao::Cima)
                | (Direcao::Esquerda, Direcao::Direita)
                | (Direcao::Direita, Direcao::Esquerda)
        )
    }
}

/// Estado completo do jogo
struct Jogo {
    cobra: VecDeque<Posicao>,
    direcao: Direcao,
    fruta: Posicao,
    pontuacao: u32,
    largura: u16,
    altura: u16,
    game_over: bool,
}
```

Usamos `VecDeque` para o corpo da cobra porque precisamos adicionar elementos na frente (cabeça) e remover do final (cauda) de forma eficiente -- ambas operações O(1). A struct `Jogo` mantém todo o estado necessário em um único lugar.

## Passo 2: Inicialização e Geração de Frutas

Agora implementamos a criação do jogo e a lógica para posicionar frutas em locais válidos.

```rust
impl Jogo {
    /// Cria um novo jogo com as dimensões do terminal
    fn novo(largura: u16, altura: u16) -> Self {
        let centro_x = largura / 2;
        let centro_y = altura / 2;

        // Cobra começa com 3 segmentos no centro
        let mut cobra = VecDeque::new();
        cobra.push_back(Posicao { x: centro_x, y: centro_y });
        cobra.push_back(Posicao { x: centro_x - 1, y: centro_y });
        cobra.push_back(Posicao { x: centro_x - 2, y: centro_y });

        let mut jogo = Jogo {
            cobra,
            direcao: Direcao::Direita,
            fruta: Posicao { x: 0, y: 0 },
            pontuacao: 0,
            largura,
            altura,
            game_over: false,
        };

        jogo.gerar_fruta();
        jogo
    }

    /// Gera uma fruta em posição aleatória que não sobreponha a cobra
    fn gerar_fruta(&mut self) {
        let mut rng = rand::thread_rng();
        loop {
            let pos = Posicao {
                x: rng.gen_range(1..self.largura - 1),
                y: rng.gen_range(1..self.altura - 1),
            };
            // Garante que a fruta não aparece sobre a cobra
            if !self.cobra.contains(&pos) {
                self.fruta = pos;
                break;
            }
        }
    }

    /// Calcula o intervalo entre frames baseado na pontuação
    fn velocidade(&self) -> Duration {
        let milissegundos = 150u64.saturating_sub(self.pontuacao as u64 * 5);
        Duration::from_millis(milissegundos.max(50))
    }
}
```

A cobra começa com 3 segmentos no centro da tela, movendo-se para a direita. A geração de frutas usa um loop que garante que a nova posição não coincida com nenhum segmento da cobra. A velocidade diminui o intervalo entre frames conforme a pontuação sobe, até um mínimo de 50ms.

## Passo 3: Lógica de Atualização e Renderização

Aqui implementamos o coração do jogo: mover a cobra, detectar colisões e desenhar tudo no terminal.

```rust
impl Jogo {
    /// Atualiza o estado do jogo: move a cobra, verifica colisões
    fn atualizar(&mut self) {
        // Calcula a nova posição da cabeça
        let cabeca = self.cobra.front().unwrap();
        let nova_cabeca = match self.direcao {
            Direcao::Cima => Posicao { x: cabeca.x, y: cabeca.y.wrapping_sub(1) },
            Direcao::Baixo => Posicao { x: cabeca.x, y: cabeca.y + 1 },
            Direcao::Esquerda => Posicao { x: cabeca.x.wrapping_sub(1), y: cabeca.y },
            Direcao::Direita => Posicao { x: cabeca.x + 1, y: cabeca.y },
        };

        // Verifica colisão com paredes
        if nova_cabeca.x == 0
            || nova_cabeca.x >= self.largura - 1
            || nova_cabeca.y == 0
            || nova_cabeca.y >= self.altura - 1
        {
            self.game_over = true;
            return;
        }

        // Verifica colisão com o próprio corpo
        if self.cobra.contains(&nova_cabeca) {
            self.game_over = true;
            return;
        }

        // Move a cabeça para a nova posição
        self.cobra.push_front(nova_cabeca);

        // Verifica se comeu a fruta
        if nova_cabeca == self.fruta {
            self.pontuacao += 1;
            self.gerar_fruta();
            // Não remove a cauda (cobra cresce)
        } else {
            // Remove a cauda (cobra mantém o tamanho)
            self.cobra.pop_back();
        }
    }

    /// Desenha as bordas do campo de jogo
    fn desenhar_bordas(&self, stdout: &mut io::Stdout) {
        // Borda superior
        for x in 0..self.largura {
            queue!(stdout, cursor::MoveTo(x, 0), style::PrintStyledContent("█".dark_grey())).unwrap();
        }
        // Borda inferior
        for x in 0..self.largura {
            queue!(stdout, cursor::MoveTo(x, self.altura - 1), style::PrintStyledContent("█".dark_grey())).unwrap();
        }
        // Bordas laterais
        for y in 0..self.altura {
            queue!(stdout, cursor::MoveTo(0, y), style::PrintStyledContent("█".dark_grey())).unwrap();
            queue!(stdout, cursor::MoveTo(self.largura - 1, y), style::PrintStyledContent("█".dark_grey())).unwrap();
        }
    }

    /// Renderiza o estado atual do jogo no terminal
    fn renderizar(&self, stdout: &mut io::Stdout) {
        queue!(stdout, terminal::Clear(ClearType::All)).unwrap();

        // Desenha as bordas
        self.desenhar_bordas(stdout);

        // Desenha a fruta
        queue!(
            stdout,
            cursor::MoveTo(self.fruta.x, self.fruta.y),
            style::PrintStyledContent("●".red())
        ).unwrap();

        // Desenha a cobra
        for (i, segmento) in self.cobra.iter().enumerate() {
            let caractere = if i == 0 { "◆" } else { "■" };
            let estilo = if i == 0 {
                caractere.green().bold()
            } else {
                caractere.green()
            };
            queue!(
                stdout,
                cursor::MoveTo(segmento.x, segmento.y),
                style::PrintStyledContent(estilo)
            ).unwrap();
        }

        // Desenha a pontuação no topo
        let info = format!(" Pontuação: {} | Tamanho: {} ", self.pontuacao, self.cobra.len());
        queue!(
            stdout,
            cursor::MoveTo(2, 0),
            style::PrintStyledContent(info.yellow().bold())
        ).unwrap();

        stdout.flush().unwrap();
    }
}
```

A função `atualizar` implementa a mecânica central: calcula a nova posição da cabeça, verifica colisões, e decide se a cauda deve ser removida (movimento normal) ou mantida (cobra comeu fruta e cresce). A renderização usa `queue!` do crossterm para acumular comandos e fazer flush uma única vez, evitando flickering.

## Passo 4: Game Loop e main.rs Completo

Agora juntamos tudo no loop principal com leitura de teclado não-bloqueante.

```rust
/// Exibe a tela de game over e retorna se o jogador quer reiniciar
fn tela_game_over(stdout: &mut io::Stdout, pontuacao: u32) -> bool {
    let msg_titulo = " GAME OVER ";
    let msg_pontuacao = format!(" Pontuação final: {} ", pontuacao);
    let msg_reiniciar = " Pressione R para reiniciar ou Q para sair ";

    execute!(
        stdout,
        cursor::MoveTo(10, 5),
        style::PrintStyledContent(msg_titulo.red().bold()),
        cursor::MoveTo(10, 7),
        style::PrintStyledContent(msg_pontuacao.white()),
        cursor::MoveTo(10, 9),
        style::PrintStyledContent(msg_reiniciar.dark_grey()),
    ).unwrap();

    loop {
        if event::poll(Duration::from_millis(100)).unwrap() {
            if let Event::Key(KeyEvent { code, .. }) = event::read().unwrap() {
                match code {
                    KeyCode::Char('r') | KeyCode::Char('R') => return true,
                    KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc => return false,
                    _ => {}
                }
            }
        }
    }
}

fn main() {
    // Configura o terminal em modo raw
    terminal::enable_raw_mode().unwrap();
    let mut stdout = io::stdout();
    execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide).unwrap();

    // Obtém as dimensões do terminal
    let (largura, altura) = terminal::size().unwrap();
    let largura_jogo = largura.min(60);
    let altura_jogo = altura.min(25);

    'jogo_principal: loop {
        let mut jogo = Jogo::novo(largura_jogo, altura_jogo);
        let mut ultimo_tick = Instant::now();

        // Game loop
        loop {
            // Processa entrada de teclado (não-bloqueante)
            if event::poll(Duration::from_millis(10)).unwrap() {
                if let Event::Key(KeyEvent { code, .. }) = event::read().unwrap() {
                    let nova_direcao = match code {
                        KeyCode::Up => Some(Direcao::Cima),
                        KeyCode::Down => Some(Direcao::Baixo),
                        KeyCode::Left => Some(Direcao::Esquerda),
                        KeyCode::Right => Some(Direcao::Direita),
                        KeyCode::Char('q') | KeyCode::Esc => break 'jogo_principal,
                        _ => None,
                    };

                    // Impede que a cobra inverta a direção sobre si mesma
                    if let Some(dir) = nova_direcao {
                        if !dir.eh_oposta(&jogo.direcao) {
                            jogo.direcao = dir;
                        }
                    }
                }
            }

            // Atualiza o jogo no ritmo definido pela velocidade
            if ultimo_tick.elapsed() >= jogo.velocidade() {
                jogo.atualizar();
                ultimo_tick = Instant::now();

                if jogo.game_over {
                    jogo.renderizar(&mut stdout);
                    if tela_game_over(&mut stdout, jogo.pontuacao) {
                        break; // Reinicia o jogo
                    } else {
                        break 'jogo_principal; // Sai do programa
                    }
                }

                jogo.renderizar(&mut stdout);
            }
        }
    }

    // Restaura o terminal ao estado original
    execute!(stdout, cursor::Show, terminal::LeaveAlternateScreen).unwrap();
    terminal::disable_raw_mode().unwrap();
    println!("Obrigado por jogar Snake!");
}
```

O game loop usa `event::poll` com timeout curto para leitura não-bloqueante do teclado, permitindo que o jogo continue atualizando mesmo sem input. O `Instant` controla o ritmo de atualização conforme a velocidade atual. Usamos `terminal::EnterAlternateScreen` para preservar o conteúdo original do terminal e restaurá-lo ao sair.

## Como Executar

Compile e execute o projeto:

```bash
cargo run
```

Controles:
- **Setas do teclado**: Mudar direção da cobra
- **Q ou Esc**: Sair do jogo
- **R**: Reiniciar após game over

Para a melhor experiência, use um terminal com pelo menos 60 colunas e 25 linhas. Para compilar em modo otimizado:

```bash
cargo build --release
./target/release/snake-game
```

## Desafios para Expandir

1. **Modos de tabuleiro** -- Adicione um modo "sem paredes" onde a cobra atravessa de um lado ao outro da tela (wrapping), e um modo com obstáculos fixos no campo.

2. **Sistema de recordes** -- Salve os 10 melhores scores em um arquivo local com nome do jogador e data, exibindo uma tabela de classificação na tela inicial.

3. **Frutas especiais** -- Implemente diferentes tipos de frutas com cores distintas: frutas douradas que valem pontos extras, frutas que reduzem a velocidade temporariamente, e frutas venenosas que reduzem o tamanho.

4. **Efeitos sonoros** -- Integre a crate `rodio` para tocar sons curtos ao comer frutas, colidir e ao fundo um loop de música 8-bit.

5. **Multiplayer local** -- Adicione uma segunda cobra controlada por WASD, permitindo dois jogadores competirem no mesmo terminal com detecção de colisão entre as cobras.

## Veja Também

- [Vec e Coleções Dinâmicas](/stdlib/vec/) -- Como funciona o Vec usado em diversas partes do jogo
- [VecDeque para Filas Duplas](/stdlib/vecdeque/) -- A estrutura ideal para representar a cobra
- [Medição de Tempo](/stdlib/time/) -- Controle de tempo para o game loop
- [Lendo Input do Usuário](/receitas/ler-input-usuario/) -- Padrões de leitura de entrada no terminal
- [Rust para Jogos](/artigos/rust-para-jogos/) -- Panorama do ecossistema de jogos em Rust
