Our tetris game wouldn’t be much fun if the shapes don’t fall. Let’s add gravity to our game.
7. Adding Gravity
To begin, we first want to conceptualize how we are going to make this work. In JavaScript, the syntax setInterval(func,intv) allows us to automatically call a function periodically at a specific milliseconds interval, where 1000 equals 1 second. Using setInterval, we can follow the following steps:
- Write a function that drop current shape by one row.
- Call function every half seconds.
With this in mind, we can start building our drop method. It is almost identical to the move method, except that we have moving down this time.
//Drop shape by one row
tetris.drop = function(){
var reverse = false;
this.fillCells(this.currentCoor,'');
this.origin.row++;
for(var i=0;i<this.currentCoor.length;i++){
this.currentCoor[i].row++;
if(this.currentCoor[i].row>21){
reverse = true;
}
}
if(reverse){
for(var i=0;i
this.currentCoor[i].row--;
}
this.origin.row--;
}
this.fillCells(this.currentCoor,'black');
}
We start our code pretty much identical to our move method, except that we are now adding one to currentCoor[i].row instead of col. We are also setting reverse to true if row number exceed 21 (last row of the playField). In our next if statement where we will be reversing our action if row number exceed 21, we will use another for loop to subtract one from each row coordinate, thus effectively reversing our action so far. Note that we are also changing our origin.row value so that our tetris.origin variable is updated. We have to do this outside of our for loop so that we are only changing our origin once. After all of these computation, we will draw cells in black.
Let’s also add the following block of code to our keydown function, so that drop function is called when we pressed the down arrow key.
else if (e.keyCode === 40){
tetris.drop();
}
Save and reload your page. You should be able to drop your shape by one row each time you press the arrow key.
Next, we will need to use setInterval to call our drop method every 500 millisecond. We will want to place this block of code inside our document.ready function, right below our keydown function.
var gravity = setInterval(function(){
tetris.drop();
},500);
Now if you save and reload, you will see that our shape is dropping by one row every 500 milliseconds.
8. Spawn New Shape
We will want a new shape to be spawned everytime currentShape reached the bottom. We already know that in our drop method, if reverse is true, our shape is at the bottom. So we basically can add something like this at the very end of our drop method…
if(reverse){
this.spawn();
}
We will just need to write our tetris.spawn function.
//Spawn random shape
tetris.spawn = function(){
var random = Math.floor(Math.random()*7);
var shapeArray = ['L','J','I','O','S','T','Z'];
this.currentShape = shapeArray[random];
this.origin = {row:2,col:5};
this.currentCoor = this.shapeToCoor(this.currentShape,this.origin);
}
Aside from the Math object that you might not have encountered before, the rest of the code should be pretty straightforward. Math.random() is a built-in JavaScript function that return a random number between 0 and 0.9999… (e.g. 0.57). By multiplying this number by 7, we will obtain a number between 0 to 6.999999…. The Math.floor method will take this random digit, and round it down to the nearest whole number (e.g. 3.9275439 = 3). By this logic, we will have a number from 0 to 6.
Next, we have declared an array of all seven types of shape. By setting currentShape equal to shapeArray[random], we are essentially grabbing a random place at shapeArray, and setting it equal to currentShape. We will then reset origin at the same place (note that I have changed it to {row:2, col:5}), and renew our new currentCoor. Since our code only animate based on the variable in currentShape, currentCoor, and origin, the shape at the bottom will no longer move once we changed these variables to reference the newly spawned shape.
Save and reload your game, and experiment with it. You will find multiple bugs with it that we will work out soon.
9. Fixing More Bug
You probably realize that there is a flaw in the logic we used to determine whether or not our shape can be move, rotate, or drop. For example,
Shapes do not stack on top of each other.
If you recall, we have been using a local variable ‘reverse’ to determine whether our shape would go out of bound, and then perform an action to reverse the previous action if it would. We will need this reverse logic to also consider the following:
- If shape would go out of bound after an action
- If shape would overlap an dead shape after an action
To solve these problem, we are going to re-write some of our codes. Here is what we are going to do.
- Write an ifReverse function that will return true if the one of the two conditions is met.
- Re-write move, rotate, and drop method to work with this new ifReverse funciton.
This sounds more intimidating than it really is. Let’s start with our new function – ifReverse:
//If we need to reverse
tetris.ifReverse = function(){
for(var i=0;i
var row = this.currentCoor[i].row;
var col = this.currentCoor[i].col;
var $coor = $('.'+row).find('#'+col);
if($coor.length === 0 || /* What HERE?*/){
return true;
}
}
return false;
}
First of all, we will be looping thru the currentCoor as we need to determine if any of the coordinates meet one of the two reverse condition. To make our code more readable, we are going to set local variables row, col, and $coor to store the JQuery expression that represent a cell at a specific coordinate. This is similar to what we have done in our fillCells method. Next, we will check out each $coor to see if any of the condition is met. If you console.log $coor in your console,
$('.21').find('#5')
===>[bgcolor='black'>]
you will see that it is an array of the DOM object related to the JQuery expression. If the DOM object doesn’t exist (out of our playField), it will simply be an empty array [].
$('.22').find('#5')
===>[]
Using this reasoning, we can set one of our condition to be if the length of $coor is 0, meaning that it is an empty array, return true.
This only takes care of one of our conditions. We will need to determine if there is a dead block in our path. How are we going to tell if we are overlapping a dead block?
To do that, we will use the JQuery method .attr. Let’s open up your console again and try this:
$('.21').find('#2').attr('bgcolor')
===>"black"
Assuming you a dead block there, this will return ‘black’. Now, using this logic, if we can set the value of bgcolor to a different value once a block become a dead block, we can then set up our second condition statement. Let’s look at our drop method and add:
//Drop shape by one row
tetris.drop = function(){
var reverse = false;
this.fillCells(this.currentCoor,'');
this.origin.row++;
for(var i=0;i
this.currentCoor[i].row++;
if(this.currentCoor[i].row>21){
reverse = true;
}
}
if(reverse){
for(var i=0;i<this.currentCoor.length;i++){
this.currentCoor[i].row--;
}
this.origin.row--;
}
this.fillCells(this.currentCoor,'black');
if(reverse){
this.fillCells(this.currentCoor,'BLACK');
this.spawn();
}
}
By adding these codes, we are setting the value of bgcolor of a dead shape to be ‘BLACK’ instead of ‘black’. Now if we save and reload our game, and use our console to check out one of the dead shape, you can see that:
$('.21').find('#2').attr('bgcolor')
===>"BLACK"
Now, we can finish our ifReverse method.
//If we need to reverse
tetris.ifReverse = function(){
for(var i=0;i<this.currentCoor.length;i++){
var row = this.currentCoor[i].row;
var col = this.currentCoor[i].col;
var $coor = $('.'+row).find('#'+col);
if($coor.length === 0 || $coor.attr('bgcolor') === 'BLACK'){
return true;
}
}
return false;
}
The function will return true if A) $coor.length === 0 or B) $coor.attr(‘bgcolor’) === ‘BLACK’. If no conditions are met after looping thru currentCoor, the function will return false.
Now that we have our ifReverse method, we will need to fix move and rotate.
for tetris.rotate:
for(var i=0;i<this.currentCoor.length;i++){
if(this.currentCoor[i].col>9 || this.currentCoor[i].col<0){
if(this.ifReverse()){
this.currentShape = lastShape;
}
}
We are getting rid of the original if statement used to determine if the shape is out of bound, and replacing with ifReverse.
Our move method will see the most change since we have made quite a few changes to our game. For example, when we first wrote our move method, we looped thru currentCoor and changed each one. Since then, we have changed to keeping track of our currentCoor as a function of currentShape and origin. Let’s re-conceptualize our move method.
- fillCells with blank.
- Move column value of origin to left or right.
- Renew currentCoor based on new origin.
- ifReverse is true, move origin back to last position.
- Renew currentCoor based on new origin again.
- fillCells with ‘black’.
This is more efficient than our old move method, to re-write it:
//Move current shape
tetris.move = function(direction){
this.fillCells(this.currentCoor,'');
//move origin
if(direction === 'right'){
this.origin.col++;
} else if (direction === 'left'){
this.origin.col--;
}
this.currentCoor = this.shapeToCoor(this.currentShape,this.origin);
if(this.ifReverse()){
if(direction === 'right'){
this.origin.col--;
} else if (direction === 'left'){
this.origin.col++;
}
}
this.currentCoor = this.shapeToCoor(this.currentShape,this.origin);
this.fillCells(this.currentCoor,'black');
}
You can see that instead of looping thru each coordinate to change their positions, we start off the method by moving origin one cell to the left or right. Immediately followed by renewing its currentCoor based on the new origin. Using the currentCoor, we can then determine if it is necessary to reverse our action. Finally, we renew our currentCoor again and draw the shape.
Save, renew, and experiment with your game! We are almost there!
We will wrap up tetris.js tomorrow by adding a method to clear a row whenever it is full, and a method to gameover when shapes are stacked to the top.