#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();
}
}
Comments