Tuesday, July 2, 2013

Recreating Tetris using Javascript and HTML5 - Part 2



In Part 1, we have created a Tetris game of square version. All the game logic has been implemented in Part 1. In Part 2, we are going to create Tetriminos and implement the related logic.


There are seven Tetriminos in a typical Tetris game, as shown on the left. We'll refer to them as I, J, L, O, S, T, Z from left to right, top to bottom. We're going to create them one by one. Start with I. I has two directions, horizontal and vertical. And it consists of four squares, as the same as other Tetriminos. When it comes to determine whether a Tetrimino should become inactive, sometimes we need to check all the four squares. Thus it would be a good idea to name these squares. The picture below shows how I name them in numbers. Knowing all its features, now let's create I.


var I = function(speed){
 var self = this;
 self.s_1 = new Square(speed, 3, 0);
 self.s_2 = new Square(speed, 4, 0);
 self.s_3 = new Square(speed, 5, 0);
 self.s_4 = new Square(speed, 6, 0);
 self.direction = dir_left;
 self.active = true;
 self.speed = speed;
 self.counter = 0;

 self.moveToLeft = function(){
  self.s_1.hPosition--;
  self.s_2.hPosition--;
  self.s_3.hPosition--;
  self.s_4.hPosition--;
 }

 self.moveToRight = function(){
  self.s_1.hPosition++;
  self.s_2.hPosition++;
  self.s_3.hPosition++;
  self.s_4.hPosition++;
 }

 self.moveDown = function(){
  self.s_1.vPosition++;
  self.s_2.vPosition++;
  self.s_3.vPosition++;
  self.s_4.vPosition++;
 }

 self.checkAndMoveToLeft = function(){
  if(self.direction == dir_left){
   if(self.s_1.hPosition > 0 && !self.s_1.fixedSquareOnLeft()){
    self.moveToLeft();
   }
  }
  else if(self.direction == dir_up){
   if(self.s_1.hPosition > 0 && !self.s_1.fixedSquareOnLeft() && !self.s_2.fixedSquareOnLeft() && !self.s_3.fixedSquareOnLeft() && !self.s_4.fixedSquareOnLeft()){    
self.moveToLeft();
   }
  }
 }
 self.checkAndMoveToRight = function(){
  if(self.direction == dir_left){
   if(self.s_4.hPosition < 9 && !self.s_4.fixedSquareOnRight()){
    self.moveToRight();
   }
  }
  else if(self.direction == dir_up){
   if(self.s_1.hPosition < 9 && !self.s_1.fixedSquareOnRight() && !self.s_2.fixedSquareOnRight() && !self.s_3.fixedSquareOnRight() && !self.s_4.fixedSquareOnRight()){    
self.moveToRight();
  
 }
  }
 }

 self.checkAndMoveDown = function(){
  if(self.direction == dir_left){
   if(self.s_1.vPosition < 19 && !self.s_1.fixedSquareUnder() && !self.s_2.fixedSquareUnder() && !self.s_3.fixedSquareUnder() && !self.s_4.fixedSquareUnder()){    
self.moveDown();
   }
  }
  else if
(self.direction == dir_up){
   if(self.s_4.vPosition < 19 && !self.s_4.fixedSquareUnder()){
    self.moveDown();
   }
  }
 }

 self.checkAndRotate = function(){
  var rotate_center_i = self.s_2.vPosition;
  var rotate_center_j = self.s_2.hPosition;

  if(self.direction == dir_left){
   if(rotate_center_i <= 17 && (rotate_center_i==0 || gameGrid[rotate_center_i-1][rotate_center_j]==0) && gameGrid[rotate_center_i+1][rotate_center_j]==0 && gameGrid[rotate_center_i+2][rotate_center_j]==0){    
self.direction = dir_up;

    // rotate
    self
.s_1.vPosition = rotate_center_i-1;
    self.s_1.hPosition = rotate_center_j;
    self.s_3.vPosition = rotate_center_i+1;
    self.s_3.hPosition = rotate_center_j;
    self.s_4.vPosition = rotate_center_i+2;
    self.s_4.hPosition = rotate_center_j;
   }
  }
  else if(self.direction == dir_up){
   if(rotate_center_j>=1 && rotate_center_j<=7 && gameGrid[rotate_center_i][rotate_center_j-1]==0 && gameGrid[rotate_center_i][rotate_center_j+1]==0 && gameGrid[rotate_center_i][rotate_center_j+2]==0){
    self.direction = dir_left;

    // rotate
    self.s_1.vPosition = rotate_center_i;
    self.s_1.hPosition = rotate_center_j-1;
    self.s_3.vPosition = rotate_center_i;
    self.s_3.hPosition = rotate_center_j+1;
    self.s_4.vPosition = rotate_center_i;
    self.s_4.hPosition = rotate_center_j+2;
   }
  }
 }

 self.fall = function(){
  if(self.counter >= self.speed){
   var s_1_can_fall = self.s_1.canFall();
   var s_2_can_fall = self.s_2.canFall();
   var s_3_can_fall = self.s_3.canFall();
   var s_4_can_fall = self.s_4.canFall();

   if(s_1_can_fall && s_2_can_fall && s_3_can_fall && s_4_can_fall){
    self.moveDown(); 
   }
   else{
    self.freeze();
    self.activee;
    checkLinesToClear();
   }

   self.counter = 0;
  }
  self.counter++;
 }

 self.freeze = function(){
  gameGrid[self.s_1.vPosition][self.s_1.hPosition] = 1;
  gameGrid[self.s_2.vPosition][self.s_2.hPosition] = 1;
  gameGrid[self.s_3.vPosition][self.s_3.hPosition] = 1;
  gameGrid[self.s_4.vPosition][self.s_4.hPosition] = 1;
 }

 self.draw = function(){
  if(self.active){
   self.s_1.draw();
   self.s_2.draw();
   self.s_3.draw();
   self.s_4.draw();
  }
 } 
  
 return self;
}

First of all, I has 7 non-function properties, s_1, s_2, s_3, s_4, direction, speed, and active. s_1~4 wrap the squares that form I. speed is how long I is going to stay at a certain height, same as the speed of squares we implemented before. The larger the number is, the slower it falls. active marks if I is still in falling, just like the active property of a square. Direction, is where the square s_1 points to. There are totally 4 directions for all the seven Tetriminoes, up, down, left, right. But not all the Tetriminoes have all the 4 directions. I, for instance, has only two of them, up and left. When it rotates, it rotates using square s_2 as the rotating point, as shown in the picture below. Note how the Square function is modified, I add two more parameters, x and y, which are the initial hPosition and vPosition of the new Square. See below

var Square = function(speed, x, y){
        ...
 self.vPosition = y;
 self.hPosition = x;
        ...


So for each new I, it will always appear at the top of the game area with s_1 pointing to left. The three function moveToLeft, moveToRight, and moveDown are pretty simple, they just move each square one position to whatever the direction is. The following four functions, checkAndMoveToLeft, checkAndMoveToRight, checkAndMoveDown and checkAndRotate are specific to I. They all first check if I is in currently in a postion that can be moved to left, right, down or rotate, and then perform the action.  The function fall uses the function canFall of Square to check if all the squares of an I is allowed to fall. If one of them is blocked, then I will be fixed using the function freeze. Otherwise, it is allowed to move down. Whenever an I freezes, we mark it as inactive and call function checkLinesToClear to see if there are any lines that can be cleared. The original function fall of Square is actually replaced by the function canFall, because you need to move some of the logic from Square to Tetriminoes. See below,


// self.fall = function(){
//  if(self.counter >= self.speed){
//   if(!self.fixedSquareUnder()){
//    self.vPosition++;    
//   }
//   else{
//    gameGrid[self.vPosition][self.hPosition] = 1;
//    self.active = false;
//    checkLinesToClear();
//   }
//   self.counter = 0;
//  }
//  self.counter++;

//  return self.active;
// }
self.canFall = function(){
 if(self.fixedSquareUnder())
  self.active = false;

 return self.active;
}

The last function draw just draw each of the square on the canvas. Among all these functions, we can easily find that almost half of them can be shared by other Tetriminoes. So we're gonna add another level of wrapping, creating a new function called Tetrimino.


var Tetrimino = function(speed){
 var self = this;
 self.active = true;
 self.speed = speed;
 self.counter = 0;

 self.moveToLeft = function(){
  self.s_1.hPosition--;
  self.s_2.hPosition--;
  self.s_3.hPosition--;
  self.s_4.hPosition--;
 }

 self.moveToRight = function(){
  self.s_1.hPosition++;
  self.s_2.hPosition++;
  self.s_3.hPosition++;
  self.s_4.hPosition++;
 }

 self.moveDown = function(){
  self.s_1.vPosition++;
  self.s_2.vPosition++;
  self.s_3.vPosition++;
  self.s_4.vPosition++;
 }

 self.fall = function(){
  if(self.counter >= self.speed){
   var s_1_can_fall = self.s_1.canFall();
   var s_2_can_fall = self.s_2.canFall();
   var s_3_can_fall = self.s_3.canFall();
   var s_4_can_fall = self.s_4.canFall();

   if(s_1_can_fall && s_2_can_fall && s_3_can_fall && s_4_can_fall){
    self.moveDown(); 
   }
   else{
    self.freeze();
    self.active = false;
    checkLinesToClear();
   }

   self.counter = 0;
  }
  self.counter++;
 }

 self.freeze = function(){
  gameGrid[self.s_1.vPosition][self.s_1.hPosition] = 1;
  gameGrid[self.s_2.vPosition][self.s_2.hPosition] = 1;
  gameGrid[self.s_3.vPosition][self.s_3.hPosition] = 1;
  gameGrid[self.s_4.vPosition][self.s_4.hPosition] = 1;
 }

 self.draw = function(){
  if(self.active){
   self.s_1.draw();
   self.s_2.draw();
   self.s_3.draw();
   self.s_4.draw();
  }
 }

 return self;
}

As we can see, I move properties/functions active, speed, moveToLeft, moveToRight, moveDown, fall, , freeze, and draw into the new function Tetrimino, because they are the same among all the Tetriminoes. And change the beginning of the function I a little bit, see below

var I = function(speed){
 var self = new Tetrimino(speed);
        ...

To experiment what we've achieved, we still need to modify some existing code. Now we've create our first Tetrimino, we actually need to control the Tetrimino directly, so we need to change the minitorKeyboard function, shown below

var monitorKeyboard = function (){
            $(document).keydown(function(evt){
              if(evt.which == 39){
               currTetrimino.checkAndMoveToRight();
              }
              else if(evt.which == 37){
               currTetrimino.checkAndMoveToLeft();
              }
              else if(evt.which == 40){
               currTetrimino.checkAndMoveDown();
              }
              else if(evt.which == 38){
               currTetrimino.checkAndRotate();
              }
              else if(evt.which == 32){

              }
            });
         }

which uses the four function, checkAndMoveToRight, checkAndMoveToLeft, checkAndMoveDown and checkAndRotate we've mentioned before. Also you need to change all the occurrence of currentSquare to currTetrimino, and assign new objects of I to currTetrimino, as shown belown

var generateNextTetrimino = function(){
 if(!currTetrimino.active) 
  currTetrimino = new I(currSpeed);
}
var currTetrimino = new I(currSpeed);

add pic ===>

Now you have a Tetris game that works perfectly, but it has only one type of Tetrimino. What we need to do next, obviously, is to implement the other six types of Tetriminoes. You can just model the other Tetriminoes after I. Some might take more work, as they have four directions, while the others, like O, which is unable to rotate, takes much less work to implement. I won't put those code in here, and you can download them from my google drive folder. The only thing I might need to point out is that, Z, instead of rotating on s_2, actually rotates in a way that all its squares change their position. The picture below shows how their position are changed.

Now we have created all the seven Tetriminoes. What to do next is to determine the sequence of Tetriminoes. We know that's implemented using some kind of random generator. But how random could that be? It could be as easy as giving the seven Tetriminoes equal odds to appear. I tried that and played for a while and it just didn't feel right. So I did a little bit research on the Internet. It turned out that different game developer uses different random generator. One common rondom generator is to generate a sequence of all seven one-sided tetrominoes (I, J, L, O, S, T, Z) permuted randomly, as if they were drawn from a bag. Then it deals all seven tetrominoes to the piece sequence before generating another bag. 

The code to implement the rondom generator.

var typeI = 0, 
    typeJ = 1, 
    typeL = 2, 
    typeO = 3, 
    typeS = 4, 
    typeT = 5, 
    typeZ = 6; 
var bag = [typeI, typeJ, typeL, typeO, typeS, typeT, typeZ];

var RandomGenOneBag = function(){
 if(bag.length == 0){
  bag = [typeI, typeJ, typeL, typeO, typeS, typeT, typeZ];
 }

 var index = ~~(Math.random()*bag.length)

 var ranNum = bag[index];
 bag.splice(index, 1);

 switch(ranNum)
  {
  case typeI:
    return new I(currSpeed);  
    break;
  case typeJ:
    return new J(currSpeed);
    break;
  case typeL:
    return new L(currSpeed);
    break;
  case typeO:
    return new O(currSpeed);
    break;
  case typeS:
    return new S(currSpeed);
    break;
  case typeT:
    return new T(currSpeed);
    break;
  case typeZ:
    return new Z(currSpeed);
    break;
  default:
    throw 'something wrong!'
  } 
}

This way, all types Tetriminoes will be selected (randomly) at least once in one iteration. Once the bag is empty, it's regenerated again.


No comments:

Post a Comment