Posted on

Build your own falling block game like Tetris

I’ve shown you how to use shift registers to drive an LED grid, including how to draw pictures on the screen from memory. Now we’re going to use those tools to make a game similar to the classic Tetris.  I’ll show you the circuit, how to draw pieces, how to create animations, respond to user input, and more.  Learning how to build complex behavior from simple parts is a great start to thinking about how robots behave.

What do you mean, like tetris?

Tetris has a different well shape (10 wide and 24 tall), different speeds, a score keeping system, music, and in Tetris you can turn a piece either way.  In this simplified clone you can only turn the piece clockwise and the well is 8*16.

The game circuit

This is the design of how the wires should work in theory only. If you looked at the previous tutorial for LEDs and shift registers, you’ll recognize everything here. I’ve doubled the number of grids and shift registers, plus added a 5 pin connector for the joystick.
ELEC-0120 schematic

Once I’ve got the schematic done I have to tell the design software the physical shape of each part, then lay them out on the PCB. That includes the outside edge of the PCB, the writing on the PCB, and where the wires are traced on the PCB.
ELEC-0120 pcb

My design software is kind enough to offer a 3D model of the board.

ELEC-0120 pcb 3d

I took the 3D model of the board and my model of the LED grid and put everything together in Fusion360. In theory now I could make a 3d printed box to go around the whole thing.  In practice I couldn’t find a pleasing form factor – Either the battery or the joystick is in an ugly place.

ELEC-0120 assembly in fusion

Drawing one piece

There are seven pieces to draw: the two Ls, the two S, the box, the I, and the T.  The longest piece is 4 units long, and pieces can be rotated 4 different ways. I want to write code once to deal with all pieces in every rotation.  To make that happen I created a 4*4 pictures of each piece in each rotation.  Some pieces don’t look like they change when they rotate, but so what?   Here’s an example of an L piece.

const char piece_L2[] = {
  0,1,0,0,
  0,1,0,0,
  1,1,0,0,
  0,0,0,0,

  0,0,0,0,
  1,0,0,0,
  1,1,1,0,
  0,0,0,0,
 
  0,1,1,0,
  0,1,0,0,
  0,1,0,0,
  0,0,0,0,

  0,0,0,0,
  1,1,1,0,
  0,0,1,0,
  0,0,0,0,
};

To show a piece in a given rotation to the player, I copy one picture to the grid, then display the grid.  This is a bit tricky because the grid size is not the same as a piece size.

void addPieceToGrid() {
  int x, y;
 
  const char *piece = pieces[pieceID] + (pieceRotation * PIECE_H * PIECE_W);
 
  for(y=0;y<PIECE_H;++y) {
    for(x=0;x<PIECE_W;++x) {
      int nx=pieceX+x;
      int ny=pieceY+y;
      if(ny<0 || ny>=HEIGHT) continue;  // off grid
      if(nx<0 || nx>=WIDTH ) continue;  // off grid
      if(piece[y*PIECE_W+x]==1) {
        grid[ny*WIDTH+nx]=1;  // =0 to erase
      }
    }
  }
}

I created a nearly identical method called erasePieceFromGrid().

Game animations

Then I made pieceOffGrid() to see if a piece has gone too far left or right and pieceHitsRubble() to see if a piece has hit older pieces still on the screen.  Both of those methods are very similar to add and erase.  Try to work it out before you look at my solution.

With all that together we can start to make animations.  Here’s an falling piece example.

void tryToDropPiece() {
  erasePieceFromGrid();  
  if(pieceCanFit(pieceX,pieceY+1,pieceRotation)) {
    pieceY++;  // move piece down
    addPieceToGrid();
  } else {
    // hit something!
    // put it back at the old location
    addPieceToGrid();
    removeFullRows();
    if(gameIsOver()==1) {
      gameOver();
    }
    // game isn't over, choose a new piece
    chooseNewPiece();
  }
}

There are similar methods named tryToMovePieceSideways() and a tryToRotatePiece() called from a method named reactToPlayer().  There are two parts left that I think are interesting: the main game loop and picking a new piece.

void playTetris() {
  // the game plays at one speed...
  if(millis() - lastMove > moveDelay ) {
    lastMove = millis();
    reactToPlayer();
  }
 
  // ...and drops the falling block at a different speed.
  if(millis() - lastDrop > dropDelay ) {
    lastDrop = millis();
    tryToDropPiece();
  }
 
  // when it isn't doing those two things, it's redrawing the grid.
  drawGrid();
}

The piece choosing system is a bit like a game of Scrabble – all the pieces are put in a bag and drawn out at random.  When the bag is empty the pieces are put back in and drawing continues.  This way the player won’t suffer more than 12 turns before getting that one piece they really want.

void chooseNewPiece() {
  // bag empty?
  if( pieceSequenceIndex >= NUM_PIECE_TYPES ) {
    int i,j, k;
    for(i=0;i<NUM_PIECE_TYPES;++i) {
      do {
        // pick a random piece
        j = rand() % NUM_PIECE_TYPES;
        // make sure it isn't already in the bag.
        for(k=0;k<i;++k) {
          if(pieceSequence[k]==j) break;  // already in the bag
        }
      } while(k<i);
      // not in bag.  Add it.
      pieceSequence[i] = j;
    }
    // rewind sequence counter
    pieceSequenceIndex=0;
  }
 
  // get the next piece in the sequence.
  pieceID = pieceSequence[pieceSequenceIndex++];
  // always start the piece top center.
  pieceY=-4;  // -4 squares off the top of the screen.
  pieceX=3;
  // always start in the same orientation.
  pieceRotation=0;
}

The rest of the finer points can be read from the open source code.

See Also