Rainbow LED Tetris: a Mini Tetris Arcade Powered by Arduino

by makerkid in Circuits > Arduino

2181 Views, 32 Favorites, 0 Comments

Rainbow LED Tetris: a Mini Tetris Arcade Powered by Arduino

IMG_6014.jpeg
IMG_6006.jpeg

Have you ever wanted your very own retro arcade machine? well if you do in this project you’ll build a small Tetris arcade powered by an Arduino. With just a few basic parts, an Arduino nano, an LED matrix, a joystick module, screws and some wires.

Supplies

IMG_5905.png

Tools:


soldering iron (1)

hot-glue gun (1)

3D printer (1)

screwdriver (1)


Materials:


Arduino nano with USB-C port (without headers soldered on): AliExpress (1)

5 pin Dual-axis XY Joystick: AliExpress (1)

8*8 WS2812B LED Module: AliExpress (2)

M3 * 6mm screws: AliExpress (10)

20cm male to male wires: AliExpress (11)

USB-C cable: AliExpress (1)

3D Printing the Case

Screenshot 2025-07-30 165922.png

I’ve designed a custom case for the Tetris arcade using Tinkercad. You can download the files to either open and modify them, or just 3D print them.

Here are the download links:


Soldering WS2812B Panels

IMG_5937.png
IMG_5920.png

wiring and soldering the WS2812B panels:

to start, choose a top and a bottom panel, after you have done that solder 6 wires onto your top panels V+, IN, V- and V+, OUT, and V-.


soldering top panel to bottom panel:


top panels V+ to bottom panels V+

top panels OUT to bottom panels IN

top panels V- to bottom panels V-

Putting WS2812B Panels Into Case

IMG_5938.png
IMG_2903.png
IMG_2907.png
IMG_2933.png

Start by taking your 3D printed top case, now Insert the bottom matrix panel and slide into the bottom of the of the case. Now feed the main panel’s wires through the designated hole. Carefully slide both panels together until they fit snugly. Finally, place your 3D printed top lid onto the top of the case and secure it with two M3 screws. (do not Overtighten the screws, it can break the plastic)

Soldering Joystick

IMG_5961.jpg

now onto soldering the joystick. this is an easy step you just solder 5 wires onto the joysticks pins.

Putting Joystick Into Case

IMG_5972.jpg

To start get your 3D printed Bottom case and grab your joystick. now place your joystick into the case and set it on the joystick pad. now align your joysticks holes with the case holes and screw in 4 M3 screw to secure the joystick.

Soldering Joystick and WS2812B Panels to Arduino Nano

IMG_5975.jpg

time to solder the joystick and matrix panels onto the Arduino.


Soldering the joystick:


joystick GND to Arduino GND

joystick 5v to Arduino 5v

joystick VRx to Arduino A0

joystick VRy to Arduino A1

joystick SW to Arduino D2


soldering WS2812B panels to Arduino:


top panels V+ to Arduino 5v

top panels IN to Arduino D6

top panels V- to Arduino GND

(the wires for the panels are different colors because i had to replace them)

Putting It All Together

IMG_5978.jpg
IMG_6005.jpg
IMG_5988.jpg

now time to put everything together. First take your hot glue gun and glue the Arduino nano to the right side you should see your nano's USB-C port from the hole. Then take your 3D printed top and bottom case and stack them on top of each other and screw in 4 M3 screws into the screw holes.

Uploading the Code

Screenshot 2025-07-27 120336.png
Screenshot 2025-07-27 120116.png

Installing Arduino IDE: to install Arduino IDE go to there website or use this link. Download the newest version of 2.0 and install.

Libraries: To install libraries in Arduino IDE go to the sidebar and select Library manager. now search for FastLED and install the one by Daniel Garcia

uploading code: to upload the code, connect your Arduino nano with a USB-C cable and select Arduino nano in ports, then click upload.

Here is the code:

#include <FastLED.h>
#include <string.h>

#define DATA_PIN 6
#define LED_TYPE WS2812B
#define COLOR_ORDER GRB
#define WIDTH 8
#define HEIGHT 16
#define PANEL_H (HEIGHT/2)
#define NUM_LEDS (WIDTH*HEIGHT)
#define JOY_X A0
#define JOY_Y A1
#define JOY_BTN 2

// Drop timing
const unsigned long NORMAL_INTERVAL = 4000; // ms per drop step (base)
const unsigned long FAST_INTERVAL = 200; // ms soft drop
const unsigned long SPEEDUP_PER_LINE = 100; // ms reduction per line cleared
const unsigned long MIN_INTERVAL = 100; // floor interval
const unsigned long HARD_DROP_INTERVAL = 50; // ms per step during hard drop (faster)

// DAS/ARR and lock
#define DAS_DELAY 200 // ms initial sideways delay
#define ARR_INTERVAL 100 // ms auto-repeat rate
#define LOCK_DELAY 500 // ms lock delay
#define BUTTON_DELAY 200 // ms hard-drop debounce

// Deadzones & smoothing
#define X_DZ_LOW 450
#define X_DZ_HIGH 574
#define Y_DZ_LOW 450
#define Y_DZ_HIGH 574
const float ALPHA = 0.7; // joystick smoothing
#define ROTATE_DELAY 200 // ms rotation debounce

// Game state
CRGB leds[NUM_LEDS];
uint8_t board[HEIGHT][WIDTH];
unsigned int linesCleared = 0;

double joyXf, joyYf;
int lastDir = 0, lastYdir = 0;
unsigned long lastFall, lastMove, lastRepeat, lastRotate, lastHardDrop, lockStart;
bool lockActive = false;

// 7-bag randomizer
int bag[7], bagIndex = 7;

// Active piece
enum Piece { I, J, L, O, S, Z, T };
int curType, curRot, curX, curY;

// Tetromino shapes
const uint16_t shapes[7][4] = {
{0x0F00,0x2222,0x0F00,0x2222},
{0x8E00,0x6440,0x0E20,0x44C0},
{0x2E00,0x4460,0x0E80,0xC440},
{0x6600,0x6600,0x6600,0x6600},
{0x6C00,0x4620,0x6C00,0x4620},
{0xC600,0x2640,0xC600,0x2640},
{0x4E00,0x4640,0x0E40,0x4C40}
};

// 5×5 glyphs & digits
const uint8_t bmp3[5] = {0b01110,0b00001,0b00110,0b00001,0b01110};
const uint8_t bmp2[5] = {0b01110,0b10001,0b00010,0b00100,0b11111};
const uint8_t bmp1[5] = {0b00100,0b01100,0b00100,0b00100,0b01110};
const uint8_t bmpG[5] = {0b01110,0b10000,0b10110,0b10001,0b01110};
const uint8_t bmpO[5] = {0b01110,0b10001,0b10001,0b10001,0b01110};
const uint8_t bmpA[5] = {0b01110,0b10001,0b11111,0b10001,0b10001};
const uint8_t bmpM[5] = {0b10001,0b11011,0b10101,0b10001,0b10001};
const uint8_t bmpE[5] = {0b11111,0b10000,0b11110,0b10000,0b11111};
const uint8_t bmpV[5] = {0b10001,0b10001,0b10001,0b01010,0b00100};
const uint8_t bmpR[5] = {0b11110,0b10001,0b11110,0b10100,0b10010};
const uint8_t digits[10][5] = {
{0b01110,0b10001,0b10001,0b10001,0b01110},
{0b00100,0b01100,0b00100,0b00100,0b01110},
{0b01110,0b10001,0b00010,0b00100,0b11111},
{0b01110,0b00001,0b00110,0b00001,0b01110},
{0b10010,0b10010,0b11111,0b00010,0b00010},
{0b11111,0b10000,0b11110,0b00001,0b11110},
{0b01110,0b10000,0b11110,0b10001,0b01110},
{0b11111,0b00010,0b00100,0b01000,0b01000},
{0b01110,0b10001,0b01110,0b10001,0b01110},
{0b01110,0b10001,0b01111,0b00001,0b01110}
};

// Function prototypes
void clearBoard();
void refillBag();
void spawnPiece();
bool valid(int x,int y,int r);
bool movePiece(int dx,int dy);
void rotatePiece();
void lockPiece();
void hardDrop();
void readInput();
void drawFrame();
void showScore();
void gameOver();
void resetGame();
void draw5x5(const uint8_t bmp[5], int bx, int by, CHSV col);
void showBitmapAnim(const uint8_t bmp[5], int dir);
void animateStartup();

// ------------------ CORE ------------------
void setup() {
FastLED.addLeds<LED_TYPE,DATA_PIN,COLOR_ORDER>(leds,NUM_LEDS);
FastLED.setBrightness(10);
pinMode(JOY_BTN, INPUT_PULLUP);
joyXf = analogRead(JOY_X);
joyYf = analogRead(JOY_Y);
unsigned long seed = 0; for (int i=0;i<32;i++) { seed=(seed<<1)^analogRead(A5); delay(1);} randomSeed(seed ^ micros());
refillBag();
animateStartup();
linesCleared = 0;
clearBoard();
spawnPiece();
lastFall = lastMove = lastRepeat = lastRotate = lastHardDrop = millis();
lockActive = false;
}

void loop() {
unsigned long now = millis();
// Classic simple speed-up by lines cleared
unsigned long speedup = linesCleared * SPEEDUP_PER_LINE;
unsigned long interval = (speedup >= NORMAL_INTERVAL - MIN_INTERVAL) ? MIN_INTERVAL : (NORMAL_INTERVAL - speedup);
// Soft drop when holding down
if (analogRead(JOY_Y) > Y_DZ_HIGH) interval = FAST_INTERVAL;

if (now - lastFall > interval) {
if (!movePiece(0,1)) {
if (!lockActive) { lockActive = true; lockStart = now; }
} else {
lockActive = false;
}
lastFall = now;
}
if (lockActive && now - lockStart >= LOCK_DELAY) { lockPiece(); lockActive = false; }

readInput();
drawFrame();
}

void clearBoard() { memset(board, 0, sizeof(board)); }

void refillBag() {
for (int i=0;i<7;i++) bag[i]=i;
for (int i=6;i>0;i--) { int j = random(i+1); int t = bag[i]; bag[i]=bag[j]; bag[j]=t; }
bagIndex = 0;
}

void spawnPiece() {
if (bagIndex >= 7) refillBag();
curType = bag[bagIndex++];
curRot = 0;
curX = (WIDTH - 4) / 2;
curY = 0;
if (!valid(curX,curY,curRot)) gameOver();
}

bool valid(int x,int y,int r) {
uint16_t m = shapes[curType][r];
for (int i=0;i<4;i++) for (int j=0;j<4;j++)
if (m & (0x8000 >> (i*4 + j))) {
int bx = x + j, by = y + i;
if (bx<0 || bx>=WIDTH || by<0 || by>=HEIGHT || board[by][bx]) return false;
}
return true;
}

bool movePiece(int dx,int dy) {
if (valid(curX+dx, curY+dy, curRot)) { curX += dx; curY += dy; return true; }
return false;
}

void rotatePiece() {
int nr = (curRot + 1) & 3;
if (valid(curX,curY,nr)) { curRot = nr; return; }
// simple wall kicks: try left then right
if (valid(curX-1,curY,nr)) { curX--; curRot=nr; }
else if (valid(curX+1,curY,nr)) { curX++; curRot=nr; }
}

void lockPiece() {
// Place blocks
uint16_t m = shapes[curType][curRot];
for (int i=0;i<4;i++) for (int j=0;j<4;j++)
if (m & (0x8000>>(i*4+j))) board[curY+i][curX+j] = curType + 1;

// Detect full rows
bool cleared[HEIGHT];
int clearCount = 0;
for (int y=0; y<HEIGHT; y++) {
bool full = true;
for (int x=0; x<WIDTH; x++) if (!board[y][x]) { full = false; break; }
cleared[y] = full;
if (full) clearCount++;
}

if (clearCount) {
// Fade only the cleared rows while keeping others visible
for (int b = 200; b >= 0; b -= 40) {
FastLED.clear();
// Draw non-cleared rows at full brightness
for (int yy=0; yy<HEIGHT; yy++) if (!cleared[yy]) {
for (int xx=0; xx<WIDTH; xx++) if (board[yy][xx])
leds[yy*WIDTH + xx] = CHSV((board[yy][xx]-1)*32, 255, 200);
}
// Draw cleared rows at fading brightness
for (int yy=0; yy<HEIGHT; yy++) if (cleared[yy]) {
for (int xx=0; xx<WIDTH; xx++) if (board[yy][xx])
leds[yy*WIDTH + xx] = CHSV((board[yy][xx]-1)*32, 255, b);
}
FastLED.show();
delay(50);
}

// Compact the board: move non-cleared rows down, clear top
int write = HEIGHT - 1;
for (int read = HEIGHT - 1; read >= 0; --read) {
if (!cleared[read]) {
if (write != read) memcpy(board[write], board[read], WIDTH);
write--;
}
}
while (write >= 0) { memset(board[write], 0, WIDTH); write--; }

linesCleared += clearCount;
}

spawnPiece();
}

void hardDrop() {
while (movePiece(0,1)) {
drawFrame();
delay(HARD_DROP_INTERVAL);
}
lockPiece();
}

void readInput() {
unsigned long now = millis();
joyXf = ALPHA*joyXf + (1-ALPHA)*analogRead(JOY_X);
joyYf = ALPHA*joyYf + (1-ALPHA)*analogRead(JOY_Y);
int x = (int)joyXf, y = (int)joyYf;
bool btn = !digitalRead(JOY_BTN);

int dir = 0;
if (y > Y_DZ_LOW && y < Y_DZ_HIGH) {
if (x < X_DZ_LOW) dir = -1; else if (x > X_DZ_HIGH) dir = 1;
}
if (dir) {
if (dir != lastDir) {
if (now - lastMove > DAS_DELAY) { movePiece(dir,0); lastMove = now; lastRepeat = now; }
} else if (now - lastRepeat > ARR_INTERVAL) { movePiece(dir,0); lastRepeat = now; }
}
lastDir = dir;

int ydir = (y < Y_DZ_LOW ? -1 : (y > Y_DZ_HIGH ? 1 : 0));
if (ydir == -1 && lastYdir != -1 && now - lastRotate > ROTATE_DELAY) { rotatePiece(); lastRotate = now; }
lastYdir = ydir;

if (btn && now - lastHardDrop > BUTTON_DELAY) { hardDrop(); lastHardDrop = now; }
}

void drawFrame() {
FastLED.clear();
// Board
for (int yy=0; yy<HEIGHT; yy++) for (int xx=0; xx<WIDTH; xx++)
if (board[yy][xx]) leds[yy*WIDTH + xx] = CHSV((board[yy][xx]-1)*32, 255, 200);
// Active piece
uint16_t m = shapes[curType][curRot];
for (int i=0;i<4;i++) for (int j=0;j<4;j++) if (m & (0x8000>>(i*4+j))) {
int bx = curX + j, by = curY + i;
if (bx>=0 && bx<WIDTH && by>=0 && by<HEIGHT)
leds[by*WIDTH + bx] = CHSV(curType*32, 255, 200);
}
FastLED.show();
}

// ------------------ UI ------------------
void showScore() {
// Kerning with a 1‑pixel gap between digits (so "10" is close with a tiny space)
char buf[6]; sprintf(buf, "%u", linesCleared);
int len = strlen(buf);

int lefts[6];
int widths[6];
const int GAP = 1; // 1-pixel spacing between digits
int totalW = 0;

for (int i = 0; i < len; i++) {
const uint8_t* g = digits[buf[i] - '0'];
int left = 5, right = -1;
for (int c = 0; c < 5; c++) {
for (int r = 0; r < 5; r++) {
if (g[r] & (0x10 >> c)) { if (c < left) left = c; if (c > right) right = c; }
}
}
if (right < left) { left = 0; right = 4; }
lefts[i] = left;
widths[i] = (right - left + 1);
totalW += widths[i];
}
if (len > 1) totalW += GAP * (len - 1);

int startX = (WIDTH - totalW) / 2; if (startX < 0) startX = 0;

for (int dropY = -5; dropY <= HEIGHT - 5; dropY++) {
FastLED.clear();
int x = startX;
for (int i = 0; i < len; i++) {
draw5x5(digits[buf[i] - '0'], x - lefts[i], dropY, CHSV(0,255,200));
x += widths[i] + GAP;
}
FastLED.show();
delay(100);
}
delay(500);
}

void gameOver() {
// Show score before "Game Over"
showScore();
const uint8_t* msg[9] = {bmpG,bmpA,bmpM,bmpE,nullptr,bmpO,bmpV,bmpE,bmpR};
int totalW = 9*6;
for (int off = WIDTH; off > -totalW; off--) {
FastLED.clear();
for (int ci = 0; ci < 9; ci++) {
const uint8_t* b = msg[ci]; if (!b) continue;
for (int r = 0; r < 5; r++) for (int c = 0; c < 5; c++)
if (b[r] & (0x10 >> c)) {
int x = off + ci*6 + c;
int y = r + (PANEL_H - 5)/2;
if (x >= 0 && x < WIDTH) leds[y*WIDTH + x] = CHSV(0,255,200);
}
}
FastLED.show(); delay(80);
}
delay(200);
while (digitalRead(JOY_BTN) == LOW) delay(10);
while (digitalRead(JOY_BTN) == HIGH) delay(10);
resetGame();
}

void resetGame() {
linesCleared = 0;
clearBoard();
refillBag();
animateStartup();
spawnPiece();
lastFall = lastMove = lastRepeat = lastRotate = lastHardDrop = millis();
lockActive = false;
}

void draw5x5(const uint8_t bmp[5], int bx, int by, CHSV col) {
for (int r=0; r<5; r++) for (int d=0; d<5; d++) if (bmp[r] & (0x10 >> d)) {
int x = bx + d, y = by + r;
if (x>=0 && x<WIDTH && y>=0 && y<PANEL_H) leds[y*WIDTH + x] = col;
}
}

void showBitmapAnim(const uint8_t bmp[5], int dir) {
CHSV col = CHSV(random(0,255),255,200);
int tx = (WIDTH - 5) / 2;
int ty = (PANEL_H - 5) / 2;
int steps = max(WIDTH, PANEL_H) + 5;
for (int t = 0; t <= steps; t++) {
FastLED.clear();
int x = (dir == 0 ? -5 + t : dir == 2 ? WIDTH - t : tx);
int y = (dir == 1 ? -5 + t : dir == 3 ? PANEL_H - t : ty);
draw5x5(bmp, x, y, col);
FastLED.show();
delay(30);
}
for (int v = 20; v >= 0; v -= 2) {
FastLED.clear();
CHSV f = col; f.val = v;
draw5x5(bmp, tx, ty, f);
FastLED.show();
delay(30);
}
}

void animateStartup() {
showBitmapAnim(bmp3, 0);
showBitmapAnim(bmp2, 1);
showBitmapAnim(bmp1, 2);
CHSV c = CHSV(random(0,255),255,200);
int ty = (PANEL_H - 5) / 2;
for (int t = 0; t <= WIDTH + 5; t++) {
FastLED.clear();
draw5x5(bmpG, -5 + t, ty, c);
draw5x5(bmpO, WIDTH - t, ty, c);
FastLED.show();
delay(30);
}
for (int i = 0; i < 20; i++) {
FastLED.clear();
for (int p = 0; p < NUM_LEDS; p++) if (random(3) == 0)
leds[random(NUM_LEDS)] = CHSV(random(0,255),255,200);
FastLED.show();
delay(20);
}
FastLED.clear();
FastLED.show();
}


And if you prefer to download the code, here is the download:


Play!

IMG_6014.jpeg
IMG_6006.jpeg

you have completed the Tetris arcade! here is how to play the game, move the joystick down to accelerate the piece. Move left or right to shift its position, and press the joystick button for a hard drop. When the game is over, press the joystick button to restart. HAVE FUN!!!