Project tutorial
Wii Chuck NeoPixel Tetris Game

Wii Chuck NeoPixel Tetris Game © GPL3+

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

  • 16 views
  • 0 comments
  • 0 respects

Components and supplies

Necessary tools and machines

3drag
3D Printer (generic)
09507 01
Soldering iron (generic)

Apps and online services

About this project

While on Thingiverse, I came across the Proto-Tetris Machine by Ferjerez. I was intrigued by the use of his DIY Nun-chuck style controller. Since I had a spare Wii Nun-chuck controller lying around, I thought this might be a way to put it to some use. (you can buy them separately on eBay)

Video

Demonstration

The Wii Nun-chuck controller

The Wii Nun-chuck has a built in joystick, accelerometer and two buttons. It communicates using the I2C protocol. The only downside is the special plug that connects to the Nintendo Wii. Fortunately you can get a cheap adapter on eBay.

Tidying up the wiring

I designed a new case and printed circuit board to clean up the wiring and also to hold the Wii Nun-chuck adapter board.

I also modified the front cover that holds the 8-digit 7 segment display. Using a soldering iron, make a hole behind the display for the wiring. Replace the old cover with the modified version in the attached STL files.

Servo or Light variant

Like the original, I started out using servo at the top to indicate the next piece to fall when playing the game. Servos with WS2812b strips are problematic. Both require critical timing to work properly. Also a servo makes a noise when it moves and this can become a distraction to the game. After testing the hardware, I decided that the servo had to go and went about designing a top based on RGB LEDs.

To 3D print the new light top, select a 0.2mm layer height and enable supports touching build plate only in your slicer software. During printing, you will need to change the filament from black to transparent at the start of layer 36.

Using Wire-wrap wire, connect 7 WS2812B RGB LEDs together. You will be wiring from left to right looking from the front. That is the sideways T piece (red on the picture above) has the wire from the control board to its DIN pin. Its DOUT pin will go to the DIN pin of the S piece (orange piece in the picture above) and so on. All the GND pins should be connected to each other and similarly the V+ pins.

Support the LEDs on your workbench using masking tape as you solder the wires to them. I used 25mm wire lengths for the outer LEDs and 15mm wire lengths for the inner LEDs.

Test the LEDs still work before adding them to the top assembly. I used my WS2812B Tester to check the LEDs and wiring at each stage. Carefully place the LEDs in the top assembly. Once done, check the LEDs still work and use some hot glue to protect the wires at the back.

The Electronics

The schematic is very simple and you can just wire it on a piece of proto-board if you don't want to bother with a printed circuit board.

The Eagle files have been included should you wish to have the board commercially made or you can do as I did and make them yourself. I used the Toner method.

3D print the control box from the attached STL files. I used a 0.2mm layer height and no supports. Drill out the PCB mounting holes with a 2.5mm drill and create a thread using a 3mm tap. Four 6mm M3 screws hold the PCB in place.

Finally assembly

Follow Ferjerez's instructions on the 3D printing of the base and the assembling of the matrix panels. Route the wires from the top so that the clip in the channels that hold the top and bottom to the matrix assembly.


Code

ProtoTetrisV2.inoC/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);
  }
}

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
stl_zA3KtxjJUN.zip

Schematics

Schematic
Schematic   light version hbr8v3g6wp
Eagle Files (Light Version)
Schematic and PCB files for Light version in Eagle format
eagle_files_-_light_version_f9eEDyN9BP.zip
Eagle Files (Servo Version)
Schematic and PCB files for Servo version in Eagle format
eagle_files_-_servo_version_4YkIUtevMN.zip

Comments

Similar projects you might like

Tacoyaki (Lights Out) Game

Project tutorial by John Bradnam

  • 1,082 views
  • 0 comments
  • 4 respects

The Tetris

Project showcase by Archiev Kumar

  • 8,435 views
  • 9 comments
  • 7 respects

Arduino Nano Tetris Game on Homemade 16x8 Matrix

Project tutorial by Mirko Pavleski

  • 4,952 views
  • 4 comments
  • 24 respects

Arduino Pong Game on 24x16 Matrix with MAX7219

Project tutorial by Mirko Pavleski

  • 4,418 views
  • 3 comments
  • 19 respects

NeoPixel Game Console

Project showcase by pixel-and-play

  • 1,795 views
  • 1 comment
  • 5 respects
Add projectSign up / Login