Now that we have a game state grid variable, we can use it to implement basic collision detection. We want to prevent moving the current game piece into any existing tetriminos or off the grid boundary. To do this, we need to modify the setGrid function so that it can check to see if a grid cell is empty. We already use the t parameter to indicate the block type, or if we want to clear the cell, so we can also use it to indicate that we only want to check the value without actually returning anything. We’ll use a value of -1 to indicate that we are performing a check, and a value of 0 or greater to actually modify the grid. The return statement tells the JavaScript interpreter to immediately exit the function and to return a specific value to the calling function. We’ll see how that works in a minute. Modify the setGrid function to look like this:
/*************************************************
Sets a grid cell in the game state grid
x = [0,9] x-coordinate
y = [0,19] y-coordinate
t = [-1,7] test or block type
*************************************************/
function setGrid(x, y, t) {
//Check if point is in range
if(x >= 0 && x < 10 && y >= 0 && y < 20) {
//Return test result if testing
if(t < 0) return grid[y][x] == 0;
//Otherwise assign block type to the grid
grid[y][x] = t;
return true;
}
return false;
}
You may notice that I’ve used a one-line if statement. This is a compact way of writing the code that doesn’t use curly braces, but only works for a single statement.
The setGrid function is only called in the drawTetrimino function, so we need a way to check the returned value. Again, we’ll modify this function to accept a d parameter of -1 to only perform a drawing test, and then return either true or false. This means that we use a d value of 1 to draw, 0 to erase, and -1 to test. We can obtain the proper value to send to each setGrid function call with the following code:
//Determine the value to send to setGrid c = -1; if(d >= 0) c = t*d;
Now we need a way to check if all the setGrid function calls returned true. We want drawTetrimino to return true only if all of the setGrid functions return true, otherwise it should return false. To do this, we’ll use the logical AND operator, represented as && in JavaScript. We start with a variable valid initialized to true.
//Initialize validity test valid = true;
After each call to setGrid, valid is set to it’s current value ANDed with the value returned from setGrid.
valid = valid && setGrid(x,y,c);
This returns true only if both values were true, effectively setting valid to false if any of the setGrid calls return false. At the end of the function, we return the valid variable.
return valid;
The last thing we need to do to implement collision detection is actually perform the check each time before we update the current position. I’ve modified the keyDown function to perform these extra checks.
/*************************************************
Responds to a key press event
*************************************************/
function keyDown(e) {
if(e.keyCode == 37) { //Left arrow
drawTetrimino(x,y,t,o,0); //Erase the current tetrimino
x2 = x - 1;
if(drawTetrimino(x2,y,t,o,-1)) //Check if valid
x = x2;
}
else if(e.keyCode == 38) { //Up arrow
drawTetrimino(x,y,t,o,0); //Erase the current tetrimino
o2 = (o + 1) % 4;
if(drawTetrimino(x,y,t,o2,-1)) //Check if valid
o = o2;
}
else if(e.keyCode == 39) { //Right arrow
drawTetrimino(x,y,t,o,0); //Erase the current tetrimino
x2 = x + 1;
if(drawTetrimino(x2,y,t,o,-1)) //Check if valid
x = x2;
}
else if(e.keyCode == 40) { //Down arrow
drawTetrimino(x,y,t,o,0); //Erase the current tetrimino
y2 = y - 1;
if(drawTetrimino(x,y2,t,o,-1)) //Check if valid
y = y2;
}
else if(e.keyCode == 32) { //Space-bar
drawTetrimino(x,y,t,o,0); //Erase the current tetrimino
//Move down until invalid
while(drawTetrimino(x,y-1,t,o,-1))
y -= 1;
}
else {
//Create a new tetrimino
t2 = 1 + Math.floor((Math.random()*7));
x2 = 4;
y2 = 18;
o2 = 0;
//Check if valid
if(drawTetrimino(x2,y2,t2,o2,-1)) {
t = t2;
x = x2;
y = y2;
o = o2;
}
}
//Draw the current tetrimino
drawTetrimino(x,y,t,o,1);
//Redraw the grid
drawGrid();
}
For each of the possible key presses, we first erase the existing tetrimino, and then create a temporary variable with what the new position would be. We check to see if this position would be valid, and if so, we commit the change to the position variables. The exception is the general case when we create a new tetrimino. Here, we don’t erase the last tetrimino and only update the position variables if they produce a tetrimino with no collisions. Finally, we draw the tetrimino with the new position variables and call the drawGrid() function to redraw the canvas.
An addition feature I’ve included in the keyDown function is the space-bar drop. In this case, we use a while loop to continuously decrease the y coordinate until the check returns false. Similar to a for loop, a while loop will execute the contents of it’s block (in our case a single line) as long as its condition is true. The complete code should now look like this:
<!DOCTYPE html>
<html>
<head>
<title>Tetris</title>
<script>
//Golbal variables
var ctx; //Canvas object
var t; //Tetrimino type
var x, y; //Tetrimino position
var o; //Tetrimino orientation
var grid; //Game state grid
/************************************************
Initialize the drawing canvas
************************************************/
function initialize() {
//Get the canvas context object from the body
c = document.getElementById("myCanvas");
ctx = c.getContext("2d");
//Initialize tetrimino variables
t = 1 + Math.floor((Math.random()*7));
x = 4;
y = 18;
o = 0;
//Create an empty game state grid
grid = new Array(20);
for(i = 0; i < 20; i++) {
grid[i] = new Array(10);
for(j = 0; j < 10; j++)
grid[i][j] = 0;
}
}
/************************************************
Draws the current game state grid
************************************************/
function drawGrid() {
//Clear the canvas
ctx.clearRect(0,0,200,400);
//Loop over each grid cell
for(i = 0; i < 20; i++) {
for(j = 0; j < 10; j++)
drawBlock(j, i, grid[i][j]);
}
}
/************************************************
Draws a block at the specified game coordinate
x = [0,9] x-coordinate
y = [0,19] y-coordinate
t = [0,7] block type
************************************************/
function drawBlock(x, y, t) {
//Check if a block needs to be drawn
if(t > 0) {
//Get the block color
var c;
if(t == 1) //I type
c = 180; //Cyan
else if(t == 2) //J type
c = 240; //Blue
else if(t == 3) //L type
c = 40; //Orange
else if(t == 4) //O type
c = 60; //Yellow
else if(t == 5) //S type
c = 120; //Green
else if(t == 6) //T type
c = 280; //Purple
else //Z type
c = 0; //Red
//Convert game coordinaes to pixel coordinates
pixelX = x*20;
pixelY = (19-y)*20;
/**** Draw the center part of the block ****/
//Set the fill color using the supplied color
ctx.fillStyle = "hsl(" + c + ",100%,50%)";
//Create a filled rectangle
ctx.fillRect(pixelX+2,pixelY+2,16,16);
/**** Draw the top part of the block ****/
//Set the fill color slightly lighter
ctx.fillStyle = "hsl(" + c + ",100%,70%)";
//Create the top polygon and fill it
ctx.beginPath();
ctx.moveTo(pixelX,pixelY);
ctx.lineTo(pixelX+20,pixelY);
ctx.lineTo(pixelX+18,pixelY+2);
ctx.lineTo(pixelX+2,pixelY+2);
ctx.fill();
/**** Draw the sides of the block ****/
//Set the fill color slightly darker
ctx.fillStyle = "hsl(" + c + ",100%,40%)";
//Create the left polygon and fill it
ctx.beginPath();
ctx.moveTo(pixelX,pixelY);
ctx.lineTo(pixelX,pixelY+20);
ctx.lineTo(pixelX+2,pixelY+18);
ctx.lineTo(pixelX+2,pixelY+2);
ctx.fill();
//Create the right polygon and fill it
ctx.beginPath();
ctx.moveTo(pixelX+20,pixelY);
ctx.lineTo(pixelX+20,pixelY+20);
ctx.lineTo(pixelX+18,pixelY+18);
ctx.lineTo(pixelX+18,pixelY+2);
ctx.fill();
/**** Draw the bottom part of the block ****/
//Set the fill color much darker
ctx.fillStyle = "hsl(" + c + ",100%,30%)";
//Create the bottom polygon and fill it
ctx.beginPath();
ctx.moveTo(pixelX,pixelY+20);
ctx.lineTo(pixelX+20,pixelY+20);
ctx.lineTo(pixelX+18,pixelY+18);
ctx.lineTo(pixelX+2,pixelY+18);
ctx.fill();
}
}
/*************************************************
Draws a tetrimino at the specified game coordinate
with the specified orientation
x = [0,9] x-coordinate
y = [0,19] y-coordinate
t = [1,7] tetrimino type
o = [0,3] orientation
d = [-1,1] test, erase, or draw
*************************************************/
function drawTetrimino(x,y,t,o,d) {
//Determine the value to send to setGrid
c = -1;
if(d >= 0) c = t*d;
//Initialize validity test
valid = true;
/**** Pick the appropriate tetrimino type ****/
if(t == 1) { //I Type
//Get orientation
if(o == 0) {
valid = valid && setGrid(x-1,y,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x+1,y,c);
valid = valid && setGrid(x+2,y,c);
}
else if(o == 1) {
valid = valid && setGrid(x+1,y+1,c);
valid = valid && setGrid(x+1,y,c);
valid = valid && setGrid(x+1,y-1,c);
valid = valid && setGrid(x+1,y-2,c);
}
else if(o == 2) {
valid = valid && setGrid(x-1,y-1,c);
valid = valid && setGrid(x,y-1,c);
valid = valid && setGrid(x+1,y-1,c);
valid = valid && setGrid(x+2,y-1,c);
}
else if(o == 3) {
valid = valid && setGrid(x,y+1,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x,y-1,c);
valid = valid && setGrid(x,y-2,c);
}
}
if(t == 2) { //J Type
//Get orientation
if(o == 0) {
valid = valid && setGrid(x-1,y+1,c);
valid = valid && setGrid(x-1,y,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x+1,y,c);
}
else if(o == 1) {
valid = valid && setGrid(x+1,y+1,c);
valid = valid && setGrid(x,y+1,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x,y-1,c);
}
else if(o == 2) {
valid = valid && setGrid(x-1,y,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x+1,y,c);
valid = valid && setGrid(x+1,y-1,c);
}
else if(o == 3) {
valid = valid && setGrid(x,y+1,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x,y-1,c);
valid = valid && setGrid(x-1,y-1,c);
}
}
if(t == 3) { //L Type
//Get orientation
if(o == 0) {
valid = valid && setGrid(x-1,y,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x+1,y,c);
valid = valid && setGrid(x+1,y+1,c);
}
else if(o == 1) {
valid = valid && setGrid(x,y+1,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x,y-1,c);
valid = valid && setGrid(x+1,y-1,c);
}
else if(o == 2) {
valid = valid && setGrid(x-1,y-1,c);
valid = valid && setGrid(x-1,y,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x+1,y,c);
}
else if(o == 3) {
valid = valid && setGrid(x-1,y+1,c);
valid = valid && setGrid(x,y+1,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x,y-1,c);
}
}
if(t == 4) { //O Type
//Orientation doesn’t matter
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x+1,y,c);
valid = valid && setGrid(x,y+1,c);
valid = valid && setGrid(x+1,y+1,c);
}
if(t == 5) { //S Type
//Get orientation
if(o == 0) {
valid = valid && setGrid(x-1,y,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x,y+1,c);
valid = valid && setGrid(x+1,y+1,c);
}
else if(o == 1) {
valid = valid && setGrid(x,y+1,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x+1,y,c);
valid = valid && setGrid(x+1,y-1,c);
}
else if(o == 2) {
valid = valid && setGrid(x-1,y-1,c);
valid = valid && setGrid(x,y-1,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x+1,y,c);
}
else if(o == 3) {
valid = valid && setGrid(x-1,y+1,c);
valid = valid && setGrid(x-1,y,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x,y-1,c);
}
}
if(t == 6) { //T Type
//Get orientation
if(o == 0) {
valid = valid && setGrid(x-1,y,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x+1,y,c);
valid = valid && setGrid(x,y+1,c);
}
else if(o == 1) {
valid = valid && setGrid(x,y+1,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x,y-1,c);
valid = valid && setGrid(x+1,y,c);
}
else if(o == 2) {
valid = valid && setGrid(x-1,y,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x+1,y,c);
valid = valid && setGrid(x,y-1,c);
}
else if(o == 3) {
valid = valid && setGrid(x,y+1,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x,y-1,c);
valid = valid && setGrid(x-1,y,c);
}
}
if(t == 7) { //Z Type
//Get orientation
if(o == 0) {
valid = valid && setGrid(x-1,y+1,c);
valid = valid && setGrid(x,y+1,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x+1,y,c);
}
else if(o == 1) {
valid = valid && setGrid(x+1,y+1,c);
valid = valid && setGrid(x+1,y,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x,y-1,c);
}
else if(o == 2) {
valid = valid && setGrid(x-1,y,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x,y-1,c);
valid = valid && setGrid(x+1,y-1,c);
}
else if(o == 3) {
valid = valid && setGrid(x,y+1,c);
valid = valid && setGrid(x,y,c);
valid = valid && setGrid(x-1,y,c);
valid = valid && setGrid(x-1,y-1,c);
}
}
return valid;
}
/*************************************************
Sets a grid cell in the game state grid
x = [0,9] x-coordinate
y = [0,19] y-coordinate
t = [-1,7] test or block type
*************************************************/
function setGrid(x, y, t) {
//Check if point is in range
if(x >= 0 && x < 10 && y >= 0 && y < 20) {
//Return test result if testing
if(t < 0) return grid[y][x] == 0;
//Otherwise assign block type to the grid
grid[y][x] = t;
return true;
}
return false;
}
/*************************************************
Responds to a key press event
*************************************************/
function keyDown(e) {
if(e.keyCode == 37) { //Left arrow
drawTetrimino(x,y,t,o,0); //Erase the current tetrimino
x2 = x - 1;
if(drawTetrimino(x2,y,t,o,-1)) //Check if valid
x = x2;
}
else if(e.keyCode == 38) { //Up arrow
drawTetrimino(x,y,t,o,0); //Erase the current tetrimino
o2 = (o + 1) % 4;
if(drawTetrimino(x,y,t,o2,-1)) //Check if valid
o = o2;
}
else if(e.keyCode == 39) { //Right arrow
drawTetrimino(x,y,t,o,0); //Erase the current tetrimino
x2 = x + 1;
if(drawTetrimino(x2,y,t,o,-1)) //Check if valid
x = x2;
}
else if(e.keyCode == 40) { //Down arrow
drawTetrimino(x,y,t,o,0); //Erase the current tetrimino
y2 = y - 1;
if(drawTetrimino(x,y2,t,o,-1)) //Check if valid
y = y2;
}
else if(e.keyCode == 32) { //Space-bar
drawTetrimino(x,y,t,o,0); //Erase the current tetrimino
//Move down until invalid
while(drawTetrimino(x,y-1,t,o,-1))
y -= 1;
}
else {
//Create a new tetrimino
t2 = 1 + Math.floor((Math.random()*7));
x2 = 4;
y2 = 18;
o2 = 0;
//Check if valid
if(drawTetrimino(x2,y2,t2,o2,-1)) {
t = t2;
x = x2;
y = y2;
o = o2;
}
}
//Draw the current tetrimino
drawTetrimino(x,y,t,o,1);
//Redraw the grid
drawGrid();
}
</script>
<body onload="initialize();" onkeydown="keyDown(event);" style="background-color:#EEEEEE">
<canvas id="myCanvas" height="400px" width="200px" style="background-color:#444444"></canvas>
<div style="width:200px;background-color:#CCCCCC">
Score: 0
</div>
</body>
</html>
We’ll be making some more changes to this function in the future as we begin to implement game logic. If we play the game now, we can stack the blocks and notice that we can’t move over existing pieces, but the game doesn’t work by itself yet. We need to make the block drop by itself and clear lines when they are complete. We’ll look at this next time when we add game logic.
Hi, I’m struggling with the setGrid function, I don’t understand how the line if(t < 0) return grid[y][x] == 0; is used to test whether the grid location is occupied or not.
Can you help.
Thanks
Ken
The grid array is set up to store the block type at each grid location. A value greater than zero indicates that a block is present, while a value of zero indicates that the location is empty. The setGrid function can be used to either assign the value of t into the grid array if t >= 0, or check if a location is empty if t < 0. When t < 0, it indicates that we only want to return whether or not we can safely add a block in this location. This is true only if the value of grid[y][x] == 0. Perhaps it would have been clearer to use a separate variable or function to indicate whether we want to check or modify the grid array.