icon

Colorful Arduino Tetris Game - WS2812B LED Matrix Tutorial

Minimal version of Tetris with probably the lowest possible display resolution of only 64 pixels.

Tetris is a puzzle video game created in 1985 by Alexey Pajitnov. Players manipulate falling geometric shapes called "tetrominoes." The goal is to create horizontal lines without gaps. Completed lines disappear, granting the player points, and all other blocks move down the corresponding number of lines.

The game speeds up as you progress. Many different versions of Tetris have been released for PC, consoles and mobile platforms.
In this project I will present you a way to make a colorful version of Tetris with probably the lowest possible display resolution of only 64 pixels.

In fact, the display is a cheap 8x8 LED matrix consisting of 64 RGB diodes with a built-in WS2812 chip. This allows us to control the entire matrix with just one pin from the microcontroller. This project is extremely simple to make, and is suitable even for absolute beginners in this field. However, despite its simplicity, as you will see later, the game is endlessly addictive and is intended for players of all ages.

Now let's see what elements the device contains:
- Arduino Nano microcontroller board
- 8x8 Led matrix with WS2812b chips.
- 3 Buttons
- Buzzer
- and optional battery
I should mention that if the device is powered by an external 5VDC source, it should be capable of delivering a current of at least 1Ampere.
This project is sponsored by PCBWAY. This year, PCBWay is organizing the 11th badge design contest from March 3rd to April 31st. Follow the design requirements and Submit your designs in one of the given ways, and become the winner of one of the valuable prizes in cash and cupons. This contest is more than a competition—it’s a celebration of 11 years of innovation and a chance to dream about the boundless possibilities ahead with PCBWay.

All components used are standard except the LED matrix which can be found on the market in several versions. They all differ in the way and order of connecting the LEDs in them (zigzag horizontally, then vertically, snake connection, etc.). It is obvious that we cannot make hardware changes, so for this purpose I created a part of the code where any version of these matrices can be selected.

We need to select one by one of these 4 connection methods until we get the correct image on the matrix display.
First, let me introduce you to the options and how the device functions in real-world conditions. Immediately upon switching on, a characteristic sound sequence is activated and the scrolling text Mini Tetris appears on the display. Then the display is divided into two halves, one blue and the other magenta. The blue side is the normal game mode, and the magenta side is the KIDS (easy) game mode.

In normal mode, the Tetrominoes are of standard size, shape and number as in the original and a faster reaction is required, especially considering the small distance between the top and bottom rows of the display. The outer buttons move the tetromino left and right, and the middle button is for rotation. The speed of movement of the tetromino increases with time. With each cleared row, 100 points are obtained. If two rows are cleared at the same time, the reward is 400 points. The final score is displayed at the end in the form of scrolling text, followed by a smiley figure.

From this moment if we press any of the buttons, a new game begins, signaled by the display filling with green. The entire gameplay is accompanied by appropriate sound effects.
Now to select kids mode, we have to restart the device. Otherwise, the kids mode option is very useful because it is much easier and simpler to play and so children can play it. In this mode, the tetrominos are much smaller, there is no rotation option, and they are much easier to arrange. The other functions are identical to the normal mode.
A few words about the code, as you can see, it is designed in a way that allows you to easily change almost all the parameters of the game, starting from the intensity of the LEDs, changing the colors, the sounds, and probably the most important parameter, which is the initial speed of the tetromino, the degree of acceleration during the game, and the maximum speed limit.

Below you can see several gameplays in normal and kids modes.
And finally a short conclusion: This extremely simple project is actually the minimum possible version of the Tetris game made on a 64 pixel display, but still with all the standard options and sound effects, and even a Kids mode intended for the youngest.
As for the case, it is from one of my previous projects and is made of 5mm thick PVC material and has the shape of a classic arcade game console where this game was most often played many years ago. It is covered with self-adhesive wallpaper.

CODE
/*Arduino TETRIS on 8x8 Matrix WS2812b
by mircemk, April 2025
*/

#include <FastLED.h>

// LED Matrix configuration
#define LED_PIN     6
#define NUM_LEDS    64
#define BRIGHTNESS  50
#define LED_TYPE    WS2812B
#define COLOR_ORDER GRB
#define MATRIX_WIDTH 8
#define MATRIX_HEIGHT 8
#define BUZZER_PIN 2

// Button pins
#define LEFT_BUTTON_PIN  9
#define RIGHT_BUTTON_PIN 10
#define ROTATE_BUTTON_PIN 8

// Game parameters
#define INITIAL_GAME_SPEED 500  // Milliseconds
#define SPEED_INCREASE 10       // ms to decrease after each piece
#define MIN_GAME_SPEED 150      // Fastest game speed in milliseconds

#define NOTE_B0  31
#define NOTE_C1  33
#define NOTE_CS1 35
#define NOTE_D1  37
#define NOTE_DS1 39
#define NOTE_E1  41
#define NOTE_F1  44
#define NOTE_FS1 46
#define NOTE_G1  49
#define NOTE_GS1 52
#define NOTE_A1  55
#define NOTE_AS1 58
#define NOTE_B1  62
#define NOTE_C2  65
#define NOTE_CS2 69
#define NOTE_D2  73
#define NOTE_DS2 78
#define NOTE_E2  82
#define NOTE_F2  87
#define NOTE_FS2 93
#define NOTE_G2  98
#define NOTE_GS2 104
#define NOTE_A2  110
#define NOTE_AS2 117
#define NOTE_B2  123
#define NOTE_C3  131
#define NOTE_CS3 139
#define NOTE_D3  147
#define NOTE_DS3 156
#define NOTE_E3  165
#define NOTE_F3  175
#define NOTE_FS3 185
#define NOTE_G3  196
#define NOTE_GS3 208
#define NOTE_A3  220
#define NOTE_AS3 233
#define NOTE_B3  247
#define NOTE_C4  262

#define MODE_NORMAL 0
#define MODE_KIDS 1
byte gameMode = MODE_NORMAL;

bool gameOverScreenShown = false;

// Colors
CRGB leds[NUM_LEDS];
#define BLACK     CRGB(0, 0, 0)
#define RED       CRGB(255, 0, 0)
#define GREEN     CRGB(0, 255, 0)
#define BLUE      CRGB(0, 0, 255)
#define YELLOW    CRGB(255, 255, 0)
#define CYAN      CRGB(0, 255, 255)
#define MAGENTA   CRGB(255, 0, 255)
#define ORANGE    CRGB(255, 165, 0)

// Tetromino shapes
// Each tetromino is defined as 4 cells, each cell having x and y coordinates
typedef struct {
  byte shapes[4][4][2];  // [rotation][cell][x,y]
  CRGB color;
} Tetromino;

// Tetromino types (I, O, T, S, Z, J, L)
Tetromino tetrominos[7] = {
  // I-piece
  {
    {{{0,0}, {1,0}, {2,0}, {3,0}}, 
     {{0,0}, {0,1}, {0,2}, {0,3}},
     {{0,0}, {1,0}, {2,0}, {3,0}},
     {{0,0}, {0,1}, {0,2}, {0,3}}},
    CYAN
  },
  // O-piece
  {
    {{{0,0}, {1,0}, {0,1}, {1,1}},
     {{0,0}, {1,0}, {0,1}, {1,1}},
     {{0,0}, {1,0}, {0,1}, {1,1}},
     {{0,0}, {1,0}, {0,1}, {1,1}}},
    YELLOW
  },
  // T-piece
  {
    {{{0,0}, {1,0}, {2,0}, {1,1}},
     {{1,0}, {0,1}, {1,1}, {1,2}},
     {{1,0}, {0,1}, {1,1}, {2,1}},
     {{0,0}, {0,1}, {0,2}, {1,1}}},
    MAGENTA
  },
  // S-piece
  {
    {{{1,0}, {2,0}, {0,1}, {1,1}},
     {{0,0}, {0,1}, {1,1}, {1,2}},
     {{1,0}, {2,0}, {0,1}, {1,1}},
     {{0,0}, {0,1}, {1,1}, {1,2}}},
    GREEN
  },
  // Z-piece
  {
    {{{0,0}, {1,0}, {1,1}, {2,1}},
     {{1,0}, {0,1}, {1,1}, {0,2}},
     {{0,0}, {1,0}, {1,1}, {2,1}},
     {{1,0}, {0,1}, {1,1}, {0,2}}},
    RED
  },
  // J-piece
  {
    {{{0,0}, {0,1}, {1,1}, {2,1}},
     {{1,0}, {2,0}, {1,1}, {1,2}},
     {{0,0}, {1,0}, {2,0}, {2,1}},
     {{0,0}, {0,1}, {0,2}, {1,0}}},
    BLUE
  },
  // L-piece
  {
    {{{2,0}, {0,1}, {1,1}, {2,1}},
     {{0,0}, {1,0}, {1,1}, {1,2}},
     {{0,0}, {1,0}, {2,0}, {0,1}},
     {{0,0}, {0,1}, {0,2}, {1,2}}},
    ORANGE
  }
};

// simple tetrominos
Tetromino kidstetrominos[7] = {
  // Single pixel (red)
  {
    {{{0,0}, {0,0}, {0,0}, {0,0}},
     {{0,0}, {0,0}, {0,0}, {0,0}},
     {{0,0}, {0,0}, {0,0}, {0,0}},
     {{0,0}, {0,0}, {0,0}, {0,0}}},
    RED
  },
  // Two horizontal pixels (yellow)
  {
    {{{0,0}, {1,0}, {0,0}, {0,0}},
     {{0,0}, {1,0}, {0,0}, {0,0}},
     {{0,0}, {1,0}, {0,0}, {0,0}},
     {{0,0}, {1,0}, {0,0}, {0,0}}},
    YELLOW
  },
  // Two vertical pixels (blue)
  {
    {{{0,0}, {0,1}, {0,0}, {0,0}},
     {{0,0}, {0,1}, {0,0}, {0,0}},
     {{0,0}, {0,1}, {0,0}, {0,0}},
     {{0,0}, {0,1}, {0,0}, {0,0}}},
    BLUE
  },
  // Small L shape (green)
  {
    {{{0,0}, {0,1}, {1,1}, {0,0}},
     {{0,0}, {0,1}, {1,1}, {0,0}},
     {{0,0}, {0,1}, {1,1}, {0,0}},
     {{0,0}, {0,1}, {1,1}, {0,0}}},
    GREEN
  },
  // Small square (magenta)
  {
    {{{0,0}, {1,0}, {0,1}, {1,1}},
     {{0,0}, {1,0}, {0,1}, {1,1}},
     {{0,0}, {1,0}, {0,1}, {1,1}},
     {{0,0}, {1,0}, {0,1}, {1,1}}},
    MAGENTA
  },
  // Three horizontal pixels (cyan)
  {
    {{{0,0}, {1,0}, {2,0}, {0,0}},
     {{0,0}, {1,0}, {2,0}, {0,0}},
     {{0,0}, {1,0}, {2,0}, {0,0}},
     {{0,0}, {1,0}, {2,0}, {0,0}}},
    CYAN
  },
  // Diagonal two pixels (orange)
  {
    {{{0,0}, {1,1}, {0,0}, {0,0}},
     {{0,0}, {1,1}, {0,0}, {0,0}},
     {{0,0}, {1,1}, {0,0}, {0,0}},
     {{0,0}, {1,1}, {0,0}, {0,0}}},
    ORANGE
  }
};

const byte letters[][8] = {
  // M
  {B11011,
   B11011,
   B10101,
   B10001,
   B10001,
   B10001,
   B10001,
   B00000},
  // I
  {B11111,
   B00100,
   B00100,
   B00100,
   B00100,
   B00100,
   B11111,
   B00000},
  // N
  {B10001,
   B11001,
   B11101,
   B10111,
   B10011,
   B10001,
   B10001,
   B00000},
  // T
  {B11111,
   B00100,
   B00100,
   B00100,
   B00100,
   B00100,
   B00100,
   B00000},
  // E
  {B11111,
   B10000,
   B10000,
   B11110,
   B10000,
   B10000,
   B11111,
   B00000},
  // R
  {B11110,
   B10001,
   B10001,
   B11110,
   B10100,
   B10010,
   B10001,
   B00000},
  // S
  {B01111,
   B10000,
   B10000,
   B01110,
   B00001,
   B00001,
   B11110,
   B00000}
};

const byte digits[10][8] = {
  // 0
  {B00000000,
   B00111000,
   B01000100,
   B01000100,
   B01000100,
   B01000100,
   B00111000,
   B00000000},
  // 1
  {B00000000,
   B00010000,
   B00110000,
   B00010000,
   B00010000,
   B00010000,
   B00111000,
   B00000000},
  // 2
  {B00000000,
   B00111000,
   B01000100,
   B00001000,
   B00010000,
   B00100000,
   B01111100,
   B00000000},
  // 3
  {B00000000,
   B00111000,
   B01000100,
   B00001000,
   B00001100,
   B01000100,
   B00111000,
   B00000000},
  // 4
  {B00000000,
   B00001000,
   B00011000,
   B00101000,
   B01001000,
   B01111100,
   B00001000,
   B00000000},
  // 5
  {B00000000,
   B01111100,
   B01000000,
   B01111000,
   B00000100,
   B01000100,
   B00111000,
   B00000000},
  // 6
  {B00000000,
   B00111000,
   B01000000,
   B01111000,
   B01000100,
   B01000100,
   B00111000,
   B00000000},
  // 7
  {B00000000,
   B01111100,
   B00000100,
   B00001000,
   B00010000,
   B00100000,
   B00100000,
   B00000000},
  // 8
  {B00000000,
   B00111000,
   B01000100,
   B00111000,
   B01000100,
   B01000100,
   B00111000,
   B00000000},
  // 9
  {B00000000,
   B00111000,
   B01000100,
   B01000100,
   B00111100,
   B00000100,
   B00111000,
   B00000000}
};

const byte SMILEY[8] = {
  B00111100,
  B01000010,
  B10100101,
  B10000001,
  B10100101,
  B10011001,
  B01000010,
  B00111100
};


const Tetromino* currentTetrominoSet;

void displayEndAnimation() {
  // Display static smiley once
  clearDisplay();
  for (int row = 0; row < 8; row++) {
    for (int col = 0; col < 8; col++) {
      if (SMILEY[row] & (1 << (7 - col))) {
        leds[getPixelIndex(col, row)] = CRGB::Yellow;
      }
    }
  }
  FastLED.show();
  
  // Just wait for button press without redrawing
  while (true) {
    if (digitalRead(LEFT_BUTTON_PIN) == LOW || digitalRead(RIGHT_BUTTON_PIN) == LOW) {
      break;
    }
    delay(100);  // Small delay to check buttons
  }
}

void displayScrollingScore(long score) {
  // Convert score to string
  char scoreStr[7];
  sprintf(scoreStr, "%ld", score);
  int scoreLen = strlen(scoreStr);
  
  // Display each digit scrolling from right to left
  for (int pos = 8; pos >= -scoreLen * 6; pos--) {
    clearDisplay();
    
    // Display each digit in its current position
    for (int i = 0; i < scoreLen; i++) {
      int digitPos = pos + (i * 6);  // 6 pixels spacing between digits
      if (digitPos < 8 && digitPos > -6) {  // Only display if digit is visible
        int digit = scoreStr[i] - '0';
        displayLetter(digits[digit], digitPos, CRGB(255, 255, 0));  // Orange color
      }
    }
    
    FastLED.show();
    delay(100);  // Scroll speed
  }
  
  // Pause at the end
  delay(500);
}

void playMoveSound() {
  // Quick, high-pitched blip (1200 Hz)
  tone(BUZZER_PIN, 1200, 30);  // Short duration for quick response
}

void playRotateSound() {
  // Two-tone ascending sound
  tone(BUZZER_PIN, 1000, 25);
  delay(25);
  tone(BUZZER_PIN, 1500, 25);  // Higher pitch for rotation
}

void playLandSound() {
  // Descending "bounce" effect
  tone(BUZZER_PIN, 800, 100);
  delay(50);
  tone(BUZZER_PIN, 1200, 80);
  delay(30);
  tone(BUZZER_PIN, 1500, 100);
}

void playClearLineSound() {
  // Cheerful ascending arpeggio
  tone(BUZZER_PIN, 800, 50);
  delay(50);
  tone(BUZZER_PIN, 1000, 50);
  delay(50);
  tone(BUZZER_PIN, 1200, 50);
  delay(50);
  tone(BUZZER_PIN, 1500, 100);
}

void playClearLineSound(int linesCleared) {
  switch(linesCleared) {
    case 1:
      // Simple two-tone
      tone(BUZZER_PIN, 1000, 50);
      delay(50);
      tone(BUZZER_PIN, 1500, 100);
      break;
      
    case 2:
      // Triple ascending
      tone(BUZZER_PIN, 1000, 50);
      delay(50);
      tone(BUZZER_PIN, 1200, 50);
      delay(50);
      tone(BUZZER_PIN, 1500, 100);
      break;
      
    case 3:
      // Four-note ascending
      tone(BUZZER_PIN, 1000, 50);
      delay(50);
      tone(BUZZER_PIN, 1200, 50);
      delay(50);
      tone(BUZZER_PIN, 1500, 50);
      delay(50);
      tone(BUZZER_PIN, 1800, 100);
      break;
      
    case 4:
      // Special Tetris fanfare
      tone(BUZZER_PIN, 1500, 80);
      delay(80);
      tone(BUZZER_PIN, 1800, 80);
      delay(80);
      tone(BUZZER_PIN, 2000, 80);
      delay(80);
      tone(BUZZER_PIN, 2500, 300);  // Final triumphant note
      break;
  }
}

void playGameOverSound() {
  // Playful "game over" tune
  tone(BUZZER_PIN, 1500, 100);
  delay(100);
  tone(BUZZER_PIN, 1200, 100);
  delay(100);
  tone(BUZZER_PIN, 1000, 100);
  delay(100);
  tone(BUZZER_PIN, 800, 300);
}

void playStartSound() {
  // Cheerful startup fanfare
  tone(BUZZER_PIN, 1000, 80);
  delay(80);
  tone(BUZZER_PIN, 1200, 80);
  delay(80);
  tone(BUZZER_PIN, 1500, 80);
 // delay(80);
 // tone(BUZZER_PIN, 2000, 200);  // Final triumphant note
}

void playModeSelectorSound() {
  // Quick two-tone acknowledgment
  tone(BUZZER_PIN, 1200, 50);
  delay(50);
  tone(BUZZER_PIN, 1500, 100);
}
// Add this function to select game mode
void selectGameMode() {
  playStartSound();
  bool modeSelected = false;
  
  while (!modeSelected) {
    // Split screen in two colors
    for (int y = 0; y < 8; y++) {
      for (int x = 0; x < 8; x++) {
        if (x < 4) {
          // Left half - Normal mode
          leds[getPixelIndex(x, y)] = CRGB(0, 150, 255);  // Sky blue
        } else {
          // Right half - Kids mode
          leds[getPixelIndex(x, y)] = CRGB(255, 0, 255);  // Magenta
        }
      }
    }
    FastLED.show();
    
    // Check buttons
    if (digitalRead(LEFT_BUTTON_PIN) == LOW) {
      playModeSelectorSound();
      gameMode = MODE_NORMAL;
      modeSelected = true;
      currentTetrominoSet = tetrominos;  // Set normal tetrominos
      
      // Clear screen first
      clearDisplay();
      FastLED.show();
      delay(300);
      
      // Smaller 5x6 "N" letter centered on the display
      const byte letterN[8] = {
        B00000000,
        B01001000,
        B01101000,
        B01011000,
        B01001000,
        B01001000,
        B00000000,
        B00000000
      };
      
      // Display N in the middle (starting at x=1)
      for (int i = 0; i < 3; i++) {
        clearDisplay();
        displayLetter(letterN, 1, CRGB(0, 150, 255));  // Sky blue
        FastLED.show();
        delay(200);
        clearDisplay();
        FastLED.show();
        delay(200);
      }
    }
    else if (digitalRead(RIGHT_BUTTON_PIN) == LOW) {
      playModeSelectorSound();
      gameMode = MODE_KIDS;
      modeSelected = true;
      currentTetrominoSet = kidstetrominos;  // Set kids tetrominos
      
      // Clear screen first
      clearDisplay();
      FastLED.show();
      delay(300);
      
      // Smaller 5x6 "K" letter centered on the display
      const byte letterK[8] = {
        B00000000,
        B01001000,
        B01010000,
        B01100000,
        B01010000,
        B01001000,
        B00000000,
        B00000000
      };
      
      // Display K in the middle (starting at x=1)
      for (int i = 0; i < 3; i++) {
        clearDisplay();
        displayLetter(letterK, 1, CRGB(255, 0, 255));  // Magenta
        FastLED.show();
        delay(200);
        clearDisplay();
        FastLED.show();
        delay(200);
      }
    }
  }
  
  // Clear screen and add delay before starting game
  clearDisplay();
  FastLED.show();
  delay(500);
}

void displayLetter(const byte* letter, int xOffset, CRGB color) {
  for (int y = 0; y < 8; y++) {
    for (int x = 0; x < 8; x++) {
      if (xOffset + x >= 0 && xOffset + x < 8) {  // Only draw if within display bounds
        if (letter[y] & (1 << (7-x))) {
          leds[getPixelIndex(xOffset + x, y)] = color;
        }
      }
    }
  }
}



// Game state
bool gameBoard[MATRIX_WIDTH][MATRIX_HEIGHT] = {0};  // True if a cell is occupied
CRGB boardColors[MATRIX_WIDTH][MATRIX_HEIGHT];      // Color of each cell

// Current tetromino state
byte currentPiece = 0;      // Index of current tetromino
byte currentRotation = 0;   // Current rotation (0-3)
int currentX = 3;           // X position of top-left corner
int currentY = 0;           // Y position of top-left corner
unsigned long lastFallTime = 0;
unsigned long gameSpeed = INITIAL_GAME_SPEED;
boolean gameOver = false;
unsigned int score = 0;

// Button state variables
bool leftPressed = false;
bool rightPressed = false;
bool rotatePressed = false;
unsigned long lastButtonCheckTime = 0;
#define DEBOUNCE_TIME 200  // Debounce time in milliseconds

void setup() {

 randomSeed(analogRead(A0) * analogRead(A1));  // Using multiple readings for better randomness

 pinMode(BUZZER_PIN, OUTPUT);
 
  // Initialize LED strip
  FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip);
  FastLED.setBrightness(BRIGHTNESS);
  clearDisplay();
  
  // Initialize button pins
  pinMode(LEFT_BUTTON_PIN, INPUT_PULLUP);
  pinMode(RIGHT_BUTTON_PIN, INPUT_PULLUP);
  pinMode(ROTATE_BUTTON_PIN, INPUT_PULLUP);
  
  Serial.begin(9600);
  Serial.println("Tetris initialized!");
  
  displaySplashScreen();
  selectGameMode();  // Add this line after splash screen
  spawnNewPiece();
}

void loop() {
   if (gameOver) {
    if (!gameOverScreenShown) {
      displayGameOver();
      gameOverScreenShown = true;
    } else if (checkAnyButtonPressed()) {
      // Wait for button release to prevent immediate restart
      delay(200);
      resetGame();
      gameOverScreenShown = false;
    }
    return;
  }
  
  checkButtons();
  
  // Move the piece down at regular intervals
  if (millis() - lastFallTime > gameSpeed) {
    if (!movePieceDown()) {
      // Piece has landed
      placePiece();
      clearLines();
      if (!spawnNewPiece()) {
        gameOver = true;
      }
      
      // Increase game speed 
      if (gameSpeed > MIN_GAME_SPEED) {
        gameSpeed -= SPEED_INCREASE;
      }
    }
    lastFallTime = millis();
  }
  
  updateDisplay();
}

void checkButtons() {
  // Check buttons with debounce
  if (millis() - lastButtonCheckTime > DEBOUNCE_TIME) {
    // Check left button
    if (digitalRead(LEFT_BUTTON_PIN) == LOW && !leftPressed) {
      leftPressed = true;
      movePieceLeft();
      lastButtonCheckTime = millis();
    } else if (digitalRead(LEFT_BUTTON_PIN) == HIGH) {
      leftPressed = false;
    }
    
    // Check right button
    if (digitalRead(RIGHT_BUTTON_PIN) == LOW && !rightPressed) {
      rightPressed = true;
      movePieceRight();
      lastButtonCheckTime = millis();
    } else if (digitalRead(RIGHT_BUTTON_PIN) == HIGH) {
      rightPressed = false;
    }
    
    // Check rotate button
    if (digitalRead(ROTATE_BUTTON_PIN) == LOW && !rotatePressed) {
      rotatePressed = true;
      rotatePiece();
      lastButtonCheckTime = millis();
    } else if (digitalRead(ROTATE_BUTTON_PIN) == HIGH) {
      rotatePressed = false;
    }
  }
}

bool checkAnyButtonPressed() {
  return (digitalRead(LEFT_BUTTON_PIN) == LOW || 
          digitalRead(RIGHT_BUTTON_PIN) == LOW || 
          digitalRead(ROTATE_BUTTON_PIN) == LOW);
}

// Helper functions for the LED matrix Type

int getPixelIndex(int x, int y) {
  
// Simple row-major pattern (no zigzag):
   return y * MATRIX_WIDTH + x;
  
//  Simple column-major pattern (no zigzag):
//  return x * MATRIX_HEIGHT + y;

//  Column-major zigzag pattern:
//    if (x % 2 == 0) {
    // Even columns go top to bottom
//   return x * MATRIX_HEIGHT + y;
//  } else {
    // Odd columns go bottom to top
//   return x * MATRIX_HEIGHT + (MATRIX_HEIGHT - 1 - y);
//  }

//  Flipped row-major zigzag pattern:
//  if (y % 2 == 0) {
    // Even rows go right to left
//    return y * MATRIX_WIDTH + (MATRIX_WIDTH - 1 - x);
//  } else {
    // Odd rows go left to right
//    return y * MATRIX_WIDTH + x;
//  }
}

void clearDisplay() {
  fill_solid(leds, NUM_LEDS, BLACK);
  FastLED.show();
}

void updateDisplay() {
  fill_solid(leds, NUM_LEDS, BLACK);
  
  // Draw the fixed blocks
  for (int x = 0; x < MATRIX_WIDTH; x++) {
    for (int y = 0; y < MATRIX_HEIGHT; y++) {
      if (gameBoard[x][y]) {
        leds[getPixelIndex(x, y)] = boardColors[x][y];
      }
    }
  }
  
  // Draw the current piece
  for (int i = 0; i < 4; i++) {
    int x = currentX + currentTetrominoSet[currentPiece].shapes[currentRotation][i][0];
    int y = currentY + currentTetrominoSet[currentPiece].shapes[currentRotation][i][1];
    
    if (x >= 0 && x < MATRIX_WIDTH && y >= 0 && y < MATRIX_HEIGHT) {
      leds[getPixelIndex(x, y)] = currentTetrominoSet[currentPiece].color;
    }
  }
  
  FastLED.show();
}

// Game mechanics
bool isValidPosition(int pieceIndex, int rotation, int posX, int posY) {
  for (int i = 0; i < 4; i++) {
    int x = posX + currentTetrominoSet[pieceIndex].shapes[rotation][i][0];
    int y = posY + currentTetrominoSet[pieceIndex].shapes[rotation][i][1];
    
    if (x < 0 || x >= MATRIX_WIDTH || y < 0 || y >= MATRIX_HEIGHT) {
      return false;
    }
    
    if (y >= 0 && gameBoard[x][y]) {
      return false;
    }
  }
  return true;
}

bool movePieceLeft() {
  if (isValidPosition(currentPiece, currentRotation, currentX - 1, currentY)) {
    currentX--;
    playMoveSound(); 
    return true;
  }
  return false;
}

bool movePieceRight() {
  if (isValidPosition(currentPiece, currentRotation, currentX + 1, currentY)) {
    currentX++;
    playMoveSound();  
    return true;
  }
  return false;
}

bool movePieceDown() {
  if (isValidPosition(currentPiece, currentRotation, currentX, currentY + 1)) {
    currentY++;
    return true;
  }
  return false;
}

bool rotatePiece() {
  byte nextRotation = (currentRotation + 1) % 4;
  if (isValidPosition(currentPiece, nextRotation, currentX, currentY)) {
    currentRotation = nextRotation;
    playRotateSound();
    return true;
  }
  // Try wall kick (adjust the position if rotation is blocked by a wall)
  // Try moving left
  if (isValidPosition(currentPiece, nextRotation, currentX - 1, currentY)) {
    currentX--;
    currentRotation = nextRotation;
    playRotateSound();
    return true;
  }
  // Try moving right
  if (isValidPosition(currentPiece, nextRotation, currentX + 1, currentY)) {
    currentX++;
    currentRotation = nextRotation;
    playRotateSound();
    return true;
  }
  return false;
}

void placePiece() {
  for (int i = 0; i < 4; i++) {
    int x = currentX + currentTetrominoSet[currentPiece].shapes[currentRotation][i][0];
    int y = currentY + currentTetrominoSet[currentPiece].shapes[currentRotation][i][1];
    
    if (x >= 0 && x < MATRIX_WIDTH && y >= 0 && y < MATRIX_HEIGHT) {
      gameBoard[x][y] = true;
      boardColors[x][y] = currentTetrominoSet[currentPiece].color;
      playLandSound();
    }
  }
}



bool spawnNewPiece() {
  static byte lastPiece = random(0, 7);
  byte newPiece;
  
  do {
    newPiece = random(0, 7);
  } while (newPiece == lastPiece && random(0, 100) < 70);
  
  lastPiece = newPiece;
  currentPiece = newPiece;
  
  // Use different rotation options based on game mode
  if (gameMode == MODE_KIDS) {
    currentRotation = 0;  // Kids mode pieces don't need rotation
  } else {
    currentRotation = random(0, 4);
  }
  
  currentX = (MATRIX_WIDTH / 2) - 1;
  currentY = 0;
  
  if (!isValidPosition(currentPiece, currentRotation, currentX, currentY)) {
    return false;
  }
  return true;
}

void clearLines() {
  int linesCleared = 0;
  
  for (int y = MATRIX_HEIGHT - 1; y >= 0; y--) {
    bool lineIsFull = true;
    
    // Check if the line is full
    for (int x = 0; x < MATRIX_WIDTH; x++) {
      if (!gameBoard[x][y]) {
        lineIsFull = false;
        break;
      }
    }
    
    if (lineIsFull) {
      linesCleared++;
      
      // Flash the line
      for (int i = 0; i < 3; i++) {
        // Flash white
        for (int x = 0; x < MATRIX_WIDTH; x++) {
          leds[getPixelIndex(x, y)] = CRGB::White;
        }
        FastLED.show();
        delay(50);
        
        // Flash black
        for (int x = 0; x < MATRIX_WIDTH; x++) {
          leds[getPixelIndex(x, y)] = CRGB::Black;
        }
        FastLED.show();
        delay(50);
      }
      
      // Move all lines above this one down
      for (int moveY = y; moveY > 0; moveY--) {
        for (int x = 0; x < MATRIX_WIDTH; x++) {
          gameBoard[x][moveY] = gameBoard[x][moveY - 1];
          boardColors[x][moveY] = boardColors[x][moveY - 1];
        }
      }
      
      // Clear the top line
      for (int x = 0; x < MATRIX_WIDTH; x++) {
        gameBoard[x][0] = false;
      }
      
      // Since the lines have moved down, we need to check this row again
      y++;
    }
  }
  
  // Update score
  if (linesCleared > 0) {

   playClearLineSound();
    // More points for clearing multiple lines at once
    score += (linesCleared * linesCleared) * 100;
  }
}

void resetGame() {
  playStartSound();
  // Set the appropriate tetromino set based on game mode
  currentTetrominoSet = (gameMode == MODE_KIDS) ? kidstetrominos : tetrominos;
  
  // Clear the display first
  clearDisplay();
  
  // Reset game board
  for (int x = 0; x < MATRIX_WIDTH; x++) {
    for (int y = 0; y < MATRIX_HEIGHT; y++) {
      gameBoard[x][y] = false;
    }
  }
  
  // Reset game parameters
  gameSpeed = INITIAL_GAME_SPEED;
  gameOver = false;
  score = 0;
  gameOverScreenShown = false;
  
  // Show a quick start animation
  for (int i = 0; i < NUM_LEDS; i++) {
    leds[i] = CRGB::Green;
    FastLED.show();
    delay(20);
  }
  clearDisplay();
  delay(500);
  
  // Spawn a new piece
  spawnNewPiece();
}

void displaySplashScreen() {
  playStartSound();
  const char text[] = "MINI TETRIS";
  const int textLength = strlen(text);
  const int totalWidth = textLength * 8;  // Each letter is 8 pixels wide
  const CRGB colors[] = {CRGB::Red, CRGB::Green, CRGB::Blue, CRGB::Yellow, 
                        CRGB::Cyan, CRGB::Magenta, CRGB::Orange};
  const int numColors = sizeof(colors) / sizeof(colors[0]);
  
  // Scroll the entire text from right to left
  for (int scroll = 8; scroll >= -totalWidth; scroll--) {
    clearDisplay();
    
    int letterPos = 0;
    for (int i = 0; i < textLength; i++) {
      char c = text[i];
      int xPos = scroll + (i * 8);
      
      // Skip spaces
      if (c == ' ') {
        continue;
      }
      
      // Map characters to array indices
      int letterIndex;
      switch (c) {
        case 'M': letterIndex = 0; break;
        case 'I': letterIndex = 1; break;
        case 'N': letterIndex = 2; break;
        case 'T': letterIndex = 3; break;
        case 'E': letterIndex = 4; break;
        case 'R': letterIndex = 5; break;
        case 'S': letterIndex = 6; break;
        default: continue;
      }
      
      // Display letter with color cycling
      displayLetter(letters[letterIndex], xPos, colors[letterPos % numColors]);
      letterPos++;
    }
    
    FastLED.show();
    delay(60);  // Adjust speed of scrolling
  }
  
  // Final flash effect
  for (int i = 0; i < 3; i++) {
    fill_solid(leds, NUM_LEDS, CRGB::White);
    FastLED.show();
    delay(100);
    clearDisplay();
    delay(100);
  }
}



void displayGameOver() {
  playGameOverSound();
  
  // Flash "Game Over" effect
  for (int i = 0; i < 3; i++) {
    fill_solid(leds, NUM_LEDS, CRGB::Red);
    FastLED.show();
    delay(500);
    clearDisplay();
    FastLED.show();
    delay(500);
  }
  
  // Display final score
  delay(500);
  displayScrollingScore(score);
  
  // Show stable smiley and wait for restart
  displayEndAnimation();
  
  gameOverScreenShown = true;
}
License
All Rights
Reserved
licensBg
0