projeto

Mini Console com Arduino: Jogo da Cobra e Pedra-Papel-Tesoura num LCD

Mini Console com Arduino: Jogo da Cobra e Pedra-Papel-Tesoura num LCD

Uma mini consola de jogos portátil, construída à volta de um Arduino Uno, um ecrã LCD 16x2 com módulo I2C e um joystick analógico. A consola tem dois jogos completos — Jogo da Cobra e Pedra, Papel, Tesoura — navegáveis através de um menu principal, com som em quase todas as ações e gráficos personalizados desenhados pixel a pixel, apesar do ecrã ser um LCD de texto simples.

Componentes usados

  • Arduino Uno
  • Ecrã LCD 16x2 com módulo I2C (endereço 0x27)
  • Módulo joystick analógico (2 eixos + botão)
  • Buzzer piezoelétrico
  • Protoboard e jumpers

Visão geral: como a consola está organizada

O programa funciona com três "estados" principais, controlados por variáveis booleanas (inMenu, inSnake, inRPS): o menu principal, o jogo da cobra, e o jogo de pedra-papel-tesoura. O loop() principal do Arduino verifica em qual destes estados a consola está e chama a função correspondente — handleMenu(), snakeLoop() ou rpsLoop(). É uma máquina de estados simples, mas suficiente para organizar toda a lógica sem misturar os jogos entre si.

Jogo 1 — Jogo da Cobra

A cobra move-se automaticamente numa direção, controlada pelo joystick, e cresce cada vez que come uma "maçã" que aparece em posições aleatórias no ecrã. O jogo termina quando a cobra colide consigo própria.

O truque da resolução dupla no LCD

Um LCD de texto 16x2 mostra apenas caracteres, não pixels individuais — só tem 16 colunas por 2 linhas de "casas" de texto. Mas cada caractere pode ser personalizado através de lcd.createChar(), definindo um bitmap de 5x8 pixels para essa posição. O projeto usa isto a seu favor: cria dois caracteres customizados, um que preenche só a metade superior da célula (topHalf) e outro que preenche só a metade inferior (botHalf). Isto permite tratar cada célula de texto como duas posições verticais independentes, duplicando a resolução vertical efetiva de 2 linhas para 4 "linhas lógicas" (LOGICAL_ROWS = LCD_ROWS * 2) — dando à cobra espaço suficiente para se mover com alguma fluidez, em vez de ficar limitada a saltar entre só duas posições. A mesma técnica é aplicada à maçã (appleTop/appleBot), para que a comida respeite a mesma grelha duplicada.

Como o corpo da cobra é gerido

A posição de cada segmento da cobra é guardada em dois arrays paralelos, snakeX[] e snakeY[], com a posição da cabeça sempre no índice 0. A cada movimento (controlado por um temporizador, moveInterval, que diminui ao longo do jogo para aumentar a dificuldade):

  1. Calcula-se a nova posição da cabeça, somando a direção atual (dirX, dirY).
  2. Se a cabeça sair de um lado do ecrã, reaparece do lado oposto (efeito "wraparound").
  3. Verifica-se se a nova posição da cabeça coincide com algum segmento existente do corpo — se sim, é gameOver.
  4. Apaga-se visualmente a última posição da cauda.
  5. Cada segmento do corpo "empurra-se" para a posição do segmento anterior (o corpo persegue a cabeça).
  6. Se a nova posição da cabeça coincidir com a maçã, a cobra cresce (snakeLen++), a pontuação sobe, o intervalo de movimento diminui (fica mais rápido) e uma nova maçã é colocada numa posição aleatória que não colida com o corpo da cobra.

Leitura do joystick no jogo da cobra

O joystick é lido nos eixos X e Y em modo analógico (0 a 1023), com o centro de repouso perto de 512. Para evitar mudanças de direção acidentais por pequenas variações do sensor, existe uma "zona morta" (dead = 250) — só valores claramente afastados do centro contam como intenção real de movimento. Um temporizador (lastInput) também impede leituras repetidas demasiado depressa, tornando o controlo mais previsível. Há ainda uma regra de segurança: a cobra nunca pode inverter diretamente a direção (por exemplo, ir de "direita" para "esquerda" instantaneamente), o que evitaria que ela colidisse consigo própria de forma injusta.

Jogo 2 — Pedra, Papel, Tesoura

Um jogo simples contra o computador. O jogador navega entre as três opções ("PEDRA", "PAPEL", "TESOURA") movendo o joystick para a esquerda ou direita no eixo X, e confirma a escolha com o botão do joystick. Nesse momento, o Arduino gera uma escolha aleatória (random(0, 3)) para representar a jogada do computador, e o resultado é calculado comparando os índices das duas escolhas com a lógica clássica do jogo (pedra vence tesoura, tesoura vence papel, papel vence pedra). O resultado — "EMPATE!", "VOCE GANHOU!" ou "VOCE PERDEU!" — é mostrado no ecrã, com música de derrota a tocar apenas quando o jogador perde.

Sistema de som

Todo o som é gerado através da função nativa tone() do Arduino, associada a um único buzzer piezoelétrico. O código define notas musicais como constantes de frequência (C4, D4, E4, etc., em Hertz) e usa pequenas melodias de 2 a 3 notas para cada evento:

  • Navegação no menu — uma nota curta e aguda a cada movimento.
  • Início de jogo — uma melodia ascendente de três notas (Dó-Mi-Sol).
  • Comer a maçã — duas notas curtas e satisfatórias.
  • Perder — duas notas descendentes, tocadas apenas uma vez por jogo (controlado pela variável playedLoseMusic, para não repetir o som em cada frame enquanto o ecrã de "GAME OVER" está visível).
  • Seleção no Pedra-Papel-Tesoura — uma nota curta ao mudar de opção.

Menu principal

O menu é navegado no eixo Y do joystick — para cima ou para baixo alterna entre as duas opções ("Cobra" e "Pedra/Papel/Tesoura"), com um ">" a indicar a opção selecionada no momento. Ao premir o botão do joystick, a consola muda de estado (desliga inMenu, liga inSnake ou inRPS) e inicializa o jogo escolhido — no caso da cobra, reiniciando posições e pontuação (resetSnake()); no caso do Pedra-Papel-Tesoura, apenas desenhando o ecrã inicial (drawRPS()).

Código completo (Arduino IDE)

Este é o código completo, pronto a copiar diretamente para o Arduino IDE:

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);

// ------------------- JOYSTICK -------------------
const int JOY_X = A0;
const int JOY_Y = A1;
const int JOY_BTN = 7;

// ------------------- BUZZER -------------------
const int BUZZER = 11; // Pino do buzzer

// Notas musicais básicas
#define C4  262
#define D4  294
#define E4  330
#define F4  349
#define G4  392
#define A4  440
#define B4  494
#define C5  523
#define G3  196

// ------------------- MENU -------------------
int menuIndex = 0;
bool inMenu = true;
bool inSnake = false;
bool inRPS = false; // Pedra Papel Tesoura

// ------------------- JOGO DA COBRA -------------------
const int LCD_COLS = 16;
const int LCD_ROWS = 2;
const int LOGICAL_ROWS = LCD_ROWS * 2;
const int MAX_SNAKE = 64;

int snakeX[MAX_SNAKE];
int snakeY[MAX_SNAKE];
int snakeLen = 3;
int dirX = 1, dirY = 0;
unsigned long lastMove = 0;
unsigned long moveInterval = 200;
unsigned long lastInput = 0;

int foodX, foodY;
int score = 0;
bool gameOver = false;
bool playedLoseMusic = false; // controla para tocar música de perda apenas 1 vez

// ------------------- JOGO RPS -------------------
const char* choices[3] = {"PEDRA", "PAPEL", "TESOURA"};
int playerChoice = 0;
int computerChoice = 0;
bool rpsResultShown = false;

// ------------------- CUSTOM CHARS -------------------
byte topHalf[8] = {
  B11111, B11111, B11111, B11111,
  B00000, B00000, B00000, B00000
};
byte botHalf[8] = {
  B00000, B00000, B00000, B00000,
  B11111, B11111, B11111, B11111
};

// Metade da célula para a maçã (igual ao bloco da cobra)
byte appleTop[8] = {
  B11111, B11111, B11111, B11111,
  B00000, B00000, B00000, B00000
};
byte appleBot[8] = {
  B00000, B00000, B00000, B00000,
  B11111, B11111, B11111, B11111
};

// ----------------------------------------------------
// ==================== MÚSICAS =====================
void playMenuMove() {
  tone(BUZZER, C5, 80); delay(80); noTone(BUZZER);
}

void playStartMusic() {
  int melody[] = {C4, E4, G4};
  int duration[] = {200, 200, 200};
  for (int i = 0; i < 3; i++) {
    tone(BUZZER, melody[i], duration[i]);
    delay(duration[i]);
    noTone(BUZZER);
  }
}

void playEatApple() {
  int melody[] = {G4, C5};
  int duration[] = {100, 100};
  for (int i = 0; i < 2; i++) {
    tone(BUZZER, melody[i], duration[i]);
    delay(duration[i]);
    noTone(BUZZER);
  }
}

void playLoseMusic() {
  if (playedLoseMusic) return;
  playedLoseMusic = true;
  int melody[] = {C4, G3};
  int duration[] = {300, 300};
  for (int i = 0; i < 2; i++) {
    tone(BUZZER, melody[i], duration[i]);
    delay(duration[i]);
    noTone(BUZZER);
  }
}

void playRPSSelect() {
  tone(BUZZER, E4, 80); delay(80); noTone(BUZZER);
}

// =====================================================
void setup() {
  pinMode(JOY_BTN, INPUT_PULLUP);
  pinMode(BUZZER, OUTPUT);
  lcd.init();
  lcd.backlight();

  lcd.createChar(0, topHalf);
  lcd.createChar(1, botHalf);
  lcd.createChar(2, appleTop);
  lcd.createChar(3, appleBot);

  drawMenu();
  playStartMusic();
}

// ====================================================
// ==================== LOOP ==========================
void loop() {
  if (inMenu) handleMenu();
  else if (inSnake) snakeLoop();
  else if (inRPS) rpsLoop();
}

// ====================================================
// ==================== MENU ==========================
void drawMenu() {
  lcd.clear();
  lcd.setCursor(3, 0);
  lcd.print(menuIndex == 0 ? "> Cobra" : "  Cobra");
  lcd.setCursor(3, 1);
  lcd.print(menuIndex == 1 ? "> Pedra/Papel/Tes" : "  Pedra/Papel/Tes");
}

void handleMenu() {
  int y = analogRead(JOY_Y);
  if (y < 300) { menuIndex = (menuIndex + 1) % 2; drawMenu(); playMenuMove(); delay(200);}
  else if (y > 700) { menuIndex = (menuIndex == 0) ? 1 : 0; drawMenu(); playMenuMove(); delay(200);}

  if (digitalRead(JOY_BTN) == LOW) {
    delay(200);
    playedLoseMusic = false;
    playStartMusic();
    if (menuIndex == 0) {
      inMenu = false; inSnake = true;
      resetSnake();
      drawAll(true);
    } else {
      inMenu = false; inRPS = true;
      drawRPS();
    }
  }
}

// ====================================================
// ==================== JOGO DA COBRA =================
void snakeLoop() {
  if (gameOver) {
    lcd.clear();
    lcd.setCursor(4, 0);
    lcd.print("GAME OVER");
    lcd.setCursor(3, 1);
    lcd.print("Pontos: ");
    lcd.print(score);
    playLoseMusic();
    delay(500);

    if (digitalRead(JOY_BTN) == LOW) {
      inSnake = false;
      inMenu = true;
      drawMenu();
      playedLoseMusic = false;
      playStartMusic();
    }
    return;
  }

  readJoystickSnake();
  unsigned long now = millis();
  if (now - lastMove >= moveInterval) {
    lastMove = now;
    moveSnake();
  }
}

void readJoystickSnake() {
  unsigned long now = millis();
  if (now - lastInput < 200) return;

  int x = analogRead(JOY_X);
  int y = analogRead(JOY_Y);
  int center = 512;
  int dead = 250;

  if (x < center - dead && dirX != 1) { dirX = -1; dirY = 0; lastInput = now; }
  else if (x > center + dead && dirX != -1) { dirX = 1; dirY = 0; lastInput = now; }
  else if (y < center - dead && dirY != 1) { dirX = 0; dirY = -1; lastInput = now; }
  else if (y > center + dead && dirY != -1) { dirX = 0; dirY = 1; lastInput = now; }
}

void moveSnake() {
  int newX = snakeX[0] + dirX;
  int newY = snakeY[0] + dirY;

  if (newX < 0) newX = LCD_COLS - 1;
  if (newX >= LCD_COLS) newX = 0;
  if (newY < 0) newY = LOGICAL_ROWS - 1;
  if (newY >= LOGICAL_ROWS) newY = 0;

  for (int i = 0; i < snakeLen; i++) {
    if (snakeX[i] == newX && snakeY[i] == newY) { gameOver = true; return; }
  }

  int tailX = snakeX[snakeLen - 1];
  int tailY = snakeY[snakeLen - 1];
  drawCell(tailX, tailY, ' ');

  for (int i = snakeLen - 1; i > 0; i--) {
    snakeX[i] = snakeX[i - 1];
    snakeY[i] = snakeY[i - 1];
  }

  snakeX[0] = newX;
  snakeY[0] = newY;

  if (newX == foodX && newY == foodY) {
    if (snakeLen < MAX_SNAKE) snakeLen++;
    score++;
    if (moveInterval > 60) moveInterval -= 8;
    placeFood();
    playEatApple();
  }

  drawCell(snakeX[0], snakeY[0], 'S');
  drawCell(foodX, foodY, 'A');
}

void placeFood() {
  bool ok = false;
  while (!ok) {
    ok = true;
    foodX = random(0, LCD_COLS);
    foodY = random(0, LOGICAL_ROWS);
    for (int i = 0; i < snakeLen; i++) {
      if (snakeX[i] == foodX && snakeY[i] == foodY) { ok = false; break; }
    }
  }
}

void drawAll(bool fullRedraw) {
  if (fullRedraw) lcd.clear();
  for (int i = 0; i < snakeLen; i++) drawCell(snakeX[i], snakeY[i], 'S');
  drawCell(foodX, foodY, 'A');
}

void drawCell(int x, int y, char type) {
  int lcdRow = y / 2;
  int half = y % 2;
  lcd.setCursor(x, lcdRow);

  if (type == 'S') lcd.write(half == 0 ? byte(0) : byte(1));
  else if (type == 'A') lcd.write(half == 0 ? byte(2) : byte(3));
  else lcd.print(' ');
}

void resetSnake() {
  snakeLen = 3;
  int startX = LCD_COLS / 2;
  int startY = LOGICAL_ROWS / 2;
  for (int i = 0; i < snakeLen; i++) {
    snakeX[i] = startX - i;
    snakeY[i] = startY;
  }
  dirX = 1; dirY = 0;
  moveInterval = 200;
  score = 0;
  gameOver = false;
  playedLoseMusic = false;
  placeFood();
}

// ====================================================
// ==================== PEDRA PAPEL TESOURA ============
void drawRPS() {
  lcd.clear();
  lcd.setCursor(3, 0);
  lcd.print("Sua escolha:");
  lcd.setCursor(3, 1);
  lcd.print(choices[playerChoice]);
}

void rpsLoop() {
  int x = analogRead(JOY_X);
  if (x < 300) { playerChoice = (playerChoice + 2) % 3; drawRPS(); playRPSSelect(); delay(200);}
  else if (x > 700) { playerChoice = (playerChoice + 1) % 3; drawRPS(); playRPSSelect(); delay(200);}

  if (digitalRead(JOY_BTN) == LOW) {
    delay(300);
    if (!rpsResultShown) {
      computerChoice = random(0, 3);
      showRPSResult();
      rpsResultShown = true;
    } else {
      inRPS = false;
      inMenu = true;
      rpsResultShown = false;
      drawMenu();
      playStartMusic();
    }
  }
}

void showRPSResult() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Voce: ");
  lcd.print(choices[playerChoice]);
  lcd.setCursor(0, 1);
  lcd.print("CPU: ");
  lcd.print(choices[computerChoice]);

  delay(700);
  lcd.clear();
  lcd.setCursor(4, 0);
  if (playerChoice == computerChoice) lcd.print("EMPATE!");
  else if ((playerChoice == 0 && computerChoice == 2) ||
           (playerChoice == 1 && computerChoice == 0) ||
           (playerChoice == 2 && computerChoice == 1))
    lcd.print("VOCE GANHOU!");
  else {
    lcd.print("VOCE PERDEU!");
    playLoseMusic();
  }
  delay(1000);
  lcd.setCursor(0, 1);
  lcd.print("Btn p/ menu...");
}