Tetris

By Paul , 15 September 2025
#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