Quartz Components
Published © GPL3+

Touchscreen Tetris Game Using TFT LCD & Arduino

Built my own touchscreen Arduino Tetris game. This project brings the classic puzzle game to your fingertips using an Arduino Uno. The TFT

BeginnerFull instructions provided1 hour20
Touchscreen Tetris Game Using TFT LCD & Arduino

Things used in this project

Hardware components

Arduino Arduin Uno
×1
2.4 Inch TFT Touchscreen LCD Display for Arduino Uno
×1
Arduino Uno/MEGA Programming Cable
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

tetris_gameplay_(1)_ob2WNFK2rl.webp

Connect the Display Shield onto the Arduino Uno.

Code

Source Code

C/C++
Just Copy and paste it into your Arduino IDE and uplaod the code into your arduino uno . That's it , you are ready to go
#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();
  }
}

Credits

Quartz Components
3 projects • 5 followers
Quartz Components builds robotics, IoT, AI hardware, Arduino, ESP32, automation, sensors, and innovative DIY electronics projects.

Comments