Flash Games

Tetris is a great game to write as a basic teaching exercise, because it's a very simple game to understand but has some moderately interesting logic.

This is a reference implementation of the game in Flash. Click in the game to start, then use the arrow keys: left and right move the piece, up rotates it, and down drops it faster.

The basic setup

The board is 10 cells wide and 20 high, and it's an array of rows, each an array of cells. A cell holds 0 when it's empty, or the colour of the block in it.

The board has a border one cell thick all the way round it, filled with 1s, so its rows and columns run from -1 to the width and height. ActionScript arrays are objects, so an index of -1 works like any other:

board_w = 10;
board_h = 20;
board = new Array();
for (j = -1; j <= board_h; j++) {
  board[j] = new Array();
  for (i = -1; i <= board_w; i++) {
    board[j][i] = i < 0 || j < 0 || i == board_w || j == board_h ? 1 : 0;
  }
}
Setting up the board, with a border of 1s round the edge
############
#..........#
#..........#
#..........#
     ...
#..........#
############
The board: 10 cells by 20 inside its border (trimmed)

That border is the walls and the floor. To the rest of the game they're just more blocks, so nothing ever has to check whether a piece is about to leave the board. A 1 is a colour too, very nearly black, so drawing the board draws the walls along with everything else.

Each cell is 4 pixels square, and the game draws into four movie clips, one above the other: the background, the board, the falling piece, and a layer for flashing the full rows. A cell is drawn as a filled square with moveTo, beginFill and lineTo. The piece is drawn again whenever it moves, and the board only when a piece lands or rows are cleared.

The game runs at 20 frames a second. Each frame, it reads the keys, and every 10 frames it moves the piece down a row, so pieces fall twice a second. When a piece can't move down any further, it becomes part of the board, and the next piece starts at the top:

function doGravity() {
  if (!moveCurrentPieceDown()) {
    placeCurrentPieceInBoard();
    setUpNextPiece();
  }
}
Moving the piece down a row, or landing it and starting the next one

Left and right move the piece a cell on the first frame, then wait two frames, then move it every frame for as long as the key is held. Up turns the piece once for each press, and down moves it a row on every frame it's held.

The pieces

There are seven pieces, and each one is stored as four pictures, one for each way round it can be. Each picture is a 4x4 grid of 0s and 1s. These are all of them, a row for each piece and its four rotations along the row:

I   ..#.  ....  .#..  ....
    ..#.  ....  .#..  ....
    ..#.  ####  .#..  ####
    ..#.  ....  .#..  ....

T   ....  ....  ....  ....
    ..#.  ..#.  ....  ..#.
    .###  ..##  .###  .##.
    ....  ..#.  ..#.  ..#.

S   ....  ....  ....  ....
    ..##  ..#.  ....  .#..
    .##.  ..##  ..##  .##.
    ....  ...#  .##.  ..#.

Z   ....  ....  ....  ....
    .##.  ...#  ....  ..#.
    ..##  ..##  .##.  .##.
    ....  ..#.  ..##  .#..

J   ....  ....  ....  ....
    .#..  ..##  ....  ..#.
    .###  ..#.  .###  ..#.
    ....  ..#.  ...#  .##.

L   ....  .##.  ...#  ..#.
    .###  ..#.  .###  ..#.
    .#..  ..#.  ....  ..##
    ....  ....  ....  ....

O   ....  ....  ....  ....
    .##.  .##.  .##.  .##.
    .##.  .##.  .##.  .##.
    ....  ....  ....  ....
The seven pieces, each in its four rotations

In the code they're one array, pieces, with an entry for each piece. A piece's entry is 16 rows of 4 cells, and they're laid out so the data looks like the pictures: each line holds the same row of all four rotations, so reading down a column of the data gives one rotation. Here's the T:

pieces = [
  ...
  [
    [0,0,0,0],  [0,0,0,0],  [0,0,0,0],  [0,0,0,0],
    [0,0,1,0],  [0,0,1,0],  [0,0,0,0],  [0,0,1,0],
    [0,1,1,1],  [0,0,1,1],  [0,1,1,1],  [0,1,1,0],
    [0,0,0,0],  [0,0,1,0],  [0,0,1,0],  [0,0,1,0]
  ],
  ...
];
The T in the pieces array: each column of the data is one rotation (trimmed)

So row j of rotation r is entry j * 4 + r. Each piece also has a colour, in a second array in the same order:

PieceColour
IGrey, 0x999999
TRed, 0xFF0000
SGreen, 0x009900
ZBlue, 0x0066FF
JOrange, 0xFF9900
LPurple, 0x9900FF
ODark grey, 0x333333
The pieces' colours

A new piece is picked at random, and so is which way round it starts. It starts at the top of the board, with its grid three cells in from the left.

Rotation

The falling piece is kept in its own 4x4 grid, piece, along with which piece it is, which way round it is and where it is on the board. Setting it up copies the rotation's rows out of the pieces array:

function setCurrentPiece(piece_index, piece_rotate) {
  var i;
  var j;
  for (j = 0; j < 4; j++) {
    for (i = 0; i < 4; i++) {
      piece[j][i] = pieces[piece_index][j * 4 + piece_rotate][i];
    }
  }
}
Copying one rotation of a piece into the falling piece's grid

So turning a piece doesn't need any maths. It's just picking the next of its four pictures. The rotation is a number from 0 to 3, and ANDing it with 3 wraps it round from 3 back to 0. The new rotation is copied in and tested against the board, and if it doesn't fit, the old one is put back:

function rotatePiece(d) {
  var r = (piece_rotate + d) & 3;
  setCurrentPiece(piece_index, r);
  if (!testPieceAgainstBoard(0, 0)) {
    piece_rotate = r;
    drawPiece();
  } else {
    setCurrentPiece(piece_index, piece_rotate);
  }
}
Turning the piece, and turning it back if the new way round doesn't fit

Because each rotation is a picture, the data also decides where each piece turns around. Most of the pieces stay in the same three columns of their grid as they turn, but the I stands in the third column, then lies along the third row, then stands in the second column, so it moves sideways a little as it turns.

Collisions

Everything the piece does is checked with one function. It lays the piece's grid over the board, moved by an offset, and if any filled cell of the piece lands on a filled cell of the board, the piece doesn't fit:

function testPieceAgainstBoard(x, y) {
  var i;
  var j;
  x += piece_x;
  y += piece_y;
  for (j = 0; j < 4; j++) {
    for (i = 0; i < 4; i++) {
      if (piece[j][i] && board[y + j][x + i]) {
        return true;
      }
    }
  }
  return false;
}
Testing the piece against the board, a cell to the side or below if asked

Moving left or right tests an offset of -1 or 1 across, falling tests 1 down, and rotating tests the new rotation where the piece is now, with no offset. The walls and the floor are filled cells of the board, so they block the piece just like the blocks that have already landed.

When the piece lands, it's added into the board: each of its cells, 0 or 1, times its colour is added to the board cell under it, so its blocks take on its colour and its empty cells add nothing.

Edge cases

When the piece is against a wall and turning it would put part of it into the wall, the rotation is simply refused, and the piece stays as it was. It doesn't get pushed away from the wall to make room. So to turn an I standing against the right-hand wall, you have to move it away from the wall first, as lying flat it would need the column the wall is in. A T pointing right and pushed against the left-hand wall is the same: pointing down, it would need one more column on the left.

The same goes for the floor, and for blocks that have already landed. If the new rotation overlaps anything at all, it doesn't happen.

Cells beyond the border aren't in the array at all, and reading one gives undefined, which counts as empty. That never matters, because a piece would have to go through the wall to reach them.

A new piece isn't tested against the board when it starts. Once the board is full to the top, it's placed over whatever is already there, so the game never ends.

Detecting lines

After a piece lands, the board is counted, row by row. The count of filled cells in each row is kept in board_count, and the function returns how many rows are full:

function countBoard() {
  var i;
  var j;
  var c;
  var r;
  r = 0;
  for (j = 0; j < board_h; j++) {
    c = 0;
    for (i = 0; i < board_w; i++) {
      if (board[j][i]) {
        c++;
      }
    }
    board_count[j] = c;
    if (c == board_w) {
      r++;
    }
  }
  return r;
}
Counting the filled cells in each row, and how many rows are full

If any rows are full, a white bar is drawn over each one, and the game stops dropping for a second while the bars flash, five frames on and five off. Then the full rows are taken out and everything above them moves down:

function doFall() {
  var i;
  var j;
  var jj;
  var c = -1;
  j = board_h - 1;
  jj = j;
  while (j >= 0 && c) {
    c = 0;
    for (i = 0; i < board_w; i++) {
      var b = board[j][i];
      c += b > 0;
      board[jj][i] = b;
    }
    board_count[jj] = c;
    if (c == board_w) {
      jj++;
    }
    j--;
    jj--;
  }
  var rows = jj - j;
  for (j = jj; j > jj - rows; j--) {
    for (i = 0; i < board_w; i++) {
      board[j][i] = 0;
      board_count[j] = 0;
    }
  }
  return rows;
}
Taking out the full rows by copying each row down over them

It works up from the bottom with two row numbers: j, the row it's reading, and jj, the row it's writing. Each row is copied from j to jj and counted as it goes. When the row was full, jj stays where it is, so the next row up is written over it, and from then on every row lands one further down than it came from.

It stops at the first empty row. Nothing can be above an empty row, because every piece lands on something, so the rest of the board doesn't need copying. Then the rows at the top of what was copied, one for each full row, are cleared.

Source code

The ActionScript was recovered from the movie with the SWF disassembler. Flash keeps some local variables in registers, which have no names in a SWF, so they come out as r1, r2 and r3, and in the excerpts above they've been given names.

Scripts

Download