Build BRINK GAMER: a DIY ESP32 Handheld Game Console

by elismar silva in Circuits > Arduino

365 Views, 1 Favorites, 0 Comments

Build BRINK GAMER: a DIY ESP32 Handheld Game Console

WhatsApp Image 2026-01-08 at 16.43.42 (1).jpeg
video

BRINK GAMER is a DIY handheld game console powered by an ESP32.

This project combines electronics, programming, and gaming in a compact and educational device.

The goal is to demonstrate how the ESP32 can be used to handle graphics, buttons, and sound, making it ideal for embedded game projects.

Supplies

Electronic Components:

  1. ESP32 Development Board
  2. 18650 Li-ion Battery (3.7V)
  3. Battery Charger Module 03962A
  4. Step-Up Voltage Converter (3.7V to 5V)
  5. Graphic Display(ST7567 LCD)
  6. Push Buttons (Up, Down, Left, Right, Action)
  7. Power On/Off Switch
  8. Jumper Wires

Structural and Assembly Materials:

  1. Perforated Board (Prototype Board)
  2. MDF Board (Clipboard-style base)
  3. Instant Glue
  4. Paint

Tools:

  1. Utility Knife (Cutter)
  2. Sandpaper

Where to Start

WhatsApp Image 2026-01-08 at 16.43.46 (1).jpeg

BRINK GAMER is a DIY handheld game console built using an ESP32.

This project was created to combine electronics, programming, and creativity in a compact and portable device.

The console uses a graphic display, physical buttons, and sound feedback to create a simple but fun gaming experience.

Powered by a 18650 Li-ion battery, BRINK GAMER is fully portable and designed as an educational project for anyone interested in embedded systems and game development with microcontrollers.

This project can be reproduced using accessible components and basic tools, making it ideal for makers, students, and electronics enthusiasts.

Cutting and Preparing the Wooden Base

WhatsApp Image 2026-01-08 at 16.43.47.jpeg
WhatsApp Image 2026-01-08 at 16.43.46.jpeg
WhatsApp Image 2026-01-08 at 16.43.45 (1).jpeg
WhatsApp Image 2026-01-08 at 16.43.44 (1).jpeg






Soldering the Headers on the Perfboard

WhatsApp Image 2026-01-08 at 16.43.43 (1).jpeg
WhatsApp Image 2026-01-08 at 16.43.43.jpeg
WhatsApp Image 2026-01-08 at 16.43.41 (1).jpeg
WhatsApp Image 2026-01-08 at 16.43.41.jpeg

In this step, the goal is to prepare a perforated board to easily connect and test the ESP32 and the display.

First, solder pin headers (female or male, depending on your setup) onto the perforated board. These headers will allow the ESP32 and the display module to be plugged in and removed easily during testing.

Make sure the headers are properly aligned before soldering to ensure a good fit and reliable connections.

Once soldered, place the ESP32 and the display onto the headers.

Then, connect the wires from the ESP32 to the display as shown in the figure, ensuring that all signal and power connections are correct.

After wiring, perform basic connection tests to confirm that the display is working properly. This setup helps verify wiring and functionality before moving to the final assembly.

This approach makes debugging easier and prevents damage to the components.

Connections and Pin Mapping

In this step, all electronic connections are explained based on the firmware configuration used in the project.

📟 Display Connections (ST7567 – SPI)

The graphic display communicates with the ESP32 using SPI.

The following pins are defined in the code:

  1. CS (Chip Select) → ESP32 GPIO 5
  2. DC (Data/Command) → ESP32 GPIO 4
  3. RST (Reset) → ESP32 GPIO 15

The SPI communication uses the ESP32 hardware SPI pins internally (MOSI and SCK), while CS, DC, and RST are defined manually.

Make sure the display is powered according to its specifications (3.3V or 5V, depending on the module).

🎮 Button Connections

All buttons are configured using INPUT_PULLUP, meaning each button is connected between the ESP32 pin and GND.

Directional Buttons:

  1. UP → GPIO 25
  2. DOWN → GPIO 26
  3. LEFT → GPIO 27
  4. RIGHT → GPIO 14

Menu Buttons:

  1. SELECT → GPIO 32
  2. OK / START → GPIO 33

Because internal pull-up resistors are enabled, no external resistors are required.

🔊 Buzzer Connection

  1. Buzzer signal → GPIO 13
  2. Buzzer GND → GND

The buzzer is controlled using the tone() function to generate sound effects and startup music.

🔋 Power System

  1. 18650 battery (3.7V) powers the system
  2. The battery is connected to the 03962A charging module
  3. A step-up converter (3.7V to 5V) is used to supply stable voltage
  4. A power on/off switch is placed between the battery and the circuit

This setup allows safe charging and portable operation.

🧪 Final Checks

Before proceeding to the final step:

  1. Verify all connections carefully
  2. Ensure there are no short circuits
  3. Test button inputs and display initialization

Once everything is working correctly, you can proceed to the final step.

Firmware and Code

#include <U8g2lib.h>
#include <SPI.h>
#include <Wire.h>

// Arquivos.h
#include "cobra.h"
#include "pong.h"
#include "shooter.h"
#include "racer.h"
#include "tetris.h"

// ================== PINOS ==================
#define PIN_CS 5
#define PIN_DC 4
#define PIN_RST 15

#define PIN_BTN_UP 25
#define PIN_BTN_DOWN 26
#define PIN_BTN_LEFT 27
#define PIN_BTN_RIGHT 14
#define PIN_BTN_SEL 32
#define PIN_BTN_OK 33
#define PIN_BUZZER 13

// ================== DISPLAY ==================
U8G2_ST7567_JLX12864_F_4W_HW_SPI u8g2(
U8G2_R0, PIN_CS, PIN_DC, PIN_RST
);

// ================== TELAS ==================
enum Tela {
TELA_START,
TELA_MENU,
TELA_COBRA,
TELA_PONG,
TELA_SHOOTER,
TELA_RACER,
TELA_TETRIS
};

Tela telaAtual = TELA_START;

// ================== MENU ==================
int jogoSelecionado = 0;
const int TOTAL_JOGOS = 5;

// ================== START CONTROLE ==================
unsigned long startBlinkTimer = 0;
bool startBlink = true;

// 🎵 controle da música start
unsigned long startMusicTimer = 0;
int startMusicCount = 0;
bool startMusicFinished = false;

// ================== BUZZER ==================
void beep(int f = 2000, int d = 60) {
tone(PIN_BUZZER, f, d);
}

// ================== BOTÕES ==================
void setupButtons() {
pinMode(PIN_BTN_UP, INPUT_PULLUP);
pinMode(PIN_BTN_DOWN, INPUT_PULLUP);
pinMode(PIN_BTN_LEFT, INPUT_PULLUP);
pinMode(PIN_BTN_RIGHT, INPUT_PULLUP);
pinMode(PIN_BTN_SEL, INPUT_PULLUP);
pinMode(PIN_BTN_OK, INPUT_PULLUP);
}

// ================== START MUSIC ==================
void playStartSound() {
tone(PIN_BUZZER, 1200, 120); delay(150);
tone(PIN_BUZZER, 1600, 120); delay(150);
tone(PIN_BUZZER, 2000, 200);
}

// ================== START SCREEN ==================
void drawStartScreen() {

if (millis() - startBlinkTimer > 400) {
startBlinkTimer = millis();
startBlink = !startBlink;
}

u8g2.clearBuffer();

u8g2.setFont(u8g2_font_8x13B_tf);
u8g2.drawStr(18, 16, "GAMER MODE");

// ÍCONE CONTROLE
u8g2.drawRFrame(28, 24, 72, 28, 6);
u8g2.drawDisc(42, 38, 3);
u8g2.drawDisc(86, 38, 3);
u8g2.drawBox(60, 34, 6, 2);

u8g2.setFont(u8g2_font_5x8_tf);
if (startBlink)
u8g2.drawStr(30, 62, "PRESS START");

u8g2.sendBuffer();
}

// ================== ÍCONES MENU ==================
void drawCobraIcon() {
for (int i = 0; i < 5; i++)
u8g2.drawBox(20 + i * 12, 32, 10, 10);
u8g2.drawFrame(80, 32, 10, 10);
}

void drawPongIcon() {
u8g2.drawBox(10, 26, 4, 24);
u8g2.drawBox(114, 26, 4, 24);
u8g2.drawDisc(62, 38, 3);
}

void drawShooterIcon() {
u8g2.drawBox(56, 50, 16, 4);
u8g2.drawBox(60, 46, 8, 4);
u8g2.drawDisc(64, 40, 2);
for (int i = 0; i < 4; i++)
u8g2.drawFrame(20 + i * 20, 26, 12, 6);
}

void drawRacerIcon() {
for (int i = 0; i < 64; i += 8) {
u8g2.drawVLine(20, i, 4);
u8g2.drawVLine(108, i, 4);
}

int px = 56, py = 50;
u8g2.drawBox(px, py, 3, 3);
u8g2.drawBox(px + 8, py, 3, 3);
u8g2.drawBox(px + 4, py + 4, 3, 3);
u8g2.drawBox(px, py + 8, 3, 3);
u8g2.drawBox(px + 8, py + 8, 3, 3);
}

void drawTetrisIcon() {
int x = 52, y = 26, s = 6;
u8g2.drawBox(x, y, s, s);
u8g2.drawBox(x + s + 2, y, s, s);
u8g2.drawBox(x + s / 2 + 1, y + s + 2, s, s);
u8g2.drawBox(x, y + 2 * s + 4, s, s);
u8g2.drawBox(x + s + 2, y + 2 * s + 4, s, s);
}

// ================== MENU ==================
void drawMenu() {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_6x12_tf);

if (jogoSelecionado == 0) { u8g2.drawStr(30, 12, "COBRINHA"); drawCobraIcon(); }
else if (jogoSelecionado == 1) { u8g2.drawStr(46, 12, "PONG"); drawPongIcon(); }
else if (jogoSelecionado == 2) { u8g2.drawStr(26, 12, "SHOOTER"); drawShooterIcon(); }
else if (jogoSelecionado == 3) { u8g2.drawStr(34, 12, "RACER"); drawRacerIcon(); }
else if (jogoSelecionado == 4) { u8g2.drawStr(40, 12, "TETRIS"); drawTetrisIcon(); }

u8g2.setFont(u8g2_font_5x8_tf);
u8g2.drawStr(18, 62, "< SEL OK >");
u8g2.sendBuffer();
}

void handleMenuButtons() {
static unsigned long debounce = 0;
if (millis() - debounce < 200) return;

if (!digitalRead(PIN_BTN_SEL)) {
jogoSelecionado = (jogoSelecionado + 1) % TOTAL_JOGOS;
beep(1400);
debounce = millis();
}

if (!digitalRead(PIN_BTN_OK)) {
beep(2200, 100);

if (jogoSelecionado == 0) { cobraReset(); telaAtual = TELA_COBRA; }
else if (jogoSelecionado == 1) { pongReset(); telaAtual = TELA_PONG; }
else if (jogoSelecionado == 2) { shooterReset(); telaAtual = TELA_SHOOTER; }
else if (jogoSelecionado == 3) { racerReset(); telaAtual = TELA_RACER; }
else if (jogoSelecionado == 4) { tetrisReset(); telaAtual = TELA_TETRIS; }

debounce = millis();
}
}

// ================== SETUP ==================
void setup() {
u8g2.begin();
u8g2.setContrast(120);
setupButtons();

cobraInit(&u8g2, PIN_BUZZER);
pongInit(&u8g2, PIN_BUZZER);
shooterInit(&u8g2, PIN_BUZZER);
racerInit(&u8g2, PIN_BUZZER);
tetrisInit(&u8g2, PIN_BUZZER);
}

// ================== LOOP ==================
void loop() {

// ===== START =====
if (telaAtual == TELA_START) {

// 🎵 Música START 3x
if (!startMusicFinished) {
if (millis() - startMusicTimer > 700) {
startMusicTimer = millis();
playStartSound();
startMusicCount++;

if (startMusicCount >= 3) {
startMusicFinished = true;
}
}
}

drawStartScreen();

if (!digitalRead(PIN_BTN_OK)) {
beep(2000, 120);
delay(300);

// reset música para próxima vez
startMusicCount = 0;
startMusicFinished = false;

telaAtual = TELA_MENU;
}
return;
}

// ===== MENU =====
if (telaAtual == TELA_MENU) {
handleMenuButtons();
drawMenu();
}
else if (telaAtual == TELA_COBRA) {
cobraLoop();
if (!digitalRead(PIN_BTN_SEL)) { beep(900); delay(300); telaAtual = TELA_MENU; }
}
else if (telaAtual == TELA_PONG) {
pongLoop();
if (!digitalRead(PIN_BTN_SEL)) { beep(900); delay(300); telaAtual = TELA_MENU; }
}994955555555555
else if (telaAtual == TELA_SHOOTER) {
shooterLoop();
if (!digitalRead(PIN_BTN_SEL)) { beep(900); delay(300); telaAtual = TELA_MENU; }
}
else if (telaAtual == TELA_RACER) {
racerLoop();
if (!digitalRead(PIN_BTN_SEL)) { beep(900); delay(300); telaAtual = TELA_MENU; }
}
else if (telaAtual == TELA_TETRIS) {
tetrisLoop();
if (!digitalRead(PIN_BTN_SEL)) { beep(900); delay(300); telaAtual = TELA_MENU; }
}
}



Cobra.h

#ifndef COBRA_H
#define COBRA_H

// ================= CONFIG =================
#define WIDTH 16
#define HEIGHT 8
#define BLOCK_SIZE 8

enum Dir {UP, DOWN, LEFT, RIGHT, NONE};

struct Point {
int x;
int y;
};

// ================= VARIÁVEIS =================
U8G2 *display;
int buzzerPin;

Point snake[128];
int snakeLength = 2;
Point fruit;
bool fruitExists = false;

Dir direction = NONE;

unsigned long lastMove = 0;
int moveInterval = 300;
int score = 0;
bool gameStarted = false;

// ================= BUZZER =================
void cobraBeep(int freq=2000, int dur=80) {
tone(buzzerPin, freq, dur);
}

// ================= INIT =================
void cobraInit(U8G2 *u8, int buzzer) {
display = u8;
buzzerPin = buzzer;
randomSeed(analogRead(0) + micros());
}

// ================= RESET =================
void cobraReset() {
snakeLength = 2;
score = 0;
direction = NONE;
gameStarted = false;
moveInterval = 300;

snake[0] = {1, 0};
snake[1] = {0, 0};

fruitExists = false;
}

// ================= FRUTA =================
void spawnFruit() {
while (true) {
fruit.x = random(0, WIDTH);
fruit.y = random(0, HEIGHT);

bool ok = true;
for (int i = 0; i < snakeLength; i++) {
if (snake[i].x == fruit.x && snake[i].y == fruit.y) {
ok = false;
break;
}
}

if (ok) {
fruitExists = true;
return;
}
}
}

// ================= COLISÃO =================
bool checkCollision(int x, int y) {
if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return true;

for (int i = 1; i < snakeLength; i++)
if (snake[i].x == x && snake[i].y == y) return true;

return false;
}

// ================= BOTÕES =================
void cobraButtons() {
static unsigned long debounce = 0;
if (millis() - debounce < 150) return;

if (digitalRead(25) == LOW && direction != DOWN) { direction = UP; gameStarted = true; debounce = millis(); }
if (digitalRead(26) == LOW && direction != UP) { direction = DOWN; gameStarted = true; debounce = millis(); }
if (digitalRead(27) == LOW && direction != RIGHT){ direction = LEFT; gameStarted = true; debounce = millis(); }
if (digitalRead(14) == LOW && direction != LEFT) { direction = RIGHT; gameStarted = true; debounce = millis(); }
}

// ================= MOVIMENTO =================
void moveSnake() {
if (!gameStarted || direction == NONE) return;

Point head = snake[0];

if (direction == UP) head.y--;
if (direction == DOWN) head.y++;
if (direction == LEFT) head.x--;
if (direction == RIGHT) head.x++;

if (checkCollision(head.x, head.y)) {
cobraBeep(400, 300);
cobraReset();
return;
}

for (int i = snakeLength; i > 0; i--)
snake[i] = snake[i - 1];

snake[0] = head;

if (!fruitExists) spawnFruit();

if (head.x == fruit.x && head.y == fruit.y) {
snakeLength++;
score++;
fruitExists = false;
cobraBeep(2500, 100);
}
}

// ================= DRAW =================
void drawSnake() {
display->clearBuffer();
display->setFont(u8g2_font_5x8_tf);

char s[16];
sprintf(s, "Score:%d", score);
display->drawStr(0, 8, s);

if (!gameStarted)
display->drawStr(40, 8, "START");

if (fruitExists)
display->drawBox(fruit.x * BLOCK_SIZE, fruit.y * BLOCK_SIZE + 10, BLOCK_SIZE, BLOCK_SIZE);

for (int i = 0; i < snakeLength; i++) {
display->drawBox(
snake[i].x * BLOCK_SIZE,
snake[i].y * BLOCK_SIZE + 10,
BLOCK_SIZE,
BLOCK_SIZE
);
}

display->sendBuffer();
}

// ================= LOOP =================
void cobraLoop() {
cobraButtons();

if (millis() - lastMove > moveInterval) {
lastMove = millis();
moveSnake();
drawSnake();
}
}

#endif

Tetris.h

#ifndef TETRIS_H
#define TETRIS_H

#include <U8g2lib.h>

// ================= PROTÓTIPOS =================
void tetrisInit(U8G2 *u8g2, int buzzerPin);
void tetrisReset();
void tetrisLoop();

// ================= DISPLAY / BUZZER =================
static U8G2 *tetrisDisplay;
static int tetrisBuzzer;

// ================= PINOS =================
#define TETRIS_BTN_LEFT 27
#define TETRIS_BTN_RIGHT 14
#define TETRIS_BTN_ROTATE 25
#define TETRIS_BTN_START 33

// ================= DIMENSÕES =================
#define TETRIS_CELL 4
#define TETRIS_COLS 22
#define TETRIS_ROWS 16
#define TETRIS_OFFSET_X 20

static byte tetrisGrid[TETRIS_ROWS][TETRIS_COLS];

// ================= ESTADO =================
enum TetrisState { TETRIS_START, TETRIS_PLAY, TETRIS_GAMEOVER };
static TetrisState tetrisState = TETRIS_START;

// ================= PEÇA =================
struct TetrisPiece {
int x, y;
byte shape[4][4];
};
static TetrisPiece tetrisPiece;

// ================= CONTROLE =================
static unsigned long tetrisLastFall = 0;
static unsigned long tetrisBlinkTimer = 0;
static unsigned long tetrisMusicTimer = 0;
static bool tetrisBlinkOn = true;
static bool tetrisEsperandoSoltar = true;
static bool tetrisRotSolto = true;
static int tetrisPontos = 0;
static int tetrisMusicIndex = 0;

// ================= PEÇAS =================
static const byte tetrisPieces[7][4][4] = {
{{0,1,1,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}},
{{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}},
{{0,1,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},
{{1,0,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},
{{0,0,1,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},
{{0,1,1,0},{1,1,0,0},{0,0,0,0},{0,0,0,0}},
{{1,1,0,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}}
};

// ================= MÚSICA =================
static const int tetrisStartMusic[][2] = {
{900,80},{1200,80},{1500,120}
};
static const int tetrisStartMusicLen = 3;

static const int tetrisGameOverMusic[][2] = {
{800,120},{500,200}
};
static const int tetrisGameOverMusicLen = 2;

// ================= SOM =================
static void tetrisBeep(int f,int d){ tone(tetrisBuzzer,f,d); }

// ================= ÍCONE =================
void tetrisDrawIcon(int x,int y){
int s=6;
tetrisDisplay->drawBox(x,y,s,s);
tetrisDisplay->drawBox(x+s+2,y,s,s);
tetrisDisplay->drawBox(x+s/2+1,y+s+2,s,s);
tetrisDisplay->drawBox(x,y+2*s+4,s,s);
tetrisDisplay->drawBox(x+s+2,y+2*s+4,s,s);
}

// ================= DESENHO =================
void tetrisDrawCell(int gx,int gy){
tetrisDisplay->drawBox(
TETRIS_OFFSET_X + gx*TETRIS_CELL,
gy*TETRIS_CELL,
TETRIS_CELL-1,
TETRIS_CELL-1
);
}

void tetrisDrawGrid(){
for(int y=0;y<TETRIS_ROWS;y++)
for(int x=0;x<TETRIS_COLS;x++)
if(tetrisGrid[y][x]) tetrisDrawCell(x,y);
}

void tetrisDrawPiece(){
for(int r=0;r<4;r++)
for(int c=0;c<4;c++)
if(tetrisPiece.shape[r][c]){
int gx=tetrisPiece.x+c;
int gy=tetrisPiece.y+r;
if(gy>=0) tetrisDrawCell(gx,gy);
}
}

void tetrisDrawBorders(){
int l=TETRIS_OFFSET_X-1;
int r=TETRIS_OFFSET_X+TETRIS_COLS*TETRIS_CELL;
tetrisDisplay->drawVLine(l,0,64);
tetrisDisplay->drawVLine(r,0,64);
}

// ================= COLISÃO =================
bool tetrisCollision(int nx,int ny){
for(int r=0;r<4;r++)
for(int c=0;c<4;c++)
if(tetrisPiece.shape[r][c]){
int gx=nx+c, gy=ny+r;
if(gx<0||gx>=TETRIS_COLS||gy>=TETRIS_ROWS) return true;
if(gy>=0 && tetrisGrid[gy][gx]) return true;
}
return false;
}

// ================= ROTAÇÃO =================
void tetrisRotatePiece(){
byte tmp[4][4];
for(int r=0;r<4;r++)
for(int c=0;c<4;c++)
tmp[c][3-r]=tetrisPiece.shape[r][c];

for(int r=0;r<4;r++)
for(int c=0;c<4;c++)
if(tmp[r][c]){
int gx=tetrisPiece.x+c, gy=tetrisPiece.y+r;
if(gx<0||gx>=TETRIS_COLS||gy>=TETRIS_ROWS) return;
if(gy>=0 && tetrisGrid[gy][gx]) return;
}

memcpy(tetrisPiece.shape,tmp,16);
tetrisBeep(900,40);
}

// ================= FIXAR =================
void tetrisFixPiece(){
for(int r=0;r<4;r++)
for(int c=0;c<4;c++)
if(tetrisPiece.shape[r][c]){
int gy=tetrisPiece.y+r;
if(gy>=0) tetrisGrid[gy][tetrisPiece.x+c]=1;
}
}

// ================= LIMPAR LINHAS =================
void tetrisClearLines(){
for(int y=TETRIS_ROWS-1;y>=0;y--){
bool full=true;
for(int x=0;x<TETRIS_COLS;x++) if(!tetrisGrid[y][x]) full=false;
if(full){
for(int yy=y;yy>0;yy--) memcpy(tetrisGrid[yy],tetrisGrid[yy-1],TETRIS_COLS);
memset(tetrisGrid[0],0,TETRIS_COLS);
tetrisPontos++;
tetrisBeep(1200,80);
y++;
}
}
}

// ================= NOVA PEÇA =================
void tetrisNewPiece(){
memcpy(tetrisPiece.shape,tetrisPieces[random(0,7)],16);
tetrisPiece.x=(TETRIS_COLS/2)-2;
tetrisPiece.y=-1;
if(tetrisCollision(tetrisPiece.x,tetrisPiece.y))
tetrisState=TETRIS_GAMEOVER;
}

// ================= INIT =================
void tetrisInit(U8G2 *u8g2,int buzzerPin){
tetrisDisplay=u8g2;
tetrisBuzzer=buzzerPin;

pinMode(TETRIS_BTN_LEFT,INPUT_PULLUP);
pinMode(TETRIS_BTN_RIGHT,INPUT_PULLUP);
pinMode(TETRIS_BTN_ROTATE,INPUT_PULLUP);
pinMode(TETRIS_BTN_START,INPUT_PULLUP);

randomSeed(millis());
tetrisReset();
}

// ================= RESET =================
void tetrisReset(){
memset(tetrisGrid,0,sizeof(tetrisGrid));
tetrisPontos=0;
tetrisBlinkTimer=millis();
tetrisMusicTimer=millis();
tetrisMusicIndex=0;
tetrisBlinkOn=true;
tetrisEsperandoSoltar=true;
tetrisState=TETRIS_START;
tetrisNewPiece();
}

// ================= LOOP =================
void tetrisLoop(){

// ===== START =====
if(tetrisState==TETRIS_START){

if(millis()-tetrisBlinkTimer>400){
tetrisBlinkTimer=millis();
tetrisBlinkOn=!tetrisBlinkOn;
}

if(millis()-tetrisMusicTimer>300){
tetrisMusicTimer=millis();
tetrisBeep(
tetrisStartMusic[tetrisMusicIndex][0],
tetrisStartMusic[tetrisMusicIndex][1]
);
tetrisMusicIndex=(tetrisMusicIndex+1)%tetrisStartMusicLen;
}

if(tetrisEsperandoSoltar){
if(digitalRead(TETRIS_BTN_START)==HIGH)
tetrisEsperandoSoltar=false;
} else if(digitalRead(TETRIS_BTN_START)==LOW){
noTone(tetrisBuzzer);
tetrisBeep(1000,150);
tetrisState=TETRIS_PLAY;
delay(200);
}

tetrisDisplay->clearBuffer();
tetrisDisplay->setFont(u8g2_font_6x12_tf);
tetrisDisplay->drawStr(42,12,"TETRIS");
tetrisDrawIcon(54,18);
if(tetrisBlinkOn)
tetrisDisplay->drawStr(18,56,"PRESSIONE START");
tetrisDisplay->sendBuffer();
return;
}

// ===== PLAY =====
if(tetrisState==TETRIS_PLAY){

if(!digitalRead(TETRIS_BTN_LEFT)&&!tetrisCollision(tetrisPiece.x-1,tetrisPiece.y)){
tetrisPiece.x--; delay(70);
}
if(!digitalRead(TETRIS_BTN_RIGHT)&&!tetrisCollision(tetrisPiece.x+1,tetrisPiece.y)){
tetrisPiece.x++; delay(70);
}

if(tetrisRotSolto && !digitalRead(TETRIS_BTN_ROTATE)){
tetrisRotatePiece(); tetrisRotSolto=false;
}
if(digitalRead(TETRIS_BTN_ROTATE)) tetrisRotSolto=true;

if(millis()-tetrisLastFall>500){
tetrisLastFall=millis();
if(!tetrisCollision(tetrisPiece.x,tetrisPiece.y+1))
tetrisPiece.y++;
else{
tetrisFixPiece();
tetrisClearLines();
tetrisNewPiece();
}
}

tetrisDisplay->clearBuffer();
tetrisDrawBorders();
tetrisDisplay->setFont(u8g2_font_5x8_tf);
tetrisDisplay->setCursor(0,8);
tetrisDisplay->print("PTS:");
tetrisDisplay->print(tetrisPontos);
tetrisDrawGrid();
tetrisDrawPiece();
tetrisDisplay->sendBuffer();
return;
}

// ===== GAME OVER =====
if(tetrisState==TETRIS_GAMEOVER){

static bool tocou=false;
if(!tocou){
for(int i=0;i<tetrisGameOverMusicLen;i++){
tetrisBeep(tetrisGameOverMusic[i][0],tetrisGameOverMusic[i][1]);
delay(220);
}
tocou=true;
}

if(millis()-tetrisBlinkTimer>400){
tetrisBlinkTimer=millis();
tetrisBlinkOn=!tetrisBlinkOn;
}

tetrisDisplay->clearBuffer();
tetrisDisplay->setFont(u8g2_font_6x12_tf);
tetrisDisplay->drawStr(32,22,"GAME OVER");
if(tetrisBlinkOn)
tetrisDisplay->drawStr(18,52,"PRESSIONE START");
tetrisDisplay->sendBuffer();

if(!digitalRead(TETRIS_BTN_START)){
tocou=false;
tetrisReset();
delay(300);
}
}
}

#endif

Pong.h

#ifndef PONG_H
#define PONG_H

#include <U8g2lib.h>

// ================= PROTÓTIPOS =================
void pongInit(U8G2 *u8g2, int buzzerPin);
void pongReset();
void pongLoop();

// ================= DISPLAY / BUZZER =================
static U8G2 *pongDisplay;
static int pongBuzzer;

// ================= PINOS =================
#define PIN_BTN_LEFT 27
#define PIN_BTN_RIGHT 14
#define PIN_BTN_OK 33

// ================= ESTADOS =================
enum PongEstado {
PONG_TELA_INICIO,
PONG_JOGANDO,
PONG_GAME_OVER
};

static PongEstado pongEstado = PONG_TELA_INICIO;

// ================= VARIÁVEIS =================
static int paddleX;
static const int paddleW = 24;
static const int paddleY = 58;

static int ballX, ballY;
static int ballDX, ballDY;

static int pontos = 0;

// ================= TEMPO =================
static unsigned long lastBallMove = 0;
static unsigned long lastPaddleMove = 0;
static unsigned long blinkTimer = 0;
static unsigned long musicTimer = 0;

// ================= CONTROLE =================
static bool blinkOn = true;
static bool aguardandoSoltarBotao = true;
static int musicIndex = 0;

// ================= BEEP =================
static void pongBeep(int f, int d) {
tone(pongBuzzer, f, d);
}

static void pongStopSound() {
noTone(pongBuzzer);
}

// ================= MUSIQUINHA =================
static const int introMusic[][2] = {
{1200, 120},
{1500, 120},
{1800, 120},
{1500, 120}
};
static const int introMusicLen = 4;

// ================= DESENHO CONTROLE =================
static void drawGamerIcon(int x, int y) {
// Corpo
pongDisplay->drawRFrame(x, y, 40, 18, 4);

// Direcional
pongDisplay->drawBox(x + 6, y + 7, 6, 2);
pongDisplay->drawBox(x + 8, y + 5, 2, 6);

// Botões
pongDisplay->drawDisc(x + 30, y + 7, 2);
pongDisplay->drawDisc(x + 34, y + 10, 2);
}

// ================= INIT =================
void pongInit(U8G2 *u8g2, int buzzerPin) {
pongDisplay = u8g2;
pongBuzzer = buzzerPin;
pongReset();
}

// ================= RESET =================
void pongReset() {
paddleX = 52;
ballX = 64;
ballY = 40;
ballDX = 1;
ballDY = -1;

pontos = 0;
pongEstado = PONG_TELA_INICIO;

aguardandoSoltarBotao = true;
blinkTimer = millis();
musicTimer = millis();
musicIndex = 0;
blinkOn = true;
}

// ================= LOOP =================
void pongLoop() {

// ======= TELA START =======
if (pongEstado == PONG_TELA_INICIO) {

if (millis() - blinkTimer > 400) {
blinkTimer = millis();
blinkOn = !blinkOn;
}

if (millis() - musicTimer > 200) {
musicTimer = millis();
pongBeep(introMusic[musicIndex][0], introMusic[musicIndex][1]);
musicIndex++;
if (musicIndex >= introMusicLen) musicIndex = 0;
}

if (aguardandoSoltarBotao) {
if (digitalRead(PIN_BTN_OK) == HIGH)
aguardandoSoltarBotao = false;
} else {
if (digitalRead(PIN_BTN_OK) == LOW) {
pongStopSound();
pongBeep(2500, 120);
pongEstado = PONG_JOGANDO;
delay(200);
}
}

pongDisplay->clearBuffer();
pongDisplay->setFont(u8g2_font_6x12_tf);

pongDisplay->drawStr(42, 12, "PONG");

drawGamerIcon(44, 18);

if (blinkOn)
pongDisplay->drawStr(18, 55, "PRESSIONE START");

pongDisplay->sendBuffer();
return;
}

// ======= GAME OVER =======
if (pongEstado == PONG_GAME_OVER) {

if (digitalRead(PIN_BTN_OK) == LOW) {
pongReset();
delay(300);
}

pongDisplay->clearBuffer();
pongDisplay->setFont(u8g2_font_6x12_tf);

pongDisplay->drawStr(28, 20, "GAME OVER");

char txt[20];
sprintf(txt, "PONTOS: %d", pontos);
pongDisplay->drawStr(24, 38, txt);

pongDisplay->drawStr(10, 58, "OK = REINICIAR");
pongDisplay->sendBuffer();
return;
}

// ======= JOGANDO =======
if (millis() - lastPaddleMove > 20) {
lastPaddleMove = millis();

if (digitalRead(PIN_BTN_LEFT) == LOW && paddleX > 0)
paddleX -= 1;

if (digitalRead(PIN_BTN_RIGHT) == LOW && paddleX < 128 - paddleW)
paddleX += 1;
}

if (millis() - lastBallMove > 35) {
lastBallMove = millis();

ballX += ballDX;
ballY += ballDY;

if (ballX <= 0 || ballX >= 125) {
ballDX = -ballDX;
pongBeep(1200, 20);
}

if (ballY <= 0) {
ballDY = -ballDY;
pongBeep(1200, 20);
}

if (ballY >= paddleY - 2 &&
ballX >= paddleX &&
ballX <= paddleX + paddleW) {
ballDY = -ballDY;
pontos++;
pongBeep(2000, 30);
}

if (ballY > 63) {
pongBeep(400, 300);
pongEstado = PONG_GAME_OVER;
delay(200);
}
}

pongDisplay->clearBuffer();

pongDisplay->drawBox(paddleX, paddleY, paddleW, 4);
pongDisplay->drawBox(ballX, ballY, 3, 3);

pongDisplay->setFont(u8g2_font_5x8_tf);
char s[15];
sprintf(s, "PONTOS:%d", pontos);
pongDisplay->drawStr(2, 8, s);

pongDisplay->sendBuffer();
}

#endif

Shooter.h

#ifndef SHOOTER_H
#define SHOOTER_H

#include <U8g2lib.h>

// ================= PROTÓTIPOS =================
void shooterInit(U8G2 *u8g2, int buzzerPin);
void shooterReset();
void shooterLoop();

// ================= DISPLAY / BUZZER =================
static U8G2 *shooterDisplay;
static int shooterBuzzer;

// ================= PINOS =================
#define PIN_BTN_LEFT 27
#define PIN_BTN_RIGHT 14
#define PIN_BTN_UP 25 // ATIRAR / START
#define PIN_BTN_OK 33

// ================= ESTADO DO JOGO =================
enum ShooterState { SHOOTER_START, SHOOTER_PLAY, SHOOTER_GAMEOVER };
static ShooterState shooterState = SHOOTER_START;

// ================= NAVE =================
static int naveX;
static const int naveY = 54;
static const int naveW = 20;
static const int naveH = 6;

// ================= TIROS =================
#define MAX_TIROS 12
struct Tiro { bool ativo; int x, y; };
static Tiro tiros[MAX_TIROS];

// ================= INIMIGOS =================
#define MAX_INIMIGOS 5
static int inimigoX[MAX_INIMIGOS];
static int inimigoY[MAX_INIMIGOS];

// ================= CONTROLE =================
static int shooterPontos = 0;
static unsigned long shooterLastMove = 0;
static unsigned long shooterLastEnemy = 0;
static unsigned long shooterLastShot = 0;
static unsigned long shooterBlinkTimer = 0;
static bool shooterBlinkOn = true;
static bool shooterAguardandoSoltarBotao = true;
static unsigned long shooterMusicTimer = 0;
static int shooterMusicIndex = 0;

// ================= MUSIQUINHA =================
// Start: notas suaves e ascendentes
static const int shooterIntroMusic[][2] = {
{600,120}, {800,120}, {1000,120}, {1200,120}, {1400,150}
};
// Game Over: notas descendentes suaves
static const int shooterGameOverMusic[][2] = {
{800,150}, {700,150}, {600,150}, {500,200}, {400,250}
};
static const int shooterIntroMusicLen = 5;
static const int shooterGameOverMusicLen = 5;

// ================= SOM =================
static void sBeep(int f,int d){tone(shooterBuzzer,f,d);}
static void shooterStopSound(){noTone(shooterBuzzer);}

// ================= DESENHOS =================
static void drawShooterGamerIcon(int x,int y){
shooterDisplay->drawRFrame(x,y,40,18,4);
shooterDisplay->drawBox(x+6,y+7,6,2);
shooterDisplay->drawBox(x+8,y+5,2,6);
shooterDisplay->drawDisc(x+30,y+7,2);
shooterDisplay->drawDisc(x+34,y+10,2);
}

static void drawNave(int x,int y){
shooterDisplay->drawBox(x,y,naveW,naveH);
shooterDisplay->drawBox(x+naveW/2-2,y-6,4,6);
}

// ===== Tiros grandes e lentos =====
static void drawTiro(int x,int y){
shooterDisplay->drawDisc(x,y,6);
}

static void drawInimigo(int x,int y){
shooterDisplay->drawFrame(x,y,12,8);
shooterDisplay->drawBox(x+2,y+2,8,3);
}

// ================= INIT =================
void shooterInit(U8G2 *u8g2,int buzzerPin){
shooterDisplay = u8g2;
shooterBuzzer = buzzerPin;
pinMode(PIN_BTN_LEFT,INPUT_PULLUP);
pinMode(PIN_BTN_RIGHT,INPUT_PULLUP);
pinMode(PIN_BTN_UP,INPUT_PULLUP);
pinMode(PIN_BTN_OK,INPUT_PULLUP);
shooterReset();
}

// ================= RESET =================
void shooterReset(){
shooterState = SHOOTER_START;
naveX = (128-naveW)/2;
shooterPontos = 0;
for(int i=0;i<MAX_INIMIGOS;i++){
inimigoX[i]=random(4,118);
inimigoY[i]=random(-80,-10);
}
for(int i=0;i<MAX_TIROS;i++) tiros[i].ativo=false;
shooterBlinkTimer=millis();
shooterBlinkOn=true;
shooterAguardandoSoltarBotao=true;
shooterMusicTimer=millis();
shooterMusicIndex=0;
shooterLastShot=0;
}

// ================= LOOP =================
void shooterLoop(){

// ===== START =====
if(shooterState==SHOOTER_START){
if(millis()-shooterBlinkTimer>400){shooterBlinkTimer=millis();shooterBlinkOn=!shooterBlinkOn;}
if(millis()-shooterMusicTimer>300){
shooterMusicTimer=millis();
sBeep(shooterIntroMusic[shooterMusicIndex][0],shooterIntroMusic[shooterMusicIndex][1]);
shooterMusicIndex=(shooterMusicIndex+1)%shooterIntroMusicLen;
}
if(shooterAguardandoSoltarBotao){
if(digitalRead(PIN_BTN_OK)==HIGH) shooterAguardandoSoltarBotao=false;
} else if(digitalRead(PIN_BTN_OK)==LOW){
shooterStopSound();
sBeep(1000,150);
shooterState=SHOOTER_PLAY;
delay(200);
}
shooterDisplay->clearBuffer();
shooterDisplay->setFont(u8g2_font_6x12_tf);
shooterDisplay->drawStr(28,12,"SHOOTER");
drawShooterGamerIcon(44,18);
if(shooterBlinkOn) shooterDisplay->drawStr(18,55,"PRESSIONE START");
shooterDisplay->sendBuffer();
return;
}

// ===== PLAY =====
if(shooterState==SHOOTER_PLAY){

// MOVIMENTO NAVE
if(millis()-shooterLastMove>15){shooterLastMove=millis();
if(!digitalRead(PIN_BTN_LEFT) && naveX>0) naveX-=2;
if(!digitalRead(PIN_BTN_RIGHT) && naveX<128-naveW) naveX+=2;
}

// DISPARO: ainda mais lento
if(!digitalRead(PIN_BTN_UP) && millis()-shooterLastShot>400){
for(int i=0;i<MAX_TIROS;i++){
if(!tiros[i].ativo){
tiros[i].ativo = true;
tiros[i].x = naveX + naveW/2;
tiros[i].y = naveY - 4;
sBeep(2000,40);
shooterLastShot = millis();
break;
}
}
}

// MOVIMENTO TIROS: mais lento
for(int i=0;i<MAX_TIROS;i++){
if(tiros[i].ativo){
tiros[i].y -= 1;
if(tiros[i].y < 0) tiros[i].ativo = false;
}
}

// INIMIGOS
if(millis()-shooterLastEnemy>350){shooterLastEnemy=millis();
for(int i=0;i<MAX_INIMIGOS;i++){
inimigoY[i]+=2;
if(inimigoY[i]>=naveY){sBeep(300,500); shooterState=SHOOTER_GAMEOVER;}
}
}

// COLISÃO
for(int i=0;i<MAX_INIMIGOS;i++){
for(int j=0;j<MAX_TIROS;j++){
if(tiros[j].ativo &&
tiros[j].x>=inimigoX[i] &&
tiros[j].x<=inimigoX[i]+12 &&
tiros[j].y>=inimigoY[i] &&
tiros[j].y<=inimigoY[i]+8){
tiros[j].ativo=false;
shooterPontos++;
sBeep(1500,80);
inimigoX[i]=random(4,118);
inimigoY[i]=random(-80,-10);
}
}
}

// DESENHO
shooterDisplay->clearBuffer();
drawNave(naveX,naveY);
for(int i=0;i<MAX_TIROS;i++) if(tiros[i].ativo) drawTiro(tiros[i].x,tiros[i].y);
for(int i=0;i<MAX_INIMIGOS;i++) drawInimigo(inimigoX[i],inimigoY[i]);
shooterDisplay->setFont(u8g2_font_5x8_tf);
char txt[16]; sprintf(txt,"PONTOS:%d",shooterPontos);
shooterDisplay->drawStr(2,8,txt);
shooterDisplay->sendBuffer();
return;
}

// ===== GAME OVER =====
if(shooterState==SHOOTER_GAMEOVER){
if(millis()-shooterBlinkTimer>400){shooterBlinkTimer=millis(); shooterBlinkOn=!shooterBlinkOn;}
if(millis()-shooterMusicTimer>400){ // música Game Over
shooterMusicTimer=millis();
sBeep(shooterGameOverMusic[shooterMusicIndex][0], shooterGameOverMusic[shooterMusicIndex][1]);
shooterMusicIndex=(shooterMusicIndex+1)%shooterGameOverMusicLen;
}
shooterDisplay->clearBuffer();
shooterDisplay->setFont(u8g2_font_6x12_tf);
shooterDisplay->drawStr(26,20,"GAME OVER");
char txt[20]; sprintf(txt,"PONTOS:%d",shooterPontos);
shooterDisplay->drawStr(28,38,txt);
if(shooterBlinkOn) shooterDisplay->drawStr(12,58,"OK = REINICIAR");
shooterDisplay->sendBuffer();
if(!digitalRead(PIN_BTN_UP)){ shooterReset(); delay(300);}
}

}

#endif

Racer.h

#ifndef RACER_H
#define RACER_H

#include <U8g2lib.h>

// ================= PROTÓTIPOS =================
void racerInit(U8G2 *u8g2, int buzzerPin);
void racerReset();
void racerLoop();

// ================= DISPLAY / BUZZER =================
static U8G2 *racerDisplay;
static int racerBuzzer;

// ================= PINOS =================
#define RACER_PIN_LEFT 27
#define RACER_PIN_RIGHT 14
#define RACER_PIN_OK 33 // START / REINICIAR

// ================= ESTADO DO JOGO =================
enum RacerState { RACER_START, RACER_PLAY, RACER_GAMEOVER };
static RacerState racerState = RACER_START;

// ================= JOGADOR =================
static int racerPlayerX;
static const int racerPlayerY = 50;
static const int racerPlayerW = 11;
static const int racerPlayerH = 11;

// ================= INIMIGOS =================
#define RACER_MAX_ENEMIES 4
struct RacerEnemy { bool ativo; int x, y; int speed; };
static RacerEnemy racerEnemies[RACER_MAX_ENEMIES];

// ================= CONTROLE =================
static int racerPontos = 0;
static unsigned long racerLastMove = 0;
static unsigned long racerLastEnemyFall = 0;
static unsigned long racerBlinkTimer = 0;
static bool racerBlinkOn = true;
static bool racerEsperandoBotao = true;
static unsigned long racerMusicTimer = 0;
static int racerMusicIndex = 0;
static unsigned long racerEngineTimer = 0;

// ================= MUSIQUINHA =================
static const int racerStartMusic[][2] = {{600,120},{800,120},{1000,120},{1200,150}};
static const int racerGameOverMusic[][2] = {{800,150},{700,150},{600,150},{500,200}};
static const int racerStartMusicLen = 4;
static const int racerGameOverMusicLen = 4;

// ================= SOM =================
static void racerBeep(int f,int d){tone(racerBuzzer,f,d);}
static void racerStopSound(){noTone(racerBuzzer);}
static void racerEngineSound() {
static bool toggle = false;
int freq = toggle ? 700 : 900;
toggle = !toggle;
tone(racerBuzzer, freq, 50);
}

// ================= DESENHOS =================
static void racerDrawPlayer(int x,int y){
racerDisplay->drawBox(x, y, 3, 3);
racerDisplay->drawBox(x + 8, y, 3, 3);
racerDisplay->drawBox(x + 4, y + 4, 3, 3);
racerDisplay->drawBox(x, y + 8, 3, 3);
racerDisplay->drawBox(x + 8, y + 8, 3, 3);
}

static void racerDrawEnemy(int x,int y){
racerDrawPlayer(x, y);
}

static void racerDrawTrack(){
for(int i=0;i<64;i+=8){
racerDisplay->drawVLine(20,i,4);
racerDisplay->drawVLine(108,i,4);
}
}

// ================= INIT =================
void racerInit(U8G2 *u8g2,int buzzerPin){
racerDisplay = u8g2;
racerBuzzer = buzzerPin;
pinMode(RACER_PIN_LEFT,INPUT_PULLUP);
pinMode(RACER_PIN_RIGHT,INPUT_PULLUP);
pinMode(RACER_PIN_OK,INPUT_PULLUP);
racerReset();
}

// ================= RESET =================
void racerReset(){
racerState = RACER_START;
racerPlayerX = 64;
racerPontos = 0;
for(int i=0;i<RACER_MAX_ENEMIES;i++){
racerEnemies[i].ativo = false;
racerEnemies[i].x = 0;
racerEnemies[i].y = -30 * i; // espaçamento inicial
racerEnemies[i].speed = random(2,5);
}
racerBlinkTimer = millis();
racerBlinkOn = true;
racerEsperandoBotao = true;
racerMusicTimer = millis();
racerMusicIndex = 0;
racerLastMove = 0;
racerLastEnemyFall = 0;
racerEngineTimer = 0;
}

// ================= LOOP =================
void racerLoop(){

// ===== START =====
if(racerState==RACER_START){
if(millis()-racerBlinkTimer>400){racerBlinkTimer=millis(); racerBlinkOn=!racerBlinkOn;}
if(millis()-racerMusicTimer>300){
racerMusicTimer=millis();
racerBeep(racerStartMusic[racerMusicIndex][0], racerStartMusic[racerMusicIndex][1]);
racerMusicIndex=(racerMusicIndex+1)%racerStartMusicLen;
}
// aguarda soltar o botão
if(racerEsperandoBotao){
if(digitalRead(RACER_PIN_OK)==HIGH) racerEsperandoBotao=false;
} else if(digitalRead(RACER_PIN_OK)==LOW){
racerStopSound();
racerBeep(1000,150);
racerState=RACER_PLAY;
delay(200);
}

// Desenho da tela de START
racerDisplay->clearBuffer();
racerDisplay->setFont(u8g2_font_6x12_tf);
racerDisplay->drawStr(30,12,"RACER GAME");

// ================= ÍCONE / CARRO MAIOR =================
int iconeX = 54;
int iconeY = 20;
racerDisplay->drawBox(iconeX, iconeY, 6, 6); // topo esquerda
racerDisplay->drawBox(iconeX + 12, iconeY, 6, 6); // topo direita
racerDisplay->drawBox(iconeX + 6, iconeY + 6, 6, 6);// meio
racerDisplay->drawBox(iconeX, iconeY + 12, 6, 6); // base esquerda
racerDisplay->drawBox(iconeX + 12, iconeY + 12, 6, 6);// base direita

// Mensagem piscando
if(racerBlinkOn) racerDisplay->drawStr(18,55,"PRESSIONE START");
racerDisplay->sendBuffer();
return;
}


// ===== PLAY =====
if(racerState==RACER_PLAY){

// MOVIMENTO JOGADOR
if(millis()-racerLastMove>15){ racerLastMove=millis();
if(!digitalRead(RACER_PIN_LEFT) && racerPlayerX>24) racerPlayerX-=2;
if(!digitalRead(RACER_PIN_RIGHT) && racerPlayerX<108-racerPlayerW) racerPlayerX+=2;
}

// INIMIGOS DESCENDO
if(millis()-racerLastEnemyFall>100){ racerLastEnemyFall=millis();
for(int i=0;i<RACER_MAX_ENEMIES;i++){
if(racerEnemies[i].ativo){
racerEnemies[i].y += racerEnemies[i].speed;
if(racerEnemies[i].y >= 64){
racerEnemies[i].ativo=false;
racerPontos++;
}
// COLISÃO
if(racerEnemies[i].y + 11 >= racerPlayerY && racerEnemies[i].y <= racerPlayerY + racerPlayerH &&
racerEnemies[i].x + 11 >= racerPlayerX && racerEnemies[i].x <= racerPlayerX + racerPlayerW){
racerState=RACER_GAMEOVER;
racerBeep(300,500);
}
} else {
// novo inimigo só aparece se houver espaço vertical livre
bool podeSpawn = true;
for(int j=0;j<RACER_MAX_ENEMIES;j++){
if(racerEnemies[j].ativo && abs(racerEnemies[j].y)<20) podeSpawn=false;
}
if(podeSpawn){
racerEnemies[i].ativo = true;
racerEnemies[i].x = random(28, 94);
racerEnemies[i].y = random(-50,-10);
racerEnemies[i].speed = random(2,5);
}
}
}
}

// SOM MOTOR
if(millis() - racerEngineTimer > 60){
racerEngineTimer = millis();
racerEngineSound();
}

// DESENHO
racerDisplay->clearBuffer();
racerDrawTrack();
racerDrawPlayer(racerPlayerX,racerPlayerY);
for(int i=0;i<RACER_MAX_ENEMIES;i++) if(racerEnemies[i].ativo) racerDrawEnemy(racerEnemies[i].x,racerEnemies[i].y);
racerDisplay->setFont(u8g2_font_5x8_tf);
char txt[16]; sprintf(txt,"PONTOS:%d",racerPontos);
racerDisplay->drawStr(2,8,txt);
racerDisplay->sendBuffer();
return;
}

// ===== GAME OVER =====
if(racerState==RACER_GAMEOVER){
if(millis()-racerBlinkTimer>400){racerBlinkTimer=millis(); racerBlinkOn=!racerBlinkOn;}
if(millis()-racerMusicTimer>400){
racerMusicTimer=millis();
racerBeep(racerGameOverMusic[racerMusicIndex][0], racerGameOverMusic[racerMusicIndex][1]);
racerMusicIndex=(racerMusicIndex+1)%racerGameOverMusicLen;
}
racerDisplay->clearBuffer();
racerDisplay->setFont(u8g2_font_6x12_tf);
racerDisplay->drawStr(26,20,"GAME OVER");
char txt[20]; sprintf(txt,"PONTOS:%d",racerPontos);
racerDisplay->drawStr(28,38,txt);
if(racerBlinkOn) racerDisplay->drawStr(12,58,"OK = REINICIAR");
racerDisplay->sendBuffer();
if(!digitalRead(RACER_PIN_OK)) { racerReset(); delay(300);}
}

}

#endif

Spacetrasher.h

#ifndef SPACETRASH_H
#define SPACETRASH_H

#include <U8g2lib.h>
#include <Arduino.h>

// ================== CONFIGURAÇÕES ==================
#define ST_OBJ_CNT 30
#define ST_FP 4
#define ST_AREA_HEIGHT 56
#define ST_AREA_WIDTH 128

// ================== ESTRUTURAS ==================
typedef struct {
uint8_t ot; // tipo do objeto
int8_t tmp; // valor temporário
int16_t x, y; // posição
int8_t x0, y0, x1, y1; // bounding box
} st_obj;

// ================== VARIÁVEIS GLOBAIS ==================
static U8G2 *st_u8g2 = nullptr;
static uint8_t st_buzzerPin = 0;
static uint8_t st_player_pos = 28;
static uint16_t st_player_points = 0;
static uint16_t st_player_points_delayed = 0;
static uint16_t st_highscore = 0;
static uint8_t st_state = 0;
static uint8_t st_difficulty = 1;
static uint16_t st_to_diff_cnt = 0;
static st_obj st_objects[ST_OBJ_CNT];
static uint8_t st_fire_player = 0;
static uint8_t st_is_fire_last_value = 1;

// Estados
#define ST_STATE_PREPARE 0
#define ST_STATE_IPREPARE 1
#define ST_STATE_GAME 2
#define ST_STATE_END 3
#define ST_STATE_IEND 4

// Tipos de objetos
#define ST_OT_TRASH1 4
#define ST_OT_PLAYER 5
#define ST_OT_MISSLE 3
#define ST_OT_BIG_TRASH 2
#define ST_OT_TRASH2 9

// Bitmaps
static const uint8_t st_bitmap_player1[] = { 0x060, 0x0f8, 0x07e, 0x0f8, 0x060 };
static const uint8_t st_bitmap_trash_5x5_1[] = { 0x070, 0x0f0, 0x0f8, 0x078, 0x030 };
static const uint8_t st_bitmap_trash_5x5_2[] = { 0x030, 0x0f8, 0x0f8, 0x0f0, 0x070 };
static const uint8_t st_bitmap_trash_7x7[] = { 0x038, 0x07c, 0x0fc, 0x0fe, 0x0fe, 0x07e, 0x078 };

// ================== FUNÇÕES INTERNAS ==================
static void st_beep(int f = 2000, int d = 60) {
if(st_buzzerPin) tone(st_buzzerPin, f, d);
}

static void st_ClrObjs() {
for(int i = 0; i < ST_OBJ_CNT; i++) {
st_objects[i].ot = 0;
}
}

static int8_t st_NewObj() {
for(int i = 0; i < ST_OBJ_CNT; i++) {
if(st_objects[i].ot == 0) return i;
}
return -1;
}

static void st_SetXY(st_obj *o, uint8_t x, uint8_t y) {
o->x = ((int16_t)x) << ST_FP;
o->y = ((int16_t)y) << ST_FP;
}

static void st_InitTrash(uint8_t x, uint8_t y) {
int8_t objnr = st_NewObj();
if(objnr < 0) return;
st_obj *o = &st_objects[objnr];
o->ot = (rand() & 1) ? ST_OT_TRASH1 : ST_OT_TRASH2;
o->tmp = (rand() & 1) ? 1 : -1;
st_SetXY(o, x, y);
o->x0 = -3; o->x1 = 1;
o->y0 = -2; o->y1 = 2;
// 25% de chance de ser lixo grande em dificuldade alta
if(st_difficulty >= 5 && (rand() & 3) == 0) {
o->ot = ST_OT_BIG_TRASH;
o->y0--; o->y1++;
o->x0--; o->x1++;
}
}

static void st_NewPlayerMissle(uint8_t x, uint8_t y) {
int8_t objnr = st_NewObj();
if(objnr < 0) return;
st_obj *o = &st_objects[objnr];
o->ot = ST_OT_MISSLE;
st_SetXY(o, x, y);
o->x0 = -4; o->x1 = 1;
o->y0 = 0; o->y1 = 0;
}

static void st_NewPlayer() {
int8_t objnr = st_NewObj();
if(objnr < 0) return;
st_obj *o = &st_objects[objnr];
o->x = 6 << ST_FP;
o->y = (ST_AREA_HEIGHT / 2) << ST_FP;
o->x0 = -6; o->x1 = 0;
o->y0 = -2; o->y1 = 2;
o->ot = ST_OT_PLAYER;
}

static void st_FireStep(uint8_t is_fire) {
if(st_fire_player < 20) {
st_fire_player++;
} else {
if(st_is_fire_last_value && !is_fire) {
st_fire_player = 0;
}
}
st_is_fire_last_value = is_fire;
}

static void st_InitDeltaTrash() {
// Contar lixo atual
uint8_t trash_cnt = 0;
uint8_t max_x = 0;
for(int i = 0; i < ST_OBJ_CNT; i++) {
uint8_t ot = st_objects[i].ot;
if(ot == ST_OT_TRASH1 || ot == ST_OT_TRASH2 || ot == ST_OT_BIG_TRASH) {
trash_cnt++;
uint8_t x = st_objects[i].x >> ST_FP;
if(x > max_x) max_x = x;
}
}
// Criar novo lixo se necessário
if(trash_cnt < 20 && max_x < (ST_AREA_WIDTH - 20)) {
st_InitTrash(ST_AREA_WIDTH - 1, rand() % ST_AREA_HEIGHT);
}
}

static void st_DrawBitmap(uint8_t x, uint8_t y, const uint8_t *bm, uint8_t w, uint8_t h) {
uint8_t screen_y = st_u8g2->getHeight() - 1 - y;
if(screen_y < h) return; // Ajuste para não desenhar fora da tela
st_u8g2->drawBitmap(x, screen_y - h + 1, (w + 7) / 8, h, bm);
}

static void st_DrawObjects() {
for(int i = 0; i < ST_OBJ_CNT; i++) {
if(st_objects[i].ot == 0) continue;
uint8_t x = st_objects[i].x >> ST_FP;
uint8_t y = st_objects[i].y >> ST_FP;
switch(st_objects[i].ot) {
case ST_OT_PLAYER:
st_DrawBitmap(x, y, st_bitmap_player1, 7, 5);
break;
case ST_OT_TRASH1:
st_DrawBitmap(x, y, st_bitmap_trash_5x5_1, 5, 5);
break;
case ST_OT_TRASH2:
st_DrawBitmap(x, y, st_bitmap_trash_5x5_2, 5, 5);
break;
case ST_OT_BIG_TRASH:
st_DrawBitmap(x, y, st_bitmap_trash_7x7, 7, 7);
break;
case ST_OT_MISSLE:
st_u8g2->drawBox(x, st_u8g2->getHeight() - 1 - y, 3, 1);
break;
}
}
}

static void st_DrawHUD() {
char buf[16];
// Pontuação
st_u8g2->setFont(u8g2_font_5x7_tf);
sprintf(buf, "%d", st_player_points_delayed);
st_u8g2->drawStr(ST_AREA_WIDTH - strlen(buf) * 5 - 2, 6, buf);
// Dificuldade
sprintf(buf, "Lvl:%d", st_difficulty);
st_u8g2->drawStr(2, 6, buf);
// Barra de progresso da dificuldade
uint8_t progress = map(st_to_diff_cnt, 0, 1500, 0, ST_AREA_WIDTH - 40);
st_u8g2->drawFrame(40, 0, ST_AREA_WIDTH - 40, 3);
st_u8g2->drawBox(40, 0, progress, 3);
}

static void st_DrawStartScreen() {
st_u8g2->setFont(u8g2_font_6x10_tf);
st_u8g2->drawStr(30, 20, "SPACE TRASH");
st_u8g2->setFont(u8g2_font_5x8_tf);
st_u8g2->drawStr(25, 40, "PRESS FIRE");
// Desenha nave do jogador
st_DrawBitmap(60, 50, st_bitmap_player1, 7, 5);
}

static void st_DrawGameOver() {
st_u8g2->setFont(u8g2_font_6x10_tf);
st_u8g2->drawStr(30, 20, "GAME OVER");
st_u8g2->setFont(u8g2_font_5x8_tf);
char buf[32];
sprintf(buf, "SCORE: %d", st_player_points);
st_u8g2->drawStr(30, 35, buf);
if(st_player_points > st_highscore) {
st_highscore = st_player_points;
st_u8g2->drawStr(25, 45, "NEW HIGHSCORE!");
} else {
sprintf(buf, "HIGH: %d", st_highscore);
st_u8g2->drawStr(35, 45, buf);
}
st_u8g2->drawStr(20, 55, "FIRE TO RESTART");
}

// ================== FUNÇÕES PÚBLICAS ==================
void spacetrashInit(U8G2 *display, uint8_t buzzerPin) {
st_u8g2 = display;
st_buzzerPin = buzzerPin;
}

void spacetrashReset() {
st_player_points = 0;
st_player_points_delayed = 0;
st_difficulty = 1;
st_to_diff_cnt = 0;
st_ClrObjs();
st_NewPlayer();
st_state = ST_STATE_PREPARE;
st_beep(1500, 100);
}

void spacetrashLoop() {
if(!st_u8g2) return;
// Limpar buffer
st_u8g2->clearBuffer();
// Desenhar de acordo com o estado
switch(st_state) {
case ST_STATE_PREPARE:
st_DrawStartScreen();
break;
case ST_STATE_GAME:
// Desenhar objetos
st_DrawObjects();
// Desenhar HUD
st_DrawHUD();
// Atualizar lógica do jogo
for(int i = 0; i < ST_OBJ_CNT; i++) {
if(st_objects[i].ot) {
// Mover para esquerda
st_objects[i].x -= (1 << ST_FP) / 8 + st_difficulty;
// Remover se saiu da tela
if((st_objects[i].x >> ST_FP) < -10) {
st_objects[i].ot = 0;
}
}
}
// Gerar novo lixo
if(rand() % (30 - st_difficulty) == 0) {
st_InitDeltaTrash();
}
// Atualizar pontuação
if(st_player_points_delayed < st_player_points) {
st_player_points_delayed++;
}
// Aumentar dificuldade
st_to_diff_cnt++;
if(st_to_diff_cnt >= 1500) {
st_to_diff_cnt = 0;
st_difficulty++;
st_player_points += 25;
st_beep(1200, 50);
}
// Verificar colisões (simplificado)
for(int i = 0; i < ST_OBJ_CNT; i++) {
if(st_objects[i].ot >= ST_OT_TRASH1 && st_objects[i].ot <= ST_OT_BIG_TRASH) {
uint8_t trash_x = st_objects[i].x >> ST_FP;
uint8_t trash_y = st_objects[i].y >> ST_FP;
// Colisão com jogador
if(trash_x < 10 && trash_x > 0 &&
abs(trash_y - st_player_pos) < 5) {
st_state = ST_STATE_END;
st_beep(300, 200);
}
// Colisão com mísseis
for(int j = 0; j < ST_OBJ_CNT; j++) {
if(st_objects[j].ot == ST_OT_MISSLE) {
uint8_t missle_x = st_objects[j].x >> ST_FP;
uint8_t missle_y = st_objects[j].y >> ST_FP;
if(abs(missle_x - trash_x) < 5 && abs(missle_y - trash_y) < 5) {
st_objects[i].ot = 0;
st_objects[j].ot = 0;
st_player_points += st_difficulty * 5;
st_beep(1000, 30);
}
}
}
}
}
break;
case ST_STATE_END:
st_DrawGameOver();
break;
}
// Enviar buffer para o display
st_u8g2->sendBuffer();
}

// Função para ser chamada no loop principal com os botões
void spacetrashHandleInput(uint8_t up, uint8_t down, uint8_t fire) {
static uint8_t lastFire = 1;
switch(st_state) {
case ST_STATE_PREPARE:
if(!fire && lastFire) {
st_state = ST_STATE_GAME;
spacetrashReset();
st_beep(2000, 100);
}
break;
case ST_STATE_GAME:
// Movimento
if(up && st_player_pos > 5) st_player_pos--;
if(down && st_player_pos < ST_AREA_HEIGHT - 5) st_player_pos++;
// Atualizar posição do jogador
for(int i = 0; i < ST_OBJ_CNT; i++) {
if(st_objects[i].ot == ST_OT_PLAYER) {
st_objects[i].y = st_player_pos << ST_FP;
break;
}
}
// Tiro
st_FireStep(fire);
if(st_fire_player == 0) {
for(int i = 0; i < ST_OBJ_CNT; i++) {
if(st_objects[i].ot == ST_OT_PLAYER) {
uint8_t x = (st_objects[i].x >> ST_FP) + 6;
uint8_t y = st_objects[i].y >> ST_FP;
st_NewPlayerMissle(x, y);
st_beep(4000, 20);
break;
}
}
}
break;
case ST_STATE_END:
if(!fire && lastFire) {
st_state = ST_STATE_PREPARE;
st_beep(1800, 100);
}
break;
}
lastFire = fire;
}

#endif