John Bradnam
Published © GPL3+

Wii Chuck NeoPixel Tetris Game

A desktop tetris game using 135 WS2812B RGB LEDs and played with a Wii Nunchuck controller.

IntermediateFull instructions provided8 hours825
Wii Chuck NeoPixel Tetris Game

Things used in this project

Hardware components

8x8 WS2812B RGB matrix panel
×2
WS2812B RGB LED
×7
Arduino Nano R3
Arduino Nano R3
×1
3A Mini DC-DC Step Down Converter Module Adjustable
×1
Capacitor 220 µF
Capacitor 220 µF
×2
Resistor 330 ohm
Resistor 330 ohm
×2
Capacitor 10uF 1206 SMD ceramic
×1
Buzzer
Buzzer
×1
Wii Nunchuck Game Controller
×1
Arduino Wii Nunchuck game controller adapter
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

STL files for modified parts

Case bottom and top, modified 7 seg display cover, dial for light version. For the rest of the files, get them from https://www.thingiverse.com/thing:2676560/files

Schematics

Schematic

PCB

Eagle Files (Light Version)

Schematic and PCB files for Light version in Eagle format

Eagle Files (Servo Version)

Schematic and PCB files for Servo version in Eagle format

Code

ProtoTetrisV2.ino

C/C++
/* 

  Tetris for Arduino Uno/Nano
  Fernando Jerez 2017
  License Creative Commons - Attribution
  https://www.thingiverse.com/thing:2676560

  2020-05-23: John Bradnam
    Make: https://www.hackster.io/john-bradnam/wii-chuck-neopixel-tetris-game-047fdc
    Removed servo and replaced with 7 WS2812B leds
    Removed switches and replaced with Wii Nunchuck
    Added buzzer for sound effects
*/

#include <Wire.h>
#include <ArduinoNunchuk.h>
#include <FastLED.h>
#include <LedControl.h>

/**************
   PINOUT
 **************/

#define MATRIX_PIN 5
#define MATRIX_PIXELS 128
#define MATRIX_BRIGHTNESS 8
#define NEXT_PIN 9
#define NEXT_PIXELS 7
#define NEXT_BRIGHTNESS 255

#define DATA 2
#define CLOCK 4
#define LOAD 3

#define SPEAKER A0

LedControl lc=LedControl(DATA,CLOCK,LOAD,1);

ArduinoNunchuk nunchuk = ArduinoNunchuk();

//#define ACCEL

#define FRAMES_PER_SECOND  20
CRGB leds[MATRIX_PIXELS];
byte board[MATRIX_PIXELS];
CRGB nextLeds[NEXT_PIXELS];
int nextCount = 0;

int speed = 5; //lower one every 5 frames (smaller = faster)
int frameCount = 0;

boolean gameover = true;

// PIECES

#define EMPTY 7

typedef struct {
  int width; // width
  int height;  // height
  int picture[6]; // picture
  int turns; // number of possible turns
  int cx; // center of rotation X
  int cy; // center y
  CRGB color; // color 
  int next; // LED number for Next LED strip
} Piece;

Piece pieces[7] = {
  {3, 2, {0, 1, 0, 1, 1, 1}, 4, 1, 1, CRGB(255, 0, 0), 0 },   // T
  {3, 2, {0, 1, 1, 1, 1, 0}, 2, 1, 0, CRGB(255, 70, 0), 1},   // S
  {2, 3, {1, 0, 1, 0, 1, 1}, 4, 0, 1, CRGB(255, 255, 0), 2},  // L
  {4, 1, {1, 1, 1, 1}, 2, 2, 0, CRGB(0, 255, 0), 3},          // I
  {2, 3, {0, 1, 0, 1, 1, 1}, 4, 1, 1, CRGB(0, 255, 255), 4},  // J
  {3, 2, {1, 1, 0, 0, 1, 1}, 2, 1, 0, CRGB(0, 0, 255), 5},    // Z
  {2, 2, {1, 1, 1, 1}, 1, 0, 0, CRGB(255, 0, 255), 6}         // O
};

int rotation = 0; //0,1,2,3
int npiece, next;
int xpos, ypos;

// Avoid the auto-click
boolean pressed1 = false, pressed2 = false;
boolean joy1 = false, joy2 = false;
int pause1 = 5, pause2 = 5; // Delay for joystick movements

// Points
long points = 0;
int lines = 0;


int lastAnalogX = 0;
int lastAnalogY = 0;
int lastAccelX = 0;
int lastAccelY = 0;
int lastzButton = 0;    //Big button
int lastcButton = 0;    //Small button
    
void setup() 
{
  Serial.begin(115200);
  delay(2000); // 2 second delay for recovery

  pinMode(SPEAKER,OUTPUT);

  lc.shutdown(0, false);
  lc.setIntensity(0, 8);
  lc.clearDisplay(0);
  lc.setDigit(0, 7, 3, false);

  FastLED.addLeds<NEOPIXEL, MATRIX_PIN>(leds, MATRIX_PIXELS);
  FastLED.addLeds<NEOPIXEL, NEXT_PIN>(nextLeds, NEXT_PIXELS);
  FastLED.setBrightness(MATRIX_BRIGHTNESS);

  nunchuk.init();

  cleanTable();

  npiece = random(0, 7);
  next = random(0, 7);
  xpos = 4;
  ypos = pieces[npiece].cy;
  rotation = 0;
}

void loop()
{


  writeNumber(points); // Write points on 7-Led display

  //TIMSK0 &= ~TOIE0;
  nunchuk.update();
  //displayNunchuckValues();
  //TIMSK0 |= TOIE0;
  int jx = nunchuk.analogX;
  int jy = nunchuk.analogY;

  if (!gameover) 
  {
    /*

      GAME
    
    */
    paintBoard();
    frameCount++;

    if (jx == 255) 
    {
      pause1 = max(0, pause1 - 1);
      if (joy1 == false || pause1 == 0) 
      {
        joy1 = true;
        beepTurn();
        if (checkColision(npiece, xpos + 1, ypos, rotation)) 
        {
          xpos++;
        }
      }
    } 
    else 
    {
      pause1 = 5;
      joy1 = false;
    }


    if (jx == 0) 
    {
      pause2 = max(0, pause2 - 1);
      if (joy2 == false || pause2 == 0) 
      {
        joy2 = true;
        beepTurn();
        if (checkColision(npiece, xpos - 1, ypos, rotation)) 
        {
          xpos--;
        }
      }
    } 
    else 
    {
      pause2 = 5;
      joy2 = false;
    }


    if (jy == 0) 
    {
      if (checkColision(npiece, xpos, ypos + 1, rotation)) 
      {
        ypos++;
        points++;
        frameCount = 1;
        beepTurn();
      }
    }

    if (nunchuk.zButton == HIGH) 
    {
      if (!pressed1) 
      {

        int nrot = (rotation + 1) % pieces[npiece].turns;
        if (checkColision(npiece, xpos, ypos, nrot)) 
        {
          rotation  = nrot;
        }
        pressed1 = true;
        beepTurn();
      }
    } 
    else 
    {
      pressed1 = false;
    }
    if (nunchuk.cButton == HIGH) 
    {
      if (!pressed2) 
      {

        int nrot = (rotation + pieces[npiece].turns - 1) % pieces[npiece].turns;
        if (checkColision(npiece, xpos, ypos, nrot)) 
        {
          rotation  = nrot;
        }
        pressed2 = true;
        beepTurn();
      }
    } 
    else 
    {
      pressed2 = false;
    }

    paintPiece(npiece, xpos, ypos, rotation);


    // Low
    if (frameCount % speed == 0) 
    {
      frameCount = 1;
      if (checkColision(npiece, xpos, ypos + 1, rotation)) 
      {
        ypos++;
      } 
      else 
      {
        // paint on board
        paintOnBoard(npiece, xpos, ypos, rotation);

        // Check lines
        checkBoardLines();

        // Take out new piece
        npiece = next;
        next = random(0, 7);
        xpos = 4;
        ypos = pieces[npiece].cy;
        rotation = 0;
        clearNextLeds(false);
        nextLeds[pieces[npiece].next] = pieces[npiece].color;
        FastLED[1].showLeds(NEXT_BRIGHTNESS);
        FastLED[0].showLeds(MATRIX_BRIGHTNESS);
        beepRelease();

        delay(200);
        // Check that it doesn't crash (if GAME OVER crashes)
        if (!checkColision(npiece, xpos, ypos, rotation)) 
        {
          // GAME OVER
          gameover = true;
          paintOnBoard(npiece, xpos, ypos, rotation);
          paintPiece(npiece, xpos, ypos, rotation);
          // servo a 90
          clearNextLeds(true);
          //servo.write(map(90, 0, 180, SERVO_MIN, SERVO_MAX));
          playLoseMusic();
        }

      }
    }

  } 
  else 
  {
    // GAME OVER
    // Press A / START to start
    
    if (jy == 255) 
    {
      FastLED.clear();
      gameover = false;
      points = 0;
      lines = 0;
      clearNextLeds(true);
      //servo.write(map(pieces[next].servo, 0, 180, SERVO_MIN, SERVO_MAX));
    }
    frameCount++;
    if (frameCount % 5 == 0) 
    {
      frameCount = 0;
      for (int i = MATRIX_PIXELS - 1; i >= 0; i--) 
      {
        board[i] = EMPTY;
        // Fade out
        //      leds[i].r = max(leds[i].r-10,0);
        //      leds[i].g = max(leds[i].g-10,0);
        //      leds[i].b = max(leds[i].b-10,0);
        // Scroll down
        if (i >= 8) {
          leds[i].r = leds[i - 8].r;
          leds[i].g = leds[i - 8].g;
          leds[i].b = leds[i - 8].b;
        } else {
          leds[i] = CRGB(0, 0, 0);
        }

      }

      boolean fits = true;
      int p, px, py, pr;
      do {
        npiece = next;
        next = random(0, 7);
        pr = random(0, 5);
        px = random(0, 8);
        py = 1;//random(0,16);

      } while (!checkColision(next, px, py, pr));
      clearNextLeds(false);
      int nx = (nextCount < NEXT_PIXELS) ? nextCount : NEXT_PIXELS * 2 - nextCount - 1;
      nextLeds[pieces[nx].next] = pieces[nx].color;
      nextCount = (nextCount + 1) % (NEXT_PIXELS * 2);

      FastLED[1].showLeds(NEXT_BRIGHTNESS);
      paintPiece(next, px, py, pr);
    }
  }

  // send the 'leds' array out to the actual LED strip
  FastLED[0].showLeds(MATRIX_BRIGHTNESS);
  // insert a delay to keep the framerate modest
  //FastLED.delay(1000 / FRAMES_PER_SECOND);
  delay(1000 / FRAMES_PER_SECOND);

}

/* Paint the piece in the LED matrix */
void paintPiece(int piece, int xpos, int ypos, int rot) 
{

  for (int i = 0; i < pieces[piece].width; i++) 
  {
    for (int j = 0; j < pieces[piece].height; j++) 
    {

      switch (rot) 
      {
        case 0:
          if (pieces[piece].picture[i + j * pieces[piece].width] == 1) {
            leds[(ypos + j - pieces[piece].cy) * 8 + (xpos + i - pieces[piece].cx)] = pieces[piece].color;
          }
          break;
          
        case 1:
          if (pieces[piece].picture[i + j * pieces[piece].width] == 1) 
          {
            leds[(ypos + i - pieces[piece].cx) * 8 + (xpos - j + pieces[piece].cy)] = pieces[piece].color;
          }
          break;
          
        case 2:
          if (pieces[piece].picture[i + j * pieces[piece].width] == 1) 
          {
            leds[(ypos - j + pieces[piece].cy) * 8 + (xpos - i + pieces[piece].cx)] = pieces[piece].color;
          }
          break;
          
        case 3:
          if (pieces[piece].picture[i + j * pieces[piece].width] == 1) 
          {
            leds[(ypos - i + pieces[piece].cx) * 8 + (xpos + j - pieces[piece].cy)] = pieces[piece].color;
          }
          break;
      }
    }
  }
}

// Paint the piece on the board (In memory buffer)
void paintOnBoard(int piece, int xpos, int ypos, int rot) 
{

  for (int i = 0; i < pieces[piece].width; i++) 
  {
    for (int j = 0; j < pieces[piece].height; j++) 
    {

      switch (rot) 
      {
        case 0:
          if (pieces[piece].picture[i + j * pieces[piece].width] == 1) 
          {
            board[(ypos + j - pieces[piece].cy) * 8 + (xpos + i - pieces[piece].cx)] = (byte)piece;
          }
          break;
          
        case 1:
          if (pieces[piece].picture[i + j * pieces[piece].width] == 1) 
          {
            board[(ypos + i - pieces[piece].cx) * 8 + (xpos - j + pieces[piece].cy)] = (byte)piece;
          }
          break;
          
        case 2:
          if (pieces[piece].picture[i + j * pieces[piece].width] == 1) {
            board[(ypos - j + pieces[piece].cy) * 8 + (xpos - i + pieces[piece].cx)] = (byte)piece;
          }
          break;
          
        case 3:
          if (pieces[piece].picture[i + j * pieces[piece].width] == 1) 
          {
            board[(ypos - i + pieces[piece].cx) * 8 + (xpos + j - pieces[piece].cy)] = (byte)piece;
          }
          break;
      }
    }
  }
}

boolean checkColision(int piece, int xpos, int ypos, int rot) 
{
  // returns false if it crashes, true if not
  for (int i = 0; i < pieces[piece].width; i++) 
  {
    for (int j = 0; j < pieces[piece].height; j++) 
    {

      switch (rot) 
      {
        case 0:
          if (pieces[piece].picture[i + j * pieces[piece].width] == 1) 
          {
            int xx = xpos + i - pieces[piece].cx;
            int yy = ypos + j - pieces[piece].cy;
            if (xx < 0 || xx > 7 || yy < 0 || yy > 15) return false;
            if (board[xx + yy * 8] != EMPTY) return false;
          }
          break;
          
        case 1:
          if (pieces[piece].picture[i + j * pieces[piece].width] == 1) 
          {
            int xx = xpos - j + pieces[piece].cy;
            int yy = ypos + i - pieces[piece].cx;
            if (xx < 0 || xx > 7 || yy < 0 || yy > 15) return false;
            if (board[xx + yy * 8] != EMPTY) return false;
          }
          break;
          
        case 2:
          if (pieces[piece].picture[i + j * pieces[piece].width] == 1) 
          {
            int xx = xpos - i + pieces[piece].cx;
            int yy = ypos - j + pieces[piece].cy;
            if (xx < 0 || xx > 7 || yy < 0 || yy > 15) return false;
            if (board[xx + yy * 8] != EMPTY) return false;
          }
          break;
          
        case 3:
          if (pieces[piece].picture[i + j * pieces[piece].width] == 1) 
          {
            int xx = xpos + j - pieces[piece].cy;
            int yy = ypos - i + pieces[piece].cx;
            if (xx < 0 || xx > 7 || yy < 0 || yy > 15) return false;
            if (board[xx + yy * 8] != EMPTY) return false;
          }
          break;
      }
    }
  }
  return true;
}


// Clean full board lines

void checkBoardLines() 
{
  boolean complete;
  int account = 0;
  for (int y = 15; y >= 0; y--) 
  {
    do {
      complete = true;
      for (int x = 0; x < 8; x++) 
      {
        if (board[x + y * 8] == EMPTY) complete = false;
      }
      if (complete) 
      {
        account++;
        lines++;
        removeLine(y);
        // low board
        for (int yy = y; yy >= 0; yy--) 
        {
          for (int xx = 0; xx < 8; xx++) 
          {
            if (yy == 0) 
            {
              board[xx] = EMPTY;
            } 
            else 
            {
              board[xx + yy * 8] = board[xx + (yy - 1) * 8];
            }
          }
        }
        paintBoard();
        FastLED[0].showLeds(MATRIX_BRIGHTNESS);
      }

    } while (complete);
  }
  if (account >= 1) 
  {
    // 50,150,400,900
    switch (account) 
    {
      case 1: points += 50; break;
      case 2: points += 150; break;
      case 3: points += 400; break;
      case 4: points += 900; break;
    }
    //points +=pow(10,account);
    // Update speed
    speed = max(0, 5 - floor(lines / 10));
  }
}

/* Animation deleting line */
void removeLine(int y) 
{
  for (int x = 0; x < 8; x++) 
  {
    leds[x + y * 8] = CRGB(100, 0, 0);
    FastLED[0].showLeds(MATRIX_BRIGHTNESS);
    delay(50);
    leds[x + y * 8] = CRGB(0, 0, 0);
  }
}

/* Paint the board in the LED matrix */
void paintBoard() 
{
  for (int i = 0; i < MATRIX_PIXELS; i++) 
  {
    if (board[i] != EMPTY) 
    {
      leds[i] = pieces[board[i]].color;
    } 
    else 
    {
      leds[i] = CRGB(0, 0, 0);
    }
  }
}

// Fill the board with EMPTY
void cleanTable() 
{
  for (int i = 0; i < MATRIX_PIXELS; i++) 
  {
    board[i] = EMPTY;
  }
}

//Turn off all the top LEDs
void clearNextLeds(bool update)
{          
  for (int i = 0; i < NEXT_PIXELS; i++)
  {
    nextLeds[i] = CRGB(0, 0, 0);
  }
  if (update)
  {
    FastLED[1].showLeds(NEXT_BRIGHTNESS);
  }
}

void writeNumber(long n) 
{
  byte i = n % 10;
  lc.setDigit(0, 0, i, false);
  if (n >= 10) lc.setDigit(0, 1, (n / 10) % 10, false); else lc.setChar(0, 1, ' ', false);
  if (n >= 100) lc.setDigit(0, 2, (n / 100) % 10, false); else lc.setChar(0, 2, ' ', false);
  if (n >= 1000) lc.setDigit(0, 3, (n / 1000) % 10, false); else lc.setChar(0, 3, ' ', false);
  if (n >= 10000) lc.setDigit(0, 4, (n / 10000) % 10, false); else lc.setChar(0, 4, ' ', false);
  if (n >= 100000) lc.setDigit(0, 5, (n / 100000) % 10, false); else lc.setChar(0, 5, ' ', false);
  if (n >= 1000000) lc.setDigit(0, 6, (n / 1000000) % 10, false); else lc.setChar(0, 6, ' ', false);
  if (n >= 10000000) lc.setDigit(0, 7, (n / 10000000) % 10, false); else lc.setChar(0, 7, ' ', false);
}

//Turn on and off buzzer quickly
void beepRelease() 
{
  digitalWrite(SPEAKER, HIGH);                                     // turn on buzzer
  delay(20);
  digitalWrite(SPEAKER, LOW);                                      // turn off buzzer
}

//Turn on and off buzzer quickly
void beepTurn() 
{
  digitalWrite(SPEAKER, HIGH);                                     // turn on buzzer
  delay(5);
  digitalWrite(SPEAKER, LOW);                                      // turn off buzzer
}

//Play wah wah wah wahwahwahwahwahwah
void playLoseMusic()
{
  delay(400);
  //wah wah wah wahwahwahwahwahwah
  for(double wah=0; wah<4; wah+=6.541)
  {
    playSound(440+wah, 50);
  }
  playSound(466.164, 100);
  delay(80);
  for(double wah=0; wah<5; wah+=4.939)
  {
    playSound(415.305+wah, 50);
  }
  playSound(440.000, 100);
  delay(80);
  for(double wah=0; wah<5; wah+=4.662)
  {
    playSound(391.995+wah, 50);
  }
  playSound(415.305, 100);
  delay(80);
  for(int j=0; j<7; j++)
  {
    playSound(391.995, 70);
    playSound(415.305, 70);
  }
  delay(400);
}

//Play a sound of a frequency in Hz for a duration in mS
void playSound(double freqHz, int durationMs)
{
  //Calculate the period in microseconds
  int periodMicro = int((1/freqHz)*1000000);
  int halfPeriod = periodMicro/2;
   
  //store start time
  long startTime = millis();
   
  //(millis() - startTime) is elapsed play time
  while((millis() - startTime) < durationMs)
  {
    digitalWrite(SPEAKER, HIGH);
    delayMicroseconds(halfPeriod);
    digitalWrite(SPEAKER, LOW);
    delayMicroseconds(halfPeriod);
  }
}

Credits

John Bradnam

John Bradnam

30 projects • 19 followers
Thanks to Ferjerez.

Comments