RO EN

Tetris

01/07/2026

This is a very cool and easy project to build. On top of that, it's also fun. You can find the video on Instagram: here.

It probably took me more than 2 hours to write all the code (I got inspired by different websites and searched online), but I'm leaving the code below so it's much easier for you. I used the LedControl library because MD_MAX72XX was giving me errors.

To move the pieces, move the joystick left and right. To rotate them, press the joystick (the button). To speed up the falling, move the joystick down. When the pieces reach the top, the screen turns red and displays "GAME OVER". To exit this screen, press the joystick button.

Now let me explain how to build it.

Required components:

  • Arduino Nano (or any Arduino-compatible microcontroller)

  • 4× 8×8 LED Matrix with MAX7219

  • 5-pin analog joystick

  • Jumper wires

  • Breadboard (optional, but recommended)

Connections:

MAX7219 LED Matrix modules (4 modules in series):

  • DIN → D11

  • CLK → D12

  • CS → D10

  • VCC → 5V

  • GND → GND

5-pin analog joystick:

  • VRx → A0 (left/right)

  • VRy → A1 (up/down, optional)

  • SW → D2 (button for rotation)

  • VCC → 5V

  • GND → GND

Code:

#include <LedControl.h>  // Library for driving MAX7219 LED matrices

// MAX7219 wiring pins
#define DIN_PIN     6    // Data input
#define CLK_PIN     5    // Clock pin
#define CS_PIN      3    // Chip select
#define NUM_MODULES 4    // Number of 8×8 modules in cascade

LedControl lc(DIN_PIN, CLK_PIN, CS_PIN, NUM_MODULES);

// Joystick and button pins
#define VRx A0  // Joystick X-axis (left/right)
#define VRy A1  // Joystick Y-axis (up/down)
#define SW  2   // Push-button switch for rotation

// Display dimensions
const int SCREEN_W = 8;                     // Width of one module
const int SCREEN_H = SCREEN_W * NUM_MODULES; // Total height (32 rows)

// Playfield buffer: each byte is one row of 8 bits
uint8_t field[SCREEN_H];

// Timing control
unsigned long lastDrop     = 0;    // Time of last automatic drop
unsigned long dropInterval = 500;  // Drop interval in ms (adjusted by joystick)
unsigned long lastMove     = 0;    // Time of last horizontal move
const unsigned long moveInterval    = 200; // Min ms between moves
const unsigned long refreshInterval = 33;  // ~30 FPS
unsigned long lastRefresh  = 0;    // Time of last screen refresh

// Buffer to track previous frame for diff updates
uint8_t prevBuf[NUM_MODULES][SCREEN_W];

// Structure for current falling block
struct Block {
  const int (*shape)[2];  // Pointer to array of {x,y} offsets
  int len;                // Number of cells (always 4)
  int x, y;               // Top-left origin position
  int rotation;           // Rotation index
  char type;              // Block type identifier
} current;

// Definitions of the seven Tetris shapes and their rotations
const int I_SHAPE[2][4][2] = {
  {{0,0},{0,1},{0,2},{0,3}},   // Vertical
  {{-1,1},{0,1},{1,1},{2,1}}   // Horizontal
};
const int O_SHAPE[1][4][2] = {
  {{0,0},{1,0},{0,1},{1,1}}    // Square (no rotation)
};
const int T_SHAPE[4][4][2] = {
  {{1,0},{0,1},{1,1},{2,1}},
  {{1,0},{1,1},{1,2},{0,1}},
  {{0,1},{1,1},{2,1},{1,2}},
  {{1,0},{1,1},{1,2},{2,1}}
};
const int L_SHAPE[4][4][2] = {
  {{0,0},{0,1},{0,2},{1,2}},
  {{0,0},{1,0},{2,0},{0,1}},
  {{0,0},{1,0},{1,1},{1,2}},
  {{2,0},{0,1},{1,1},{2,1}}
};
const int J_SHAPE[4][4][2] = {
  {{1,0},{1,1},{1,2},{0,2}},
  {{0,0},{0,1},{1,1},{2,1}},
  {{0,0},{1,0},{0,1},{0,2}},
  {{0,0},{1,0},{2,0},{2,1}}
};
const int S_SHAPE[2][4][2] = {
  {{1,0},{2,0},{0,1},{1,1}},
  {{1,0},{1,1},{2,1},{2,2}}
};
const int Z_SHAPE[2][4][2] = {
  {{0,0},{1,0},{1,1},{2,1}},
  {{2,0},{1,1},{2,1},{1,2}}
};

// 8×8 bitmaps for letters in the Game Over screen
static const uint8_t PAT_G[8] = {0x3C,0x42,0x40,0x4E,0x42,0x42,0x3C,0x00};
static const uint8_t PAT_A[8] = {0x18,0x24,0x42,0x7E,0x42,0x42,0x42,0x00};
static const uint8_t PAT_M[8] = {0x42,0x66,0x5A,0x5A,0x42,0x42,0x42,0x00};
static const uint8_t PAT_E[8] = {0x7E,0x40,0x5C,0x40,0x40,0x40,0x7E,0x00};
static const uint8_t PAT_O[8] = {0x3C,0x42,0x42,0x42,0x42,0x42,0x3C,0x00};
static const uint8_t PAT_V[8] = {0x42,0x42,0x42,0x42,0x42,0x24,0x18,0x00};
static const uint8_t PAT_R[8] = {0x7C,0x42,0x42,0x7C,0x48,0x44,0x42,0x00};

// Clear all LEDs on every module
void clearAll() {
  for (int m = 0; m < NUM_MODULES; m++) {
    lc.clearDisplay(m);
  }
}

// Read and debounce the push-button switch
bool readButton() {
  if (digitalRead(SW) == LOW) {
    delay(20);
    if (digitalRead(SW) == LOW) {
      while (digitalRead(SW) == LOW); // Wait for release
      return true;
    }
  }
  return false;
}

// Return the bitmap for a given character
const uint8_t* letterPattern(char c) {
  switch (c) {
    case 'G': return PAT_G;
    case 'A': return PAT_A;
    case 'M': return PAT_M;
    case 'E': return PAT_E;
    case 'O': return PAT_O;
    case 'V': return PAT_V;
    case 'R': return PAT_R;
    default:  return PAT_E;
  }
}

// Game Over animation: flash, display "GAME", wait 1s, then display "OVER"
void gameOverSequence() {
  // 1) Flash all LEDs three times
  for (int i = 0; i < 3; i++) {
    clearAll();
    delay(500);
    for (int m = 0; m < NUM_MODULES; m++)
      for (int r = 0; r < SCREEN_W; r++)
        lc.setRow(m, r, 0xFF);
    delay(500);
  }

  // 2) Display "GAME" rotated 90° CW
  const char* w1 = "GAME";
  for (int seg = 0; seg < 4; seg++) {
    const uint8_t* pat = letterPattern(w1[seg]);
    uint8_t rot[8] = {};
    // Rotate 90° CW: (x,y) → (7-y, x)
    for (int y = 0; y < 8; y++) {
      for (int x = 0; x < 8; x++) {
        if (pat[y] & (1 << x)) {
          int nx = 7 - y;
          int ny = x;
          rot[ny] |= (1 << nx);
        }
      }
    }
    int module = NUM_MODULES - 1 - seg;
    for (int row = 0; row < 8; row++) {
      lc.setRow(module, row, rot[row]);
    }
  }
  delay(1000);  // Wait 1 second before showing OVER

  // 3) Display "OVER" rotated 90° CW
  const char* w2 = "OVER";
  for (int seg = 0; seg < 4; seg++) {
    const uint8_t* pat = letterPattern(w2[seg]);
    uint8_t rot[8] = {};
    for (int y = 0; y < 8; y++) {
      for (int x = 0; x < 8; x++) {
        if (pat[y] & (1 << x)) {
          int nx = 7 - y;
          int ny = x;
          rot[ny] |= (1 << nx);
        }
      }
    }
    int module = NUM_MODULES - 1 - seg;
    for (int row = 0; row < 8; row++) {
      lc.setRow(module, row, rot[row]);
    }
  }
  delay(1000);  // Hold OVER for 1 second

  // 4) Wait for button press to restart
  while (digitalRead(SW) != LOW) delay(10);
  while (digitalRead(SW) == LOW) delay(10);
}

// Spawn a new random Tetris block at the top center
void spawnBlock() {
  int r = random(7);
  int sx = SCREEN_W / 2 - 2;  // Center X
  current.rotation = 0;
  switch (r) {
    case 0: current = {I_SHAPE[0],4,sx,0,0,'I'}; break;
    case 1: current = {O_SHAPE[0],4,sx,0,0,'O'}; break;
    case 2: current = {T_SHAPE[0],4,sx,0,0,'T'}; break;
    case 3: current = {L_SHAPE[0],4,sx,0,0,'L'}; break;
    case 4: current = {J_SHAPE[0],4,sx,0,0,'J'}; break;
    case 5: current = {S_SHAPE[0],4,sx,0,0,'S'}; break;
    case 6: current = {Z_SHAPE[0],4,sx,0,0,'Z'}; break;
  }
}

// Reset game state: clear playfield and display
void resetGame() {
  memset(field, 0, sizeof(field));
  clearAll();
  for (int m = 0; m < NUM_MODULES; m++)
    for (int r = 0; r < SCREEN_W; r++)
      prevBuf[m][r] = 0;
  spawnBlock();
  lastDrop    = millis();
  lastRefresh = millis();
}

// Draw playfield and current block with diff updates
void writeBuffer() {
  uint8_t buf[NUM_MODULES][SCREEN_W] = {};

  // Draw fixed blocks
  for (int y = 0; y < SCREEN_H; y++) {
    uint8_t row = field[y];
    if (!row) continue;
    int mod = NUM_MODULES - 1 - (y / SCREEN_W);
    int bit = 1 << (7 - (y % SCREEN_W));
    for (int x = 0; x < SCREEN_W; x++) {
      if (row & (1 << x)) buf[mod][x] |= bit;
    }
  }

  // Draw current falling block
  for (int i = 0; i < current.len; i++) {
    int xx = current.x + current.shape[i][0];
    int yy = current.y + current.shape[i][1];
    if (xx < 0 || xx >= SCREEN_W || yy < 0 || yy >= SCREEN_H) continue;
    int mod = NUM_MODULES - 1 - (yy / SCREEN_W);
    int bit = 1 << (7 - (yy % SCREEN_W));
    buf[mod][xx] |= bit;
  }

  // Update only changed rows
  for (int m = 0; m < NUM_MODULES; m++) {
    for (int r = 0; r < SCREEN_W; r++) {
      if (buf[m][r] != prevBuf[m][r]) {
        lc.setRow(m, r, buf[m][r]);
        prevBuf[m][r] = buf[m][r];
      }
    }
  }
}

// Check for collision at position (nx, ny)
bool checkCollision(int nx, int ny) {
  for (int i = 0; i < current.len; i++) {
    int xx = nx + current.shape[i][0];
    int yy = ny + current.shape[i][1];
    if (xx < 0 || xx >= SCREEN_W || yy >= SCREEN_H) return true;
    if (yy >= 0 && (field[yy] & (1 << xx))) return true;
  }
  return false;
}

// Fix current block into field and clear full lines
void placeBlock() {
  for (int i = 0; i < current.len; i++) {
    int xx = current.x + current.shape[i][0];
    int yy = current.y + current.shape[i][1];
    if (yy >= 0 && yy < SCREEN_H) field[yy] |= (1 << xx);
  }
  // Clear any full rows
  for (int y = 0; y < SCREEN_H; y++) {
    if (field[y] == 0xFF) {
      for (int j = y; j > 0; j--) field[j] = field[j - 1];
      field[0] = 0;
    }
  }
}

// Rotate block with rollback on collision
void rotateBlock() {
  int limit = (current.type=='I'||current.type=='S'||current.type=='Z') ? 2
            : (current.type=='O' ? 1 : 4);
  int nr = (current.rotation + 1) % limit;
  const int (*ns)[2] = nullptr;
  if      (current.type=='I') ns = I_SHAPE[nr];
  else if (current.type=='O') ns = O_SHAPE[0];
  else if (current.type=='T') ns = T_SHAPE[nr];
  else if (current.type=='L') ns = L_SHAPE[nr];
  else if (current.type=='J') ns = J_SHAPE[nr];
  else if (current.type=='S') ns = S_SHAPE[nr];
  else if (current.type=='Z') ns = Z_SHAPE[nr];

  Block bak = current;
  current.shape    = ns;
  current.rotation = nr;
  if (checkCollision(current.x, current.y)) current = bak;
}

void setup() {
  pinMode(SW, INPUT_PULLUP);     // Button input with pull-up
  randomSeed(analogRead(0));     // Seed RNG
  for (int m = 0; m < NUM_MODULES; m++) {
    lc.shutdown(m, false);
    lc.setIntensity(m, 8);
    lc.clearDisplay(m);
    for (int r = 0; r < SCREEN_W; r++) prevBuf[m][r] = 0;
  }
  resetGame();  // Start the game
}

void loop() {
  unsigned long now = millis();

  // Horizontal movement via joystick X-axis
  int ax = analogRead(VRx);
  if (now - lastMove > moveInterval) {
    if (ax < 400 && !checkCollision(current.x + 1, current.y)) {
      current.x++; lastMove = now;
    } else if (ax > 600 && !checkCollision(current.x - 1, current.y)) {
      current.x--; lastMove = now;
    }
  }

  // Rotate on button press
  if (readButton()) rotateBlock();

  // Adjust drop speed via joystick Y-axis (down = faster)
  int ay = analogRead(VRy);
  dropInterval = 700 - constrain(map(ay,512,1023,0,690),0,690);

  // Automatic drop & top-hit detection
  if (now - lastDrop > dropInterval) {
    lastDrop = now;
    if (!checkCollision(current.x, current.y + 1)) {
      current.y++;
    } else {
      // Check for game over (block at top)
      bool hitTop = false;
      for (int i = 0; i < current.len; i++) {
        if (current.y + current.shape[i][1] == 0) {
          hitTop = true; break;
        }
      }
      if (hitTop) {
        gameOverSequence();
        resetGame();
        return;
      } else {
        placeBlock();
        spawnBlock();
      }
    }
  }

  // Refresh display at ~30 FPS
  if (now - lastRefresh >= refreshInterval) {
    writeBuffer();
    lastRefresh = now;
  }
}