Built-in project · Arduino Mega 2560 R3

Tetris on LED matrices, on an Arduino Mega

Tetris on LED matrices with an Arduino Mega: two 8x8 MAX7219s make an 8 by 16 field, with four buttons, all seven pieces, line clears and a high score. The whole build, an Arduino Mega and 7 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Advanced Runs in your browser. Free, and no account needed.

The parts retail for about $59. On Mokxi they're free.

Tetris on LED matrices · Arduino Mega 2560 R3live0.000 s 0.00x
Press Run, watch the computer play, then press any button
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Mega 2560 R3
// Tetris on two 8x8 LED matrices (MAX7219), stacked into a field 8 wide and
// 16 tall, with four arcade buttons and a piezo, on an Arduino Mega.
//
//   top matrix      DIN -> 31, CS -> 33, CLK -> 35, VCC -> 5 V, GND -> GND
//   bottom matrix   DIN -> 47, CS -> 49, CLK -> 51, VCC -> 5 V, GND -> GND
//   buttons         left -> 5, right -> 4, rotate -> 3, drop -> 2, each to GND
//   piezo           A1 and GND
//
// The seven pieces fall one at a time. Move them with left and right, turn
// them with rotate, and drop slams one straight down. Fill a whole row and it
// disappears; clear two, three or four at once for more points. Every five
// rows the pieces fall faster. When there is no room for the next piece, the
// game is over. The best score is kept in the EEPROM.
//
// Before a game the computer plays, choosing where each piece goes. Press
// any button to take over.
//
// # The field is sixteen bytes
//
// One byte per row, one bit per LED: bit 7 is the left column, bit 0 the
// right. A full row is 0xFF. Clearing it is moving every row above it down
// one. Checking whether a piece fits is an AND between its row and the
// field's row: anything left over means two blocks in one place.
//
// # A piece is sixteen bits
//
// Each piece, in each of its four turns, is a 4x4 box of bits stored in one
// 16-bit number, the top row in the highest four bits. Turning a piece is
// just using the next number in its list, so there is no rotation maths.
//
// # The matrices
//
// A MAX7219 holds one byte for each row of its 8x8 LEDs. Writing a row is
// sixteen bits: the row's register number (1 to 8), then the byte. Each
// matrix here has its own three pins, so the Mega simply talks to one or the
// other, and only rows that changed are sent.

#include <EEPROM.h>

const uint8_t DIN[2] = {31, 47};
const uint8_t CS[2] = {33, 49};
const uint8_t CLK[2] = {35, 51};
const uint8_t LEFT = 5;
const uint8_t RIGHT = 4;
const uint8_t ROTATE = 3;
const uint8_t DROP = 2;
const uint8_t PIEZO = A1;
const int BEST_ADDRESS = 0;  // two bytes

const uint8_t ROWS = 16;

// The seven pieces, I O T S Z J L, each in its four turns.
const uint16_t PIECE[7][4] = {
    {0x0F00, 0x2222, 0x00F0, 0x4444},  // I
    {0x6600, 0x6600, 0x6600, 0x6600},  // O
    {0x4E00, 0x4640, 0x0E40, 0x4C40},  // T
    {0x6C00, 0x4620, 0x06C0, 0x8C40},  // S
    {0xC600, 0x2640, 0x0C60, 0x4C80},  // Z
    {0x8E00, 0x6440, 0x0E20, 0x44C0},  // J
    {0x2E00, 0x4460, 0x0E80, 0xC440},  // L
};

uint8_t field[ROWS];
uint8_t shown[ROWS];
uint8_t piece, turn;
int8_t px, py;  // where the piece's 4x4 box is: column of its left edge, row of its top
uint8_t bag[7];
uint8_t bagLeft = 0;
unsigned int score, best;
unsigned int lines;
uint8_t level;
bool demo = true;
unsigned long fallAt = 0;

// --- the matrices ---

void send(uint8_t m, uint8_t reg, uint8_t data) {
  digitalWrite(CS[m], LOW);
  shiftOut(DIN[m], CLK[m], MSBFIRST, reg);
  shiftOut(DIN[m], CLK[m], MSBFIRST, data);
  digitalWrite(CS[m], HIGH);
}

void setupMatrix(uint8_t m) {
  pinMode(DIN[m], OUTPUT);
  pinMode(CS[m], OUTPUT);
  pinMode(CLK[m], OUTPUT);
  digitalWrite(CS[m], HIGH);
  send(m, 0x0F, 0);  // display test off
  send(m, 0x09, 0);  // no digit decoding: raw rows
  send(m, 0x0B, 7);  // scan all eight rows
  send(m, 0x0A, 4);  // brightness, 0 to 15
  send(m, 0x0C, 1);  // wake up
}

// The four rows of a piece's box, each as the byte it makes at column x.
uint8_t pieceRow(uint16_t shape, uint8_t r, int8_t x) {
  uint8_t bits = (shape >> (12 - 4 * r)) & 0x0F;  // left column in bit 3
  int shift = 4 - x;  // bit 3 of the box lands in bit 7 - x of the row
  return shift >= 0 ? (uint8_t)(bits << shift) : (uint8_t)(bits >> -shift);
}

// Would this piece fit here? Off the sides or the bottom, or on top of
// another block, it would not.
bool fits(uint16_t shape, int8_t x, int8_t y) {
  for (uint8_t r = 0; r < 4; r++) {
    uint8_t bits = (shape >> (12 - 4 * r)) & 0x0F;
    if (!bits) continue;
    // A block past the left or right edge drops off when shifted: compare.
    for (uint8_t c = 0; c < 4; c++) {
      if (!(bits & (8 >> c))) continue;
      if (x + c < 0 || x + c > 7) return false;
    }
    int row = y + r;
    if (row >= ROWS) return false;
    if (row >= 0 && (field[row] & pieceRow(shape, r, x))) return false;
  }
  return true;
}

void draw() {
  uint8_t rows[ROWS];
  for (uint8_t i = 0; i < ROWS; i++) rows[i] = field[i];
  uint16_t shape = PIECE[piece][turn];
  for (uint8_t r = 0; r < 4; r++) {
    int row = py + r;
    if (row >= 0 && row < ROWS) rows[row] |= pieceRow(shape, r, px);
  }
  for (uint8_t i = 0; i < ROWS; i++) {
    if (rows[i] == shown[i]) continue;
    send(i / 8, i % 8 + 1, rows[i]);
    shown[i] = rows[i];
  }
}

// --- the game ---

// A shuffled bag of all seven pieces: every piece comes once in every seven,
// so there are never long droughts of the long I piece.
uint8_t nextPiece() {
  if (bagLeft == 0) {
    for (uint8_t i = 0; i < 7; i++) bag[i] = i;
    for (uint8_t i = 6; i > 0; i--) {
      uint8_t j = random(i + 1);
      uint8_t t = bag[i];
      bag[i] = bag[j];
      bag[j] = t;
    }
    bagLeft = 7;
  }
  return bag[--bagLeft];
}

unsigned long fallMs() {
  return max(600UL - 50UL * level, 120UL);
}

// Take out one row: everything above it moves down one.
void removeRow(uint8_t row) {
  for (uint8_t r = row; r > 0; r--) field[r] = field[r - 1];
  field[0] = 0;
}

void clearLines() {
  uint8_t cleared = 0;
  uint8_t row = ROWS;
  while (row > 0) {
    if (field[row - 1] == 0xFF) {
      removeRow(row - 1);  // and look at the same row again
      cleared++;
    } else {
      row--;
    }
  }
  if (!cleared) return;
  const unsigned int POINTS[5] = {0, 40, 100, 300, 1200};
  score += POINTS[cleared] * (level + 1);
  lines += cleared;
  level = lines / 5;
  tone(PIEZO, 880 + 220 * cleared, 120);
  if (demo) {
    Serial.print("demo: ");
    Serial.print(lines);
    Serial.println(lines == 1 ? " line" : " lines");
  } else {
    Serial.print(cleared);
    Serial.print(cleared == 1 ? " line! " : " lines! ");
    Serial.print("score ");
    Serial.print(score);
    Serial.print(", level ");
    Serial.println(level);
  }
}

void newGame(bool asDemo);

bool spawn() {
  piece = nextPiece();
  turn = 0;
  px = 2;
  py = 0;
  fallAt = millis();
  return fits(PIECE[piece][turn], px, py);
}

void gameOver() {
  tone(PIEZO, 110, 700);
  if (!demo) {
    Serial.print("game over: score ");
    Serial.print(score);
    Serial.print(", ");
    Serial.print(lines);
    Serial.println(lines == 1 ? " line" : " lines");
    if (score > best) {
      best = score;
      EEPROM.put(BEST_ADDRESS, best);
      Serial.print("new best: ");
      Serial.println(best);
    }
  }
  // fill the field from the bottom up, then start again
  for (int row = ROWS - 1; row >= 0; row--) {
    field[row] = 0xFF;
    draw();
    delay(60);
  }
  delay(800);
  newGame(true);
}

// The piece is down: add it to the field, clear rows, bring the next one.
void lock() {
  uint16_t shape = PIECE[piece][turn];
  for (uint8_t r = 0; r < 4; r++) {
    int row = py + r;
    if (row >= 0 && row < ROWS) field[row] |= pieceRow(shape, r, px);
  }
  tone(PIEZO, 220, 20);
  clearLines();
  if (!spawn()) gameOver();
}

void newGame(bool asDemo) {
  demo = asDemo;
  for (uint8_t i = 0; i < ROWS; i++) field[i] = 0;
  score = 0;
  lines = 0;
  level = 0;
  bagLeft = 0;
  spawn();
  draw();
  Serial.println(demo ? "demo: press any button to play" : "game on: left, right, rotate, drop");
}

// --- the computer player ---

// How good is the field after dropping a piece? Fewer holes, a lower stack
// and more full rows are better.
int rate(const uint8_t *f) {
  int holes = 0, height = 0, full = 0;
  for (uint8_t c = 0; c < 8; c++) {
    uint8_t bit = 0x80 >> c;
    bool roof = false;
    for (uint8_t r = 0; r < ROWS; r++) {
      if (f[r] & bit) {
        if (!roof) height += ROWS - r;
        roof = true;
      } else if (roof) {
        holes++;
      }
    }
  }
  for (uint8_t r = 0; r < ROWS; r++) full += f[r] == 0xFF;
  return full * 40 - holes * 30 - height;
}

// Try every turn at every column, and remember the best.
uint8_t aimTurn;
int8_t aimX;

void plan() {
  int bestScore = -32000;
  aimTurn = turn;
  aimX = px;
  for (uint8_t t = 0; t < 4; t++) {
    uint16_t shape = PIECE[piece][t];
    for (int8_t x = -2; x < 8; x++) {
      if (!fits(shape, x, py)) continue;
      int8_t y = py;
      while (fits(shape, x, y + 1)) y++;
      uint8_t f[ROWS];
      for (uint8_t i = 0; i < ROWS; i++) f[i] = field[i];
      for (uint8_t r = 0; r < 4; r++) {
        if (y + r >= 0 && y + r < ROWS) f[y + r] |= pieceRow(shape, r, x);
      }
      int s = rate(f);
      if (s > bestScore) {
        bestScore = s;
        aimTurn = t;
        aimX = x;
      }
    }
  }
}

// --- buttons ---

const uint8_t BUTTON[4] = {LEFT, RIGHT, ROTATE, DROP};
bool wasDown[4];
unsigned long repeatAt[4];

// True on a press, and again every 90 ms while left or right is held after
// the first 180 ms, so a held button slides the piece along.
bool pressed(uint8_t b, unsigned long now) {
  bool down = digitalRead(BUTTON[b]) == LOW;
  bool fire = false;
  if (down && !wasDown[b]) {
    fire = true;
    repeatAt[b] = now + 180;
  } else if (down && b < 2 && (long)(now - repeatAt[b]) >= 0) {
    fire = true;
    repeatAt[b] = now + 90;
  }
  wasDown[b] = down;
  return fire;
}

void setup() {
  Serial.begin(115200);
  for (uint8_t b = 0; b < 4; b++) pinMode(BUTTON[b], INPUT_PULLUP);
  pinMode(PIEZO, OUTPUT);
  setupMatrix(0);
  setupMatrix(1);
  for (uint8_t i = 0; i < ROWS; i++) shown[i] = 0xAA;  // force the first draw
  // A blank EEPROM reads 0xFFFF: no best score yet.
  EEPROM.get(BEST_ADDRESS, best);
  if (best == 0xFFFF) best = 0;
  // Nothing is wired to A0, so it reads noise: a different game each time.
  randomSeed(analogRead(A0));
  newGame(true);
}

void loop() {
  unsigned long now = millis();
  bool left = pressed(0, now);
  bool right = pressed(1, now);
  bool rotate = pressed(2, now);
  bool drop = pressed(3, now);

  if (demo) {
    if (left || right || rotate || drop) {
      newGame(false);
      return;
    }
    // Every 120 ms the computer makes one move toward its plan.
    static unsigned long thinkAt = 0;
    if (now - thinkAt >= 120) {
      thinkAt = now;
      plan();
      if (turn != aimTurn) {
        rotate = true;
      } else if (px > aimX) {
        left = true;
      } else if (px < aimX) {
        right = true;
      } else {
        drop = true;
      }
    }
  }

  if (left && fits(PIECE[piece][turn], px - 1, py)) {
    px--;
    tone(PIEZO, 660, 8);
  }
  if (right && fits(PIECE[piece][turn], px + 1, py)) {
    px++;
    tone(PIEZO, 660, 8);
  }
  if (rotate) {
    uint8_t next = (turn + 1) % 4;
    // If the turned piece would stick into a wall, try it one step in.
    for (int8_t kick = 0; kick < 3; kick++) {
      int8_t x = px + (kick == 1 ? -1 : kick == 2 ? 1 : 0);
      if (fits(PIECE[piece][next], x, py)) {
        turn = next;
        px = x;
        tone(PIEZO, 990, 10);
        break;
      }
    }
  }
  if (drop) {
    while (fits(PIECE[piece][turn], px, py + 1)) {
      py++;
      if (!demo) score++;  // a point for every row it falls
    }
    lock();
  } else if (now - fallAt >= fallMs()) {
    fallAt = now;
    if (fits(PIECE[piece][turn], px, py + 1)) {
      py++;
    } else {
      lock();
    }
  }
  draw();
  delay(10);
}

Parts list

10 parts, plus the jumper wires. They retail for about $59; on Mokxi they're free.

How it is wired

20 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.

  • Arduino Mega 2560 R3 pin 5; Arcade button (1) pin 2
  • Arduino Mega 2560 R3 pin 4; Arcade button (2) pin 2
  • Arduino Mega 2560 R3 pin 3; Arcade button (3) pin 2
  • Arduino Mega 2560 R3 pin 2; Arcade button (4) pin 2
  • Ground: Arduino Mega 2560 R3 pin GND; Piezo speaker pin 2
  • Arduino Mega 2560 R3 pin A1; Piezo speaker pin 1
  • Arduino Mega 2560 R3 pin 31; LED matrix, 8x8 (1) pin DIN
  • Arduino Mega 2560 R3 pin 33; LED matrix, 8x8 (1) pin CS
  • Arduino Mega 2560 R3 pin 35; LED matrix, 8x8 (1) pin CLK
  • Arduino Mega 2560 R3 pin 47; LED matrix, 8x8 (2) pin DIN
  • Arduino Mega 2560 R3 pin 49; LED matrix, 8x8 (2) pin CS
  • Arduino Mega 2560 R3 pin 51; LED matrix, 8x8 (2) pin CLK
  • 5 V: LED matrix, 8x8 (1) pin VCC
  • Ground: LED matrix, 8x8 (1) pin GND
  • 5 V: LED matrix, 8x8 (2) pin VCC
  • Ground: LED matrix, 8x8 (2) pin GND
  • Ground: Arcade button (1) pin 1
  • Ground: Arcade button (2) pin 1
  • Ground: Arcade button (3) pin 1
  • Ground: Arcade button (4) pin 1

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.