/*------------------------------------------------------------------------------
// Tetris for 16x32 LED Matrix with Arduino Uno and WS2812 LEDs.
// Modified for button controls and Wokwi compatibility.
//------------------------------------------------------------------------------
*/
#include <FastLED.h>
// Matrix Configuration
#define LED_PIN 6
#define MATRIX_WIDTH 16
#define MATRIX_HEIGHT 32
#define GAME_WIDTH 10
#define GAME_HEIGHT 32
#define NUM_LEDS (MATRIX_WIDTH * MATRIX_HEIGHT)
// Button Pins
#define BUTTON_LEFT 2
#define BUTTON_RIGHT 3
#define BUTTON_DOWN 4
#define BUTTON_ROTATE 5
CRGB leds[NUM_LEDS];
// Game variables
uint16_t grid[GAME_HEIGHT]; // Game grid using 16-bit rows (fits GAME_WIDTH of 10)
uint16_t currentPiece[4]; // Current piece (4 rows, 16 bits max per row)
int pieceX = 3, pieceY = 0; // Piece position
// Button states
bool leftPressed = false, rightPressed = false, downPressed = false, rotatePressed = false;
// Tetrimino shapes stored in PROGMEM to save memory
const uint16_t TETRIMINOS[7][4] PROGMEM = {
{0b1111, 0b0000, 0b0000, 0b0000}, // I
{0b0100, 0b1110, 0b0000, 0b0000}, // T
{0b1100, 0b1100, 0b0000, 0b0000}, // O
{0b1000, 0b1110, 0b0000, 0b0000}, // L
{0b0010, 0b1110, 0b0000, 0b0000}, // J
{0b0110, 0b1100, 0b0000, 0b0000}, // S
{0b1100, 0b0110, 0b0000, 0b0000} // Z
};
void setup() {
FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.clear();
randomSeed(analogRead(A0)); // Initialize random seed
loadNewPiece();
// Configure button pins
pinMode(BUTTON_LEFT, INPUT_PULLUP);
pinMode(BUTTON_RIGHT, INPUT_PULLUP);
pinMode(BUTTON_DOWN, INPUT_PULLUP);
pinMode(BUTTON_ROTATE, INPUT_PULLUP);
}
void loop() {
static unsigned long lastUpdate = 0;
static unsigned long lastInputCheck = 0;
// Check for button inputs every 50ms to debounce
if (millis() - lastInputCheck > 50) {
handleButtons();
lastInputCheck = millis();
}
// Game logic update every 500ms (or less if the down button is pressed)
unsigned long delay = downPressed ? 100 : 500;
if (millis() - lastUpdate > delay) {
if (!movePiece(0, 1)) {
placePiece();
clearRows();
loadNewPiece();
if (!canMove(0, 0)) {
gameOver();
return;
}
}
draw();
lastUpdate = millis();
}
}
// Clear the game grid
void clearGrid() {
memset(grid, 0, sizeof(grid));
}
// Load a new piece
void loadNewPiece() {
memcpy_P(currentPiece, TETRIMINOS[random(7)], sizeof(currentPiece));
pieceX = 3;
pieceY = 0;
}
// Check if a piece can move to a new position
bool canMove(int dx, int dy) {
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
if (currentPiece[y] & (1 << x)) {
int nx = pieceX + x + dx;
int ny = pieceY + y + dy;
if (nx < 0 || nx >= GAME_WIDTH || ny >= GAME_HEIGHT || (ny >= 0 && (grid[ny] & (1 << nx)))) {
return false;
}
}
}
}
return true;
}
// Move the piece
bool movePiece(int dx, int dy) {
if (canMove(dx, dy)) {
pieceX += dx;
pieceY += dy;
return true;
}
return false;
}
// Place the piece on the grid
void placePiece() {
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
if (currentPiece[y] & (1 << x)) {
int nx = pieceX + x;
int ny = pieceY + y;
if (nx >= 0 && nx < GAME_WIDTH && ny >= 0 && ny < GAME_HEIGHT) {
grid[ny] |= (1 << nx);
}
}
}
}
}
// Clear full rows
void clearRows() {
for (int y = 0; y < GAME_HEIGHT; y++) {
if (grid[y] == (1 << GAME_WIDTH) - 1) {
for (int ny = y; ny > 0; ny--) {
grid[ny] = grid[ny - 1];
}
grid[0] = 0;
}
}
}
// Draw the grid and piece
void draw() {
FastLED.clear();
// Draw the grid
for (int y = 0; y < GAME_HEIGHT; y++) {
for (int x = 0; x < GAME_WIDTH; x++) {
if (grid[y] & (1 << x)) {
leds[y * MATRIX_WIDTH + x] = CRGB::Blue;
}
}
}
// Draw the current piece
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
if (currentPiece[y] & (1 << x)) {
int nx = pieceX + x;
int ny = pieceY + y;
if (nx >= 0 && nx < GAME_WIDTH && ny >= 0 && ny < GAME_HEIGHT) {
leds[ny * MATRIX_WIDTH + nx] = CRGB::Green;
}
}
}
}
FastLED.show();
}
void rotatePiece() {
uint16_t rotated[4] = {0};
// Step 1: Perform Rotation - Transpose and Reverse Rows
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
if (currentPiece[y] & (1 << x)) {
rotated[x] |= (1 << (3 - y)); // Transpose and reverse rows
}
}
}
// Step 2: Check Wall Kicks to Adjust Position
int kickOffsets[4][2] = {
{0, 0}, {1, 0}, {-1, 0}, {0, -1}
};
bool rotationValid = false;
for (int i = 0; i < 4; i++) {
int dx = kickOffsets[i][0];
int dy = kickOffsets[i][1];
if (canRotate(rotated, dx, dy)) {
memcpy(currentPiece, rotated, sizeof(rotated));
pieceX += dx;
pieceY += dy;
rotationValid = true;
break;
}
}
// If all wall kicks fail, do not rotate
if (!rotationValid) {
return;
}
}
bool canRotate(uint16_t testPiece[4], int dx, int dy) {
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
if (testPiece[y] & (1 << x)) {
int nx = pieceX + x + dx;
int ny = pieceY + y + dy;
if (nx < 0 || nx >= GAME_WIDTH || ny < 0 || ny >= GAME_HEIGHT || (ny >= 0 && (grid[ny] & (1 << nx)))) {
return false;
}
}
}
}
return true;
}
// Handle button inputs
void handleButtons() {
// Left button
if (digitalRead(BUTTON_LEFT) == LOW) {
if (!leftPressed) {
movePiece(-1, 0);
draw();
}
leftPressed = true;
} else {
leftPressed = false;
}
// Right button
if (digitalRead(BUTTON_RIGHT) == LOW) {
if (!rightPressed) {
movePiece(1, 0);
draw();
}
rightPressed = true;
} else {
rightPressed = false;
}
// Down button
downPressed = (digitalRead(BUTTON_DOWN) == LOW);
// Rotate button
if (digitalRead(BUTTON_ROTATE) == LOW) {
if (!rotatePressed) {
rotatePiece();
draw();
}
rotatePressed = true;
} else {
rotatePressed = false;
}
}
// Handle game over
void gameOver() {
FastLED.clear();
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = CRGB::Red;
}
FastLED.show();
while (1);
}