Arduino Tetris 3

We’ve got control of our pieces. They move and they can rotate. Today we’re going to get them to fall and stop when they hit the bottom of our LED.

Making it Fall

Let’s check out our code right away:

                      // goes with the other defines
#define NUM_PIECE_TYPES  (7)
                      // goes with the other globals
long fall_clock;      // time since last fall

void setup() {
                      // ... everything we already have...
  next_piece();
  fall_clock = clock = millis();
}

void next_piece() {
  piece_id=rand()%NUM_PIECE_TYPES;
  px=3;               // middle-ish
  py=-4;              // off the top of the screen
}

void loop() {
                      // ... everything we already have...
  move_piece();
  fall_piece();
  draw_piece();       // light for a fraction of a millisecond
}

void fall_piece() {
                      // 2 times a second, (timed event)
  if(millis()-clock>500) {
                      // get ready to wait another 1/10th of a second
    clock=millis();

    py++;

                      // when piece hits the bottom (conditional event)
    if(py == GRID_H-PIECE_H) {
      next_piece();
    }
  }
}

This is okay, I suppose. Our pieces are going to fall, but they sometimes stop before reaching the bottom, and they’ll also disappear at the start of next_piece().

Reach the Bottom!

Let’s go back and take a look at our pieces. Here’s one rotation of the T piece.

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

Now let’s recall:

if(py == GRID_H-PIECE_H) {

This “if()” will be true when the lights on the board looks exactly like this:

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

It’s bothersome, but not unsolvable. All we have to do is redraw each piece to look like this:

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

Remember the fallen

We shifted it down a bit, and now it’s on the bottom. When “if()” is true, the piece will be touching the bottom of the grid, unlike the first iteration.

So far, we’ve been using variables like px to remember each piece’s position. We’ve also been using arrays to remember what each piece looks like. We’re Not just telling the computer how to draw a piece or how to make it fall, but also telling it what to remember and how exactly to store things so that it remembers those things.

The moment next_piece() is called, Arduino recycles the spot used to remember the current piece. Instead, how about we teach Arduino to remember the whole grid and the current piece? Let’s grab an array that has one storage space for every dot on the grid.

int grid[GRID_W*GRID_H];

void setup() {
  for(i=0;i<GRID_W*GRID_H;++i) {
    grid[i]=0;
  }
}

void loop() {
  move_piece();
  fall_piece();
                       // we don't draw_piece() any more
  draw_grid();
}

void draw_grid() {
  int x,y;
  for(y=0;y<GRID_H;++y) {
    for(x=0;x<GRID_W;++x) {
      if( grid[y*GRID_W+x] > 0 ) p(x,y,0);
    }
  }
}

This bit of code adds each piece to “grid[]” and draws that. So, when I want a piece to fall, I just remove it from “grid[]”, move it down one row, then stick it back into there.

                      // looks a lot like draw_piece()!
void add_piece_to_grid() {
  int x, y;
  
                      // if piece_id is 0 then pieces[piece_id] is equal to piece_I.
  const char *piece = pieces[piece_id] + (piece_rotation * PIECE_H * PIECE_W);
  
  for(y=0;y<PIECE_H;++y) {
    for(x=0;x<PIECE_W;++x) {
      if( piece[y*PIECE_W+x] == 1 ) {
                      // find the matching spot in grid and fill it in
        int nx = px+x;
        int ny = py+y;
        grid[ny*GRID_W+nx] = 1;
      }
    }
  }
}

void remove_piece_from_grid() {
  int x, y;
                       // if piece_id is 0 then pieces[piece_id] is equal to piece_I.
  const char *piece = pieces[piece_id] + (piece_rotation * PIECE_H * PIECE_W);
  
  for(y=0;y<PIECE_H;++y) {
    for(x=0;x<PIECE_W;++x) {
      if( piece[y*PIECE_W+x] == 1 ) {
                       // find the matching spot in grid and erase it
        int nx = px+x;
        int ny = py+y;
        grid[ny*GRID_W+nx] = 0;
      }
    }
  }
}

Move_piece() and fall_piece() both change using the same method. Below you’ll see I only show fall_piece(). Try and get move by yourself – it’ll still be posted in the github if you can’t figure it out.

void fall_piece() {
                       // 2 times a second, (timed event)
  if(millis()-clock>500) {
                       // get ready to wait another 1/10th of a second
    clock=millis();
    remove_piece_from_grid();
    py++;
    add_piece_to_grid();
                       // when piece hits the bottom (conditional event)
    if(py == GRID_H-PIECE_H) {
      next_piece();
    }
  }
}

It’s clicking now! The next time that next_piece() is called, the px, py, piece_rotation, and piece_id values are all going to be recycled, but the 1s in the “grid[]” will be left unchanged. When next_piece() is called a second time, a new piece will start to fall – only now there’s already some lights on and there’ll be two pieces on the grid!

ruby rod, hot hot hot

… and then new piece falls right through the first one, perhaps even slicing a chunk off it as it flies off.

khaaan

Detect Collision

Okay, well, so much for that. I need a way to get the pieces to stop upon hitting another piece. It can’t be too hard, right? Basically, I want to say this:

 remove_piece_from_grid();
 new place = current place + change
 if(piece_can_fit(new place)==1) {
   current place = new place
 }
 add_piece_to_grid();

This is fine and all, but I need it to work for falls, sideway movements, and all rotations. Piece_can_fit() has to be able to check for the sides of the grid, the bottom, and old pieces.

int piece_can_fit(int npx,int npy,int npr) {
  if( piece_outside_grid(npx,npy,npr) ) return 0;  // can't fit
  if( piece_hits_rubble(npx,npy,npr) ) return 0;   // can't fit
  return 1;  // it fits, I sits.
}

// this looks a lot like add_piece_to_grid()
int piece_outside_grid(int npx,int npy,int npr) {
  int x, y;
   // if piece_id is 0 then pieces[piece_id] is 
   // equal to piece_I.
  const char *piece = pieces[piece_id] + (piece_rotation * PIECE_H * PIECE_W);
  
  for(y=0;y<PIECE_H;++y) {
    for(x=0;x<PIECE_W;++x) {
      if( piece[y*PIECE_W+x] == 1 ) {
        int nx = px+x;
        int ny = py+y;
        if( nx < 0       ) return 1;            // off left side
        if( nx >= GRID_W ) return 1;            // off right side
        if( ny < 0       ) return 1;            // off top - true for every new piece.
        if( ny >= GRID_H ) return 1;            // off bottom
      }
    }
  }
  return 0;  // in grid!
}

int piece_hits_rubble(int npx,int npy,int npr) {
  int x, y;
  // if piece_id is 0 then pieces[piece_id] is equal to piece_I.
  const char *piece = pieces[piece_id] + (piece_rotation * PIECE_H * PIECE_W);
  
  for(y=0;y<PIECE_H;++y) {
    for(x=0;x<PIECE_W;++x) {
      if( piece[y*PIECE_W+x] == 1 ) {
        int nx = px+x;
        int ny = py+y;
        if( grid[ny*GRID_W+nx] == 1 ) return 1;     // collision!
      }
    }
  }
  return 0;  // no collision
}

This seems to do the trick. Now new_pieces() won’t create bulldozing pieces and each one will stop nicely upon collision.

Questions

  • Check out this pseudocode. Try and see if you can turn it into real code and get it working.
remove_piece_from_grid();
//new place = current place + change 
if(piece_can_fit(npx,npy,npr)==1) {
  //current place = new place 
  add_piece_to_grid();
}