#include <LedControl.h> // Library for driving MAX7219 LED matrices
// MAX7219 wiring pins
#define DIN_PIN 6 // Data input
#define CLK_PIN 5 // Clock pin
#define CS_PIN 3 // Chip select
#define NUM_MODULES 4 // Number of 8×8 modules in cascade
LedControl lc(DIN_PIN, CLK_PIN, CS_PIN, NUM_MODULES);
// Joystick and button pins
#define VRx A0 // Joystick X-axis (left/right)
#define VRy A1 // Joystick Y-axis (up/down)
#define SW 2 // Push-button switch for rotation
// Display dimensions
const int SCREEN_W = 8; // Width of one module
const int SCREEN_H = SCREEN_W * NUM_MODULES; // Total height (32 rows)
// Playfield buffer: each byte is one row of 8 bits
uint8_t field[SCREEN_H];
// Timing control
unsigned long lastDrop = 0; // Time of last automatic drop
unsigned long dropInterval = 500; // Drop interval in ms (adjusted by joystick)
unsigned long lastMove = 0; // Time of last horizontal move
const unsigned long moveInterval = 200; // Min ms between moves
const unsigned long refreshInterval = 33; // ~30 FPS
unsigned long lastRefresh = 0; // Time of last screen refresh
// Buffer to track previous frame for diff updates
uint8_t prevBuf[NUM_MODULES][SCREEN_W];
// Structure for current falling block
struct Block {
const int (*shape)[2]; // Pointer to array of {x,y} offsets
int len; // Number of cells (always 4)
int x, y; // Top-left origin position
int rotation; // Rotation index
char type; // Block type identifier
} current;
// Definitions of the seven Tetris shapes and their rotations
const int I_SHAPE[2][4][2] = {
{{0,0},{0,1},{0,2},{0,3}}, // Vertical
{{-1,1},{0,1},{1,1},{2,1}} // Horizontal
};
const int O_SHAPE[1][4][2] = {
{{0,0},{1,0},{0,1},{1,1}} // Square (no rotation)
};
const int T_SHAPE[4][4][2] = {
{{1,0},{0,1},{1,1},{2,1}}, // T pointing up
{{1,0},{1,1},{1,2},{0,1}}, // T pointing right
{{0,1},{1,1},{2,1},{1,2}}, // T pointing down
{{1,0},{1,1},{1,2},{2,1}} // T pointing left
};
const int L_SHAPE[4][4][2] = {
{{0,0},{0,1},{0,2},{1,2}}, // L standard
{{0,0},{1,0},{2,0},{0,1}}, // L rotated 90°
{{0,0},{1,0},{1,1},{1,2}}, // L rotated 180°
{{2,0},{0,1},{1,1},{2,1}} // L rotated 270°
};
const int J_SHAPE[4][4][2] = {
{{1,0},{1,1},{1,2},{0,2}}, // J standard
{{0,0},{0,1},{1,1},{2,1}}, // J rotated 90°
{{0,0},{1,0},{0,1},{0,2}}, // J rotated 180°
{{0,0},{1,0},{2,0},{2,1}} // J rotated 270°
};
const int S_SHAPE[2][4][2] = {
{{1,0},{2,0},{0,1},{1,1}}, // S horizontal
{{1,0},{1,1},{2,1},{2,2}} // S vertical
};
const int Z_SHAPE[2][4][2] = {
{{0,0},{1,0},{1,1},{2,1}}, // Z horizontal
{{2,0},{1,1},{2,1},{1,2}} // Z vertical
};
// 8×8 bitmaps for letters in the Game Over screen
static const uint8_t PAT_G[8] = {0x3C,0x42,0x40,0x4E,0x42,0x42,0x3C,0x00};
static const uint8_t PAT_A[8] = {0x18,0x24,0x42,0x7E,0x42,0x42,0x42,0x00};
static const uint8_t PAT_M[8] = {0x42,0x66,0x5A,0x5A,0x42,0x42,0x42,0x00};
static const uint8_t PAT_E[8] = {0x7E,0x40,0x5C,0x40,0x40,0x40,0x7E,0x00};
static const uint8_t PAT_O[8] = {0x3C,0x42,0x42,0x42,0x42,0x42,0x3C,0x00};
static const uint8_t PAT_V[8] = {0x42,0x42,0x42,0x42,0x42,0x24,0x18,0x00};
static const uint8_t PAT_R[8] = {0x7C,0x42,0x42,0x7C,0x48,0x44,0x42,0x00};
// Clear all LEDs on every module
void clearAll() {
for (int m = 0; m < NUM_MODULES; m++) {
lc.clearDisplay(m);
}
}
// Read and debounce the push-button switch
bool readButton() {
if (digitalRead(SW) == LOW) {
delay(20); // Debounce delay
if (digitalRead(SW) == LOW) {
while (digitalRead(SW) == LOW) {
delay(10); // Wait for release
}
return true;
}
}
return false;
}
// Return the bitmap for a given character
const uint8_t* letterPattern(char c) {
switch (c) {
case 'G': return PAT_G;
case 'A': return PAT_A;
case 'M': return PAT_M;
case 'E': return PAT_E;
case 'O': return PAT_O;
case 'V': return PAT_V;
case 'R': return PAT_R;
default: return PAT_E; // Default to 'E' pattern
}
}
// Rotate a letter pattern 90° clockwise
void rotateLetter90CW(const uint8_t* pattern, uint8_t* rotated) {
// Clear the rotated array
for (int i = 0; i < 8; i++) {
rotated[i] = 0;
}
// Rotate 90° CW: (x,y) → (7-y, x)
for (int y = 0; y < 8; y++) {
for (int x = 0; x < 8; x++) {
if (pattern[y] & (1 << x)) {
int nx = 7 - y;
int ny = x;
rotated[ny] |= (1 << nx);
}
}
}
}
// Game Over animation: flash, display "GAME", wait 1s, then display "OVER"
void gameOverSequence() {
// 1) Flash all LEDs three times
for (int i = 0; i < 3; i++) {
clearAll();
delay(500);
// Turn on all LEDs
for (int m = 0; m < NUM_MODULES; m++) {
for (int r = 0; r < SCREEN_W; r++) {
lc.setRow(m, r, 0xFF);
}
}
delay(500);
}
// 2) Display "GAME" rotated 90° CW
const char* gameText = "GAME";
for (int seg = 0; seg < 4; seg++) {
const uint8_t* pattern = letterPattern(gameText[seg]);
uint8_t rotatedPattern[8];
rotateLetter90CW(pattern, rotatedPattern);
int module = NUM_MODULES - 1 - seg;
for (int row = 0; row < 8; row++) {
lc.setRow(module, row, rotatedPattern[row]);
}
}
delay(1000); // Wait 1 second before showing OVER
// 3) Display "OVER" rotated 90° CW
const char* overText = "OVER";
for (int seg = 0; seg < 4; seg++) {
const uint8_t* pattern = letterPattern(overText[seg]);
uint8_t rotatedPattern[8];
rotateLetter90CW(pattern, rotatedPattern);
int module = NUM_MODULES - 1 - seg;
for (int row = 0; row < 8; row++) {
lc.setRow(module, row, rotatedPattern[row]);
}
}
delay(1000); // Hold OVER for 1 second
// 4) Wait for button press to restart
while (digitalRead(SW) != LOW) {
delay(10);
}
while (digitalRead(SW) == LOW) {
delay(10);
}
}
// Spawn a new random Tetris block at the top center
void spawnBlock() {
int randomShape = random(7);
int startX = SCREEN_W / 2 - 2; // Center X position
current.rotation = 0;
switch (randomShape) {
case 0: current = {I_SHAPE[0], 4, startX, 0, 0, 'I'}; break;
case 1: current = {O_SHAPE[0], 4, startX, 0, 0, 'O'}; break;
case 2: current = {T_SHAPE[0], 4, startX, 0, 0, 'T'}; break;
case 3: current = {L_SHAPE[0], 4, startX, 0, 0, 'L'}; break;
case 4: current = {J_SHAPE[0], 4, startX, 0, 0, 'J'}; break;
case 5: current = {S_SHAPE[0], 4, startX, 0, 0, 'S'}; break;
case 6: current = {Z_SHAPE[0], 4, startX, 0, 0, 'Z'}; break;
}
}
// Reset game state: clear playfield and display
void resetGame() {
// Clear the playfield
memset(field, 0, sizeof(field));
// Clear all displays
clearAll();
// Reset previous buffer
for (int m = 0; m < NUM_MODULES; m++) {
for (int r = 0; r < SCREEN_W; r++) {
prevBuf[m][r] = 0;
}
}
// Spawn first block and reset timing
spawnBlock();
lastDrop = millis();
lastRefresh = millis();
}
// Draw playfield and current block with differential updates
void writeBuffer() {
uint8_t buf[NUM_MODULES][SCREEN_W];
// Initialize buffer
for (int m = 0; m < NUM_MODULES; m++) {
for (int r = 0; r < SCREEN_W; r++) {
buf[m][r] = 0;
}
}
// Draw fixed blocks from playfield
for (int y = 0; y < SCREEN_H; y++) {
uint8_t row = field[y];
if (row == 0) continue; // Skip empty rows
int module = NUM_MODULES - 1 - (y / SCREEN_W);
int bitPosition = 1 << (7 - (y % SCREEN_W));
for (int x = 0; x < SCREEN_W; x++) {
if (row & (1 << x)) {
buf[module][x] |= bitPosition;
}
}
}
// Draw current falling block
for (int i = 0; i < current.len; i++) {
int blockX = current.x + current.shape[i][0];
int blockY = current.y + current.shape[i][1];
// Check bounds
if (blockX < 0 || blockX >= SCREEN_W || blockY < 0 || blockY >= SCREEN_H) {
continue;
}
int module = NUM_MODULES - 1 - (blockY / SCREEN_W);
int bitPosition = 1 << (7 - (blockY % SCREEN_W));
buf[module][blockX] |= bitPosition;
}
// Update only changed rows for efficiency
for (int m = 0; m < NUM_MODULES; m++) {
for (int r = 0; r < SCREEN_W; r++) {
if (buf[m][r] != prevBuf[m][r]) {
lc.setRow(m, r, buf[m][r]);
prevBuf[m][r] = buf[m][r];
}
}
}
}
// Check for collision at position (nx, ny)
bool checkCollision(int nx, int ny) {
for (int i = 0; i < current.len; i++) {
int blockX = nx + current.shape[i][0];
int blockY = ny + current.shape[i][1];
// Check boundary collisions
if (blockX < 0 || blockX >= SCREEN_W || blockY >= SCREEN_H) {
return true;
}
// Check collision with placed blocks
if (blockY >= 0 && (field[blockY] & (1 << blockX))) {
return true;
}
}
return false;
}
// Fix current block into field and clear full lines
void placeBlock() {
// Place the block in the field
for (int i = 0; i < current.len; i++) {
int blockX = current.x + current.shape[i][0];
int blockY = current.y + current.shape[i][1];
if (blockY >= 0 && blockY < SCREEN_H) {
field[blockY] |= (1 << blockX);
}
}
// Clear any full rows (from bottom to top to avoid index issues)
for (int y = SCREEN_H - 1; y >= 0; y--) {
if (field[y] == 0xFF) { // Full row
// Move all rows above down by one
for (int moveY = y; moveY > 0; moveY--) {
field[moveY] = field[moveY - 1];
}
field[0] = 0; // Clear top row
y++; // Check this row again since we moved everything down
}
}
}
// Rotate block with rollback on collision
void rotateBlock() {
// Determine rotation limits for each block type
int rotationLimit;
switch (current.type) {
case 'I':
case 'S':
case 'Z': rotationLimit = 2; break;
case 'O': rotationLimit = 1; break;
default: rotationLimit = 4; break;
}
int newRotation = (current.rotation + 1) % rotationLimit;
const int (*newShape)[2] = nullptr;
// Get the new shape based on type and rotation
switch (current.type) {
case 'I': newShape = I_SHAPE[newRotation]; break;
case 'O': newShape = O_SHAPE[0]; break;
case 'T': newShape = T_SHAPE[newRotation]; break;
case 'L': newShape = L_SHAPE[newRotation]; break;
case 'J': newShape = J_SHAPE[newRotation]; break;
case 'S': newShape = S_SHAPE[newRotation]; break;
case 'Z': newShape = Z_SHAPE[newRotation]; break;
}
// Save current state for rollback
Block backup = current;
// Apply rotation
current.shape = newShape;
current.rotation = newRotation;
// Check for collision and rollback if necessary
if (checkCollision(current.x, current.y)) {
current = backup;
}
}
void setup() {
// Initialize button pin
pinMode(SW, INPUT_PULLUP);
// Seed random number generator
randomSeed(analogRead(0));
// Initialize all LED modules
for (int m = 0; m < NUM_MODULES; m++) {
lc.shutdown(m, false); // Wake up MAX7219
lc.setIntensity(m, 8); // Set brightness (0-15)
lc.clearDisplay(m); // Clear display
// Initialize previous buffer
for (int r = 0; r < SCREEN_W; r++) {
prevBuf[m][r] = 0;
}
}
// Start the game
resetGame();
}
void loop() {
unsigned long currentTime = millis();
// Handle horizontal movement via joystick X-axis
int joystickX = analogRead(VRx);
if (currentTime - lastMove > moveInterval) {
if (joystickX < 400 && !checkCollision(current.x + 1, current.y)) {
current.x++;
lastMove = currentTime;
} else if (joystickX > 600 && !checkCollision(current.x - 1, current.y)) {
current.x--;
lastMove = currentTime;
}
}
// Handle rotation on button press
if (readButton()) {
rotateBlock();
}
// Adjust drop speed via joystick Y-axis (down = faster)
int joystickY = analogRead(VRy);
dropInterval = 700 - constrain(map(joystickY, 512, 1023, 0, 690), 0, 690);
// Handle automatic drop and collision detection
if (currentTime - lastDrop > dropInterval) {
lastDrop = currentTime;
if (!checkCollision(current.x, current.y + 1)) {
// Block can move down
current.y++;
} else {
// Block has landed - check for game over
bool gameOver = false;
for (int i = 0; i < current.len; i++) {
if (current.y + current.shape[i][1] <= 0) {
gameOver = true;
break;
}
}
if (gameOver) {
gameOverSequence();
resetGame();
return;
} else {
placeBlock();
spawnBlock();
}
}
}
// Refresh display at approximately 30 FPS
if (currentTime - lastRefresh >= refreshInterval) {
writeBuffer();
lastRefresh = currentTime;
}
}
Comments