Thursday, 30 March 2023

Web Tutorial: Easter Egg Line Game (Part 4/4)

We've built most of the moving parts, now let's get to scoring, and ending the game. To do this, we need more properties in data. Add the pointsPerEgg property. Set it at 10. We can change this later if you feel like it.
data:
{
    started: false,
    paused: false,
    max: 100,
    moves: 50,
    remainingMoves: 50,
    pointsPerEgg: 10,
    score: 0,
    grid: [],
    selection: [],
    rainbow:
    {
        red: 0,
        orange: 0,
        yellow: 0,
        green: 0,
        blue: 0,
        indigo: 0,
        violet: 0
    }
},


Remember the confirm() method? The first For loop was for the selection array. In it, declare inc as a formula. If i is greater than 1, square the values of i less 1, then multiply by pointsPerEgg. If i is 1 or less, inc is 0. And then increment score by inc. What this does is, as long as there are more than 2 elements in selection, the player earns points. The more eggs above 2, the more points! If the player only has 2 eggs selected when clicking "CONFIRM", there are zero points.
for (var i = 0; i < this.selection.length; i++)
{
    var inc = (i > 1 ? (i - 1) * (i - 1) * this.pointsPerEgg : 0);
    this.score += inc;


    arr[this.selection[i][0]][this.selection[i][1]].style = "";
    arr[this.selection[i][0]][this.selection[i][1]].color = "";
}


Let's give this a go. If I select these 2 blue eggs and click "CONFIRM", I get 0 points.




But if I select these 4 blue eggs and click "CONFIRM"...




...I get 140 points!




Go on, count it. If pointsPerEgg is 10...

Egg 0: 0 points
Egg 1: 0 points
Egg 2: 1 x 1 x 10 = 10 points
Egg 3: 2 x 2 x 10 = 40 points
Egg 4: 3 x 3 x 10 = 90 points

The total is (10 + 40 + 90 = 140) points!

Now for more scoring! In the For loop, add a variable, color. Set it to the color property of the element in arr that is pointed to by the appropriate values in the current values of selection. Basically, that will be the color of the eggs in the selected line.
for (var i = 0; i < this.selection.length; i++)
{
    var color = arr[this.selection[i][0]][this.selection[i][1]].color;
    var inc = (i > 1 ? (i - 1) * (i - 1) * this.pointsPerEgg : 0);
    this.score += inc;

    arr[this.selection[i][0]][this.selection[i][1]].style = "";
    arr[this.selection[i][0]][this.selection[i][1]].color = "";
}


Increment the appropriate color in the rainbow object by 10. If the value has exceeded the value of max, set it to max.
for (var i = 0; i < this.selection.length; i++)
{
    var color = arr[this.selection[i][0]][this.selection[i][1]].color;
    var inc = (i > 1 ? (i - 1) * (i - 1) * this.pointsPerEgg : 0);
    this.score += inc;
    this.rainbow[color] += 10;
    if (this.rainbow[color] >= this.max) this.rainbow[color] = this.max;


    arr[this.selection[i][0]][this.selection[i][1]].style = "";
    arr[this.selection[i][0]][this.selection[i][1]].color = "";
}


Let's see what this does! If I select 4 green eggs, for example...




...the green stripe in the rainbow is filled up by 40%! Try selecting more green eggs and see if you can get above 100%. You shouldn't be able to (because of that If block we added to prevent that specifically), but you'll continue to score points anyway.




Now, what happens if we fill up the entire rainbow? We're about to handle that. In the confirm() method, we set paused to false at the end. Now instead, we will set it to the value returned by running the isEndGame() method, which will return either true or false. Thus, if it is an endgame scenario, the game will remain paused. Otherwise, pause is set to false and the game continues.
setTimeout(
    ()=>
    {
        this.fillMissingEggs();
        this.paused = this.isEndGame();
    },
    500
);


Create the method. In it, declare variable endGameScenario and set it to false by default. At the end of the method, return endGameScenario.
            setTimeout(
                ()=>
                {
                    this.fillMissingEggs();
                    this.paused = this.isEndGame();
                },
                500
            );
        },
        1000
    );
},
isEndGame: function()
{
    var endGameScenario = false;

    return endGameScenario;
},

isPossibleToMove: function()
{
    var possible = false;
    var arr = this.grid;

    for (var i = 0; i < arr.length; i++)
    {
        if (possible) break;

        for (var j = 0; j < arr[i].length; j++)
        {
            if (i > 0)
            {
                if (arr[i][j].color == arr[i - 1][j].color)
                {
                    possible = true;
                    break;
                }
            }

            if (i < arr.length - 1)
            {
                if (arr[i][j].color == arr[i + 1][j].color)
                {
                    possible = true;
                    break;
                }                                            
            }

            if (j > 0)
            {
                if (arr[i][j].color == arr[i][j - 1].color)
                {
                    possible = true;
                    break;
                }        
            }

            if (j < arr[i].length - 1)
            {
                if (arr[i][j].color == arr[i][j + 1].color)
                {
                    possible = true;
                    break;
                }    
            }
        }
    }

    return possible;
}


Right! Now, if remainingMoves is 0, that means there are no more moves and thus the game is over. In this case, we set endGameScenario to true.
isEndGame: function()
{
    var endGameScenario = false;

    if (this.remainingMoves == 0) endGameScenario = true;

    return endGameScenario;
},


If every property in the rainbow array is the value of max, then the game is also over.
isEndGame: function()
{
    var endGameScenario = false;

    if (this.remainingMoves == 0) endGameScenario = true;
    if (this.rainbow.red == this.max && this.rainbow.orange == this.max && this.rainbow.yellow == this.max && this.rainbow.green == this.max && this.rainbow.blue == this.max && this.rainbow.indigo == this.max && this.rainbow.violet == this.max) endGameScenario = true;

    return endGameScenario;
},


Here, we check if it's an endGameScenario. If not, we need to run the handleimpossibleScenario() method.
isEndGame: function()
{
    var endGameScenario = false;

    if (this.remainingMoves == 0) endGameScenario = true;
    if (this.rainbow.red == this.max && this.rainbow.orange == this.max && this.rainbow.yellow == this.max && this.rainbow.green == this.max && this.rainbow.blue == this.max && this.rainbow.indigo == this.max && this.rainbow.violet == this.max) endGameScenario = true;

    if (endGameScenario)
    {

    }
    else
    {
        this.handleimpossibleScenario();
    }


    return endGameScenario;
},


If it's time to end the game, we will do this fancy animation. We want the final score to appear, but we want the number to run! At the same time, the number of moves remaining will go to zero. The more moves remaining there are, the more points the user scores. So first, let us add this property, pointsPerMove, to data. Set it at 100. You can change it later.
data:
{
    started: false,
    paused: false,
    max: 100,
    moves: 50,
    remainingMoves: 50,
    pointsPerEgg: 10,
    pointsPerMove: 100,
    score: 0,
    grid: [],
    selection: [],
    rainbow:
    {
        red: 0,
        orange: 0,
        yellow: 0,
        green: 0,
        blue: 0,
        indigo: 0,
        violet: 0
    }
},


Back to the isEndGame() method, we declare scoreTimer and set it by running the setInterval() function with a 100 millisecond delay. It will run the code within every 100 milliseconds.
isEndGame: function()
{
    var endGameScenario = false;

    if (this.remainingMoves == 0) endGameScenario = true;
    if (this.rainbow.red == this.max && this.rainbow.orange == this.max && this.rainbow.yellow == this.max && this.rainbow.green == this.max && this.rainbow.blue == this.max && this.rainbow.indigo == this.max && this.rainbow.violet == this.max) endGameScenario = true;

    if (endGameScenario)
    {
        var scoreTimer = setInterval(
            ()=>
            {

            },
            100
        );

    }
    else
    {
        this.handleimpossibleScenario();
    }
    
    return endGameScenario;
},


In here, we check for the value of remainingMoves.
var scoreTimer = setInterval(
    ()=>
    {
        if (this.remainingMoves > 0)
        {

        }
        else
        {

        }

    },
    100
);


If it's more than 0, decrement it. And then increment score by pointsPerMove.
var scoreTimer = setInterval(
    ()=>
    {
        if (this.remainingMoves > 0)
        {
            this.remainingMoves--;
            this.score += this.pointsPerMove;

        }
        else
        {

        }
    },
    100
);


If it's arrived at 0, run clearInterval() on scoreTimer to stop the sequence. Then add the string "(GAME OVER)" to score. That's lazy AF, but it works, so blow me.
var scoreTimer = setInterval(
    ()=>
    {
        if (this.remainingMoves > 0)
        {
            this.remainingMoves--;
            this.score += this.pointsPerMove;
        }
        else
        {
            clearInterval(scoreTimer);
            this.score += " (GAME OVER)";

        }
    },
    100
);


Now we're going to test this. Play till you fill up the entire rainbow. Once you fill up the last stripe, you'll find that you can't click on the eggs anymore, or at least, there's no effect when you do. And then the numbers start moving till "Moves Remaining" is 0, with the scoreline increasing each time! You can also test what happens when you use up all your moves.




You may have noticed that the "REPLAY" button came on, as well, once the game ended. Do this, so that when the user clicks on it, it restarts the game.
<div class="bottom">
    <br />
    <button v-on:click="confirm()" v-bind:style=getButtonVisibility("confirm")>CONFIRM</button>
    <button v-on:click="setStarted(true)" v-bind:style=getButtonVisibility("start")>BEGIN</button>
    <button v-on:click="setStarted(false)" v-bind:style=getButtonVisibility("stop")>STOP GAME</button>
    <button v-on:click="setStarted(true)" v-bind:style=getButtonVisibility("restart")>REPLAY</button>
</div>


Cleanup and stuff

Now let's give the game a nice header, using the styles we created.
<div class="title">
    <div class="letter red">E</div>
    <div class="letter orange">A</div>
    <div class="letter yellow">S</div>
    <div class="letter green">T</div>
    <div class="letter blue">E</div>
    <div class="letter indigo">R</div>
    <div class="square">
        <div class="egg style1 violet">
            <div>&nbsp;</div>
            <div>&nbsp;</div>
            <div>&nbsp;</div>
        </div>
    </div>
    <br style="clear:both" />
    <h2><i>line game</i></h2>

</div>


Here is the letter CSS class. We'll set height, width, and make it float left. Text is aligned center.
    .violet
    {
        background-color: rgb(200, 50, 200);
    }

    .letter
    {
        width: 50px;
        height: 55px;
        font-weight: bold;
        font-size: 3em;
        float: left;
        text-align: center;
        color: rgb(0, 0, 0);
    }

</style>


While we're doing this, let's remove the red outline.
div {outline: 0px solid red;}


Now this looks respectable! I really liked being able to add an egg at the end.




Phew, that's it!

This was quite a Herculean effort. But I think I did justice to this Easter's web tutorial. Go try it, and have fun playing it!

Have an AMAZING Easter,
T___T

Monday, 27 March 2023

Web Tutorial: Easter Egg Line Game (Part 3/4)

In this part, we will need more data for gameplay.

Add started and paused as Boolean values, set to false. started and paused are used to determine what buttons are available. paused is also meant to disable certain functions while animation is running.
data:
{
    started: false,
    paused: false,

    max: 100,
    moves: 50,
    remainingMoves: 50,
    score: 0,
    grid: [],
    rainbow:
    {
        red: 0,
        orange: 0,
        yellow: 0,
        green: 0,
        blue: 0,
        indigo: 0,
        violet: 0
    }
},


In the reset() method, set paused to false. That's because we want to un-pause whenever there's a reset.
reset: function()
{
    this.resetEggs();
    this.remainingMoves = this.moves;
    this.score = 0;
    this.rainbow = {
        red: 0,
        orange: 0,
        yellow: 0,
        green: 0,
        blue: 0,
        indigo: 0,
        violet: 0
    };

    this.handleimpossibleScenario();

    this.paused = false;
},


Now in the HTML, bind the styles of the buttons to the setButtonVisibility() method, passing in the names of the buttons as arguments.
<div class="bottom">
    <br />
    <button v-bind:style=getButtonVisibility("confirm")>CONFIRM</button>
    <button v-bind:style=getButtonVisibility("start")>BEGIN</button>
    <button v-bind:style=getButtonVisibility("stop")>STOP GAME</button>
    <button v-bind:style=getButtonVisibility("restart")>REPLAY</button>
</div>


And then create the method. Declare vis. By default, it is "none". We return a style string using the value of vis.
getMarginTop: function(color)
{
    var percentage = this.max - this.rainbow[color];
    return "margin-top:" + ((percentage / this.max) * 480) + "px";
},
getButtonVisibility: function(section)
{
    var vis = "none";

    return "display:" + vis;
},

getEggClass: function(c, s)
{
    return "egg " + this.grid[c][s].style + " " + this.grid[c][s].color;
},


So first of all, we handle the scenarios for "start". We use the "start" button only if started is false and we want to set it to true. But regardless, if the game is paused, the button is hidden.
getButtonVisibility: function(section)
{
    var vis = "none";

    if (section == "start" && !this.started) vis = "block";
    if (this.paused) vis = "none";


    return "display:" + vis;
},


And then there's the other buttons. We should only be able to stop a game if started is true. And we should only be able to restart a game if remainingMoves is 0 (which means the game is over).
getButtonVisibility: function(section)
{
    var vis = "none";

    if (section == "start" && !this.started) vis = "block";
    if (this.paused) vis = "none";
    if (section == "stop" && this.started) vis = "block";
    if (section == "restart" && this.started && this.remainingMoves == 0) vis = "block";


    return "display:" + vis;
},


Now, because started is false, only the "BEGIN" button is visible!




Let us add an action to the button. It calls the setStarted() method, passing the value true in as an argument.
<div class="bottom">
    <br />
    <button v-bind:style=getButtonVisibility("confirm")>CONFIRM</button>
    <button v-on:click="setStarted(true)" v-bind:style=getButtonVisibility("start")>BEGIN</button>
    <button v-bind:style=getButtonVisibility("stop")>STOP GAME</button>
    <button v-bind:style=getButtonVisibility("restart")>REPLAY</button>
</div>


The method starts off by setting started to the value of the parameter started, which is either true or false.
getEggClass: function(c, s)
{
    return "egg " + this.grid[c][s].style + " " + this.grid[c][s].color;
},
setStarted: function(started)
{
    this.started = started;
},

fillMissingEggs: function()
{
    var arr = this.grid;

    for (var i = 0; i < arr.length; i++)
    {
        for (var j = 0; j < arr[i].length; j++)
        {
            if (arr[i][j].style == "" && arr[i][j].color == "")
            {
                arr[i][j] = this.getRandomPair();
            }
        }
    }

    this.grid = [];
    this.grid = arr;
},


If the value of the parameter started is true, we also call reset(). If not, we set the value of paused to false.
setStarted: function(started)
{
    this.started = started;

    if (started)
    {
        this.reset();
    }
    else
    {
        this.paused = false;
    }

},


Now we set an action to the "STOP GAME" button. Again, we call setStarted() but pass in false as an argument.
<div class="bottom">
    <br />
    <button v-bind:style=getButtonVisibility("confirm")>CONFIRM</button>
    <button v-on:click="setStarted(true)" v-bind:style=getButtonVisibility("start")>BEGIN</button>
    <button v-on:click="setStarted(false)" v-bind:style=getButtonVisibility("stop")>STOP GAME</button>
</div>


Refresh. Click the "BEGIN" button. You should see it disappear to be replaced by the "STOP GAME" button. If you click that, it should revert to the "BEGIN" button.




Let's spruce up this interface a little in the meantime. Make this change to the styling for middle. Remove the specification for height. Add two other properties. overflow should be set to hidden, and we set transition to 1 second because we're going to animate this.
.middle
{
    width: 100%;
    float: left;
    /*height: 500px;*/
    overflow: hidden;
    webKit-transition: all 1s;
    transition: all 1s;
}


In the div styled using middle, bind the style attribute to the getMiddleHeight() method, passing in "game" as an argument.
<div class="middle" v-bind:style=getMiddleHeight("game")>


Now add a div, also styled using middle, also with the style attribute bound. This time, however, the argument should be "instructions".
<div class="middle" v-bind:style=getMiddleHeight("instructions")>

</div>


<div class="middle" v-bind:style=getMiddleHeight("game")>
    <div class="grid">


Maybe add some text.
<div class="middle" v-bind:style=getMiddleHeight("instructions")>
    <h1>Instructions</h1>
    <p>Your objective is to form a full rainbow in as few moves as possible.</p>
    <p>Select a line (2 or more) of similarly colored eggs and click CONFIRM. This counts as one move, and we fill up the appropriate color on the meter.</p>
    <p>A line can be any combination of horizontal or vertical lines.</p>
    <p>The game ends when you run out of moves, or when a full rainbow is formed.</p>
    <p>Extra points are earned from lines greater than 2 eggs.</p>

</div>


And then create the getMiddleHeight() method. We start by declaring height, which is set to 0 by default. At the end, we return a style string using the value of height.
getMarginTop: function(color)
{
    var percentage = this.max - this.rainbow[color];
    return "margin-top:" + ((percentage / this.max) * 480) + "px";
},
getMiddleHeight: function(section)
{
    var height = 0;

    return "height:" + height + "px";
},

getButtonVisibility: function(section)
{
    var vis = "none";

    if (section == "start" && !this.started) vis = "block";
    //if (section == "confirm" && this.selection.length >= 2) vis = "block";
    if (this.paused) vis = "none";
    if (section == "stop" && this.started) vis = "block";
    if (section == "restart" && this.started && this.remainingMoves == 0) vis = "block";

    return "display:" + vis;
},


And here, we handle the cases. If the value of section is "game" and started is true, we set height to 500. If the value of section is "instructions" and the game is not started, we set height to 500. This basically means that either one of the divs that have been styled using middle, will be visible depending on the value of started.
getMiddleHeight: function(section)
{
    var height = 0;

    if (section == "game" && this.started) height = 500;
    if (section == "instructions" && !this.started) height = 500;

    return "height:" + height + "px";
},


Try it! Now you should see a panel of instructions when you refresh the game. When you click on "BEGIN GAME", thus setting started to true, the grid appears to scroll up! It's actually because the height of that div was 0 when started was false, and since the overflow property is hidden, that means the grid is not visible. And only partially visible when the height of the div is more than 0. Same for the instructions panel!








Just to make it look nicer, encase the instructions in a div and style it using instructions.
<div class="middle" v-bind:style=getMiddleHeight("instructions")>
    <div class="instructions">
        <h1>Instructions</h1>
        <p>Your objective is to form a full rainbow in as few moves as possible.</p>
        <p>Select a line (2 or more) of similarly colored eggs and click CONFIRM. This counts as one move, and will fill up the appropriate color on the meter.</p>
        <p>A line can be any combination of horizontal or vertical lines.</p>
        <p>The game ends when you run out of moves, or when a full rainbow is formed.</p>
        <p>Extra points are earned from lines greater than 2 eggs.</p>
    </div>
</div>


Here's the styling for instructions. Just making it look pretty, so don't worry about it.
.middle
{
    width: 100%;
    float: left;
    overflow: hidden;
    webKit-transition: all 1s;
    transition: all 1s;
}

.instructions
{
    border-radius: 10px;
    background-color: rgb(100, 100, 100);
    width: 80%;
    height: 80%;
    padding: 10px;
    margin: 5% auto 0 auto;
    overflow: hidden;
}


.grid
{
    width: 500px;
    height: 100%;
    float: left;        
}


Yup!




Time for gameplay!

We're supposed to be able to click the eggs to select them, so add an event to each square. Clicking on them will run the selectSquare() method, passing in the values of c and s as arguments.
<div class="square" v-for="(s, square) in col" v-on:click="selectSquare(c, s)">
    <div v-bind:class="getEggClass(c, s)">
        <div>&nbsp;</div>
        <div>&nbsp;</div>
        <div>&nbsp;</div>
    </div>
</div>


Create the method.
setStarted: function(started)
{
    this.started = started;

    if (started)
    {
        this.reset();
    }
    else
    {
        this.paused = false;
    }
},
selectSquare: function(c, s)
{

},

fillMissingEggs: function()
{
    var arr = this.grid;

    for (var i = 0; i < arr.length; i++)
    {
        for (var j = 0; j < arr[i].length; j++)
        {
            if (arr[i][j].style == "" && arr[i][j].color == "")
            {
                arr[i][j] = this.getRandomPair();
            }
        }
    }

    this.grid = [];
    this.grid = arr;
},


Because we will be working with the selection array, add this to data as an empty array.
data:
{
    started: false,
    paused: false,
    max: 100,
    moves: 50,
    remainingMoves: 50,
    score: 0,
    grid: [],
    selection: [],
    rainbow:
    {
        red: 0,
        orange: 0,
        yellow: 0,
        green: 0,
        blue: 0,
        indigo: 0,
        violet: 0
    }
},


And make sure that in reset(), it's set to an empty array.
reset: function()
{
    this.resetEggs();
    this.remainingMoves = this.moves;
    this.score = 0;
    this.selection = [];
    this.rainbow = {
        red: 0,
        orange: 0,
        yellow: 0,
        green: 0,
        blue: 0,
        indigo: 0,
        violet: 0
    };

    this.handleimpossibleScenario();

    this.paused = false;
},


Now back to the selectSquare() method. Firstly, exit early if the value of paused is true. If the game is paused, we don't want anything to happen.
selectSquare: function(c, s)
{
    if (this.paused) return;
},


Declare the variable reset and set to false by default. At the end of the method, if reset has somehow been set to true, set selection to an empty array.
selectSquare: function(c, s)
{
    if (this.paused) return;

    var reset = false;

    if (reset) this.selection = [];

},


Now declare insertPos and set the value by calling the isLinkableAtPos() method, passing in the value of c and s. The method should return either 0 (if linkable at the beginning of selection) or 1 (linkable at the end of selection) or null (not linkable at all).
selectSquare: function(c, s)
{
    if (this.paused) return;

    var reset = false;
    var insertPos = this.isLinkableAtPos(c, s);

    if (reset) this.selection = [];
},


Now, we have another method, isInSelection(). We call it and pass in the value of c and s. It's going to return either true (yes, that particular square is already selected) or false (no, it isn't). So if the result is false and the value of insertPos is null, we need to reset the selection array.
selectSquare: function(c, s)
{
    if (this.paused) return;

    var reset = false;
    var insertPos = this.isLinkableAtPos(c, s);

    if (!this.isInSelection(c, s) && insertPos == null)
    {
        reset = true;
    }


    if (reset) this.selection = [];
},


Now, if insertPos is 0, we call unshift() to insert the array containing c and s in the beginning of selection. If it's 1, use push() to insert at the end of selection.
selectSquare: function(c, s)
{
    if (this.paused) return;

    var reset = false;
    var insertPos = this.isLinkableAtPos(c, s);

    if (!this.isInSelection(c, s) && insertPos == null)
    {
        reset = true;
    }

    if (insertPos == 0 && insertPos != null)
    {
        this.selection.unshift([c, s]);
    }
    else
    {
        this.selection.push([c, s]);
    }


    if (reset) this.selection = [];
},


And of course, we need to define the methods isLinkableAtPos() and isInSelection().
getEggClass: function(c, s)
{
    return "egg " + this.grid[c][s].style + " " + this.grid[c][s].color;
},
isInSelection: function(c, s)
{

},
isLinkableAtPos: function(c, s)
{

},

setStarted: function(started)
{
    this.started = started;

    if (started)
    {
        this.reset();
    }
    else
    {
        this.paused = false;
    }
},


isInSelection() is simpler, so let's do that first. By default, it returns false. In this method, there are two parameters, c and s.
isInSelection: function(c, s)
{
    return false;
},
isLinkableAtPos: function(c, s)
{

},


Before that, we iterate through selection. If at any time, the array containing c and s shows up, we return true.
isInSelection: function(c, s)
{
    for (var i = 0; i < this.selection.length; i++)
    {
        if (this.selection[i][0] == c && this.selection[i][1] == s) return true;
    }


    return false;
},


That was simple enough! Now we will work on isLinkableAtPos(). In this method, again, there are two parameters, c and s. First, if there are no elements in the selection array, we return 1 to tell the parent method, selectSquare(), that we can push the array containing c and s into selection. By default, we return null.
isLinkableAtPos: function(c, s)
{
    if (this.selection.length == 0) return 1;
    
    return null;

},


Let's break away for a bit to add this into the getEggClass() method. In here, we will add the outline CSS class if a call to isInSelection() returns true.
getEggClass: function(c, s)
{
    return "egg " + this.grid[c][s].style + " " + this.grid[c][s].color + " " + (this.isInSelection(c, s) ? "outline" : "");
},


Here's the outline CSS class. In fact, while we're at it, we will set this to the hover pseudoselector of egg. So if you mouse over an egg, it has a white dotted outline. If you click on it and add it to the selection array, the outline should be solid!
.egg
{
    width: 70%;
    height: 90%;
    border-radius: 50%/60% 60% 40% 40%;
    overflow: hidden;
    position: relative;
    margin: 2px auto 0 auto;
    filter: opacity(100);
}

.egg:hover
{
    outline: 2px dotted rgb(255, 255, 255);
}

.egg.outline
{
    outline: 2px solid rgb(255, 255, 255);
}


.egg:after
{
    display: block;
    position: absolute;
    content: "";
    width: 100%;
    height: 100%;
}


Now try positioning your mouse cursor over an egg (in this example, it's one of the red eggs in the middle. Does the white dotted outline appear? If you mouse out, it should disappear.




If you click on it, it should now be a solid white outline! If you click elsewhere, the outline should disappear because null was returned.




Now back to the isLinkableAtPos() method. We need to cater to other cases. First, we declare variables. firstNodeLocationCompatible is a flag that determines if the egg selected is right next to the first egg in the selection array, either horizontally or vertically. lastNodeLocationCompatible does the same, except for the last egg in the selection array. colorCompatible checks if the selected egg is the same color as all elements in the selection array. By default, the value is false.
isLinkableAtPos: function(c, s)
{
    if (this.selection.length == 0) return 1;

    var firstNodeLocationCompatible = false;
    var lastNodeLocationCompatible = false;
    var colorCompatible = false;


    return null;
},


Declare firstNode, and set it to the first element in the selection array. Also declare lastNode as the last element in the selection array. At this point, there should be at least one element in the array because the case where there are no elements, has already been handled with the guard clause at the beginning of the method.
isLinkableAtPos: function(c, s)
{
    if (this.selection.length == 0) return 1;

    var firstNodeLocationCompatible = false;
    var lastNodeLocationCompatible = false;
    var colorCompatible = false;

    var firstNode = this.selection[0];
    var lastNode = this.selection[this.selection.length - 1];


    return null;
},


Now remember that firstNode and lastNode are both arrays containing two elements - column and row indexes. First we need to determine if firstNodeLocationCompatible is true. So if the first element of firstNode matches c and and the second element of firstNode is 1 removed from s (we use abs() to determine this), this means that the selected egg is next to the first egg vertically.
isLinkableAtPos: function(c, s)
{
    if (this.selection.length == 0) return 1;

    var firstNodeLocationCompatible = false;
    var lastNodeLocationCompatible = false;
    var colorCompatible = false;

    var firstNode = this.selection[0];
    var lastNode = this.selection[this.selection.length - 1];

    if ((firstNode[0] == c && Math.abs(firstNode[1] - s) == 1)) firstNodeLocationCompatible = true;

    return null;
},


If the second element of firstNode matches s and and the first element of firstNode is 1 removed from c (again, we use abs() to determine this), this means that the selected egg is next to the first egg horizontally. Now if the selected egg is either horizontally or vertically next to that first egg, firstNodeLocationCompatible is true.
isLinkableAtPos: function(c, s)
{
    if (this.selection.length == 0) return 1;

    var firstNodeLocationCompatible = false;
    var lastNodeLocationCompatible = false;
    var colorCompatible = false;

    var firstNode = this.selection[0];
    var lastNode = this.selection[this.selection.length - 1];

    if ((firstNode[0] == c && Math.abs(firstNode[1] - s) == 1) || (firstNode[1] == s && Math.abs(firstNode[0] - c) == 1)) firstNodeLocationCompatible = true;

    return null;
},



The same logic goes for lastNode and lastNodeLocationCompatible.
isLinkableAtPos: function(c, s)
{
    if (this.selection.length == 0) return 1;

    var firstNodeLocationCompatible = false;
    var lastNodeLocationCompatible = false;
    var colorCompatible = false;

    var firstNode = this.selection[0];
    var lastNode = this.selection[this.selection.length - 1];

    if ((firstNode[0] == c && Math.abs(firstNode[1] - s) == 1) || (firstNode[1] == s && Math.abs(firstNode[0] - c) == 1)) firstNodeLocationCompatible = true;
    if ((lastNode[0] == c && Math.abs(lastNode[1] - s) == 1) || (lastNode[1] == s && Math.abs(lastNode[0] - c) == 1)) lastNodeLocationCompatible = true;

    return null;
},


And then we get the color property from the element of grid pointed to by c and s, and compare it to the color property from the element of grid pointed to by the array in firstNode. If there's a match, colorCompatible is true.
isLinkableAtPos: function(c, s)
{
    if (this.selection.length == 0) return 1;

    var firstNodeLocationCompatible = false;
    var lastNodeLocationCompatible = false;
    var colorCompatible = false;

    var firstNode = this.selection[0];
    var lastNode = this.selection[this.selection.length - 1];

    if ((firstNode[0] == c && Math.abs(firstNode[1] - s) == 1) || (firstNode[1] == s && Math.abs(firstNode[0] - c) == 1)) firstNodeLocationCompatible = true;
    if ((lastNode[0] == c && Math.abs(lastNode[1] - s) == 1) || (lastNode[1] == s && Math.abs(lastNode[0] - c) == 1)) lastNodeLocationCompatible = true;
    if (this.grid[firstNode[0]][firstNode[1]].color == this.grid[c][s].color) colorCompatible = true;

    return null;
},


If colorCompatible is true and firstNodeLocationCompatible is true, that means we can return 0 to say that the selected egg can be added to the beginning of the selection array. If colorCompatible is true and lastNodeLocationCompatible is true, we return 1 to say that the selected egg can be added to the end of the selection array.
isLinkableAtPos: function(c, s)
{
    if (this.selection.length == 0) return 1;

    var firstNodeLocationCompatible = false;
    var lastNodeLocationCompatible = false;
    var colorCompatible = false;

    var firstNode = this.selection[0];
    var lastNode = this.selection[this.selection.length - 1];

    if ((firstNode[0] == c && Math.abs(firstNode[1] - s) == 1) || (firstNode[1] == s && Math.abs(firstNode[0] - c) == 1)) firstNodeLocationCompatible = true;
    if ((lastNode[0] == c && Math.abs(lastNode[1] - s) == 1) || (lastNode[1] == s && Math.abs(lastNode[0] - c) == 1)) lastNodeLocationCompatible = true;
    if (this.grid[firstNode[0]][firstNode[1]].color == this.grid[c][s].color) colorCompatible = true;

    if (firstNodeLocationCompatible && colorCompatible) return 0;
    if (lastNodeLocationCompatible && colorCompatible) return 1;


    return null;
},


Now test this! Select an egg. In this example, I've selected a violet egg from the large clump of violet eggs in the middle. But if we then click on the green egg directly above it, it would get deselected, meaning the selection array would be empty.




If we were to select the violet egg two eggs to the right, it would also be deselected.




You can only select violet eggs right net to the initially selected violet egg, and the first and last eggs in the "line" changes with each egg added. For instance, now, if we were to click on the violet egg directly below the selected line of violet eggs, right next to the indigo egg, the selection array would get cleared as well. Because that egg, while the same color, is not horizontally or vertically next to the first and last eggs in the line.




Now that we're able to select eggs, we need to make sure that the "CONFIRM" button is shown when a minimum of two eggs are selected. In getButtonVisibility(), make sure that if section is "confirm" and selection has at least 2 elements, we set vis accordingly. Of course, if paused is true, everything is still hidden. The following line takes care of that.
getButtonVisibility: function(section)
{
    var vis = "none";

    if (section == "start" && !this.started) vis = "block";
    if (section == "confirm" && this.selection.length >= 2) vis = "block";
    if (this.paused) vis = "none";
    if (section == "stop" && this.started) vis = "block";
    if (section == "restart" && this.started && this.remainingMoves == 0) vis = "block";

    return "display:" + vis;
},


See what we did? When the two blue eggs are selected, the "CONFIRM" button appears! Try deselecting. The button should disappear again!




Let's set the "CONFIRM" button to run the method confirm().
<div class="bottom">
    <br />
    <button v-on:click="confirm()" v-bind:style=getButtonVisibility("confirm")>CONFIRM</button>
    <button v-on:click="setStarted(true)" v-bind:style=getButtonVisibility("start")>BEGIN</button>
    <button v-on:click="setStarted(false)" v-bind:style=getButtonVisibility("stop")>STOP GAME</button>
    <button v-bind:style=getButtonVisibility("restart")>REPLAY</button>
</div>


Create the confirm() method. We begin by decrementing remainingMoves.
getRandomPair: function()
{
    var randomIndex;
    randomIndex = Math.floor(Math.random() * 3) + 1;
    var style = "style" + randomIndex;

    randomIndex = Math.floor(Math.random() * 7);
    var color = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"][randomIndex];

    return {"style": style, "color": color};
},
confirm: function()
{
    this.remainingMoves--;
},

isPossibleToMove: function()
{
    var possible = false;
    var arr = this.grid;


We create arr as a temporary variable, setting it to the value of grid because we don't want the grid to re-render until we're good and ready. We want to make those changes in arr first. And then we set paused to true because there is going to be animation and we want to make sure that clicking stuff while it's going on, does not do anything.
confirm: function()
{
    this.remainingMoves--;

    var arr = this.grid;
    this.paused = true;

},


Then we iterate through the selection array and make sure that all elements in arr that correspond with the element in selection, have the CSS class fadeout added to their styling. This means that any egg inside the grid that has been selected, is now styled using fadeout. And then we set grid to the value of arr.
confirm: function()
{
    this.remainingMoves--;

    var arr = this.grid;
    this.paused = true;

    for (var i = 0; i < this.selection.length; i++)
    {
        arr[this.selection[i][0]][this.selection[i][1]].style += " fadeout";
    }


    this.grid = [];
    this.grid = arr;
},


This is the fadeout CSS class. In here, we ensure that opacity is set to 0 and we have a transition duration of 1.
.egg
{
    width: 70%;
    height: 90%;
    border-radius: 50%/60% 60% 40% 40%;
    overflow: hidden;
    position: relative;
    margin: 2px auto 0 auto;
    filter: opacity(100);
}

.fadeout
{
    webkit-transition: all 1s;
    transition: all 1s;
    filter: opacity(0);
}


.egg:hover
{
    outline: 2px dotted rgb(255, 255, 255);
}


Now do a selection and click the "CONFIRM" button. For illustration, I have selected a bunch of indigo eggs.




And watch the indigo eggs fade out! The "CONFIRM" button disappears because pause is now true. Also note that the top right display now says "49 / 50" because remaningMoves has been decremented.




Back to the confirm() method! Now we're left with gaping black hole within the grid. We will need to fill them in... but it's not as simple as it sounds. First, we use the setTimeout() function to ensure that the next piece of code executes only after the fadeout animation is done. So if the fadeout CSS class is set to 1 second duration, make sure the setTimeout function delays for 1000 milliseconds.
confirm: function()
{
    this.remainingMoves--;

    var arr = this.grid;
    this.paused = true;

    for (var i = 0; i < this.selection.length; i++)
    {
        arr[this.selection[i][0]][this.selection[i][1]].style += " fadeout";
    }

    this.grid = [];
    this.grid = arr;

    setTimeout(
        ()=>
        {

        },
        1000
    );

},


So first, we iterate through the selection array to ensure that all those elements that we added the fadeout CSS class to, now have the style and color properties reset to empty strings.
confirm: function()
{
    this.remainingMoves--;

    var arr = this.grid;
    this.paused = true;

    for (var i = 0; i < this.selection.length; i++)
    {
        arr[this.selection[i][0]][this.selection[i][1]].style += " fadeout";
    }

    this.grid = [];
    this.grid = arr;

    setTimeout(
        ()=>
        {
            for (var i = 0; i < this.selection.length; i++)
            {
                arr[this.selection[i][0]][this.selection[i][1]].style = "";
                arr[this.selection[i][0]][this.selection[i][1]].color = "";
            }

        },
        1000
    );
},


The next bit is going to be crazy. We want to go through every column, and look for any squares that are empty (style and color are empty strings), When we find them, we want to replace them with the values from the eggs above. So it looks like eggs are "falling" into place. So first, we examine each and every column of arr using a For loop.
confirm: function()
{
    this.remainingMoves--;

    var arr = this.grid;
    this.paused = true;

    for (var i = 0; i < this.selection.length; i++)
    {
        arr[this.selection[i][0]][this.selection[i][1]].style += " fadeout";
    }

    this.grid = [];
    this.grid = arr;

    setTimeout(
        ()=>
        {
            for (var i = 0; i < this.selection.length; i++)
            {
                arr[this.selection[i][0]][this.selection[i][1]].style = "";
                arr[this.selection[i][0]][this.selection[i][1]].color = "";
            }

            for (var i = 0; i < arr.length; i++)
            {

            }

        },
        1000
    );
},


Now, we iterate through the current array inside arr, from the rear. Meaning, we work from the bottom. We don't do it for element 0 because there's no element above 0. So if there's any element at position 0 (at the top of the column) is empty, they will be filled in with random eggs. More on that later.
for (var i = 0; i < arr.length; i++)
{
    for (var j = arr[i].length - 1; j > 0; j--)
    {

    }

}


Now we check to see if the style property is an empty string.
for (var i = 0; i < arr.length; i++)
{
    for (var j = arr[i].length - 1; j > 0; j--)
    {
        if (arr[i][j].style == "")
        {
                                                
        }

    }
}


If so, we traverse that particular array from that index onwards, all the way up to 0...
for (var i = 0; i < arr.length; i++)
{
    for (var j = arr[i].length - 1; j > 0; j--)
    {
        if (arr[i][j].style == "")
        {
            for (var k = j; k >= 0; k--)
            {

            }  
                                             
        }
    }
}


...and during that traversal, if the current element is not empty, the previous element needs to take on its style and color properties. And we will set the current element to empty.
for (var i = 0; i < arr.length; i++)
{
    for (var j = arr[i].length - 1; j > 0; j--)
    {
        if (arr[i][j].style == "")
        {
            for (var k = j; k >= 0; k--)
            {
                if (arr[i][k].style != "")
                {
                    arr[i][j].style = arr[i][k].style;
                    arr[i][j].color = arr[i][k].color;
                    arr[i][k].style = "";
                    arr[i][k].color = "";
                    break;
                }

            }                                                
        }
    }
}


And then we will empty selection and update grid!
confirm: function()
{
    this.remainingMoves--;

    var arr = this.grid;
    this.paused = true;

    for (var i = 0; i < this.selection.length; i++)
    {
        arr[this.selection[i][0]][this.selection[i][1]].style += " fadeout";
    }

    this.grid = [];
    this.grid = arr;

    setTimeout(
        ()=>
        {
            for (var i = 0; i < this.selection.length; i++)
            {
                arr[this.selection[i][0]][this.selection[i][1]].style = "";
                arr[this.selection[i][0]][this.selection[i][1]].color = "";
            }

            for (var i = 0; i < arr.length; i++)
            {
                for (var j = arr[i].length - 1; j > 0; j--)
                {
                    if (arr[i][j].style == "")
                    {
                        for (var k = j; k >= 0; k--)
                        {
                            if (arr[i][k].style != "")
                            {
                                arr[i][j].style = arr[i][k].style;
                                arr[i][j].color = arr[i][k].color;
                                arr[i][k].style = "";
                                arr[i][k].color = "";
                                break;
                            }
                        }                                                
                    }
                }
            }

            this.selection = [];
            this.grid = arr;

        },
        1000
    );
},


Try this. Select a line of eggs. For example, I have selected green eggs. Then click "CONFIRM".




Now there's the fadeout, leaving a gaping hole...




And then you see the eggs at the top have fallen down to fill in the holes, leaving the gaps at the top!




Now use setTimeout() again with a delay of 500 milliseconds. In there, use fillMissingEggs() to fill in the empty elements with random eggs. And then set paused to false so that the "CONFIRM" button appears after the animation is done.
confirm: function()
{
    this.remainingMoves--;

    var arr = this.grid;
    this.paused = true;

    for (var i = 0; i < this.selection.length; i++)
    {
        arr[this.selection[i][0]][this.selection[i][1]].style += " fadeout";
    }

    this.grid = [];
    this.grid = arr;

    setTimeout(
        ()=>
        {
            for (var i = 0; i < this.selection.length; i++)
            {
                arr[this.selection[i][0]][this.selection[i][1]].style = "";
                arr[this.selection[i][0]][this.selection[i][1]].color = "";
            }

            for (var i = 0; i < arr.length; i++)
            {
                for (var j = arr[i].length - 1; j > 0; j--)
                {
                    if (arr[i][j].style == "")
                    {
                        for (var k = j; k >= 0; k--)
                        {
                            if (arr[i][k].style != "")
                            {
                                arr[i][j].style = arr[i][k].style;
                                arr[i][j].color = arr[i][k].color;
                                arr[i][k].style = "";
                                arr[i][k].color = "";
                                break;
                            }
                        }                                                
                    }
                }
            }

            this.selection = [];
            this.grid = arr;

            setTimeout(
                ()=>
                {
                    this.fillMissingEggs();
                    this.paused = false;
                },
                500
            );

        },
        1000
    );
},


No more screenshots. Just try the animations and see if it's working for you.

Next

Scoring, filling up the rainbow, and ending the game!