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.

/*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;
}
