Wednesday, 12 July 2023

Web Tutorial: Tetris in vanilla JavaScript (Part 4/4)

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

Monday, 10 July 2023

Web Tutorial: Tetris in vanilla JavaScript (Part 3/4)

Now that we have a block, we are going to move it - left, right and down. And of course, a control to make it drop all the way down. Moving the block is simple enough; it's implementing restrictions that gets a little tricky.

In the HTML for the directional pad, add click event handlers for the buttons to move the block left, right and down when clicking. For the Down button, this is just temporary and we're using it for testing.
<div class="buttons">
    <div class="pad">&nbsp;</div>
    <div class="pad dirPad"  onclick="game.setPause(false)"><button>&#9650;</button></div>
    <div class="pad">&nbsp;</div>
    <div class="pad dirPad" onclick="game.moveLeft()"><button>&#9664;</button></div>
    <div class="pad dirPad">&nbsp;</div>
    <div class="pad dirPad" onclick="game.moveRight()"><button>&#9654;</button></div>
    <div class="pad">&nbsp;</div>
    <div class="pad dirPad" onclick="game.moveDown()"><button>&#9660;</button></div>
    <div class="pad">&nbsp;</div>
</div>


Also, this button runs the rotateBlock() method.
<div class="buttons">
    <br />
    <button onclick="game.rotateBlock()">&#8635;</button>
</div>

<div class="buttons">
    <br />
    <button onclick="game.setStop()">&#9737;</button>
</div>


We will define all these methods - rotateBlock(), moveLeft(), moveRight(), moveDown() and drop().
positionBlock: function()
{
    container = document.getElementById("movingBlock");
    container.style.marginLeft = (this.currentPosition.x * 20) + "px";
    container.style.marginTop = (this.currentPosition.y * 20) + "px";
},
rotateBlock: function()
{

},
moveLeft: function()
{

},
moveRight: function()
{

},    
moveDown: function()
{

},
drop: function()
{

},

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");
    }
},


We will handle rotateBLock() first. Firstly, if the game is either paused or stopped, we proceed no further and exit early.
rotateBlock: function()
{
    if (this.paused || this.stopped) return;
},


The next step is to remember that there are only 4 rotations - 0 to 3. So if the r property of currentPosition is 3, change it to 0. If not, just increment it, and assign the value to the variable r.
rotateBlock: function()
{
    if (this.paused || this.stopped) return;

    var r = (this.currentPosition.r == 3 ? 0 : this.currentPosition.r + 1);
},


We then assign the value of the r property of currentPosition, to the value of r, and call the method positionBlock(). If this seems a little superfluous, your instincts are correct. We're only doing it this way because there is more code to add later.
rotateBlock: function()
{
    if (this.paused || this.stopped) return;

    var r = (this.currentPosition.r == 3 ? 0 : this.currentPosition.r + 1);

    this.currentPosition.r = r;
    this.positionBlock();

},


In positionBlock, which we have already written, add more code. Define containerAspects. It will be the HTML element movingBlockAspects. Remember the div that is 4 times the width of its parent? Yes, that one. Then we use the r property of currentPosition to determine the left margin. Basically, it can be 0, -100, -200 or -300. This controls which rotation is visible at any one time within movingBlock.
positionBlock: function()
{
    var containerAspects = document.getElementById("movingBlockAspects");
    containerAspects.style.marginLeft = "-" + (this.currentPosition.r * 100) + "%";


    container = document.getElementById("movingBlock");
    container.style.marginLeft = (this.currentPosition.x * 20) + "px";
    container.style.marginTop = (this.currentPosition.y * 20) + "px";
},


Now try it! Click on the Rotate button...




...and you see that movingBlockAspects has moved one quarter to the left. Click again...




...and again...




... and at this point, if you click the Rotate button again, movingBlockAspects should move to its original position.




This effect will be easier to see if we set the overflow property of the movingBlock CSS class to hidden.
#movingBlock
{
    position: absolute;
    width: 80px;
    height: 80px;
    overflow: hidden;
    margin-top: 0px;
    margin-left: 0px;
}


Now you see that all the other rotations are hidden! And if you click the Rotate button, even though it just causes movingBlockAspects to shift left, what it actually looks like is that the block is rotating!




Back to this later. Let's work on moving the whole block left, right and down. In the moveLeft() method, return early if paused or stopped is true, just as we did for rotateBlock().
moveLeft: function()
{
    if (this.paused || this.stopped) return;
},


Then we declare x and set it to one less than the x property of currentPosition.
moveLeft: function()
{
    if (this.paused || this.stopped) return;

    var x = (this.currentPosition.x - 1);
},


We set the x property of currentPosition to x, then run the positionBlock() method.
moveLeft: function()
{
    if (this.paused || this.stopped) return;

    var x = (this.currentPosition.x - 1);
    
    this.currentPosition.x = x;
    this.positionBlock();

},


We do almost exactly the same for moveRight(), except that x is incremented by 1 instead of decremented.
moveLeft: function()
{
    if (this.paused || this.stopped) return;

    var x = (this.currentPosition.x - 1);
    
    this.currentPosition.x = x;
    this.positionBlock();
},
moveRight: function()
{
    if (this.paused || this.stopped) return;

    var x = (this.currentPosition.x + 1);
    
    this.currentPosition.x = x;
    this.positionBlock();

},    


Click the left and right arrows. Does the block move? You'll notice that as in the screenshot below, it's possible to move left or right out of the confines of the grid. We will be handling that case soon.




But for now, let us work on the moveDown() method. Again, we exit early if paused or stopped is true. Then we set y to the incremented value of the y property of currentPosition. We set the y property of currentPosition to y, and call positionBlock(). Note that we work on the y property rather than x, because we are moving vertically.
moveDown: function()
{
    if (this.paused || this.stopped) return;
    
    var y = (this.currentPosition.y + 1);
    
    this.currentPosition.y = y;
    this.positionBlock();

},


Try clicking on the Down button. Again, note that we're able to move it outside of the bounds of the grid. This simply will not do, so we are taking care of that next!




For that, we create the testPosition() method. It has three parameters - x, y and r. We return true if the position represented by x, y and r is viable, and false if not. By default, we return true.
positionBlock: function()
{
    var containerAspects = document.getElementById("movingBlockAspects");
    containerAspects.style.marginLeft = "-" + (this.currentPosition.r * 100) + "%";

    var container = document.getElementById("movingBlock");
    container.style.marginLeft = (this.currentPosition.x * 20) + "px";
    container.style.marginTop = (this.currentPosition.y * 20) + "px";
},
testPosition: function(x, y, r)
{
    return true;
},

rotateBlock: function()
{
    if (this.paused || this.stopped) return;

    var r = (this.currentPosition.r == 3 ? 0 : this.currentPosition.r + 1);

    this.currentPosition.r = r;
    this.positionBlock();
},


Now we declare shapeArr, and set to it the element in shapes that is pointed to by the shape property of the currentBlock property. We use r to point to the correct rotation.
testPosition: function(x, y, r)
{
    var shapeArr = this.shapes[this.currentBlock.shape][r];

    return true;
},


Now we traverse the two-dimensional array shapeArr using a nested For loop.
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++)
        {

        }
    }


    return true;
},


Now within the array, we only bother if the sub-element's value is 1. If it's 0, it is outside of the block and we don't have to bother with it.
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)
            {

            }

        }
    }

    return true;
},


In a bit of a surprising inversion, i is the row, so we add y to it to ensure that we can compare the correct value vertically. Now, if this is less than 0, that means that part of the block is still past the top of the grid, we don't bother with it.
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;
            }
        }
    }

    return true;
},


This next line tests if horizontally, the block is still within the bounds of the block. If not, false is returned.
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;
            }
        }
    }

    return true;
},


This next line tests if the occupied part of the block is past the bottom of the grid.
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;
            }
        }
    }

    return true;
},


Lastly, we test if that part of the grid that coincides with the occupied part of the block, is not null, which means that there is already a colored block there. If so, we return false.
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;
},


Now let's call the testPosition() method where applicable! In rotateBlock(), we call it before setting currentPosition's r property. We're supposed to pass in x, y and r. In this case, we pass in the x property of currentPosition and the y property of currentPosition, then the proposed value of r. If this returns false, the proposed rotation is not viable and we exit before setting the value.
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();
},


Similarly for all the others!
moveLeft: function()
{
    if (this.paused || this.stopped) 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;

    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)) return;
    
    this.currentPosition.y = y;
    this.positionBlock();
},


Now you see that we can't rotate or move the block outside of the grid!




Let's create the drop() method. We will change this call (remember I said this was just temporary?). Thus, the Down button will now call drop().
<div class="pad dirPad" onclick="game.drop()"><button>&#9660;</button></div>


We create drop() here, starting the same as the other methods such as moveRight(), moveLeft(), rotateBlock() and moveDown().
moveDown: function()
{
    if (this.paused || this.stopped) return;

    var y = (this.currentPosition.y + 1);
    if (!this.testPosition(this.currentPosition.x, y, this.currentPosition.r))
    {
        return;
    }
    
    this.currentPosition.y = y;
    this.positionBlock();
},
drop: function()
{
    if (this.paused || this.stopped) return;
},

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");
    }
},


Then we use a While loop. The condition is the testPosition() method with an incremented y property of currentPosition.
drop: function()
{
    if (this.paused || this.stopped) return;

    while(this.testPosition(this.currentPosition.x, this.currentPosition.y + 1, this.currentPosition.r))
    {    

    }

},


And while that returns true, we run positionBlock() after incrementing the y property of currentPosition. In essence, we pretty much run moveDown() until we reach the bottom.
drop: function()
{
    if (this.paused || this.stopped) 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();

    }
},


Reload. Now if you click the Down button, the block goes right to the bottom of the grid.




We are going to create one more method, stopBlock(), which will be run at moveDown() and drop(). This method basically is meant to be run once the block cannot move "down" any more, at which point the colors of the block will be transferred to the grid, and we repeat the cycle of the moving block again.
drop: function()
{
    if (this.paused || this.stopped) 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();
    }
},
stopBlock: function()
{

},

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");
    }
},


First, we declare shapeArr and get the current shape's two-dimensional array. For that, we'll need the shape property of the currentBlock property and the r property of the currentPosition property. These two values will point to the correct data within the shapes property. We will also declare stopGame, and set it to false.
stopBlock: function()
{
    var shapeArr = this.shapes[this.currentBlock.shape][this.currentPosition.r];
    var stopGame = false;

},


We are going to determine if stopGame should be true instead. For this, we use a nested For loop to traverse shapeArr.
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++)
        {

        }
    }

},


Now, we will only take action if the current sub-element is 1.
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)
            {

            }

        }
    }
},


We then check if the vertical position of that current sub-element is out of the top of the grid. If so, set stopGame to true. That means that when stopBlock() is being called and the block is out of the top of the grid, it's Game Over.
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)
                {

                }
                else
                {
                    stopGame = true;
                }

            }
        }
    }
},


If not, we set that current corresponding sub-element in grid to the color property of currentBlock.
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;
                }
            }
        }
    }
},


Out of that loop we then check if stopGame is true. If so, call setStop().
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;
                }
            }
        }
    }

    if (stopGame)
    {
        this.setStop();
    }
    else
    {

    }

},


If not, that means the game is going to continue. currentBlock will be set to the value of nextBlock, while nextBlock will need to be generated using getRandomBlock(). And then we call renderGrid() and display the nextBlock by running populateAspect() with the arguments we used in reset().
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;
                }
            }
        }
    }

    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 this is where we will call stopBlock(). In moveDown(), we call stopBlock() if the block can no longer move down. In drop(), we call it at the end of the method because that, by definition, is when the block can no longer move down.
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;

    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();
},


Let's test this now. Note that the current block is the cyan Z-shaped one. The next block is the purple L-shaped one. Click on the Down button...




...the cyan block drops to the bottom. The purple L-shaped block should now be at the top and it's now your current block. And a new block (a red Z-shaped one) is the next block.




And if you do this enough times...




...it's Game Over!




Now we have most of the moving parts. It's just a matter of applying timer functions.


Next

Scoring, animations and beautification!