Skip to content

support@quartzcomponents.com

Free Shipping Over INR 500

Electronics Projects

Touchscreen Tetris Game Using TFT LCD & Arduino

by RISHABH JANGID 22 May 2026 0 Comments
Build your own touchscreen Arduino Tetris game. This project brings the classic puzzle game to your fingertips using an Arduino Uno. The TFT shield works as both the vibrant color display and the touch controller, allowing you to move and rotate blocks with simple taps. The system supports full Tetromino arrays, smooth block rendering, collision detection, and score tracking. The project demonstrates practical implementation of game loops, touchscreen interfacing, and 2D array matrix manipulation using embedded hardware.
Arduino Tetris Gameplay

Components Required

About the Components

Arduino Uno R3 Development Board

The Arduino Uno R3 is one of the most popular microcontroller development boards based on the ATmega328P microcontroller. It is widely used for electronics prototyping, embedded system development, robotics, automation, and educational projects. The board includes multiple digital and analog input/output pins, onboard USB programming support, and a simple development environment suitable for beginners and advanced users.

  • Based on ATmega328P microcontroller
  • 14 Digital input/output pins
  • 6 Analog input pins
  • 16MHz clock frequency
  • Onboard CH340 USB to Serial converter
  • Supports USB and DC power input
  • Operating voltage of 5V
  • Compatible with Arduino IDE
  • Removable microcontroller IC
  • Supports SPI, I2C, and UART communication

In this project, the Arduino Uno works as the main controller that handles the Tetris game logic, touchscreen input processing, score calculation, block movement, collision detection, and display rendering.

2.4 Inch TFT LCD Touch Shield

The 2.4-inch TFT LCD Touch Shield is a colorful graphical display module designed specially for Arduino Uno boards. It directly plugs onto the Arduino like a shield and provides both display and touchscreen functionality in a compact form factor. The display supports 240x320 resolution graphics and includes a resistive touchscreen layer for interactive control.

  • 2.4 inch TFT LCD display
  • 240x320 pixel resolution
  • 18-bit colorful display output
  • 4-wire resistive touchscreen
  • SPFD5408 display controller
  • Built-in video RAM buffer
  • Bright white LED backlight
  • 8-bit digital display interface
  • Compatible with 3.3V and 5V logic
  • Direct shield compatibility with Arduino Uno

In this project, the TFT Touch Shield displays the complete Tetris game interface including falling blocks, score area, next block preview, and touch control zones. The touchscreen functionality is used for moving, rotating, and dropping the blocks without requiring external buttons.

Assembly

Simply place the shield on the Arduino. No extra wiring needed. Align the pins of the TFT Touch Shield with the female headers on the Arduino Uno and press down gently until secure.

Assembly Diagram

Fig. Mounting shield directly on Arduino Uno

Code Explanation

Display and Touchscreen Initialization

The code begins by including the required libraries for the TFT LCD display and resistive touchscreen. The MCUFRIEND_kbv library handles graphical rendering while the TouchScreen library reads touch input from the shield.

Touchscreen calibration values are defined to correctly map raw touch readings into screen coordinates. Color constants are also created for rendering the Tetromino blocks and user interface.

Arduino · Display Setup
#include <MCUFRIEND_kbv.h>
#include <TouchScreen.h>

MCUFRIEND_kbv tft;

const int XP = 6;
const int XM = A2;
const int YP = A1;
const int YM = 7;

const int TS_LEFT = 907;
const int TS_RT   = 136;
const int TS_TOP  = 942;
const int TS_BOT  = 139;

TouchScreen ts = TouchScreen(XP, YP, XM, YM, 300);

#define BLACK   0x0000
#define BLUE    0x001F
#define RED     0xF800
#define GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF

Game Board and Variables

The game board is represented using a 2D integer array where each cell stores either an empty state or the color index of a locked block. Variables are also used for tracking the current falling piece, previous drawing position, next piece preview, score, level, line count, and game speed.

The board dimensions are configured as a 14×20 Tetris grid while the block size determines the pixel dimensions of each rendered square.

Arduino · Game Variables
#define COLS  14
#define ROWS  20
#define BLOCK 12

int board[ROWS][COLS];

int pieceX       = 0;
int pieceY       = 0;
int currentPiece = 0;
int rotation     = 0;

int prevX        = 0;
int prevY        = 0;
int prevRot      = 0;
int prevPiece    = 0;

int nextPiece    = 0;

unsigned long lastDrop = 0;
int dropDelay          = 500;

bool gameOver    = false;
long score       = 0;
int  level       = 1;
int  totalLines  = 0;

Tetromino Shape Arrays

The seven classic Tetris pieces are stored in a four-dimensional array. Every Tetromino contains four different rotation states represented using 4×4 matrix grids.

This structure allows the code to rotate blocks dynamically and render different shapes efficiently during gameplay.

Arduino · Tetromino Arrays
const byte tetromino[7][4][4][4] = {

  // Piece 0 : I Block
  {
    { {0,0,0,0}, {1,1,1,1}, {0,0,0,0}, {0,0,0,0} },
    { {0,1,0,0}, {0,1,0,0}, {0,1,0,0}, {0,1,0,0} },
    { {0,0,0,0}, {1,1,1,1}, {0,0,0,0}, {0,0,0,0} },
    { {0,1,0,0}, {0,1,0,0}, {0,1,0,0}, {0,1,0,0} }
  },

  // Piece 1 : J Block
  {
    { {1,0,0,0}, {1,1,1,0}, {0,0,0,0}, {0,0,0,0} },
    { {0,1,1,0}, {0,1,0,0}, {0,1,0,0}, {0,0,0,0} },
    { {0,0,0,0}, {1,1,1,0}, {0,0,1,0}, {0,0,0,0} },
    { {0,1,0,0}, {0,1,0,0}, {1,1,0,0}, {0,0,0,0} }
  }
};

Rendering and Flicker-Free Drawing

The rendering system is optimized to reduce screen flickering by updating only the modified cells instead of redrawing the entire display every frame.

The drawCell() function renders individual Tetris squares while erasePiece() removes the previous block position. The drawGame() function restores hidden board cells and redraws the active piece at its updated position.

The redrawBoard() function performs a full board refresh only when necessary, such as after clearing lines or spawning a new piece.

Arduino · Rendering Functions
void drawCell(int col, int row, uint16_t color) {
  tft.fillRect(
    GAME_X + col * BLOCK,
    GAME_Y + row * BLOCK,
    BLOCK - 1,
    BLOCK - 1,
    color
  );
}

void erasePiece(int col, int row, int oldRotation, int oldPiece) {
  for (int r = 0; r < 4; r++) {
    for (int c = 0; c < 4; c++) {
      if (tetromino[oldPiece][oldRotation][r][c]) {
        drawCell(col + c, row + r, BLACK);
      }
    }
  }
}

void drawPiece(int col, int row, int rot, int piece) {
  for (int r = 0; r < 4; r++) {
    for (int c = 0; c < 4; c++) {
      if (tetromino[piece][rot][r][c]) {
        drawCell(col + c, row + r, pieceColors[piece]);
      }
    }
  }
}

User Interface System

The TFT display includes a complete user interface with score display, level indicator, next-piece preview window, game border, and touch control buttons.

The bottom panel contains four touch buttons for moving left, moving right, dropping downward, and rotating the Tetromino piece.

Arduino · UI Functions
void drawStaticUI() {
  tft.fillScreen(BLACK);
  drawBoardBorder();
  drawScorePanel();
  drawButtons();
}

void drawBoardBorder() {
  tft.drawRect(GAME_X - 1, GAME_Y, GAME_W + 2, GAME_H, WHITE);
  tft.drawRect(GAME_X - 2, GAME_Y, GAME_W + 4, GAME_H, WHITE);
}

Touch Controls and Collision Detection

The touchscreen controls use circular hit detection to identify button presses. The code maps raw touchscreen coordinates into screen positions and checks whether the user touched one of the four virtual control buttons.

Collision detection prevents Tetromino blocks from moving outside the board boundaries or overlapping with already locked pieces.

Arduino · Touch + Collision
bool checkCollision(int newX, int newY, int newRotation) {
  for (int row = 0; row < 4; row++) {
    for (int col = 0; col < 4; col++) {

      if (tetromino[currentPiece][newRotation][row][col]) {

        int boardX = newX + col;
        int boardY = newY + row;

        if (boardX < 0 || boardX >= COLS || boardY >= ROWS) {
          return true;
        }

        if (boardY >= 0 && board[boardY][boardX]) {
          return true;
        }
      }
    }
  }

  return false;
}

Score System and Level Progression

The game rewards points based on the number of cleared lines. Completed rows are removed from the board and all rows above shift downward automatically.

The level increases every 10 cleared lines and the falling speed becomes progressively faster, increasing gameplay difficulty.

Arduino · Score System
void addScore(int lines) {

  const int pointsPerLine[5] = {
    0, 100, 300, 500, 800
  };

  if (lines >= 1 && lines <= 4) {
    score += (long)pointsPerLine[lines] * level;
  }

  totalLines += lines;
  level       = totalLines / 10 + 1;

  dropDelay = max(80, 500 - (level - 1) * 45);
}

Main Game Loop

The main game loop continuously handles touch input, automatically drops the active Tetromino piece using a timer, checks collisions, merges locked blocks into the board, clears completed rows, updates the score panel, and spawns new pieces.

If a new Tetromino immediately collides after spawning, the game displays a GAME OVER screen along with the final score.

Arduino · Main Loop
void loop() {

  if (gameOver) {

    tft.fillScreen(BLACK);

    tft.drawRect(15, 100, 210, 120, WHITE);

    tft.setTextColor(RED);
    tft.setTextSize(3);

    tft.setCursor(50, 115);
    tft.print("GAME");

    tft.setCursor(50, 150);
    tft.print("OVER");

    while (1);
  }

  handleTouch();

  if (millis() - lastDrop > dropDelay) {

    if (!checkCollision(pieceX, pieceY + 1, rotation)) {

      pieceY++;
      drawGame();

    } else {

      mergePiece();

      int lines = clearLines();

      if (lines > 0) {
        addScore(lines);
      }

      newPiece();
      redrawBoard();
    }

    lastDrop = millis();
  }
}

Result

The Arduino Uno successfully runs a complete touchscreen-based Tetris game using the TFT LCD shield. The project features smooth block rendering, responsive touch controls, score tracking, level progression, next-piece preview, collision handling, and dynamic game speed adjustment.

The optimized rendering system minimizes display flickering while maintaining stable gameplay performance on the ATmega328P microcontroller.

Arduino Touchscreen Tetris Gameplay

Checkout the full tutorial :

 


Complete Code

The following Arduino code implements the complete touchscreen Tetris game using the Arduino Uno and 2.4-inch TFT LCD Touch Shield. The code includes smooth rendering, touch controls, score system, level progression, collision detection, next piece preview, and game over handling.

Arduino Touchscreen Tetris Complete Code
#include <MCUFRIEND_kbv.h>
#include <TouchScreen.h>

// ========================================
// DISPLAY AND TOUCH SETUP
// ========================================

MCUFRIEND_kbv tft;

const int XP = 6;
const int XM = A2;
const int YP = A1;
const int YM = 7;

const int TS_LEFT = 907;
const int TS_RT   = 136;
const int TS_TOP  = 942;
const int TS_BOT  = 139;

TouchScreen ts = TouchScreen(XP, YP, XM, YM, 300);

#define MINPRESSURE 200
#define MAXPRESSURE 1000

// ========================================
// COLORS
// ========================================

#define BLACK   0x0000
#define BLUE    0x001F
#define RED     0xF800
#define GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF
#define ORANGE  0xFD20

// ========================================
// GAME BOARD SETTINGS
// ========================================

#define COLS  14
#define ROWS  20
#define BLOCK 12

// Game board starts at x=60, y=0
// Board width  = 14 x 12 = 168 pixels
// Board height = 20 x 12 = 240 pixels
#define GAME_X  60
#define GAME_Y  0
#define GAME_W  (COLS * BLOCK)
#define GAME_H  (ROWS * BLOCK)

// Bottom panel for buttons
#define PANEL_Y  240
#define PANEL_H  80

// ========================================
// GAME VARIABLES
// ========================================

// The game board grid (0 = empty, 1-7 = block color)
int board[ROWS][COLS];

// Current falling piece
int pieceX       = 0;
int pieceY       = 0;
int currentPiece = 0;
int rotation     = 0;

// Previous piece position (used to erase without flicker)
int prevX        = 0;
int prevY        = 0;
int prevRot      = 0;
int prevPiece    = 0;

// Next piece to fall
int nextPiece    = 0;

// Timing
unsigned long lastDrop = 0;
int dropDelay          = 500;

// Game state
bool gameOver    = false;
long score       = 0;
int  level       = 1;
int  totalLines  = 0;

// ========================================
// PIECE COLORS
// ========================================

uint16_t pieceColors[7] = {
  CYAN,     // I Block
  BLUE,     // J Block
  ORANGE,   // L Block
  YELLOW,   // O Block
  GREEN,    // S Block
  MAGENTA,  // T Block
  RED       // Z Block
};

// ========================================
// TETROMINO SHAPES
// Each piece has 4 rotations, each rotation is a 4x4 grid
// ========================================

const byte tetromino[7][4][4][4] = {

  // Piece 0 : I Block
  {
    { {0,0,0,0}, {1,1,1,1}, {0,0,0,0}, {0,0,0,0} },
    { {0,1,0,0}, {0,1,0,0}, {0,1,0,0}, {0,1,0,0} },
    { {0,0,0,0}, {1,1,1,1}, {0,0,0,0}, {0,0,0,0} },
    { {0,1,0,0}, {0,1,0,0}, {0,1,0,0}, {0,1,0,0} }
  },

  // Piece 1 : J Block
  {
    { {1,0,0,0}, {1,1,1,0}, {0,0,0,0}, {0,0,0,0} },
    { {0,1,1,0}, {0,1,0,0}, {0,1,0,0}, {0,0,0,0} },
    { {0,0,0,0}, {1,1,1,0}, {0,0,1,0}, {0,0,0,0} },
    { {0,1,0,0}, {0,1,0,0}, {1,1,0,0}, {0,0,0,0} }
  },

  // Piece 2 : L Block
  {
    { {0,0,1,0}, {1,1,1,0}, {0,0,0,0}, {0,0,0,0} },
    { {0,1,0,0}, {0,1,0,0}, {0,1,1,0}, {0,0,0,0} },
    { {0,0,0,0}, {1,1,1,0}, {1,0,0,0}, {0,0,0,0} },
    { {1,1,0,0}, {0,1,0,0}, {0,1,0,0}, {0,0,0,0} }
  },

  // Piece 3 : O Block
  {
    { {0,1,1,0}, {0,1,1,0}, {0,0,0,0}, {0,0,0,0} },
    { {0,1,1,0}, {0,1,1,0}, {0,0,0,0}, {0,0,0,0} },
    { {0,1,1,0}, {0,1,1,0}, {0,0,0,0}, {0,0,0,0} },
    { {0,1,1,0}, {0,1,1,0}, {0,0,0,0}, {0,0,0,0} }
  },

  // Piece 4 : S Block
  {
    { {0,1,1,0}, {1,1,0,0}, {0,0,0,0}, {0,0,0,0} },
    { {0,1,0,0}, {0,1,1,0}, {0,0,1,0}, {0,0,0,0} },
    { {0,1,1,0}, {1,1,0,0}, {0,0,0,0}, {0,0,0,0} },
    { {0,1,0,0}, {0,1,1,0}, {0,0,1,0}, {0,0,0,0} }
  },

  // Piece 5 : T Block
  {
    { {0,1,0,0}, {1,1,1,0}, {0,0,0,0}, {0,0,0,0} },
    { {0,1,0,0}, {0,1,1,0}, {0,1,0,0}, {0,0,0,0} },
    { {0,0,0,0}, {1,1,1,0}, {0,1,0,0}, {0,0,0,0} },
    { {0,1,0,0}, {1,1,0,0}, {0,1,0,0}, {0,0,0,0} }
  },

  // Piece 6 : Z Block
  {
    { {1,1,0,0}, {0,1,1,0}, {0,0,0,0}, {0,0,0,0} },
    { {0,0,1,0}, {0,1,1,0}, {0,1,0,0}, {0,0,0,0} },
    { {1,1,0,0}, {0,1,1,0}, {0,0,0,0}, {0,0,0,0} },
    { {0,0,1,0}, {0,1,1,0}, {0,1,0,0}, {0,0,0,0} }
  }
};

// ========================================
// BASIC DRAWING FUNCTIONS
// ========================================

// Draw one square block on the game board
void drawCell(int col, int row, uint16_t color) {
  tft.fillRect(
    GAME_X + col * BLOCK,
    GAME_Y + row * BLOCK,
    BLOCK - 1,
    BLOCK - 1,
    color
  );
}

// Erase a piece from its old position
void erasePiece(int col, int row, int oldRotation, int oldPiece) {
  for (int r = 0; r < 4; r++) {
    for (int c = 0; c < 4; c++) {
      if (tetromino[oldPiece][oldRotation][r][c]) {
        drawCell(col + c, row + r, BLACK);
      }
    }
  }
}

// Draw a piece at a given position
void drawPiece(int col, int row, int rot, int piece) {
  for (int r = 0; r < 4; r++) {
    for (int c = 0; c < 4; c++) {
      if (tetromino[piece][rot][r][c]) {
        drawCell(col + c, row + r, pieceColors[piece]);
      }
    }
  }
}

// Update only the cells that changed — prevents screen flicker
void drawGame() {
  // Erase piece from old position
  erasePiece(prevX, prevY, prevRot, prevPiece);

  // Restore any locked board blocks that were under the old piece
  for (int row = 0; row < 4; row++) {
    for (int col = 0; col < 4; col++) {
      if (tetromino[prevPiece][prevRot][row][col]) {
        int boardX = prevX + col;
        int boardY = prevY + row;
        if (boardY >= 0 && boardY < ROWS && boardX >= 0 && boardX < COLS) {
          if (board[boardY][boardX]) {
            drawCell(boardX, boardY, pieceColors[board[boardY][boardX] - 1]);
          }
        }
      }
    }
  }

  // Draw piece at new position
  drawPiece(pieceX, pieceY, rotation, currentPiece);

  // Save current position as previous for next frame
  prevX     = pieceX;
  prevY     = pieceY;
  prevRot   = rotation;
  prevPiece = currentPiece;
}

// Full board redraw — only called when board changes (line clear or new piece)
void redrawBoard() {
  tft.fillRect(GAME_X, GAME_Y, GAME_W, GAME_H, BLACK);

  for (int row = 0; row < ROWS; row++) {
    for (int col = 0; col < COLS; col++) {
      if (board[row][col]) {
        drawCell(col, row, pieceColors[board[row][col] - 1]);
      }
    }
  }

  drawPiece(pieceX, pieceY, rotation, currentPiece);

  prevX     = pieceX;
  prevY     = pieceY;
  prevRot   = rotation;
  prevPiece = currentPiece;
}

// ========================================
// UI PANELS
// ========================================

// Draw score, level, and next piece preview in the left sidebar
void drawScorePanel() {
  // Clear sidebar area
  tft.fillRect(0, 0, GAME_X - 3, PANEL_Y, BLACK);

  // Score label and value
  tft.setTextColor(WHITE);
  tft.setTextSize(1);
  tft.setCursor(2, 10);
  tft.print("SCR");

  tft.setTextColor(YELLOW);
  tft.setTextSize(1);
  tft.setCursor(2, 22);
  char buf[10];
  ltoa(score, buf, 10);
  tft.print(buf);

  // Level label and value
  tft.setTextColor(WHITE);
  tft.setTextSize(1);
  tft.setCursor(2, 50);
  tft.print("LVL");

  tft.setTextColor(CYAN);
  tft.setTextSize(2);
  tft.setCursor(8, 62);
  tft.print(level);

  // Next piece label
  tft.setTextColor(WHITE);
  tft.setTextSize(1);
  tft.setCursor(2, 110);
  tft.print("NXT");

  // Draw next piece preview
  int blockSize = 10;
  int originX   = 2;
  int originY   = 122;

  for (int row = 0; row < 4; row++) {
    for (int col = 0; col < 4; col++) {
      int drawX = originX + col * blockSize;
      int drawY = originY + row * blockSize;

      if (tetromino[nextPiece][0][row][col]) {
        tft.fillRect(drawX, drawY, blockSize - 1, blockSize - 1, pieceColors[nextPiece]);
      } else {
        tft.fillRect(drawX, drawY, blockSize - 1, blockSize - 1, BLACK);
      }
    }
  }
}

// Draw the four touch buttons at the bottom of the screen
void drawButtons() {
  // Clear button panel
  tft.fillRect(0, PANEL_Y, 240, PANEL_H, BLACK);

  // Top divider line
  tft.fillRect(0, PANEL_Y, 240, 2, WHITE);

  int centerY = PANEL_Y + PANEL_H / 2;
  int radius  = 16;

  // Button positions:
  // Left  = Move piece left
  // Down  = Move piece down faster
  // Right = Move piece right
  // R     = Rotate piece
  int   centerX[4] = { 30,    90,    150,   210  };
  uint16_t color[4] = { BLUE,  GREEN, BLUE,  RED  };
  const char* label[4] = { "<",   "v",   ">",   "R"  };

  for (int i = 0; i < 4; i++) {
    tft.fillCircle(centerX[i], centerY, radius, color[i]);
    tft.drawCircle(centerX[i], centerY, radius, WHITE);
    tft.setTextColor(WHITE);
    tft.setTextSize(2);
    tft.setCursor(centerX[i] - 5, centerY - 8);
    tft.print(label[i]);
  }
}

// Draw the white border around the game board
void drawBoardBorder() {
  tft.drawRect(GAME_X - 1, GAME_Y, GAME_W + 2, GAME_H, WHITE);
  tft.drawRect(GAME_X - 2, GAME_Y, GAME_W + 4, GAME_H, WHITE);
}

// Draw the full static UI (called once at start)
void drawStaticUI() {
  tft.fillScreen(BLACK);
  drawBoardBorder();
  drawScorePanel();
  drawButtons();
}

// ========================================
// COLLISION DETECTION
// ========================================

// Check if current piece touches a wall or locked block
bool checkCollision(int newX, int newY, int newRotation) {
  for (int row = 0; row < 4; row++) {
    for (int col = 0; col < 4; col++) {
      if (tetromino[currentPiece][newRotation][row][col]) {
        int boardX = newX + col;
        int boardY = newY + row;

        // Check wall and floor boundaries
        if (boardX < 0 || boardX >= COLS || boardY >= ROWS) {
          return true;
        }

        // Check collision with locked blocks
        if (boardY >= 0 && board[boardY][boardX]) {
          return true;
        }
      }
    }
  }
  return false;
}

// ========================================
// SCORE SYSTEM
// ========================================

// Lock the current piece into the board grid
void mergePiece() {
  for (int row = 0; row < 4; row++) {
    for (int col = 0; col < 4; col++) {
      if (tetromino[currentPiece][rotation][row][col]) {
        board[pieceY + row][pieceX + col] = currentPiece + 1;
      }
    }
  }
}

// Remove completed lines and move everything above down
int clearLines() {
  int cleared = 0;

  for (int row = ROWS - 1; row >= 0; row--) {
    bool full = true;

    for (int col = 0; col < COLS; col++) {
      if (!board[row][col]) {
        full = false;
        break;
      }
    }

    if (full) {
      // Shift all rows above down by one
      for (int above = row; above > 0; above--) {
        for (int col = 0; col < COLS; col++) {
          board[above][col] = board[above - 1][col];
        }
      }

      // Clear the top row
      for (int col = 0; col < COLS; col++) {
        board[0][col] = 0;
      }

      cleared++;
      row++; // Recheck same row index after shift
    }
  }

  return cleared;
}

// Update score and increase game speed when level increases
void addScore(int lines) {
  const int pointsPerLine[5] = { 0, 100, 300, 500, 800 };

  if (lines >= 1 && lines <= 4) {
    score += (long)pointsPerLine[lines] * level;
  }

  totalLines += lines;
  level       = totalLines / 10 + 1;

  if (level > 10) {
    level = 10;
  }

  // Increase game speed when level increases
  dropDelay = max(80, 500 - (level - 1) * 45);
}

// ========================================
// TOUCH CONTROLS
// ========================================

// Read touch input and move or rotate the piece
void handleTouch() {
  TSPoint p = ts.getPoint();
  pinMode(XM, OUTPUT);
  pinMode(YP, OUTPUT);

  if (p.z < MINPRESSURE || p.z > MAXPRESSURE) {
    return;
  }

  // Map raw touch values to screen coordinates
  int touchX = map(p.y, TS_LEFT, TS_BOT, 0, 240);
  int touchY = map(p.x, TS_TOP, TS_RT,  0, 320);

  // Calibrated button centers on the TX axis
  // Button positions: Left(<), Down(v), Right(>), Rotate(R)
  int buttonX[4]  = { 193, 142, 83, 28 };
  int buttonY     = 30;
  int tapRadius   = 30 * 30;
  bool moved      = false;

  // Left button — move piece left
  if (sq(touchX - buttonX[0]) + sq(touchY - buttonY) < tapRadius) {
    if (!checkCollision(pieceX - 1, pieceY, rotation)) {
      pieceX--;
      moved = true;
    }
  }
  // Down button — move piece down
  else if (sq(touchX - buttonX[1]) + sq(touchY - buttonY) < tapRadius) {
    if (!checkCollision(pieceX, pieceY + 1, rotation)) {
      pieceY++;
      moved = true;
    }
  }
  // Right button — move piece right
  else if (sq(touchX - buttonX[2]) + sq(touchY - buttonY) < tapRadius) {
    if (!checkCollision(pieceX + 1, pieceY, rotation)) {
      pieceX++;
      moved = true;
    }
  }
  // Rotate button — rotate piece
  else if (sq(touchX - buttonX[3]) + sq(touchY - buttonY) < tapRadius) {
    int newRotation = (rotation + 1) % 4;
    if (!checkCollision(pieceX, pieceY, newRotation)) {
      rotation = newRotation;
      moved    = true;
    }
  }

  if (moved) {
    drawGame();
    delay(80);
  }
}

// ========================================
// PIECE MANAGEMENT
// ========================================

// Spawn the next piece and generate a new upcoming piece
void newPiece() {
  currentPiece = nextPiece;

  // Generate next random Tetris piece
  nextPiece = random(0, 7);

  pieceX   = 5;
  pieceY   = 0;
  rotation = 0;

  // Sync previous position with spawn position
  prevX     = pieceX;
  prevY     = pieceY;
  prevRot   = rotation;
  prevPiece = currentPiece;

  // If new piece immediately collides, game is over
  if (checkCollision(pieceX, pieceY, rotation)) {
    gameOver = true;
  }

  drawScorePanel();
}

// ========================================
// SETUP
// ========================================

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

  // Start display
  uint16_t ID = tft.readID();
  tft.begin(ID);
  tft.setRotation(0);

  // Create random seed from floating analog pin
  randomSeed(analogRead(A5));

  // Clear game board
  memset(board, 0, sizeof(board));

  // Reset all game values
  score      = 0;
  level      = 1;
  totalLines = 0;

  // Set starting previous position
  prevX     = 5;
  prevY     = 0;
  prevRot   = 0;
  prevPiece = 0;

  // Generate first next piece
  nextPiece = random(0, 7);

  // Draw UI and start game
  drawStaticUI();
  newPiece();
  redrawBoard();
}

// ========================================
// MAIN LOOP
// ========================================

// Main game loop:
// 1. Read touch input
// 2. Move piece down automatically on timer
// 3. Check if piece can keep falling
// 4. If blocked: lock it, clear lines, spawn new piece
// 5. Update display without flicker
void loop() {

  // Show game over screen and stop
  if (gameOver) {
    tft.fillScreen(BLACK);
    tft.drawRect(15, 100, 210, 120, WHITE);
    tft.drawRect(16, 101, 208, 118, WHITE);
    tft.setTextColor(RED);
    tft.setTextSize(3);
    tft.setCursor(50, 115);
    tft.print("GAME");
    tft.setCursor(50, 150);
    tft.print("OVER");
    tft.setTextColor(YELLOW);
    tft.setTextSize(2);
    tft.setCursor(20, 190);
    tft.print("SCORE:");
    tft.print(score);
    while (1);
  }

  // Read touch input
  handleTouch();

  // Auto-drop piece on timer
  if (millis() - lastDrop > dropDelay) {

    // If block can move down, drop it one row
    if (!checkCollision(pieceX, pieceY + 1, rotation)) {
      pieceY++;
      drawGame(); // Only redraw changed cells — no flicker

    } else {
      // If block cannot move down, lock it into the board
      mergePiece();

      int lines = clearLines();

      if (lines > 0) {
        addScore(lines);
        drawScorePanel();
      }

      // Spawn next piece and do full board redraw
      newPiece();
      redrawBoard();
    }

    lastDrop = millis();
  }
}
Prev Post
Next Post

Leave a comment

Please note, comments need to be approved before they are published.

Thanks for subscribing!

This email has been registered!

Shop the look

Choose Options

Edit Option
Back In Stock Notification
is added to your shopping cart.
this is just a warning
Login