We cap off this web tutorial with the timer function! But before we can do that, we want to make sure that the Rotate, Left, Right and Down buttons don't work unless the block is fully in frame. So let's create the
isFullyInFrame() method. By default, it returns
true.
testPosition: function(x, y, r)
{
var shapeArr = this.shapes[this.currentBlock.shape][r];
for(var i = 0; i < shapeArr.length; i++)
{
for(var j = 0; j < shapeArr[i].length; j++)
{
if (shapeArr[i][j] == 1)
{
if (y + i < 0) continue;
if (j + x < 0 || j + x > this.grid[0].length - 1) return false;
if (i + y > this.grid.length - 1) return false;
if (this.grid[i + y][j + x] != null) return false;
}
}
}
return true;
},
isFullyInFrame: function()
{
return true;
},
rotateBlock: function()
{
if (this.paused || this.stopped) return;
var r = (this.currentPosition.r == 3 ? 0 : this.currentPosition.r + 1);
if (!this.testPosition(this.currentPosition.x, this.currentPosition.y, r)) return;
this.currentPosition.r = r;
this.positionBlock();
},
We first declare
shapeArr as we've done so many times, and traverse it using a nested
For loop.
isFullyInFrame: function()
{
var shapeArr = this.shapes[this.currentBlock.shape][this.currentPosition.r];
for(var i = 0; i < shapeArr.length; i++)
{
for(var j = 0; j < shapeArr[i].length; j++)
{
}
}
return true;
},
Of course, we take no action if the value of the sub-array is not 1.
isFullyInFrame: function()
{
var shapeArr = this.shapes[this.currentBlock.shape][this.currentPosition.r];
for(var i = 0; i < shapeArr.length; i++)
{
for(var j = 0; j < shapeArr[i].length; j++)
{
if (shapeArr[i][j] == 1)
{
}
}
}
return true;
},
And if the vertical position of that sub-element with regard to the grid is less than 0, which means it is outside of the top of the grid, we return
false.
isFullyInFrame: function()
{
var shapeArr = this.shapes[this.currentBlock.shape][this.currentPosition.r];
for(var i = 0; i < shapeArr.length; i++)
{
for(var j = 0; j < shapeArr[i].length; j++)
{
if (shapeArr[i][j] == 1)
{
if (i + this.currentPosition.y < 0) return false;
}
}
}
return true;
},
In
moveLeft(),
moveRight() and
drop(), we make a call to
isFullyInFrame() and if it returns
false, we exit early. We don't want to do it for
moveDown() because we want the block to move down regardless.
rotateBlock: function()
{
if (this.paused || this.stopped) return;
if (!this.isFullyInFrame()) return;
var r = (this.currentPosition.r == 3 ? 0 : this.currentPosition.r + 1);
if (!this.testPosition(this.currentPosition.x, this.currentPosition.y, r)) return;
this.currentPosition.r = r;
this.positionBlock();
},
moveLeft: function()
{
if (this.paused || this.stopped) return;
if (!this.isFullyInFrame()) return;
var x = (this.currentPosition.x - 1);
if (!this.testPosition(x, this.currentPosition.y, this.currentPosition.r)) return;
this.currentPosition.x = x;
this.positionBlock();
},
moveRight: function()
{
if (this.paused || this.stopped) return;
if (!this.isFullyInFrame()) return;
var x = (this.currentPosition.x + 1);
if (!this.testPosition(x, this.currentPosition.y, this.currentPosition.r)) return;
this.currentPosition.x = x;
this.positionBlock();
},
moveDown: function()
{
if (this.paused || this.stopped) return;
var y = (this.currentPosition.y + 1);
if (!this.testPosition(this.currentPosition.x, y, this.currentPosition.r))
{
this.stopBlock();
return;
}
this.currentPosition.y = y;
this.positionBlock();
},
drop: function()
{
if (this.paused || this.stopped) return;
if (!this.isFullyInFrame()) return;
while(this.testPosition(this.currentPosition.x, this.currentPosition.y + 1, this.currentPosition.r))
{
var y = (this.currentPosition.y + 1);
this.currentPosition.y = y;
this.positionBlock();
}
this.stopBlock();
},
If you reload and try the buttons, nothing will work. That is because the block is still off the top of the grid and this won't change until we work on the timer functions. Add
timer, stage and
score to the properties.
timer is
null by default,
stage is 1 and
score is 0.
currentPosition: { x: 0, y: 0, r: 0},
timer: null,
stage: 1,
score: 0,
paused: false,
stopped: false,
In
setPause(), we check if
paused is true after setting it. If it is, we run
clearInterval() on
timer.
setPause: function(isAuto)
{
if (this.stopped) return;
this.paused = (this.paused ? false : true);
if (!isAuto)
{
var pauseScreen = document.getElementById("pauseScreen");
pauseScreen.style.visibility = (this.paused ? "visible" : "hidden");
}
if (this.paused)
{
clearInterval(this.timer);
}
else
{
}
},
If not, that means
paused was either set to
false by the user or the game, and we need to start the timer function. We set
timer to the
setInterval() function. It will call the
moveDown() method at an interval determined by the
getInterval() method.
setPause: function(isAuto)
{
if (this.stopped) return;
this.paused = (this.paused ? false : true);
if (!isAuto)
{
var pauseScreen = document.getElementById("pauseScreen");
pauseScreen.style.visibility = (this.paused ? "visible" : "hidden");
}
if (this.paused)
{
clearInterval(this.timer);
}
else
{
this.timer = setInterval(
()=>
{
this.moveDown()
},
this.getInterval()
)
}
},
Time to work on
getInterval(). We declare
interval as 1500, and then return it at the end of the method..
setStop: function()
{
this.stopped = (this.stopped ? false : true);
var stopScreen = document.getElementById("stopScreen");
stopScreen.style.visibility = (this.stopped ? "visible" : "hidden");
},
getInterval: function()
{
var interval = 1500;
return interval;
}
We declare
diff as the
stage property multiplied by 100, then subtract
diff from interval. Basically, the higher
stage is, the smaller the interval.
getInterval: function()
{
var interval = 1500;
var diff = this.stage * 100;
interval = interval - diff;
return interval;
}
We lastly ensure that
interval is not less than 100. That will be the interval, in milliseconds.
getInterval: function()
{
var interval = 1500;
var diff = this.stage * 100;
interval = interval - diff;
if (interval < 100) interval = 100;
return interval;
},
In
setStop(), we check if
stopped is
true after setting it. If so, we clear the timer and if not, we call
reset().
setStop: function()
{
this.stopped = (this.stopped ? false : true);
var stopScreen = document.getElementById("stopScreen");
stopScreen.style.visibility = (this.stopped ? "visible" : "hidden");
if (!this.stopped)
{
this.reset();
}
else
{
clearInterval(this.timer);
}
},
We're not quite done. In
reset(), we make sure right at the start that
stopped is "manually" set to
false and
paused is "manually" set to
true, then we run
setPause() passing in
true as an argument, to unpause the game. This will result in the timer getting started.
reset: function()
{
this.stopped = false;
this.paused = true;
this.setPause(true);
this.grid = [];
for(var i = 0; i < 20; i++)
{
var temp = [];
for(var j = 0; j < 10; j++)
{
temp.push(null);
}
this.grid.push(temp);
}
this.currentBlock = this.getRandomBlock();
this.nextBlock = this.getRandomBlock();
this.renderGrid();
this.populateAspect("gridNext", this.shapes[this.nextBlock.shape][0], this.nextBlock.color, "square_small");
},
OK. You see that the block moves on its own. And the Rotate, Left, Right and Down buttons only work once it's moved past the top of the grid!
Scoring
What we need to do now is detect when
Tetris has completed rows, and react accordingly. This is handled in the
stopBlock() method. In the middle of the method, after getting all the colors from the moving block to the grid, declare
completedRows and set it to the value returned by calling
setCompletedRows() with the argument "white".
stopBlock: function()
{
var shapeArr = this.shapes[this.currentBlock.shape][this.currentPosition.r];
var stopGame = false;
for(var i = 0; i < shapeArr.length; i++)
{
for(var j = 0; j < shapeArr[i].length; j++)
{
if (shapeArr[i][j] == 1)
{
if (i + this.currentPosition.y >= 0)
{
this.grid[i + this.currentPosition.y][j + this.currentPosition.x] = this.currentBlock.color;
}
else
{
stopGame = true;
}
}
}
}
var completedRows = this.setCompletedRows("white");
if (stopGame)
{
this.setStop();
}
else
{
this.currentBlock = this.nextBlock;
this.nextBlock = this.getRandomBlock();
this.renderGrid();
this.populateAspect("gridNext", this.shapes[this.nextBlock.shape][0], this.nextBlock.color, "square_small");
}
},
And here, we create
setCompletedRows(). It has a parameter,
color. We declare
completedRows as 0 and return that value at the end of the method. Basically, what we want to do is count the number of completed rows in the grid, and set the values of the elements of those rows to
color.
getInterval: function()
{
var interval = 1500;
var diff = this.stage * 100;
interval = interval - diff;
if (interval < 100) interval = 100;
return interval;
},
setCompletedRows: function(color)
{
var completedRows = 0;
return completedRows;
}
We then go through the number of elements in grid using a
For loop. In it,
filled is declared, and the starting value is the number of sub-elements in that current element.
setCompletedRows: function(color)
{
var completedRows = 0;
for(var i = 0; i < this.grid.length; i++)
{
var filled = this.grid[i].length;
}
return completedRows;
}
We run through the sub-elements using a
For loop, and decrement
filled each time we find a sub-element that is not
null.
setCompletedRows: function(color)
{
var completedRows = 0;
for(var i = 0; i < this.grid.length; i++)
{
var filled = this.grid[i].length;
for(var j = 0; j < this.grid[i].length; j++)
{
if (this.grid[i][j]) filled--;
}
}
return completedRows;
}
If, at the end of that,
filled is 0, that means there are no
null values in that row. We increment
completedRows...
setCompletedRows: function(color)
{
var completedRows = 0;
for(var i = 0; i < this.grid.length; i++)
{
var filled = this.grid[i].length;
for(var j = 0; j < this.grid[i].length; j++)
{
if (this.grid[i][j]) filled--;
}
if (filled == 0)
{
completedRows++;
}
}
return completedRows;
},
And then we run through the sub-elements again with a
For loop and set the value to
color.
setCompletedRows: function(color)
{
var completedRows = 0;
for(var i = 0; i < this.grid.length; i++)
{
var filled = this.grid[i].length;
for(var j = 0; j < this.grid[i].length; j++)
{
if (this.grid[i][j]) filled--;
}
if (filled == 0)
{
completedRows++;
for(var j = 0; j < this.grid[i].length; j++)
{
this.grid[i][j] = color;
}
}
}
return completedRows;
},
Back to
stopBlock(), after running
setCompletedRows(), we check if
completedRows() is more than 0. In the
Else block, we encapsulate the rest of the existing code.
var completedRows = this.setCompletedRows("white");
if (completedRows > 0)
{
}
else
{
if (stopGame)
{
this.setStop();
}
else
{
this.currentBlock = this.nextBlock;
this.nextBlock = this.getRandomBlock();
this.renderGrid();
this.populateAspect("gridNext", this.shapes[this.nextBlock.shape][0], this.nextBlock.color, "square_small");
}
}
So if there are completed rows, we want the game to pause, so we call
setPause() and pass in
true as an argument because this is a system call, and then call
renderGrid().
var completedRows = this.setCompletedRows("white");
if (completedRows > 0)
{
this.setPause(true);
this.renderGrid();
}
else
{
if (stopGame)
{
this.setStop();
}
else
{
this.currentBlock = this.nextBlock;
this.nextBlock = this.getRandomBlock();
this.renderGrid();
this.populateAspect("gridNext", this.shapes[this.nextBlock.shape][0], this.nextBlock.color, "square_small");
}
}
Then we use
setTimeout() with an interval of 500 milliseconds. In it, we call
setCompletedRows() with a value of "x". Any value other than the values in
colors or "white", will just appear as no color. After that, we call
renderGrid() to re-render the grid once more.
var completedRows = this.setCompletedRows("white");
if (completedRows > 0)
{
this.setPause(true);
this.renderGrid();
setTimeout(
() =>
{
this.setCompletedRows("x");
this.renderGrid();
},
500
);
}
else
{
if (stopGame)
{
this.setStop();
}
else
{
this.currentBlock = this.nextBlock;
this.nextBlock = this.getRandomBlock();
this.renderGrid();
this.populateAspect("gridNext", this.shapes[this.nextBlock.shape][0], this.nextBlock.color, "square_small");
}
}
In there, we have a nested
setTimeout() with another 500 milliseconds interval! In it, we run
compactRows(). We will handle this later.
var completedRows = this.setCompletedRows("white");
if (completedRows > 0)
{
this.setPause(true);
this.renderGrid();
setTimeout(
() =>
{
this.setCompletedRows("x");
this.renderGrid();
setTimeout(
() =>
{
this.compactRows();
},
500
);
},
500
);
}
else
{
if (stopGame)
{
this.setStop();
}
else
{
this.currentBlock = this.nextBlock;
this.nextBlock = this.getRandomBlock();
this.renderGrid();
this.populateAspect("gridNext", this.shapes[this.nextBlock.shape][0], this.nextBlock.color, "square_small");
}
}
And in here, we do what we did earlier - set
currentBlock and
nextBlock, re-render the grid and the Next Block display. And then we run
setPause() again to un-pause the game.
var completedRows = this.setCompletedRows("white");
if (completedRows > 0)
{
this.setPause(true);
this.renderGrid();
setTimeout(
() =>
{
this.setCompletedRows("x");
this.renderGrid();
setTimeout(
() =>
{
this.compactRows();
this.currentBlock = this.nextBlock;
this.nextBlock = this.getRandomBlock();
this.renderGrid();
this.populateAspect("gridNext", this.shapes[this.nextBlock.shape][0], this.nextBlock.color, "square_small");
this.setPause(true);
},
500
);
},
500
);
}
else
{
if (stopGame)
{
this.setStop();
}
else
{
this.currentBlock = this.nextBlock;
this.nextBlock = this.getRandomBlock();
this.renderGrid();
this.populateAspect("gridNext", this.shapes[this.nextBlock.shape][0], this.nextBlock.color, "square_small");
}
}
OK, we have to create
compactRows() now. This is to remove all completed rows in grid. We create
tempArr, an empty array. At the end of the method, we set grid to the value of
tempArr.
setCompletedRows: function(color)
{
var completedRows = 0;
for(var i = 0; i < this.grid.length; i++)
{
var filled = this.grid[i].length;
for(var j = 0; j < this.grid[i].length; j++)
{
if (this.grid[i][j]) filled--;
}
if (filled == 0)
{
completedRows++;
for(var j = 0; j < this.grid[i].length; j++)
{
this.grid[i][j] = color;
}
}
}
return completedRows;
},
compactRows: function()
{
var tempArr = [];
this.grid = tempArr;
}
We next iterate through the elements of
grid. We only need to check if the first sub-element is not "x".
compactRows: function()
{
var tempArr = [];
for(var i = 0; i < this.grid.length; i++)
{
if (this.grid[i][0] != "x")
{
}
}
this.grid = tempArr;
}
That means that it's not a completed row, and we need to push the row to
tempArr.
compactRows: function()
{
var tempArr = [];
for(var i = 0; i < this.grid.length; i++)
{
if (this.grid[i][0] != "x")
{
tempArr.push(this.grid[i]);
}
}
this.grid = tempArr;
}
This means that
tempArr has all the non-completed rows. However, we still need to fill in empty rows to make the full grid. So first, we get the number of rows required,
rowsToAdd, which is the size of
grid less the size of
tempArr.
compactRows: function()
{
var tempArr = [];
for(var i = 0; i < this.grid.length; i++)
{
if (this.grid[i][0] != "x")
{
tempArr.push(this.grid[i]);
}
}
var rowsToAdd = this.grid.length - tempArr.length;
this.grid = tempArr;
}
Then we use a
For loop to insert an array of 10
null values into
tempArr (via the
unshift() method)
rowsToAdd times.
compactRows: function()
{
var tempArr = [];
for(var i = 0; i < this.grid.length; i++)
{
if (this.grid[i][0] != "x")
{
tempArr.push(this.grid[i]);
}
}
var rowsToAdd = this.grid.length - tempArr.length;
for (i = 0; i < rowsToAdd; i++) tempArr.unshift([null, null, null, null, null, null, null, null, null, null]);
this.grid = tempArr;
}
So now we play! You see I'm about to get two completed rows.
The rows turn
white...
...then vanish!
And now we get to scoring. At the
stopBlock() method, just inside the
If block that checks if
completedRows is greater than 0, add this call to
addToScore(). Pass in
completedRows as an argument.
if (completedRows > 0)
{
this.addToScore(completedRows);
this.setPause(true);
this.renderGrid();
Now we create
addToScore(). Begin by declaring
pointsPerRow and
pointsPerStage. These really would be better off as properties, but I can't be arsed.
pointsPerRow is how many points you want each completed row to be worth, while
pointsPerStage is a factor of how many points it takes to get to the next stage. You can see I've entered in completely arbitrary values, though of course it would make sense for
pointsPerStage to be always greater than
pointsPerRow.
setStop: function()
{
this.stopped = (this.stopped ? false : true);
var stopScreen = document.getElementById("stopScreen");
stopScreen.style.visibility = (this.stopped ? "visible" : "hidden");
if (!this.stopped)
{
this.reset();
}
else
{
clearInterval(this.timer);
}
},
addToScore: function(rows)
{
var pointsPerRow = 100;
var pointsPerStage = 1000;
},
getInterval: function()
{
var interval = 1500;
var diff = this.stage * 100;
interval = interval - diff;
if (interval < 100) interval = 100;
return interval;
},
Here, we increment score with this formula involving
pointsPerRow and the number of completed rows, exponentially. And then we use the value of score and
pointsPerStage to calculate the value of
stage. We add 1 at the end because
stage can never be smaller than 1.
addToScore: function(rows)
{
var pointsPerRow = 100;
var pointsPerStage = 1000;
this.score += (pointsPerRow * rows * rows);
this.stage = Math.floor(this.score / pointsPerStage) + 1;
},
Finally, we call
setScore() and
setStage().
addToScore: function(rows)
{
var pointsPerRow = 100;
var pointsPerStage = 1000;
this.score += (pointsPerRow * rows * rows);
this.stage = Math.floor(this.score / pointsPerStage) + 1;
this.setScore();
this.setStage();
},
These two methods are fairly straightforward. They display the values of
stage and
score in the appropriate placeholders.
setStop: function()
{
this.stopped = (this.stopped ? false : true);
var stopScreen = document.getElementById("stopScreen");
stopScreen.style.visibility = (this.stopped ? "visible" : "hidden");
if (!this.stopped)
{
this.reset();
}
else
{
clearInterval(this.timer);
}
},
setScore: function()
{
var container = document.getElementById("txtScore");
container.innerHTML = this.score;
},
setStage: function()
{
var container = document.getElementById("txtStage");
container.innerHTML = this.stage;
},
addToScore: function(rows)
{
var pointsPerRow = 100;
var pointsPerStage = 1000;
this.score += (pointsPerRow * rows * rows);
this.stage = Math.floor(this.score / pointsPerStage) + 1;
this.setScore();
this.setStage();
},
At the beginning of the
reset() method, we make sure that these methods are called as well.
reset: function()
{
this.score = 0;
this.stage = 1;
this.setScore();
this.setStage();
this.stopped = false;
this.paused = true;
this.setPause(true);
this.grid = [];
for(var i = 0; i < 20; i++)
{
var temp = [];
for(var j = 0; j < 10; j++)
{
temp.push(null);
}
this.grid.push(temp);
}
this.currentBlock = this.getRandomBlock();
this.nextBlock = this.getRandomBlock();
this.renderGrid();
this.populateAspect("gridNext", this.shapes[this.nextBlock.shape][0], this.nextBlock.color, "square_small");
},
Also remove the placeholder values in
txtStage and
txtScore.
<div id="header_left">
<b>STAGE</b><br />
<span id="txtStage"></span><br />
<b>SCORE</b><br />
<span id="txtScore"></span><br />
</div>
Now play! Notice how when we get a completed row, the score jumps to 100?
And when I get over 1000 points, the stage is now 2! And the block moves faster.
Final touches
Let's do some cleanup here. First and foremost, we want to remove the
red outlines.
div { outline: 0px solid red;}
Then we want to ensure that the block is not visible outside of the grid.
#grid
{
width: 200px;
height: 400px;
background-color: rgba(0, 0, 0, 0.5);
margin: 5px auto 5px auto;
position: relative;
border: 5px inset rgb(155, 155, 155);
overflow: hidden;
}
Finally, we add this code in the CSS so that the word "NEXT" appears in the display panel for the next block.
#gridNext
{
width: 40px;
height: 40px;
margin: 0px auto 0 auto;
padding: 5px;
}
#gridNext::before
{
display: block;
position: absolute;
content: "Next";
}
#grid
{
width: 200px;
height: 400px;
background-color: rgba(0, 0, 0, 0.5);
margin: 5px auto 5px auto;
position: relative;
border: 5px inset rgb(155, 155, 155);
overflow: hidden;
}
See what a dramatic difference all this makes?!
Thanks for reading!
This code is a significant improvement from my code all those years back. Now there are some improvements that could still be made, such as a few optimizations, naming conventions and such. But for now, it's good enough.
Time to moveOn(),
T___T