Friday, 28 July 2023

App Review: Two Dots

Today, I want to review a game that I started playing at the beginning of this year. Its title is Two Dots and in addition to being extremely engaging, it is fun and well-crafted. This game was created by Playdots, Inc, and huge kudos to the creative team!


Two Dots is a puzzle game that doesn't require quick fingers or uncanny reflexes - just patience and perhaps a bit of luck. I had great fun obsessively playing it and trying out all the features. In the process, I took a whole bunch of screenshots and will gladly spam you with them in a bit.

The Premise

Two Dots is played as a board with several differently colored dots in a grid. There may be features on the board that react when something happens, and special dots that require a specific event to happen before activating.

You play by connecting dots of the same color. Connecting a closed loop of dots results in a special explosion which may in turn trigger some other effect.


Each game has targets to meet. You may need to accumulate a certain number of dots in a certain color, fill up an entire screen with water tiles or eliminate all fire tiles. Or any other number of criteria, in any combination.


In every game, you have a certain number of moves to use, and once you run out of moves, the game is over.



There are hundreds of levels, and it will be a long time before you run out of stuff to play.

The Aesthetics

Two Dots is both an adorable and beautiful game - there's no better way to put it. It's cartoony, colorful, and the animations are both awesome and very addictive. If it's one thing that Two Dots does not lack, it's aesthetics. For sure.

The Experience

I could spend hours just making stuff explode and zip and pop in there, and spoiler alert, I have! Calling it "fun" can be a bit of an understatement.


This game is not ultra-sexy in that sense, but that's what is amazing about it. A child could play it, an adult could play it, and both would still have fun.

The Interface

It's basically tap, drag and release, to connect dots. And once in a while, you may use special bonuses and apply them to any dot on the board. That takes an extra tap or two, but other than that, that's it! Beautifully simple!

Connecting a closed loop of dots (they call it "forming squares", which isn't geometrically accurate) causes all dots in that color on the board, to "explode".

What I liked

No time-based requirement. You can take the entire day to make a move, and there would be no penalty for your indecision. That said, that kind of defeats the entire purpose of playing this game. It's not a chess match. Suffice to say, there's no pressure to make a move right away, which is great. Less on the reflexes, more on the thinking.  


Color-blind features. This is very considerate. I approve!


Preview feature. Sometimes you just need to see the possible ramifications of your moves, and this game obligingly provides that. You just try the intended move without lifting your finger at the end, and the app lets you see the immediate outcomes.


The achievements in this game are pretty creative. I got quite a kick out of seeing these.


Treasure Hunt is nice, though a bit simplistic.




The Flip is great. I really like the concept. You complete a row or column, and you get bonuses. You complete an entire grid, and you get cool prizes!


While most of the special tiles are pretty cool, there are some that are my absolute favorites. The crystals, for one - the sound effects when they get activated are so awesome.


The water tile stages rank among my favorite.


The flower tiles are really beautiful once you activate them. Look at the way the petals burst! So poetic!


Mushroom tiles are so random and chaotic and I utterly love them.


The bird tiles are cute and make nice sound effects.


Meteors have a special place in my heart. You let them touch the bottom row, and they clear the entire column!


Sunstrike tiles are very useful and fun to play. Absolutely love the stages that feature them. Once you build them, you release them at a spot and kaboom!

What I didn't

Sometimes when an event expires while you were in the middle of playing it, and you turn off the game and come back, the game attempts to resume where you left off. That's certainly useful, except that when the event has expired, a glitch occurs. Probably a simple thing to fix.



Arcade mode. It's cute, that's for sure, but it gets old real fast having to replay from Stage 1 after failing a stage.

The music was charming at first, but quickly got annoying. Thankfully there was an option to turn it off.



Crabs and monkeys. I hate the damn things. Not in real life - I adore crabs (as food) and monkeys are lovable, but in this game, they are such a nuisance.


Fire tiles can be annoying too. They just spread and you can waste a lot of precious moves putting them out.


And the circuit tiles... they're a mixed bag. Sometimes they're easy and sometimes they are hellishly hard and not in a fun way.

Conclusion

Two Dots is not edgy. In fact, it's almost painfully kid-friendly. But the cool thing about it is that it doesn't need to be edgy. It's got a solid gaming mechanism and the graphics to back it up. And ultimately, that's pretty much all it needs.

My Rating

9.5 / 10

Two good to miss!
T___T


Monday, 24 July 2023

It's Not Personal

Employers reprimanding employees is nothing new. In my long and storied career, I've encountered quite a few instances. Some of them were incidental - I merely happened to be there while an unfortunate employee was receiving it, and some of them were even directed at me. Hey, it happens. You screw up, you get chewed out (or get fired), hopefully you learn something, life goes on.

Chewed out.

My accumulated experience in the software industry has not stopped me from screwing up (though hopefully, my mistakes are less amateurish than they used to be) and consequently, getting put on blast. However, I have noticed a certain trend. Some employers, younger ones in particular, seem to feel the need to preface their criticism with something positive, like some kind of compliment sandwich.

"I want to assure you that this is not personal."

"First of all, the work you've put into this is undeniable."

"I'm not trying to make things difficult for you, but..."

What's going on?

Why do some employers feel this need to soften the blow with something? Are they maybe afraid of (gasp!) hurting the feelings of those they are criticizing?

In particular, I really don't know what is it with this hurry to reassure me that it's "not personal". Why would this be personal? Are these employers laboring under the delusion that people work for them because they like them? I'm sorry to be the one to burst that bubble, but people work for you because the work is tolerable (and in some cases, even enjoyable), they're at least adequate at the task, and they get paid enough to justify spending their time at it. And in the case of some younger employees, because the company's goals align with theirs or some rubbish principle like that. No one has any personal feelings towards you or your company, so that bit about your criticism being "not personal" is superfluous. And a little insulting.


Stop treating me
like a kid.

You know what some of my employers say when they want to criticize my work?

"Yo man, this is half-fucked work. I can't accept this."

"You used a nested For to do this? That's sloppy!"

"Wow, watching you struggle at this is painful."

Now that is treating me like an adult professional. That is not treating me like some fragile child who needs that emotional cushion.

That's not to say employers should verbally abuse their employees, but come on, let's not swing too far in the opposite direction. If criticism is called for, let's hear it and get to the point. You should not need to say "it's not personal" because that goes without saying. And if your criticism does spring from some personal place (and yes, I've encountered those too), then "it's not personal" is a lie and you're fooling no one. Either way, get rid of it.

My first day in the military

Singapore has a law of military conscription. Rewind back a couple decades and it was my first day in Boot Camp, y'know, having been born with a penis and all. I had heard horror stories about how tough, how inhumane army life was, and I was steeling myself for the inevitable shock.

And I was shocked. Just not for the reasons I imagined. Sure, we got yelled at. We got hectored. Hazed. Verbally eviscerated. And you know what most of us were thinking while it happened?

"Bitch, please. You think you're scary? I have an Asian mom!"

This is scary.

I'm not sure, dear reader, how it was during your generation. But in my time, Asian parents were constantly critical, had unrealistic expectations and consequently, were always disappointed. Avoiding their wrath was considered a win, nevermind actually obtaining their approval. So when it came to verbal abuse, military life really wasn't anything to write home about.

Not that it stopped us from making up horror stories just to impress the ladies and the gullible youths, eh?

My point is, it's possible that all that conditioning we were put through, may have contributed to how we receive criticism in the workplace today.

One last (personal) note!

I'm not saying to grow a thicker skin. Sometimes employers do go too far, and their (supposedly) constructive criticism gets distinctively personal despite their reassurances that it's not.

But it's up to you how personally you want to take it. They should not be the ones telling you that it's not personal. We are the ones who should be reminding ourselves. Emotional detachment - it's severely underrated.

Yes, this is not personal.
T___T

Wednesday, 19 July 2023

Let's Unpack The CSS Box

Everything in HTML rendering is boxes. Everything you see on a web page can be imagined inside a box. Paragraphs of text? Boxes. Headers? Boxes. Buttons, lists, images? Boxes, boxes, boxes. These boxes are then arranged inside bigger boxes as part of your layout.

Everything is boxes!

Let that sink in, and then read on as we ruminate on the CSS Box Model. If we think of everything as boxes, then we really need to understand the properties of those boxes.

For the purposes of these examples today, we will use the ubiquitous div tag, though most other elements can be used in a pinch. I chose the div tag because it's relatively uncomplicated. The div tag adds as the content placeholder which has an orange background. For contrast, we use a paragraph tag for the content, with a beige background.
<div style="height:200px;width:200px;background-color:rgb(255,200,0)">
  <p style="background-color:rgb(255,200,100);">This is a wall of text used for testing the CSS Box Model. The text within should flow in accordance to the dimensions of this HTML element.</p>
</div>


You see that the light beige box lines up with the orange box at the top, with no spacing at the sides. The content fits the placeholder with no space to spare. That is the default.

This is a wall of text used for testing the CSS Box Model. The text within should flow in accordance to the dimensions of this HTML element.



Padding

This is a spacing that is applied on the interior of the element, causing a space between its contents and its perimeter.
<div style="height:200px;width:200px;background-color:rgb(255,200,0);padding:50px">
  <p style="background-color:rgb(255,200,100);">This is a wall of text used for testing the CSS Box Model. The text within should flow in accordance to the dimensions of this HTML element.</p>
</div>


Now you see that the light beige paragraph remains the same size, but the size of the orange box has increased! That's due to padding - the padding occurs on the interior of the div, and this has the effect of stretching the width and/or height of the div. So if the height of the div is 200 pixels and the top and bottom both have 50 pixels padding applied, then the total effective height of the orange div is now (200 + 50 + 50) = 300 pixels.

This is a wall of text used for testing the CSS Box Model. The text within should flow in accordance to the dimensions of this HTML element.



Border

This is in effect the thickness of the perimeter of an element.
<div style="height:200px;width:200px;background-color:rgb(255,200,0);border:50px solid rgb(200,100,0)">
  <p style="background-color:rgb(255,200,100);">This is a wall of text used for testing the CSS Box Model. The text within should flow in accordance to the dimensions of this HTML element.</p>
</div>


Now you see that the beige paragraph remains the same size, and the size of the orange box looks the same, but now there's a thick brown perimeter around the box. The border property is applied on the outside of the div, and this also has the effect of stretching the width and/or height of the div. So if the height of the div is 200 pixels and the top and bottom both have 50 pixels border applied, then the total effective height of the orange div is now (200 + 50 + 50) = 300 pixels.

This is a wall of text used for testing the CSS Box Model. The text within should flow in accordance to the dimensions of this HTML element.



Margin

This is the space around the perimeter of the element.

<div style="height:200px;width:200px;background-color:rgb(255,200,0);margin:50px">
  <p style="background-color:rgb(255,200,100);">This is a wall of text used for testing the CSS Box Model. The text within should flow in accordance to the dimensions of this HTML element.</p>
</div>


You can see that the size of neither the content or the container have changed, but now there's a 50 pixel space around the perimeter of the container. You could even think of it as the perimeter's perimeter! Except that the margin property is always transparent.

This is a wall of text used for testing the CSS Box Model. The text within should flow in accordance to the dimensions of this HTML element.



Putting it all together

This is probably overkill at this point, but it's still worth going into. Let's see what happens when we put them all together.

<div style="height:200px;width:200px;background-color:rgb(255,200,0);padding:50px;border:50px solid rgb(200,100,0);margin:50px">
  <p style="background-color:rgb(255,200,100);">This is a wall of text used for testing the CSS Box Model. The text within should flow in accordance to the dimensions of this HTML element.</p>
</div>


You see the empty space of 50 pixels around the box, then the 50 pixel brown border, followed by the 50 pixel margin of the orange box! The beige paragraph shows the original width of the box. So in effect, all this combines to affect the height and width of the box.

This is a wall of text used for testing the CSS Box Model. The text within should flow in accordance to the dimensions of this HTML element.



The takeaway

The CSS Box Model affects not just divs, but all HTML elements. Knowing how padding, margins and borders affect width and height can help eliminate any confusion as to why your elements are not lining up properly.

May the Force be width you!
T___T

Sunday, 16 July 2023

Meta's new platform: Threads or Threat?

I've never wanted a Twitter account. Back in the day, the thought of maintaining yet another extension of my online identity held little appeal, especially when all I could seemingly expect in return was yet another platform of celebrities airing opinions that they seemed to think I should care about, or non-celebs making those proclamations as though they were celebs. And, of course, thread after thread of shmucks with too much time on their hands people trying desperately to sound cleverer than the next guy, or just outright hurling verbal abuse when they thought they could get away with it.

Heck, if that was what I was after, I already had Facebook for that.


Thus, last week, when I saw an Instagram notification that told me some of my friends had already started posting on Meta's spanking brand new Social Media platform Threads, I was momentarily tempted to just hop on the bandwagon. Momentarily. Until I did a little homework and it turned out that Threads was basically a spiritual clone of Twitter less a few features.

Now, if I already had zero desire to have a Twitter account whether or not Elon Musk was in charge, why would I want Threads?

The case for Threads

The ad-free experience? Come on, now. We all know that once Threads hits a certain critical mass in terms of user base, Mark Zuckerberg is going to monetize the hell out of it. It's just business.

More moderation, less toxicity? Sure, right now Threads is a nice friendly place with content moderation. However, once enough users join this platform, the same thing is going to happen that happens everywhere - the name-calling, the endless virtue-signalling, the constant pseudo-intellectual bullshit. People are trash, and they don't stop being trash even if you intend to implement a level of censorship that would cause China to sigh with approval.

Censored!

It's also tempting to sign up for Threads just to spite Elon Musk, though in all honesty, a lot of that is his own fault. This techbro billionaire basically paints a target on himself every time he gets online to spout some outrageously trollish bullshit. He's a billionaire and the average Keyboard Warrior can't even begin to touch him... but they can affect his bottom line and dent his ego by going over to his competitor now that he has one. I like to think I'm above these levels of pettiness, though the simple fact is that my laziness precludes me from it.

Competition is good for the market, generally. Threads represents just that. Some might say we already have way too many Social Media platforms. I say that's only the case if one intends to sign up for all of them. As someone whose fortunes aren't being intrinsically tied to public recognition - such as, say, actors and artistes and influencers - I have the privilege of not giving a shit, one which I intend to exercise with extreme prejudice.

But back to the issue of competition. It's important to keep these guys in check. And if the world's governments can't, perhaps these competing platforms will keep each other in check. Think about it - even without a monopoly, platforms like Facebook, Twitter and the like were abusing data confidentiality and unevenly applying moderation policies. How much bolder would they get if they had a monopoly? That, my friends, is entirely unthinkable.

The case against Threads

I don't actually have anything against Threads specifically. At least, not any more than I have against Twitter and all the platforms that attempt to imitate it. It just doesn't offer me any value that would entice me to partake; I have no ambitions to become an influencer of any sort, and I certainly don't think my opinions require a Twitter-like soapbox to air them from.

Why, Zuck?!

But sure, let's take a look at the data that Threads wants to collect. That's quite a list: location, personal, health financial, web browsing... check out the screenshot, or check it out yourself on your App Store. Even by the standards of Social Media, this seems excessive. Now, being a complete nobody, I'm personally not all that concerned about data privacy. People who feel differently about the issue may beg the question: why in the ever-loving Zuck do you need all that data, Mark? Sure, these are mostly optional. But still, it's a little concerning.

There's also the issue of your Instagram account being tied to your Threads account. Apparently, once you install Threads, you can't delete your Threads account without also deleting your Instagram account, which just feels really tyrannical. I expect them to fix this at some point, though.

Lastly, there's the buzz that Threads lacks certain features that Twitter has, such as Trends and Spaces. But let's not kid ourselves, all people really need is a platform to air their opinions, exchange news of dubious veracity and argue with complete strangers. You don't need those extra features for that.

So... Threads or Threat?

Whatever one may think of Meta or Mark Zuckerberg, the move to create Threads and attract the Instagram user base was nothing short of brilliant. Already, Threads has unsurpassed rapidity of user growth and avoided (sort of) the growing pains of most other Social Media platforms. It came at an crucial time when Elon Musk seems to be struggling as Twitter's owner, and his increasingly unpopular decisions have left Twitter's user base crying out for an alternative.

Will Threads be Twitter's death blow? I sure hope not. Not that I'm a fan of either Mark Zuckerberg or Elon Musk, but the last thing we need in the Social Media arena is a monopoly. The existence of a flailing Twitter has given rise to the creation of Threads, and the existence of Threads should in turn hold Musk's feet to the fire. Already, we have news of an impending lawsuit. Good times!

Thread carefully, Elon Musk!
T___T

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