Wednesday, July 17, 2013

Recreating Tetris using Javascript and HTML5 - Part 3

 Add one more control feature
The Tetris game I'm modeling after, allows player to push any key (except up, down, left and right) to move a falling Tetrimino immediately down to the bottom and gets itself freezed. In my game, I'm gonna use the space key to handle this funciton.

To implement this functionality, I added the following functions to Tetrimino. Yep, that's it! These two functions takes care of the all the Tetriminoes in all directions. Of course, you also need to add the event handler in function monitorKeyboard

self.moveToBottom = function(){
 var numLevelMoveDownS_1 = self.s_1.findTheHighestFixedSquareUnder() - self.s_1.vPosition - 1;
 var numLevelMoveDownS_2 = self.s_2.findTheHighestFixedSquareUnder() - self.s_2.vPosition - 1;
 var numLevelMoveDownS_3 = self.s_3.findTheHighestFixedSquareUnder() - self.s_3.vPosition - 1;
 var numLevelMoveDownS_4 = self.s_4.findTheHighestFixedSquareUnder() - self.s_4.vPosition - 1;
 var numLevelMoveDown = Math.min(numLevelMoveDownS_1, numLevelMoveDownS_2, numLevelMoveDownS_3, numLevelMoveDownS_4)
 
 self.s_1.vPosition += numLevelMoveDown;
 self.s_2.vPosition += numLevelMoveDown;
 self.s_3.vPosition += numLevelMoveDown;
 self.s_4.vPosition += numLevelMoveDown;
}

self.moveToBottomAndFreeze = function(){
 self.moveToBottom();
 self.freeze();
 var n = checkLinesToClear();
}

var monitorKeyboard = function (){
            $(document).keydown(function(evt){
              ...
              else if(evt.which == 32){
               currTetrimino.moveToBottomAndFreeze();
              }
            });
         }

Pause/Restart Game
While playing, player might want to pause/restart game. To implement this, we need to stop the execution of our gameLoop and later restart it. Below is the code we need to add.

var gameOn = true;
var gameLoop;

var pause_restart = function(){
 if(gameOn){
  clearInterval(gameLoop);
  gameOn = false;
 }
 else{
  startGame();
  gameOn = true;
 }

}
var monitorKeyboard = function (){
            $(document).keydown(function(evt){
              if(evt.which == 39 && gameOn){
               currTetrimino.checkAndMoveToRight();
              }
              else if(evt.which == 37 && gameOn){
               currTetrimino.checkAndMoveToLeft();
              }
              else if(evt.which == 40 && gameOn){
               currTetrimino.checkAndMoveDown();
              }
              else if(evt.which == 38 && gameOn){
               currTetrimino.checkAndRotate();
              }
              else if(evt.which == 32 && gameOn){
               currTetrimino.moveToBottomAndFreeze();
              }
              else if(evt.which == 80){
               pause_restart();
              }
            });
         }
var startGame = function(){
 gameLoop = setInterval(function(){
  clearCanvas();
  generateNextTetrimino();
  updatePosition();
  drawOnCanvas();
 }, 1000/50);
}

The global variable gameOn indicates whether the game is in play or paused. We assign the returning value of the setInterval function to another global variable gameLoop, on which we can call clearInterval later to pause the game. I also wrap up the whole gameLoop within a new function startGame, which we can use to restart the game. We also need to add one more keyboard handler in function monitorKeyboard. I use 'p' as the key to control pausing and restarting. Don't forget to disable all the other keys when game is paused by adding "&&gameOn" after each condition.

Game Statistics
For now, we haven't written any code that deal with game statistics, i.e. at which level the game is, how many lines have been cleared, and total points. We've already reserve some space for game statistics at the right hand side of the game area. Now let's put something into it.

var statsGameLevel = 1, 
    statsLinesCleared = 0,
    statsPoints = 0;

var updateStats = function(lines){
 statsLinesCleared += lines;
 statsPoints += lines*lines*50 + lines*50;

 if(statsLinesCleared < 100){
  statsGameLevel = 1
  currSpeed = speedLevels[0];
 }
 else if(statsLinesCleared >= 100 && statsLinesCleared < 200){
  statsGameLevel = 2
  currSpeed = speedLevels[1];
 }
 else if(statsLinesCleared >= 200 && statsLinesCleared < 300){
  statsGameLevel = 3
  currSpeed = speedLevels[2];  
 }
 else if(statsLinesCleared >= 300 && statsLinesCleared < 400){
  statsGameLevel = 4
  currSpeed = speedLevels[3];  
 }
 else if(statsLinesCleared >= 400){
  statsGameLevel = 5
  currSpeed = speedLevels[4];  
 }
}

Three more global variables, statsGameLevel, statsLinesCleared, statsPoints are added. statsGameLevel  is the difficulty level of the game, divided into 5 level. From level 1 to level 5, the speed is gonna be 20, 16, 12, 10, 8. statsLinesCleared stores the number of lines have been cleared so far, for each 100 lines clear, the difficulty level will increased by 1, unless the difficulty level is 5, and it is the highest level. StatsPoints stores the points player earns for clearing lines; if a single line is cleared, player gets 100 points, and the more lines cleared at one time, player gets bonus points. The lines cleared at one time and the points earned are listed in the table below.

Lines Cleared Points Earned
1 100
2 300
300 600
4 1000

Function updateStats takes number of lines cleared as a single parameter, and updates all the three statistics variables. And this function needs to be called right after when function checkLinesToClear is called.

self.moveToBottomAndFreeze = function(){
 self.moveToBottom();
 self.freeze();
 var n = checkLinesToClear();
 updateStats(n);
}

 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();
    var n = checkLinesToClear();
    updateStats(n);
   }

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

Like any other objects that's on canvas, you need a function to draw the game statistics on canvas, shown below.

var drawStats = function(){
 ctx.fillStyle = "Black";
 ctx.font = '15pt Calibri';
 ctx.fillText("LEVEL: ", 210, 120);
 ctx.fillText("LINES: ", 210, 160);
 ctx.fillText("POINTS: ", 210, 200);
 ctx.fillText(statsGameLevel, 210, 140);
 ctx.fillText(statsLinesCleared, 210, 180);
 ctx.fillText(statsPoints, 210, 220);
}

Make The Game Nicer
Currently all the squares are all drawn in pure black color, that's neat but may not be so user-friendly. So we're gonna use the image shown below to fill out the square.



And now our game looks like this.




















There are three place we need to add or change the code. First, declare a new global Image object and points it to square.png. In the function drawFixedSquares and the draw function of Square. Comment out the code that draw the squares and replace it with the code that draws the square.png.

var squareImage = new Image();
squareImage.src = 'square.png';

var drawFixedSquares = function(){
 for(var i=0; i<20; i++){
  for(var j=0; j<10; j++){
   if(gameGrid[i][j] == 1){
    // ctx.fillStyle = "Black";  
    // ctx.fillRect(j*squareLength, i*squareLength, squareLength, squareLength);
    try{
     ctx.drawImage(squareImage, 0, 0, squareLength, squareLength, j*squareLength, i*squareLength, squareLength, squareLength)
    } catch(e)
    {
     console.log('drawImage not work')
    }
   }
  }
 }
}

 self.draw = function(){
  if(self.active){
   // ctx.fillStyle = self.color;  
   // ctx.fillRect(self.hPosition*squareLength, self.vPosition*squareLength, squareLength, squareLength);

   try{
    ctx.drawImage(squareImage, 0, 0, squareLength, squareLength, self.hPosition*squareLength, self.vPosition*squareLength, squareLength, squareLength)
   } catch(e)
   {
    console.log('drawImage not work')
   }
  }
 }

End The Game
As we know, in a Tetris game, if the Tetriminoes reaches the top of the game area, the game ends. In our game, we also need that mechanism. One thing to notice is that the Tetriminoes reaching the top does not necessarily mean the end of a game. For instance, if we have a layout like below, and the next Tetrimino is J. Upon its appearance, the player immediately move the J to the left most side, which ends with layout shown below. Because this layout doesn't prevent the next Tetrimino from generating, it is not the end of the game. The way to check the end of a game is to check if there is any fixed squares there're in the initial position of next Tetrimino. And this should be checked right before each new Tetrimino is generated on canvas. Added code is shown below.

var generateNextTetrimino = function(){
 if(!currTetrimino.active){
  currTetrimino = RandomGenOneBag();

  if(checkEndOfGame(currTetrimino))
   endGame();
 }
  
}

var checkEndOfGame = function(tetri){
 if(gameGrid[tetri.s_1.vPosition][tetri.s_1.hPosition] == 1 || gameGrid[tetri.s_2.vPosition][tetri.s_2.hPosition] == 1 || gameGrid[tetri.s_3.vPosition][tetri.s_3.hPosition] == 1 || gameGrid[tetri.s_4.vPosition][tetri.s_4.hPosition] == 1){
  return true;
 }

 return false;
}

var endGame = function(){
 clearInterval(gameLoop);
 gameOn = false;
 setTimeout(function(){
  ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
  ctx.fillRect(0, 0, 200, canvasHeight);

  ctx.fillStyle = "Black";
  ctx.font = '25pt Calibri';
  ctx.fillText("GAME OVER", 15, 180);
 },100)
}

The new function endGame stops the gameLoop, marks gameOn as false and draw a half transparent white color on the game area and put the "Game Over" in the middle.



















Add The Clearing Line Animation
In a typical Tetris game, it usually has some kind of animation when clearing lines. Old version of Tetris game usually make the lines that're going to be cleared twinkle. The twinkling can be achieved by alternating between blank line and square-filled line. Three functions are added shown below.

var lineDisappear = function(linesToClear){
 var len = linesToClear.length;
 for(var i=0; i<len; i++){
  ctx.fillStyle = 'White';
  ctx.beginPath();
  ctx.rect(0, linesToClear[i]*squareLength, canvasWidth, squareLength);
  ctx.closePath();
  ctx.fill();
 }
}

var lineAppear = function(linesToClear){
 var len = linesToClear.length;
 for(var i=0; i<len; i++){
  for(var j=0;j<10;j++){
   ctx.drawImage(squareImage, 0, 0, squareLength, squareLength, j*squareLength, linesToClear[i]*squareLength, squareLength, squareLength)
  }
 }
}

var clearingLineAnimation = function(linesToClear){
 if(linesToClear.length==0)
  return;

 setTimeout(function(){
  clearInterval(gameLoop);
 }, 1000/10);

 var animateTimes = 0;
 clearLineAnimation = setInterval(function(){
  if(animateTimes<6){
   if(animateTimes%2 ==0)
    lineDisappear(linesToClear);
   else if(animateTimes%2 ==1)
    lineAppear(linesToClear);

   animateTimes++;
  }
  else{
   clearInterval(clearLineAnimation);
   startGame();
   updateGameGrid(linesToClear);
  }
   
 }, 1000/10);
}
var checkLinesToClear = function(){
 var fixSquareNum;
 var linesToClear = [];
 var counter = 0;

 for(var i=0; i<20; i++){
  fixSquareNum = 0;
  for(var j=0; j<10; j++){
   fixSquareNum += gameGrid[i][j];
  }
  if(fixSquareNum == 10){
   linesToClear[counter] = i;
   counter++;
  }
 }
 clearingLineAnimation(linesToClear);
        updateGameGrid(linesToClear);
 return counter;
}

clearingLineAnimation takes an array of line number as parameter, if the array is not empty, it will first cease the game loop, and then it will call function lineAppear, and lineDisappear alternatively, three time for each of them. Finally, it ceases the clearLineAnimation and restart the game loop. Note that we also need to move the code updateGameGrid(linesToClear) from function checkLinesToClear to clearingLineAnimation. This way, we can see that whenever a line is ready to be cleared, it first twinkles and then all the other lines  get updated. If we leave it as it was, the animation just doesn't look right.

Implement The Next Tetrimino Display Area
Our final step is to implement the functionality that while dealing with the current Tetrimino, the user can see which Tetrimino will come next. The next Tetrimino is usually shown above the statistics. It only takes 10 minutes to implement this functionality. See the code change below.

var nextTetrimino = RandomGenOneBag();

var drawOnCanvas = function(){
 drawLine();
 currTetrimino.draw();
 nextTetrimino.drawStandBy();
 drawFixedSquares();
 drawStats();
}

self.drawStandBy = function(){
 if(self.active){

  try{
   ctx.drawImage(squareImage, 0, 0, squareLength, squareLength, self.hPosition*squareLength+155, self.vPosition*squareLength+30, squareLength, squareLength)
  } catch(e)
  {
   console.log('drawImage not work')
  }
 }
}

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

var generateNextTetrimino = function(){
 if(!currTetrimino.active){
  currTetrimino = nextTetrimino;
  nextTetrimino = RandomGenOneBag();

  if(checkEndOfGame(currTetrimino))
   endGame();
 } 
}

First of all, you need to add a new global variable nextTetrimino and assign a random Tetrimino to it. This global variable stands for the next Tetrimino that will be shown above the statistics. To make the next Tetrimino show at the right place, I wrote two functions, both called drawStandBy, and added them to Square and Tetrimino respectively. The first drawStandBy function belongs to Square, and the only difference between the draw function and drawStandBy is that the latter increases the horizontal value and vertical value by 155 and 30 respectively, which draws the square right above the statistics. The drawStandBy function of Tetrimino just calls the drawStandBy function of Square, which draws a Tetrimino above the statistics. Don't forget to call drawStandBy function in drawOnCanvas. Finally, you need to change the function generateNextTetrimino a little bit: whenever the current Tetrimino becomes inacitve, assign the nextTetrimino to currTetrimino and assign a new random Tetrimino to nextTetrimino.

Congrats! Now you have a fully functional Tetris game. Get the code here.
Play the game here.





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.