Tetris.js – Implementation of Tetris Using Javascript and JQuery (Part IV)

Let’s dive right in to wrapping up our game!

10. Empty Full Row

Check out this part on GitHub

As usual, we will first need to conceptualize how are will going to add this feature. There are many ways to accomplish this goal. We are going to go with the method below because it is the shortest way I can think of. Feel free to comment below if you have a better way.

10.concept

First we will need to loop thru our playField from bottom to top, left to right. We will scan each row to see if it has a full row of ‘BLACK’, and if it does, we will add one to a local variable called ‘drops’. Then, we will drop each row by the number of drops store in the local variable. If drops is 0, meaning that there are no full row, no changes will be made to the playField.

  1. Scan for full row starting from the bottom.
  2. If there is a full row, add one to ‘drops’.
  3. Lower each row by number of drops.

So let’s start building:

//Empty full row
tetris.emptyFullRow = function(){
  var drops = 0;

  for (var i=21; i>=0;i--){
    var rowIsFull = true;
 
    for (var j=0;j<10;j++){
      var $coor = $('.'+i).find('#'+j);
      if($coor.attr('bgcolor')!=='BLACK'){
        rowIsFull = false;
      }

      if(drops>0){
        var $newCoor = $('.'+(i+drops)).find('#'+j);
        $newCoor.attr('bgcolor',$coor.attr('bgcolor'));
      }
    }

    if(rowIsFull){
      drops++;
    }
  }
}

We will need to first declare a variable drops to store the number of full rows. Then, we will use a for loop to scan from the bottom row to the top. I am going to refer to this as the row-loop. Inside row-loop, we will first declare a boolean variable rowIsFull and set it to true. Then, we will use another for loop (the column-loop) to loop each cell from left to right. We will use a if statement to set rowIsFull to false is any of the cell does not have a bgcolor equal to ‘BLACK’. After that, still inside the column-loop, if drops is greater 0, we will set a new variable $newCoor to be the JQuery expression referencing the cell lower by the number of rows stored in drops, and we will set the ‘bgcolor’ of $newCoor to be the ‘bgcolor’ of current cell. Finally, we close our column-loop and are back to our row-loop. We will add one to drops if rowIsFull is true.

(This is one of the more complicated method we have written so far. You might want to spend more to to completely understand what is happening here.)

Now that our emptyFullRow method is done, we will need to decide where to insert this in our game. It is a good time to write out our game flow so far.

  1. drawPlayField.
  2. Renew currentCoor using currentShape and origin.
  3. Draw currentCoor using fillCells.
  4. Control current tetromino.
    1. Left/Right arrow to move.
    2. Up arrow to rotate.
    3. Down arrow to drop.
  5. Current tetromino will drop once every 500 milliseconds.
  6. Current tetromino will halt if stack at the bottom or on a dead tetromino.
  7. Spawn new tetromino.

The best place to inset our emptyFullRow method is between 6 and 7, after current tetromino halt and before new tetromino is spawned. So lets make the follow change to our drop method.

if(reverse){
  this.fillCells(this.currentCoor,'BLACK');
  this.emptyFullRow();
  this.spawn();
 }

Now, save and reload your game, and marvel at how much you have accomplished!

Afterthoughts…

I hope this tutorial is as helpful to you as it was for me. This project was the first time I felt like I could build a finished product when I first started coding. There are more features you can add to the game, such as:

  1. a gameover message when tetrominos stacked to the top of playField.
  2. some kind of points systems that reward player for clearing more rows.
  3. Start the game slow, and gravity increases as the game progress.

Give them a shot and feel free to contact me if you need help! Thanks for reading!

Demo Link

GitHub

Tetris.js – Implementation of Tetris Using Javascript and JQuery (Part III)

Our tetris game wouldn’t be much fun if the shapes don’t fall. Let’s add gravity to our game.

7. Adding Gravity

Check out this part on GitHub

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:

  1. Write a function that drop current shape by one row.
  2. 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

Check out this part on GitHub

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

Check out this part on GitHub

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.

bug3

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:

  1. If shape would go out of bound after an action
  2. 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.

  1. Write an ifReverse function that will return true if the one of the two conditions is met.
  2. 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.

  1. fillCells with blank.
  2. Move column value of origin to left or right.
  3. Renew currentCoor based on new origin.
  4. ifReverse is true, move origin back to last position.
  5. Renew currentCoor based on new origin again.
  6. 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.

Tetris.js – Implementation of Tetris Using Javascript and JQuery (Part II)

This week we are going to define different tetrominoes shape and rotate them.

4. Define Tetrominoes

Check out this part on GitHub

The seven one-sided Tetriminos in their Tetris Worlds colors. Top row, left to right: I, J, L, O. Bottom row: S, T, Z. http://en.wikipedia.org/wiki/Tetris

We will need to define all seven of these shapes so that our game knows which shape to display. We will need to think about what is the best way to store these information so that we can draw up different shapes easily. We will use the following logic:

  1. Write a function that takes in a shape name and a one-point coordinate as origin.
  2. The function will return an array of coordinates that matches the shape name, relative to the one-point coordinate.

We will first need to split up our tetris.currentCoor.

//Variable to store current coordiates
tetris.origin = {row:5,col:5};
tetris.currentShape = 'L';
tetris.currentCoor;

Using these information, once we have our new function set up, it should know that it needs to return the ‘T’ shape relative to the {row:5, col:5} origin coordinates. We will also declare a null value for currentCoor for now as a placeholder. Let’s begin writing our function:

tetris.shapeToCoor = function(shape,origin){
  if(shape === 'L'){
    return [{row:origin.row, col:origin.col},
            {row:origin.row-1, col:origin.col},
            {row:origin.row+1, col:origin.col},
            {row:origin.row+1, col:origin.col+1}]
  } 
}

The shapeToCoor function will take in two parameters as variable: shape and origin. So far, our function will first match shape to our if statement, and if shape === ‘L’, it will return an array of coordinates based on the origin provided.

The first coordinate is the origin. The second coordinate is the cell above. The third coordinate is the cell below. and the last coordinate is the cell to the lower-right.

We will then need to re-define our tetris.currentCoor using our new function. In our document-ready function, add the following between drawPlayField and fillCells:

tetris.currentCoor = tetris.shapeToCoor(tetris.currentShape,tetris.origin);

Now if you save and reload your game, you should see and ‘L’ shape, and you should be able to move it using the left and right arrow key.

4.L

Now go ahead and try to define ‘J’,’I’,’O’,’S’,’T’ and ‘Z’ on your own by adding more else..if statement to shapeToCoor. One thing you should be careful of is that you use the center point of the shape as your origin. This will make rotating the shape easier in the next section. You can refer to the source code on github here if you get stuck.

5. Rotate your Shapes

Check out this part on GitHub

The easiest way to rotate your shape is to basically think of them as different shapes. For example, we can define shape ‘L90′,’L180’, and ‘L270’ to represent the ‘L’ shape rotated by 90,180, and 270 degrees clockwise, respectively. Before we do that, let’s list out all the possible shapes and orientation we will need.

  • L Shape: L, L90, L180, L270
  • J Shape: J, J90, J180, J270
  • I Shape: I, I90
  • O Shape: O
  • S Shape: S, S90
  • T Shape: T, T90, T180, T270
  • Z Shape: Z, Z90

You will see that not all shape need 4 different orientation. For example, the S shape at 180 degrees clockwise will look the same as its origin position. Let’s define L90 together by adding the following else..if statement in our shapeToCoor funciton:

else if(shape === 'L90'){
  return [{row:origin.row,col:origin.col},
          {row:origin.row,col:origin.col+1},
          {row:origin.row,col:origin.col-1},
          {row:origin.row+1,col:origin.col-1}];
}

Note that our ‘L90’ will have the same origin coordinates as our ‘L’ shape. In order to achieve a rotating effect, we will need to use the center point as our origin.

Now we will need a function to rotate the shape. Basically, we want to achieve the following:

  1. Call rotate function when ‘up’ arrow key is pressed
  2. Set currentShape to ‘L90’ if currentShape === ‘L’
  3. Draw new shape

Let’s begin writing our rotate function:

tetris.rotate = function(){
  this.fillCells(this.currentCoor,'');
  if(this.currentShape === 'L'){
    this.currentShape = 'L90';
  } else if(this.currentShape === 'L90'){
    this.currentShape = 'L';
  }
  
  this.currentCoor = this.shapeToCoor(this.currentShape,this.origin);
  this.fillCells(this.currentCoor,'black');
}

Similar to our move function, we will first erase the current shape by calling this.fillCells(this.currentCoor,”). We will then add an if statement to change currentShape to ‘L90’ if it is ‘L’, as well as an else..if statement to change currentShape to ‘L’ if it is ‘L90‘. We will then  renew our currentCoor based on the new shape, and fillCells with ‘black’.

We will then need to add the following to our keydown function so that rotate is called every time the up arrow is pressed.

else if (e.keyCode === 38){
  tetris.rotate();
}

Now saved and reload your page. You should be able to rotate your shape by pressing the up arrow.

4.rotate

Go ahead and add the rest of the shapes to shapeToCoor and rotate functions. You can check the source code here if you have any questions. We will do our first debugging in our next section.

6. Fixing Two Bugs

Check out this part on GitHub

If you experiment with our game right now, you will find a couple bugs. The most noticeable one is that if we move our shape left or right, and then rotate it, our currentShape will pop back to the original location.

6.bug1

To fix this, we need to trace in our codes back to the root of the problem. Let visit our rotate function first. When we call our rotate function, it first uses the currentShape to determine what is the new shape. Then, it uses the origin variables to determine what are our new coordinates. Since our shape pop back to its original location, it means that our origin variable has not been updated after we move our shapes.

tetris.move = function(direction){
  var reverse = false;
  this.fillCells(this.currentCoor,'');
  
  for(var i=0;i<this.currentCoor.length;i++){
    if(direction === 'right'){
      this.currentCoor[i].col++;
      if(this.currentCoor[i].col>9){
        reverse = true;
      }
    } else if (direction === 'left'){
      this.currentCoor[i].col--;
      if(this.currentCoor[i].col<0){
        reverse = true;
      }
    }
  }
  this.fillCells(this.currentCoor,'black');

  if(reverse && direction === 'left'){
    this.move('right');
  } else if (reverse && direction === 'right'){
    this.move('left');
  }
}

Referring back to our move function, we can see that only currentCoor is changed, and there is no change to our origin variable after we move the shape. In order to fix that, let’s add the following to our move function right after the for loop.

//move origin
 if(direction === 'right'){
   this.origin.col++;
 } else if (direction === 'left'){
   this.origin.col--;
 }

Without making dramatic change to our move function, this if statement will add 1 to origin.col if right is pressed, and subtract 1 from origin.col if left is pressed.

There is another bug in our game. If we move our shape all the way to the left, and then rotate it, you will see that the shape rotated as if the border doesn’t exist.

6.bug2

We had a similar bug when we were building our move function. We can use the same logic to fix our rotate function.

tetris.rotate = function(){
  var lastShape = this.currentShape;
  this.fillCells(this.currentCoor,'');

  //all the if...else if statements...

  this.currentCoor = this.shapeToCoor(this.currentShape,this.origin);

  for(var i=0;i<this.currentCoor.length;i++){
    if(this.currentCoor[i].col>9 || this.currentCoor[i].col<0){
      this.currentShape = lastShape;
    }
  }

  this.currentCoor = this.shapeToCoor(this.currentShape,this.origin);
  this.fillCells(this.currentCoor,'black');
}

We first declare a variable called lastShape, and we will set it equal to the currentShape. After all rotation logics and currentCoor has been renewed, we will then loop around the currentCoor and see if any of the coordinates has a col value greater than 9 or less than 0. If so, we will set currentShape back to lastShape. Finally, we will renew our currentCoor again using the new shape, and then fillCells in ‘black’.

Save and reload your game. You should see shape will not rotate if rotating will cause it to go out of bound.

That’s it for now. Next post we will add gravity, spawn random shape, and the ability to clear rows when it is full.

Tetris.js – Implementation of Tetris Using Javascript and JQuery (Part I)

Re-building Tetris using simple Javascript and JQuery was one of the things I did to prepare for Hack Reactor’s technical interview. This project exposed me to some data manipulation algorithms, which ended up to be tremendously helpful for the interview. This tutorial assumes that you have already been introduced to JQuery. Let’s get started!

1. Building the DOM

Check out this part on GitHub

We will start with building the Playfield, which is simply a table that is 10 cells wide and 22 cells tall. We will want each row and cell to have a number id so that we can reference to them easily using JQuery. For example:

<tr class='0'>
  <td id='0'></td>
  <td id='1'></td>
  <td id='2'></td>
  <td id='3'></td>
</tr>

If we want to select cell #1 using JQuery, we can simply select it using JQuery selector $(‘.0’).find(‘#1’). Since we want a 10X22 grid, we will need 22 rows, and within each row, 10 cells. Instead of manually typing out all 220 lines of html, we can use JQuery’s append method to help build our table. We first need to create an index.html file with the following code:

<!DOCTYPE html>
<html>
  <head>
    <title>Tetris.js</title>
    <link rel="stylesheet" type="text/css" href="./style.css">
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    <script src='./tetris.js'></script>
  </head>
  <body> 
    <table id='playfield'></table>
  </body>
</html>

In the same folder, go ahead and create a file called tetris.js and style.css. Our javascripts and css will go there, respectively.

Now in our tetris.js file, do the following:

//Declare global object tetris
var tetris = {};
//Draw the grid
tetris.drawPlayField = function(){
  for(var row=0;row<22;row++){
    $('#playfield').append('');
    for (var col=0;col<10;col++){
      $('.'+row).append('');
    }
 }
}

The drawPlayField method contains two for loops. The first loops from 0 to 21, and insert a with the row number as class at the end of tag. The second loop lives within the first loop, and it goes from 0 to 9, inserting a tag. This method effectively creates the 10>22 grid, with each rows and columns uniquely numbered.

Let’s called our drawPlayfield after getting our document ready. Insert the following code after the drawPlayField method.

$(document).ready(function(){
  tetris.drawPlayField();
})

Now, add the following to our style.css:

td {
  width: 25px;
  height: 25px;
  border: 1px solid black;
}

This gives our cell a  1px solid black border, as well as a fixed height and width of 25px.

Saves everything and open index.html with your browser. You should see the following:

1. Grid

2. Draw on the Grid

Check out this part on GitHub

We first need a method to draw something on the grid. Basically, we want a method that:

  1. Take an array of coordinates as parameters.
  2. Loops thru the array.
  3. Fill each cell matching the coordinates black.

We need to decide how we want to format our array of coordinates. I always like to name my variables in context for ease of reading, so I am going to use an object like this to store my coordinates:

{row: 1,col: 1}

An array of coordinates will look like this:

[{row: 1,col: 1},{row:2,col:1},{row:3,col,1}]

So with this in mind, let’s build our method:

//Fill the cells
tetris.fillCells = function(coordinates){
  for(var i=0;irow = coordinates[i].row;
    var col = coordinates[i].col;
    var $coor = $('.'+row).find('#'+col);
    $coor.attr('bgcolor','black');
  }
}

The fillcells method takes in an array of coordinates and loop through each on of it. During each iteration, it will store the row number in local variable row, col number in local variable col, and the JQuery selector in local variable $coor. It will then change the bgcolor of whatever the $coor referenced to be ‘black’.

To test if this works, let’s declare a variable to store our current coordinates. For now we will use the following coordinates, which is basically a 2X2 square.

//Variable to store current coordiates
tetris.currentCoor = [{row:1,col:1},
                      {row:1,col:2},
                      {row:2,col:1},
                      {row:2,col:2}];

Then let’s add this in our document.ready function.

tetris.fillCells(tetris.currentCoor,'black');

You should see the following once you save and refresh your page.
2.fillCells

Let’s say instead of filling it black, I want it to able to choose the color I want when calling tetris.fillCells. I can then change my tetris.fillCells to the following:

//Fill the cells
tetris.fillCells = function(coordinates, fillColor){
  for(var i=0;irow = coordinates[i].row;
    var col = coordinates[i].col;
    var $coor = $('.'+row).find('#'+col);
    $coor.attr('bgcolor',fillColor);
  }
}

With this edit, let’s say if I want to fill a cell to be blue instead of black, I can simply call tetris.fillCells(tetris.currentCoor,’blue’).

3. Move the Shape Left and Right

Check out this part on GitHub

Now that we can draw shapes on our Playfield, let’s build a method to move the shape left and right. This is actually not that difficult if we break down the method into the following steps.

  1. Erase current shape.
  2. Move the column of current coordinates to +/- 1.
  3. Re-draw the shape with new coordinates.

Let’s start building.

tetris.move = function(direction){
  this.fillCells(this.currentCoor,'');
}

The move method takes in ‘left’ or ‘right’ as a parameter and move the shape accordingly. The keyword this is referring to the object in which the method was called, which is tetris in this case. So far, our method will call the fillCells method on currentCoor, and change its bgcolor to nothing.

Save and refresh your page. Now assuming you are using Chrome, hit Ctrl+Shift+J to call up the console, and the enter:

tetris.move()

You should see that your shape is now disappear. Now let’s build the next step of our method, moving the coordinates.

tetris.move = function(direction){
  this.fillCells(this.currentCoor,'');
  for(var i=0;icurrentCoor.length;i++){
    if(direction === 'right'){
      this.currentCoor[i].col++;
    } else if (direction === 'left'){
      this.currentCoor[i].col--;
    }
  }
}

This will loop thru the currentCoor array, and for each iteration, depending on if the direction parameter is ‘left’ or ‘right’, it will add or subtract 1 from the col value of current coordinate, respectively.

Now to re-draw the shape:

//Move current shape
tetris.move = function(direction){
  this.fillCells(this.currentCoor,'');
  for(var i=0;icurrentCoor.length;i++){
    if(direction === 'right'){
      this.currentCoor[i].col++;
    } else if (direction === 'left'){
      this.currentCoor[i].col--;
    }
  }
  this.fillCells(this.currentCoor,'black');
}

Save and refresh again. Now in your console, try tetris.move(‘right’) and tetris.move(‘left’). You should see your shape move!

We want to be able to move our shape using the directional key on our keyboard. In JQuery, we can use the keydown method to capture the event when a key is pressed. In our document ready function, add the following:

$(document).keydown(function(e){
  console.log(e.keyCode);
}

This function will be triggered whenever a key is pressed down anywhere in your document. The parameter e basically represent the event of keydown. Each keydown event has a unique keyCode for each key on your keyboard. You can google the keyCode for ‘left’ and ‘right’. In this example, I am adding in a line to console.log the keyCode of the keydown event whenever a key is pressed. You can then simply go to your console and see that keyCode 39 and 37 is logged whenever ‘right’ and ‘left’ is pressed, respectively.

Now, add the following:

$(document).keydown(function(e){
  console.log(e.keyCode);
  if(e.keyCode === 39){
    tetris.move('right');
  } else if (e.keyCode === 37){
    tetris.move('left');
  }
 })

We are adding a simple if…else if statement to call tetris.move(‘right’) when keyCode is 39, or tetris.move(‘left’) when keyCode is 37. Save and reload your page, and then press left and right to test your code. You should be able to move your shape now!

You will find out that the shape can go off the Playfield. That’s because our tetris.move method is not checking to see if our shape hit the border. In order to achieve this, we need to:

  1. See if our new position is off the playfield.
  2. If so, call tetris.move in the opposite direction to cancel original move.

Let’s add this in our code:

//Move current shape
tetris.move = function(direction){
  var reverse = false;
  this.fillCells(this.currentCoor,'');

  for(var i=0;i<this.currentCoor.length;i++){
    if(direction === 'right'){
      this.currentCoor[i].col++;
      if(this.currentCoor[i].col>9){
        reverse = true;
      }
    } else if (direction === 'left'){
      this.currentCoor[i].col--;
      if(this.currentCoor[i].col<0){
        reverse = true;
      }
    }
  }
  this.fillCells(this.currentCoor,'black');

  if(reverse && direction === 'left'){
    this.move('right');
  } else if (reverse && direction === 'right'){
    this.move('left');
  }
}

First, we are declaring a new local variable called reverse, and set its original value to false.  Then, we are adding two if statement within our loop, after we calculate our coordinate, and if the new coordinate is off the playfield (greater than 9 or less than 0), set reverse to true.

Finally, after our loop, we will add another if statement so that if 1) reverse is true, AND 2) direction equal ‘left‘, call tetris.move(‘right’), and vice versa. You can now save and reload your page, and see if your code works!

That’s it for now. We are about a third of the way there. Next week we will define all other tetris shape, and make them rotate. Stay tuned!