Wednesday 22 December 2021

Web Tutorial: VueJS Tile Slider Game (Part 2/2)

Welcome back to the Christmas-themed web tutorial!

We will be making things work, and for that to happen, we need to write more methods and beef up existing ones. We'll start with the isComplete() method. Basically, it checks if all the pieces are in their correct positions. By default, it returns true. And sets the button text to "RESTART".

getStyle: function(id)
{
    if (id == null) return "";

    var obj = this.pieces[id];
    var image = "background-image: url(xmas" + this.imageNo + ".jpg);";
    var offset = "background-position:" + obj.offsetX + "% " + obj.offsetY + "%";

    return image + offset;
},
isComplete: function()
{
    this.btnText = "RESTART";

    return true;
}


Now, we will handle the cases where it returns false. Iterate through the arrangement array using a For loop, and in each iteration, check if the current index is equal to blankIndex.
isComplete: function()
{
    for (var i = 0; i < this.arrangement.length; i++)
    {
        if (i != this.blankIndex)
        {

        }
    }


    this.btnText = "RESTART";

    return true;
}


If not, then check if the current element's piece property matches its id property. If any of them do not match, return false.
isComplete: function()
{
    for (var i = 0; i < this.arrangement.length; i++)
    {
        if (i != this.blankIndex)
        {
            if (this.arrangement[i].piece != this.arrangement[i].id) return false;
        }
    }

    this.btnText = "RESTART";

    return true;
}


In the HTML, add this code to the puzzleContainer div. Basically, we bind the class attribute to the result of isComplete(). If it is true, then we set the class to win.
<div id="puzzleContainer" v-bind:class="isComplete() ? 'win' : ''">


Here's the styling for win. For this, we just give the entire div a nice thick orange border.
.blank
{
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 1);
}

.win
{
    outline: 5px solid rgba(255, 100, 0, 1);
}


Let's make some changes to the getStyle() method. We add the variable clickable, and then run the isClickable() method (we'll be building that later, never fear) and set the class to clickable or unclickable based on the result.
getClass: function(id)
{
    var baseClass = (id == this.blankIndex ? "blank" : "piece");
    var clickable = (this.isClickable(id) == false ? "unclickable" : "clickable");

    return baseClass;
},


Then we return the concatenated string of baseClass and clickable, with a space in between.
getClass: function(id)
{
    var baseClass = (id == this.blankIndex ? "blank" : "piece");
    var clickable = (this.isClickable(id) == false ? "unclickable" : "clickable");

    return baseClass + " " + clickable;
},


Let's take a look at the styling for clickable and unclickable. clickable has the cursor property changed. In addition, on hover, we make the image brighter. unclickable, on the other hand, will show no response.
.blank
{
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 1);
}

.clickable
{
    cursor: pointer;
}

.clickable:hover
{
    filter: brightness(150%);
}

.unclickable
{
    cursor: default;
}


.win
{
    outline: 5px solid rgba(255, 100, 0, 1);
}


Time to create the isClickable() method. By default, it returns false. The purpose of this method is to determine if any of the tiles are clickable, and what direction they can be moved towards ("n", "s", "e" or "w"). They are clickable if they are next to the black tile.
getStyle: function(id)
{
    if (id == null) return "";

    var obj = this.pieces[id];
    var image = "background-image: url(xmas" + this.imageNo + ".jpg);";
    var offset = "background-position:" + obj.offsetX + "% " + obj.offsetY + "%";

    return image + offset;
},
isClickable: function(index)
{
    return false;
},

isComplete: function()
{
    for (var i = 0; i < this.arrangement.length; i++)
    {
        if (i != this.blankIndex)
        {
            if (this.arrangement[i].piece != this.arrangement[i].id) return false;
        }
    }

    this.btnText = "RESTART";

    return true;
}


There are two more cases for a false result. If null is passed in as the index, this means that it is the black tile and naturally cannot be clicked. Also, if the result of isComplete() is true, then the game is over and nothing can be clicked.
isClickable: function(index)
{
    if (index == null) return false;
    if (this.isComplete()) return false;


    return false;
},


Now the next part is tricky. Think of your grid as the one shown below. The numbers are the ids.
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15


So the tile can be clicked on and move right, or "e", if it's not tile id 3, 7 or 11 (the rightmost column) and blankIndex is the square on the right of the tile being examined.
isClickable: function(index)
{
    if (index == null) return false;
    if (this.isComplete()) return false;

    if (index + 1 == this.blankIndex && [3, 7, 11].indexOf(index) == -1) return "e";

    return false;
},


Similarly, the tile can be clicked on and move left, or "w", if it's not tile id 4, 8 or 12 (the leftmost column) and blankIndex is the square on the left of the tile being examined.
isClickable: function(index)
{
    if (index == null) return false;
    if (this.isComplete()) return false;

    if (index + 1 == this.blankIndex && [3, 7, 11].indexOf(index) == -1) return "e";
    if (index - 1 == this.blankIndex && [4, 8, 12].indexOf(index) == -1) return "w";

    return false;
},


The logic is similar for "n" and "s" directions, except that we add and subtract 4 instead of 1, because being one square above or below, due to the arrangement of the grid, means that the tile is actually 4 tiles ahead or behind.
isClickable: function(index)
{
    if (index == null) return false;
    if (this.isComplete()) return false;

    if (index + 1 == this.blankIndex && [3, 7, 11].indexOf(index) == -1) return "e";
    if (index - 1 == this.blankIndex && [4, 8, 12].indexOf(index) == -1) return "w";
    if (index + 4 == this.blankIndex) return "s";
    if (index - 4 == this.blankIndex) return "n";


    return false;
},


Got all that? Cool. Now if you refresh, and you run your mouse cursor over tile id 1 (top row, third column), you will see that it grows brighter.




Same for tile 7 (second row, rightmost column). That's because right now, blankIndex is 3 (as per the default) and the tiles next to tile id 3 are tile ids 4 and 7.




Moving the tiles

We've already created isClickable(), which determines what direction each tile can move when clicked. So now let's implement a method to move those tiles.

First, add this to the HTML. It's a binding to each tile for a click event. When clicked, the moveTile() method is called. Make sure to pass in the id of the tile as an argument.
<div v-bind:class="getClass(holder.id)"  v-bind:id="'piece'+holder.id" v-bind:style="getStyle(holder.piece)" v-on:click="moveTile(holder.id)">

</div>


Then we define the moveTile() method.
        isComplete: function()
        {
            for (var i = 0; i < this.arrangement.length; i++)
            {
                if (i != this.blankIndex)
                {
                    if (this.arrangement[i].piece != this.arrangement[i].id) return false;
                }
            }

            this.btnText = "RESTART";

            return true;
        },
        moveTile: function(index)
        {

        }

    },
    created: function()
    {
        this.reset();
    }                    
}


In here, we declare dir. Run the isClickable() method, passing in index as an argument. Set dir to the value of the result. If dir is falsy, that means that tile is not clickable and therefore we should exit early.
moveTile: function(index)
{
    var dir = this.isClickable(index);
    if (!dir) return;

}


Otherwise, declare piece and set it to the appropriate element by using index to determine the id. Now create four If blocks for each possible direction.
moveTile: function(index)
{
    var dir = this.isClickable(index);
    if (!dir) return;

    var piece = document.getElementById("piece" + index);

    if (dir == "e")
    {

    }

    if (dir == "w")
    {

    }

    if (dir == "s")
    {

    }

    if (dir == "n")
    {

    }

}


Declare v and set it to the value of this. That's because we are going to use a callback soon, and want to avoid scope errors.
moveTile: function(index)
{
    var dir = this.isClickable(index);
    if (!dir) return;

    var piece = document.getElementById("piece" + index);
    var v = this;

    if (dir == "e")
    {

    }

    if (dir == "w")
    {

    }

    if (dir == "s")
    {

    }

    if (dir == "n")
    {

    }
}


Now let's start with moving pieces eastwards. Set the class of piece to piece and clickable and move_e. Then use a setTimeout() function with a callback and 300 as the timeout value.
moveTile: function(index)
{
    var dir = this.isClickable(index);
    if (!dir) return;

    var piece = document.getElementById("piece" + index);
    var v = this;

    if (dir == "e")
    {
        piece.className = "piece clickable move_" + dir;

        setTimeout
        (
            function()
            {

            },
            300
        );

    }

    if (dir == "w")
    {

    }

    if (dir == "s")
    {

    }

    if (dir == "n")
    {

    }
}


Now you'll see why v needed to be declared. We are going to perform operations on the arrangement array and the property blankIndex. If we move the tile eastwards, the blank tile that was there will now take the place of the moved tile. And at the end of it, we set the class back to piece.
moveTile: function(index)
{
    var dir = this.isClickable(index);
    if (!dir) return;

    var piece = document.getElementById("piece" + index);
    var v = this;

    if (dir == "e")
    {
        piece.className = "piece clickable move_" + dir;

        setTimeout
        (
            function()
            {
                v.arrangement[index + 1].piece = v.arrangement[index].piece;
                v.arrangement[index].piece = null;
                v.blankIndex = index;
                piece.className = "piece";

            },
            300
        );
    }

    if (dir == "w")
    {

    }

    if (dir == "s")
    {

    }

    if (dir == "n")
    {

    }
}


We apply the same logic to the other directions.
moveTile: function(index)
{
    var dir = this.isClickable(index);
    if (!dir) return;

    var piece = document.getElementById("piece" + index);
    var v = this;

    if (dir == "e")
    {
        piece.className = "piece clickable move_" + dir;

        setTimeout
        (
            function()
            {
                v.arrangement[index + 1].piece = v.arrangement[index].piece;
                v.arrangement[index].piece = null;
                v.blankIndex = index;
                piece.className = "piece";
            },
            300
        );

    }

    if (dir == "w")
    {
        piece.className = "piece clickable move_" + dir;

        setTimeout
        (
            function()
            {
                v.arrangement[index - 1].piece = v.arrangement[index].piece;
                v.arrangement[index].piece = null;
                v.blankIndex = index;
                piece.className = "piece";
            },
            300
        );

    }

    if (dir == "s")
    {
        piece.className = "piece clickable move_" + dir;

        setTimeout
        (
            function()
            {
                v.arrangement[index + 4].piece = v.arrangement[index].piece;
                v.arrangement[index].piece = null;
                v.blankIndex = index;
                piece.className = "piece";
            },
            300
        );

    }

    if (dir == "n")
    {
        piece.className = "piece clickable move_" + dir;

        setTimeout
        (
            function()
            {
                v.arrangement[index - 4].piece = v.arrangement[index].piece;
                v.arrangement[index].piece = null;
                v.blankIndex = index;
                piece.className = "piece";
            },
            300
        );

    }
}


Here's the CSS for each of these classes. For move_e, we are moving the tile right. So the margin-left property will be set to 100%. For move_w, it's -100% because we're moving in the opposite direction. Same logic for the others. For all of these, transition speed has been set to 0.3 seconds, which is the same for the timeout values within the moveTile() method.
.win
{
    outline: 5px solid rgba(255, 100, 0, 1);
}

.move_e
{
    margin-left: 100%;
    -webkit-transition: all 0.3s;
    transition: all 0.3s;
}

.move_w
{
    margin-left: -100%;
    -webkit-transition: all 0.3s;
    transition: all 0.3s;
}

.move_n
{
    margin-top: -100%;
    -webkit-transition: all 0.3s;
    transition: all 0.3s;
}

.move_s
{
    margin-top: 100%;
    -webkit-transition: all 0.3s;
    transition: all 0.3s;
}


Now we will need to try this. Click on tile id 2. What happens? Does the tile move right?




Now click on tile id 6. Does it move up?




Keep clicking and shifting the tiles till you get the full picture. This could take a while, though! You should see the thick orange border appear.




If you like, add these lines to make it fade in.
#puzzleContainer
{
    width: 100%;
    height: 500px;
    -webkit-transition: all 1s;
    transition: all 1s;

}


Remove the red border.
div { outline: 0px solid #FF0000;}


Nice and clean!




Adding more images

We are going to add some more images to bring some variety to our game.

xmas1.jpg

xmas2.jpg

xmas3.jpg

xmas4.jpg



Just modify the data property, like so. We now have five images, so change the value of the images property to 5.
data:
{
    btnText: "RESET",
    pieces: [],
    arrangement: [],
    blankIndex: 3,
    images: 5,
    imageNo: 0
},


Now you'll get a random puzzle each time you click the button.




Merry Christmas!

I'm really happy to share this web tutorial with you today. I wish you all happy and fun-filled holidays!

piece on Earth, goodwill to men!
T___T

No comments:

Post a Comment