Tetris con ESP32

Introducción

¡Prepárate para un viaje nostálgico con un toque moderno! Hoy os traemos un proyecto emocionante para los amantes de la electrónica y los videojuegos retro: ¡Tetris en un ESP32! Este proyecto no solo te permitirá revivir un clásico atemporal, sino que también te introducirá en el fascinante mundo de la programación de microcontroladores y la interacción con displays y botones. El ESP32, conocido por su versatilidad, potencia de procesamiento y capacidades de Wi-Fi/Bluetooth, es la plataforma perfecta para dar vida a este icónico juego.

Con unos pocos componentes, como una pantalla TFT a color, botones y un buzzer para los efectos de sonido, podrás construir tu propia consola de Tetris. Es un excelente proyecto para aprender sobre el manejo de periféricos, la lógica de juego y cómo optimizar el código para microcontroladores. ¡Manos a la obra y veamos cómo podemos construir este divertido reto!

Materiales Necesarios

  • Placa de Desarrollo ESP32: El cerebro de nuestro proyecto.
  • Display TFT a color: (Ej: ST7735 de 1.8″ o ILI9341 de 2.2″). La pantalla que se ve en el vídeo parece ser un ST7735.
  • Protoboard: Para realizar las conexiones de forma temporal y sin soldaduras.
  • Botones Pulsadores: 5 unidades (para izquierda, derecha, rotar, bajar rápido y una para iniciar/pausar o soltar).
  • Cables Jumper: Macho-Macho para las conexiones en la protoboard.
  • Buzzer Pasivo: Para los efectos de sonido del juego.
  • Cable Micro-USB: Para programar y alimentar el ESP32.
  • (Opcional) Resistencias de 10k ohmios: Si decides usar pull-down externos para los botones en lugar de los pull-up internos del ESP32.

Esquema del montaje

El montaje de este proyecto es relativamente directo y se basa en conectar los componentes principales (el display TFT, los botones y el buzzer) al ESP32 a través de una protoboard. El ESP32 será el encargado de ejecutar la lógica del juego, enviar las instrucciones para dibujar los tetrominós en el display y generar los efectos de sonido.

Asegúrate de que el ESP32 esté firmemente conectado a la protoboard (si utilizas un módulo de breadboard-friendly) o utiliza cables jumper para todas las conexiones. Es crucial prestar atención a las conexiones del display, ya que son las más numerosas y específicas (SPI).

Conexiones:

  • Display TFT (Ej. ST7735):
    • VCC al 3.3V del ESP32
    • GND al GND del ESP32
    • CS (Chip Select) a un pin GPIO del ESP32 (ej. GPIO 5)
    • DC/RS (Data/Command) a un pin GPIO del ESP32 (ej. GPIO 4)
    • SCL (SPI Clock) a un pin GPIO del ESP32 (ej. GPIO 18, SPI CLK)
    • SDA/MOSI (SPI Data/Master Out Slave In) a un pin GPIO del ESP32 (ej. GPIO 23, SPI MOSI)
    • RES (Reset) a un pin GPIO del ESP32 (ej. GPIO 16) o directamente al 3.3V (si la librería lo permite).
  • Botones Pulsadores:
    • Conecta un extremo de cada botón al GND del ESP32.
    • Conecta el otro extremo de cada botón a un pin GPIO diferente del ESP32. Por ejemplo:
      • Botón Izquierda -> GPIO 32
      • Botón Derecha -> GPIO 33
      • Botón Rotar -> GPIO 25
      • Botón Bajar Rápido/Hard Drop -> GPIO 26
      • Botón Iniciar/Pausa -> GPIO 27
    • Nota: Configuraremos estos pines como INPUT_PULLUP en el código, eliminando la necesidad de resistencias externas si no quieres añadirlas.
  • Buzzer Pasivo:
    • Conecta una pata del buzzer a un pin GPIO del ESP32 (ej. GPIO 14).
    • Conecta la otra pata del buzzer al GND del ESP32.

Codigo Arduino

El código para Tetris en ESP32 es más extenso de lo que podemos incluir por completo aquí, ya que implica toda la lógica del juego (piezas, rotaciones, detección de líneas, puntuación, etc.) y la interacción con las librerías de gráficos. Sin embargo, te proporcionaremos una estructura básica y los elementos clave que necesitarás.

Necesitarás instalar las librerías Adafruit GFX Library y la librería específica para tu display (ej. Adafruit ST7735 Library) a través del Gestor de Librerías de Arduino IDE. Para el sonido, se puede usar la funcionalidad ledcWriteTone del ESP32.


#include <Adafruit_GFX.h>    // Core graphics library
#include <Adafruit_ST7735.h> // Hardware-specific library for ST7735
#include <SPI.h>             // For SPI communication

// --- Pin Definitions for your Display (adjust if using a different display or pins) ---
#define TFT_CS    5  // Chip select pin
#define TFT_DC    4  // Data/Command pin
#define TFT_RST   16 // Reset pin (or connect to VCC if not used by your display)

// --- Pin Definitions for Buttons ---
#define BTN_LEFT  32
#define BTN_RIGHT 33
#define BTN_ROTATE 25
#define BTN_DROP  26
#define BTN_START 27 // Or 'hard drop' depending on your game logic

// --- Pin Definition for Buzzer ---
#define BUZZER_PIN 14
#define BUZZER_CHANNEL 0 // ESP32 LEDC channel

// Initialize display library
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

// --- Game Variables (Simplified examples) ---
const int GRID_WIDTH = 10;
const int GRID_HEIGHT = 20;
int gameGrid[GRID_WIDTH][GRID_HEIGHT]; // Represents the Tetris board
int currentPiece[4][4]; // Current falling Tetromino
int pieceX, pieceY; // Position of the current piece
int score = 0;
bool gameOver = false;
unsigned long lastMoveTime = 0;
const unsigned long MOVE_INTERVAL = 500; // Time for auto-drop (milliseconds)

// --- Function Prototypes ---
void initGame();
void drawGrid();
void drawPiece(int x, int y, int piece[4][4], uint16_t color);
bool checkCollision(int x, int y, int piece[4][4]);
void movePiece(int deltaX, int deltaY);
void rotatePiece();
void hardDrop();
void handleInput();
void playTone(int frequency, int duration);
void playGameOverSound();
// ... other game logic functions (clearLines, generateNewPiece, etc.) ...

void setup() {
    Serial.begin(115200);

    // Initialize display
    tft.initR(INITR_BLACKTAB); // For 1.8" Black Tab (or other init function for your specific display)
    tft.setRotation(3); // Adjust rotation as needed
    tft.fillScreen(ST7735_BLACK); // Clear screen

    // Setup button pins with internal pull-up resistors
    pinMode(BTN_LEFT, INPUT_PULLUP);
    pinMode(BTN_RIGHT, INPUT_PULLUP);
    pinMode(BTN_ROTATE, INPUT_PULLUP);
    pinMode(BTN_DROP, INPUT_PULLUP);
    pinMode(BTN_START, INPUT_PULLUP);

    // Setup buzzer
    ledcSetup(BUZZER_CHANNEL, 2000, 8); // Channel 0, 2KHz, 8-bit resolution
    ledcAttachPin(BUZZER_PIN, BUZZER_CHANNEL);

    initGame(); // Set up initial game state
    tft.setTextSize(2);
    tft.setTextColor(ST7735_GREEN);
    tft.setCursor(tft.width() / 2 - 30, tft.height() / 2 - 10);
    tft.println("TETRIS");
    delay(2000);
    tft.fillScreen(ST7735_BLACK);
}

void loop() {
    if (gameOver) {
        // Display game over screen and wait for start button
        tft.setCursor(tft.width() / 2 - 50, tft.height() / 2 - 10);
        tft.setTextColor(ST7735_RED);
        tft.println("GAME OVER!");
        tft.setCursor(tft.width() / 2 - 40, ttht.height() / 2 + 10);
        tft.setTextColor(ST7735_WHITE);
        tft.print("Score: ");
        tft.println(score);
        playGameOverSound();
        while (digitalRead(BTN_START) == HIGH) { // Wait for button release
            // Do nothing
        }
        while (digitalRead(BTN_START) == LOW) { // Wait for button press to restart
            // Do nothing
        }
        // Restart game
        gameOver = false;
        initGame();
        tft.fillScreen(ST7735_BLACK);
    } else {
        handleInput(); // Check for button presses

        unsigned long currentTime = millis();
        if (currentTime - lastMoveTime >= MOVE_INTERVAL) {
            movePiece(0, 1); // Auto-drop the piece
            lastMoveTime = currentTime;
        }

        // Clear previous drawing
        tft.fillScreen(ST7735_BLACK); // Simple clear, better to only redraw changed parts

        // Redraw game elements
        drawGrid();
        drawPiece(pieceX, pieceY, currentPiece, ST7735_CYAN); // Example color

        // Update score display (optional, depending on display layout)
        tft.setCursor(5, 5);
        tft.setTextColor(ST7735_WHITE);
        tft.print("Score: ");
        tft.println(score);
    }
    delay(10); // Small delay to prevent flickering and excessive CPU usage
}

// --- Simplified placeholder functions ---
void initGame() {
    // Reset grid, score, generate first piece
    for (int x = 0; x < GRID_WIDTH; x++) {
        for (int y = 0; y < GRID_HEIGHT; y++) {
            gameGrid[x][y] = 0; // 0 for empty
        }
    }
    score = 0;
    // ... generate first random piece and set its position ...
    // For simplicity, let's just use a square block as a placeholder
    int dummyPiece[4][4] = {
        {0,1,1,0},
        {0,1,1,0},
        {0,0,0,0},
        {0,0,0,0}
    };
    for(int i=0; i<4; i++) {
        for(int j=0; j<4; j++) {
            currentPiece[i][j] = dummyPiece[i][j];
        }
    }
    pieceX = GRID_WIDTH / 2 - 2;
    pieceY = 0;
    lastMoveTime = millis();
}

void drawGrid() {
    // Draw the static blocks on the grid
    for (int y = 0; y < GRID_HEIGHT; y++) {
        for (int x = 0; x < GRID_WIDTH; x++) {
            if (gameGrid[x][y] != 0) {
                // Draw a colored square (adjust size and offset for your display)
                tft.fillRect(x * 8 + 1, y * 8 + 1, 6, 6, ST7735_MAGENTA); // Example block size 8x8 pixels
            }
        }
    }
}

void drawPiece(int x, int y, int piece[4][4], uint16_t color) {
    // Draw the currently falling piece
    for (int row = 0; row < 4; row++) {
        for (int col = 0; col < 4; col++) {
            if (piece[row][col] != 0) {
                tft.fillRect((x + col) * 8 + 1, (y + row) * 8 + 1, 6, 6, color);
            }
        }
    }
}

bool checkCollision(int newX, int newY, int piece[4][4]) {
    // Basic collision detection
    for (int row = 0; row < 4; row++) {
        for (int col = 0; col < 4; col++) {
            if (piece[row][col] != 0) {
                int gridX = newX + col;
                int gridY = newY + row;

                if (gridX < 0 || gridX >= GRID_WIDTH || gridY >= GRID_HEIGHT) {
                    return true; // Wall or floor collision
                }
                if (gridY < 0) continue; // Above the board is fine initially

                if (gameGrid[gridX][gridY] != 0) {
                    return true; // Collision with existing block
                }
            }
        }
    }
    return false;
}

void mergePieceToGrid() {
    // When a piece lands, merge it into the gameGrid
    for (int row = 0; row < 4; row++) {
        for (int col = 0; col < 4; col++) {
            if (currentPiece[row][col] != 0) {
                gameGrid[pieceX + col][pieceY + row] = 1; // Mark as occupied
            }
        }
    }
    // ... then check for clearable lines and generate a new piece ...
    // For simplicity, just generate a new piece for now
    initGame(); // This will clear the grid for the next piece, which is not correct Tetris logic.
                // A full Tetris would clear lines, shift down, and then generate a new piece on an *existing* grid.
}

void movePiece(int deltaX, int deltaY) {
    if (!checkCollision(pieceX + deltaX, pieceY + deltaY, currentPiece)) {
        pieceX += deltaX;
        pieceY += deltaY;
    } else if (deltaY == 1) { // Collision when trying to move down
        mergePieceToGrid();
        playTone(440, 50); // Play sound when piece lands
        if (checkCollision(pieceX, pieceY, currentPiece)) { // Check for game over immediately after merging
            gameOver = true;
        }
    }
}

void rotatePiece() {
    int rotatedPiece[4][4];
    // Simple 90-degree clockwise rotation (this is a placeholder)
    for (int row = 0; row < 4; row++) {
        for (int col = 0; col < 4; col++) {
            rotatedPiece[col][3 - row] = currentPiece[row][col];
        }
    }
    if (!checkCollision(pieceX, pieceY, rotatedPiece)) {
        for (int row = 0; row < 4; row++) {
            for (int col = 0; col < 4; col++) { currentPiece[row][col] = rotatedPiece[row][col]; } } playTone(600, 50); // Play sound on rotation } } void hardDrop() { while (!checkCollision(pieceX, pieceY + 1, currentPiece)) { pieceY++; } mergePieceToGrid(); playTone(800, 100); // Play sound for hard drop if (checkCollision(pieceX, pieceY, currentPiece)) { gameOver = true; } } void handleInput() { static unsigned long lastButtonPressTime = 0; const unsigned long BUTTON_DEBOUNCE_DELAY = 150; // Milliseconds if (millis() - lastButtonPressTime > BUTTON_DEBOUNCE_DELAY) {
        if (digitalRead(BTN_LEFT) == LOW) { // Button is pressed (LOW due to INPUT_PULLUP)
            movePiece(-1, 0);
            playTone(200, 30);
            lastButtonPressTime = millis();
        }
        if (digitalRead(BTN_RIGHT) == LOW) {
            movePiece(1, 0);
            playTone(200, 30);
            lastButtonPressTime = millis();
        }
        if (digitalRead(BTN_ROTATE) == LOW) {
            rotatePiece();
            playTone(300, 30);
            lastButtonPressTime = millis();
        }
        if (digitalRead(BTN_DROP) == LOW) {
            movePiece(0, 1); // Soft drop
            playTone(150, 20);
            lastButtonPressTime = millis();
        }
        if (digitalRead(BTN_START) == LOW) {
            hardDrop(); // Or pause game, depending on desired action
            lastButtonPressTime = millis();
        }
    }
}

void playTone(int frequency, int duration) {
    ledcWriteTone(BUZZER_CHANNEL, frequency);
    delay(duration);
    ledcWriteTone(BUZZER_CHANNEL, 0); // Stop tone
}

void playGameOverSound() {
    playTone(500, 200);
    delay(50);
    playTone(400, 200);
    delay(50);
    playTone(300, 300);
}

// The complete Tetris game logic (line clearing, scoring,
// different tetrominoes, random generation) would go here.
// This is a highly simplified example to show component interaction.
// For a full Tetris game, consider looking for existing open-source
// implementations for ESP32/Arduino on platforms like GitHub.

Nota importante: El código anterior es una estructura simplificada y un esqueleto para mostrar cómo se interactúa con los componentes. Implementar un juego completo de Tetris desde cero es una tarea compleja que requiere una lógica detallada para las piezas, rotaciones, detección de líneas, puntuación, niveles, etc. Te recomendamos buscar proyectos de Tetris ya existentes para ESP32/Arduino en plataformas como GitHub y adaptarlos a tus necesidades y hardware específico.

Como funciona

Una vez que tengas el hardware montado y el código cargado en tu ESP32, tu mini consola de Tetris cobrará vida siguiendo estos principios:

1. Inicialización del Sistema

Al encender el ESP32, el programa se inicia. Primero, configura los pines GPIO para el display, los botones y el buzzer. La pantalla TFT se inicializa y se prepara para dibujar gráficos, a menudo mostrando un mensaje de bienvenida o el título «TETRIS». Todas las variables del juego, como la matriz que representa el tablero, la puntuación y la posición de la primera pieza, se reinician.

2. El Bucle Principal del Juego (Game Loop)

El corazón del juego es un bucle infinito (loop() en Arduino) que se ejecuta constantemente, haciendo varias cosas en cada iteración:

  • Manejo de Entrada: El ESP32 monitoriza continuamente el estado de los botones. Cuando detecta un botón pulsado (gracias a los pull-ups internos que hacen que el pin vaya a LOW), interpreta la acción deseada por el jugador (mover a la izquierda, derecha, rotar, bajar rápido). Un pequeño retardo (debounce) asegura que una sola pulsación no se registre múltiples veces.
  • Actualización de la Lógica del Juego: Basado en la entrada del jugador o en el paso del tiempo, el juego calcula la nueva posición o rotación de la pieza actual. También verifica si la pieza ha colisionado con el fondo del tablero o con otras piezas ya asentadas.
  • Caída Automática: A intervalos regulares (ej. cada medio segundo), la pieza activa se mueve automáticamente una posición hacia abajo. Si colisiona, se «fusiona» con el tablero, y el juego verifica si se han completado líneas para eliminarlas y sumar puntos.
  • Detección de Juego Terminado: Si una nueva pieza no puede colocarse en la parte superior del tablero sin colisionar, el juego entra en un estado de «Game Over».
  • Renderizado Gráfico: Con cada cambio en el estado del juego (movimiento de pieza, eliminación de línea), el display se actualiza. El ESP32 utiliza las librerías de gráficos para dibujar la matriz del tablero y la pieza activa en la pantalla TFT, dando la sensación de movimiento. Para evitar el parpadeo, las implementaciones más avanzadas solo redibujan las partes de la pantalla que han cambiado.
  • Efectos de Sonido: Cada acción importante (mover pieza, rotar, aterrizar, eliminar línea, Game Over) puede ir acompañada de un sonido característico, generado por el buzzer conectado al ESP32 utilizando la función ledcWriteTone para producir diferentes frecuencias y duraciones.

3. Interacción Constante

El jugador interactúa constantemente con los botones, y el ESP32 reacciona en tiempo real, actualizando la posición de la pieza, la puntuación y la visualización en pantalla. La velocidad de caída de las piezas puede aumentar a medida que la puntuación crece, incrementando la dificultad del juego.

En esencia, el ESP32 actúa como un pequeño ordenador dedicado: toma entradas, procesa la lógica, gestiona las salidas visuales y auditivas, todo ello a una velocidad suficiente para que la experiencia de juego sea fluida y divertida.

 

Material

Aquí os dejamos los materiales que nosotros compramos cuando empezamos. Si quieres darnos una pequeña ayuda puedes comprar desde este link. Gracias por leernos!

Placa arduino: https://amzn.to/43LM4k4
Protoboard: https://amzn.to/3LZaZdM
Cables: https://amzn.to/3K5N3ox
Leds: https://amzn.to/4iryuse
Buzzer: https://amzn.to/4ilPlfX
Pack de lo necesario para empezar: https://amzn.to/3MmuDAs