#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();
}