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.
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.
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.
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.
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.
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.
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.
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} }
}
};
The rendering system is optimized to reduce screen flickering by updating only the modified cells instead of redrawing the entire display every frame.
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.
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.
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.
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.
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.
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();
}
}