Sunday 7 February 2016

Web Tutorial: Valentine's Maze Game (Part 2/4)

The board's been set, the tiles have been placed. Now we have to ensure that there is a way to win the game.

You see, all the tiles have random tile types and placements. There's no guarantee that there is a path from start to end.

And speaking of Start and End...

Make this change to your HTML.
<body onload="reset();">


Then add the reset() function. The resetboard() function will be part of your reset() function. The variables start_id and end_id are the ids of your start and end tiles. We use a Do-while loop here because this operation will be carried out at least once, until you get a path between the start and end tiles.
        <script>
            var objtiletype=[];
            objtiletype[0] = {pos:"0px 0px", n:0, e:1, s:1, w:0};
            objtiletype[1] = {pos:"-200px 0px", n:0, e:0, s:1, w:1};
            objtiletype[2] = {pos:"-200px -200px", n:1, e:0, s:0, w:1};
            objtiletype[3] = {pos:"0px -200px", n:1, e:1, s:0, w:0};
            objtiletype[4] = {pos:"-100px 0px", n:0, e:1, s:0, w:1};
            objtiletype[5] = {pos:"0px -100px", n:1, e:0, s:1, w:0};
            objtiletype[6] = {pos:"-100px -200px", n:0, e:1, s:0, w:1};
            objtiletype[7] = {pos:"-200px -100px", n:1, e:0, s:1, w:0};
            objtiletype[8] = {pos:"-52px -100px", n:0, e:1, s:0, w:0};
            objtiletype[9] = {pos:"-148px -100px", n:0, e:0, s:0, w:1}; 

            var objtiles=[];

            function reset()
            {
                var start_id,end_id;
                var tiletypeindex;

                do
                {
                    resetboard();
                }
                while ();
            }

            function resetboard()
            {


Now, some rules. The start tile can be any tile on the extreme left. Thus tile ids 0, 6, 12 and 18 qualify. The end tile can be any tile on the extreme right. Thus tile ids 5, 11, 17 and 23 qualify. The generaterandomno() function has been deployed to randomly pick any tile on the extreme left to be the start tile, and any tile on the extreme right to be the end tile. Once the id for start and end tiles have been determined, they will derive their n, s, e and w from objtiletype[7], which is the tiletype for the start tile; and objtiletype[8], which is the tiletype for the end tile. And of course, the background-position is set according to their respective tiletypes.

After specifying the start and end tiles, we fire off the createpath() function.

Modify your JavaScript. We'll add a global array named path. And the condition of the Do-while loop is - as long as the value of the last element in the path array is "-1" (which means the operation was not successful), the script will keep trying.

            var objtiletype=[];
            objtiletype[0] = {pos:"0px 0px", n:0, e:1, s:1, w:0};
            objtiletype[1] = {pos:"-200px 0px", n:0, e:0, s:1, w:1};
            objtiletype[2] = {pos:"-200px -200px", n:1, e:0, s:0, w:1};
            objtiletype[3] = {pos:"0px -200px", n:1, e:1, s:0, w:0};
            objtiletype[4] = {pos:"-100px 0px", n:0, e:1, s:0, w:1};
            objtiletype[5] = {pos:"0px -100px", n:1, e:0, s:1, w:0};
            objtiletype[6] = {pos:"-100px -200px", n:0, e:1, s:0, w:1};
            objtiletype[7] = {pos:"-200px -100px", n:1, e:0, s:1, w:0};
            objtiletype[8] = {pos:"-52px -100px", n:0, e:1, s:0, w:0};
            objtiletype[9] = {pos:"-148px -100px", n:0, e:0, s:0, w:1}; 

            var objtiles=[];
            var path=[];

            function reset()
            {
                var start_id,end_id;
                var tiletypeindex;

                do
                {
                    resetboard();
                    start_id=generaterandomno(0,3) * 6;
                    objtiles[start_id].n=objtiletype[8].n;
                    objtiles[start_id].s=objtiletype[8].s;
                    objtiles[start_id].e=objtiletype[8].e;
                    objtiles[start_id].w=objtiletype[8].w;
                    end_id=(generaterandomno(1,4) * 6) - 1;
                    objtiles[end_id].n=objtiletype[9].n;
                    objtiles[end_id].s=objtiletype[9].s;
                    objtiles[end_id].e=objtiletype[9].e;
                    objtiles[end_id].w=objtiletype[9].w;

                    document.getElementById("tile"+start_id).style.backgroundPosition=objtiletype[8].pos;
                    document.getElementById("tile"+end_id).style.backgroundPosition=objtiletype[9].pos;

                    createpath(start_id,end_id);
                }
                while (path[path.length-1]==-1);
            }


Creating a Path

Now that you have a start and end tile, the path needs to be created. This will be a trial and error process, and the script may take quite a few tries before succeeding.

Create the createpath() function in your JavaScript.
            function createpath(varstart,varend)
            {   
                path=[];
                var next_id=varstart;
                path[0]=varstart;
                var i=1;
            }

            function generaterandomno(varmin,varmax)
            {
                return Math.floor((Math.random() * (varmax-varmin+1)) + varmin);
            }


createpath() takes in the start tile's id and the end tile's id as parameters. At the start of the function, the path array is cleared. A variable next_id is declared, and set to the value of start_id. Which makes sense because the first value is always the start tile's id, right? Another variable, i, is declared, and this will keep track of how many tiles there are on the current path. The value of i is initialized to 1 because there is already one tile - the start tile.

Add a sendmessage() function and fire it off in your createpath() function. This basically displays a message to the user that the game is running some operation. It's a good practice - it keeps the user informed in the event that he has to wait.
            function createpath(varstart,varend)
            {   
                path=[];
                var next_id=varstart;
                path[0]=varstart;
                var i=1;

                sendmessage("Please wait. Evaluating paths...");
            }

            function generaterandomno(varmin,varmax)
            {
                return Math.floor((Math.random() * (varmax-varmin+1)) + varmin);
            }

            function sendmessage(varmessage)
            {
                document.getElementById("pnlMessage").innerHTML=varmessage;
            }


After the Do-while loop in the reset() function, fire off the sendmessage() function again to invite the user to begin playing.
            function reset()
            {
                var start_id,end_id;
                var tiletypeindex;

                do
                {
                    resetboard();
                    start_id=generaterandomno(0,3) * 6;
                    objtiles[start_id].n=objtiletype[8].n;
                    objtiles[start_id].s=objtiletype[8].s;
                    objtiles[start_id].e=objtiletype[8].e;
                    objtiles[start_id].w=objtiletype[8].w;
                    end_id=(generaterandomno(1,4) * 6) - 1;
                    objtiles[end_id].n=objtiletype[9].n;
                    objtiles[end_id].s=objtiletype[9].s;
                    objtiles[end_id].e=objtiletype[9].e;
                    objtiles[end_id].w=objtiletype[9].w;

                    document.getElementById("tile"+start_id).style.backgroundPosition=objtiletype[8].pos;
                    document.getElementById("tile"+end_id).style.backgroundPosition=objtiletype[9].pos;

                    createpath(start_id,end_id);
                }
                while (path[path.length-1]==-1);

                sendmessage("Click <span style='cursor:pointer' onclick='reset();'><b>here</b></span> to begin");
            }


Now add a Do-while loop in your createpath() function. This operation will be performed at least one time, to an undefined number of times until there's a definite success, ie. the value of next_id is the same as varend.
            function createpath(varstart,varend)
            {   
                path=[];
                var next_id=varstart;
                path[0]=varstart;
                var i=1;

                sendmessage("Please wait. Evaluating paths...");

                do
                {

                }
                while (next_id!=varend);
            }


In the Do-while loop, set the value of next_id by running the function nexttileid() and passing in the value of next_id. Don't have a nexttileid() function? You guessed it, we're going to create one! And then the value of next_id will be the next element in the paths array. i is then incremented, to reflect that there is another tile id in the array.
            function createpath(varstart,varend)
            {   
                path=[];
                var next_id=varstart;
                path[0]=varstart;
                var i=1;

                sendmessage("Please wait. Evaluating paths...");

                do
                {
                    next_id=nexttileid(next_id);
                    path[i]=next_id;
                    i++;
                }
                while (next_id!=varend);
            }

            function nexttileid(varid)
            {

            }

            function generaterandomno(varmin,varmax)
            {
                return Math.floor((Math.random() * (varmax-varmin+1)) + varmin);
            }


More code! If next_id is "-1", which means the path generator reached a dead end, path is cleared again and the first element reset to start_id. Needless to say, i is reset to 1.
            function createpath(varstart,varend)
            {   
                path=[];
                var next_id=varstart;
                path[0]=varstart;
                var i=1;

                sendmessage("Please wait. Evaluating paths...");

                do
                {
                    next_id=nexttileid(next_id);
                    path[i]=next_id;
                    i++;

                    if (next_id==-1)
                    {
                        path=[];
                        next_id=varstart;
                        path[0]=varstart;
                        i=1;
                    }
                }
                while (next_id!=varend);
            }


Hopefully the logic is clear at this point! If there's anything you don't quite grasp, slow down, study the code again.

We'll work on the nexttileid() function next. This function takes in the current tile id, varid, and determines the available tiles to move to next, then decides at random which one it's going to be. Tile ids that are already in the path array are not eligible. This prevents the script from going in an infinite loop.

Here, we start by declaring variables row and col. Then we set row and col to the x and y value of the tile determined by varid.
            function nexttileid(varid)
            {
                var row,col;
                row=objtiles[varid].y;
                col=objtiles[varid].x;
            }


From here, there are four directions to proceed - north, south, east and west. These are represented by the variables n, s, e and w. All of them are set to "1" by default. Now there are some conditions that would disqualify these directions. They're represented by the following If statements.
            function nexttileid(varid)
            {
                var row,col;
                row=objtiles[varid].y;
                col=objtiles[varid].x;

                var n=1,s=1,e=1,w=1;

                if (row==0) n=-1;
                if (row==3) s=-1;
                if (col==0) w=-1;
                if (col==5) e=-1;
            }


For example, Tile 2 is on row 0, meaning it's on the top row, then the next direction can't be north. And so on for the rest.


Tile 0


Tile 1
(w)


Tile 2
(current)


Tile 3
(e)


Tile 4


Tile 5


Tile 6


Tile 7


Tile 8
(s)


Tile 9


Tile 10


Tile 11


Tile 12


Tile 13


Tile 14


Tile 15


Tile 16


Tile 17


Tile 18


Tile 19


Tile 20


Tile 21


Tile 22


Tile 23


The next series of If statements. If a direction is viable (ie, the value is still "1"), then that variable is set to the id of the tile that lies in that direction.
            function nexttileid(varid)
            {
                var row,col;
                row=objtiles[varid].y;
                col=objtiles[varid].x;

                var n=1,s=1,e=1,w=1;

                if (row==0) n=-1;
                if (row==3) s=-1;
                if (col==0) w=-1;
                if (col==5) e=-1;

                if (n==1) n=varid-6;
                if (s==1) s=varid+6;
                if (w==1) w=varid-1;
                if (e==1) e=varid+1;
            }

For example, if the current tile's id is 4, then directions west, south and east are available. So w is set to 3, s is set to 10 and e is set to 5.



Tile 0


Tile 1


Tile 2


Tile 3
(w = 3)


Tile 4
(current = 4)


Tile 5
(e = 5)


Tile 6


Tile 7


Tile 8


Tile 9


Tile 10
(s = 10)


Tile 11


Tile 12


Tile 13


Tile 14


Tile 15


Tile 16


Tile 17


Tile 18


Tile 19


Tile 20


Tile 21


Tile 22


Tile 23


Here we declare the directions array and another series of If statements. If a direction is viable (ie, the value is not "-1"), then that variable is added to the directions array. The variable currentdirectionindex is meant to help add those viable directions nto the directions array.

            function nexttileid(varid)
            {
                var row,col;
                row=objtiles[varid].y;
                col=objtiles[varid].x;

                var n=1,s=1,e=1,w=1;

                if (row==0) n=-1;
                if (row==3) s=-1;
                if (col==0) w=-1;
                if (col==5) e=-1;

                if (n==1) n=varid-6;
                if (s==1) s=varid+6;
                if (w==1) w=varid-1;
                if (e==1) e=varid+1;

                var directions=[];
                var currentdirectionindex=-1;

                if (path.indexOf(n)==-1&&n!=-1) {currentdirectionindex++;directions[currentdirectionindex]=n;}
                if (path.indexOf(s)==-1&&s!=-1) {currentdirectionindex++;directions[currentdirectionindex]=s;}
                if (path.indexOf(w)==-1&&w!=-1) {currentdirectionindex++;directions[currentdirectionindex]=w;}
                if (path.indexOf(e)==-1&&e!=-1) {currentdirectionindex++;directions[currentdirectionindex]=e;}
            }


Then there's an If-else statement. First, we check if there are any elements in the directions array. If not, the nexttileid() function returns a value of "-1".

If there's only one value, then of course, the nexttileid() function returns that value. If there's more than one value, the nexttileid() function fires off generaterandomno() function and returns the result. And of course, if at any time the returned result is the same as end_id, the function createpath() registers it as a success. Otherwise, the entire process begins all over again.

In the event of a success, of course, you now have a path array that holds all the tile ids, in sequence!

            function nexttileid(varid)
            {
                var row,col;
                row=objtiles[varid].y;
                col=objtiles[varid].x;

                var n=1,s=1,e=1,w=1;

                if (row==0) n=-1;
                if (row==3) s=-1;
                if (col==0) w=-1;
                if (col==5) e=-1;

                if (n==1) n=varid-6;
                if (s==1) s=varid+6;
                if (w==1) w=varid-1;
                if (e==1) e=varid+1;

                var directions=[];
                var currentdirectionindex=-1;

                if (path.indexOf(n)==-1&&n!=-1) {currentdirectionindex++;directions[currentdirectionindex]=n;}
                if (path.indexOf(s)==-1&&s!=-1) {currentdirectionindex++;directions[currentdirectionindex]=s;}
                if (path.indexOf(w)==-1&&w!=-1) {currentdirectionindex++;directions[currentdirectionindex]=w;}
                if (path.indexOf(e)==-1&&e!=-1) {currentdirectionindex++;directions[currentdirectionindex]=e;}

                if (directions.length==0)
                {
                    return -1;
                }
                else
                {  
                    if (directions.length==1) return directions[0];
                    if (directions.length>1) return directions[generaterandomno(0,directions.length-1)];
                }
            }


So now you have a path!

But it's time to fiddle with the tiles on the board so as to give the player that one possible path to success. Back to your reset() function! Here we iterate through the path array. To be exact, we iterate from the second element to the second last element of the path array - because the first and last elements are the start and end tiles, and they've already been rendered.

            function reset()
            {
                var start_id,end_id;
                var tiletypeindex;

                do
                {
                    resetboard();
                    start_id=generaterandomno(0,3) * 6;
                    objtiles[start_id].n=objtiletype[8].n;
                    objtiles[start_id].s=objtiletype[8].s;
                    objtiles[start_id].e=objtiletype[8].e;
                    objtiles[start_id].w=objtiletype[8].w;
                    end_id=(generaterandomno(1,4) * 6) - 1;
                    objtiles[end_id].n=objtiletype[9].n;
                    objtiles[end_id].s=objtiletype[9].s;
                    objtiles[end_id].e=objtiletype[9].e;
                    objtiles[end_id].w=objtiletype[9].w;

                    document.getElementById("tile"+start_id).style.backgroundPosition=objtiletype[8].pos;
                    document.getElementById("tile"+end_id).style.backgroundPosition=objtiletype[9].pos;

                    createpath(start_id,end_id);
                }
                while (path[path.length-1]==-1);

                for (var i=1;i<path.length-1;i++)
                {

                }

                sendmessage("Click <span style='cursor:pointer' onclick='reset();'><b>here</b></span> to begin");
            }


OK, now we need to determine what tiletypes are needed. If you look at the board, other than the start and end tiles, there are basically two kinds of tiles - those with straight lines, and those with right angles. You need a tile with a straight line if your previous and next tile are have the same x or y values. Otherwise, you need a right angle. Make perfect sense so far?

Straight line



Tile 0


Tile 1


Tile 2
(next)


Tile 3


Tile 4


Tile 5


Tile 6


Tile 7


Tile 8
(current)


Tile 9


Tile 10


Tile 11


Tile 12


Tile 13


Tile 14
(previous)


Tile 15


Tile 16


Tile 17


Tile 18


Tile 19


Tile 20


Tile 21


Tile 22


Tile 23


Right angle



Tile 0


Tile 1


Tile 2


Tile 3


Tile 4


Tile 5


Tile 6


Tile 7


Tile 8


Tile 9


Tile 10


Tile 11


Tile 12


Tile 13


Tile 14
(current)


Tile 15
(next)


Tile 16


Tile 17


Tile 18


Tile 19


Tile 20
(prev)


Tile 21


Tile 22


Tile 23


The right angle tiletypes are from 0 to 3 in the obtiletypes array, and the straight line tiletypes are elements 4 to 7. So over here, by default we assign the tile's tiletype to a right angle by firing off generaterandomno() and returning a value from 0 to 3. Next, we check if the previous and next tiles lie on the same row. If so, we assign the tile's tiletype to a random straight line tiletype instead. Same if the previous and next tiles lie on the same column. Then we adjust the CSS background-position property of that tile to correspond to its tiletype; and the tile's n, s, e and w values to its tiletype's values.

            function reset()
            {
                var start_id,end_id;
                var tiletypeindex;

                do
                {
                    resetboard();
                    start_id=generaterandomno(0,3) * 6;
                    objtiles[start_id].n=objtiletype[8].n;
                    objtiles[start_id].s=objtiletype[8].s;
                    objtiles[start_id].e=objtiletype[8].e;
                    objtiles[start_id].w=objtiletype[8].w;
                    end_id=(generaterandomno(1,4) * 6) - 1;
                    objtiles[end_id].n=objtiletype[9].n;
                    objtiles[end_id].s=objtiletype[9].s;
                    objtiles[end_id].e=objtiletype[9].e;
                    objtiles[end_id].w=objtiletype[9].w;

                    document.getElementById("tile"+start_id).style.backgroundPosition=objtiletype[8].pos;
                    document.getElementById("tile"+end_id).style.backgroundPosition=objtiletype[9].pos;

                    createpath(start_id,end_id);
                }
                while (path[path.length-1]==-1);

                for (var i=1;i<path.length-1;i++)
                {
                    tiletypeindex=generaterandomno(0,3);

                    if (objtiles[path[i-1]].x==objtiles[path[i]].x&&objtiles[path[i+1]].x==objtiles[path[i]].x)
                    {
                        tiletypeindex=generaterandomno(4,7);
                    }

                    if (objtiles[path[i-1]].y==objtiles[path[i]].y&&objtiles[path[i+1]].y==objtiles[path[i]].y)
                    {
                        tiletypeindex=generaterandomno(4,7);
                    }

                    document.getElementById("tile"+path[i]).style.backgroundPosition=objtiletype[tiletypeindex].pos;

                    objtiles[path[i]].n=objtiletype[tiletypeindex].n;
                    objtiles[path[i]].e=objtiletype[tiletypeindex].e;
                    objtiles[path[i]].s=objtiletype[tiletypeindex].s;
                    objtiles[path[i]].w=objtiletype[tiletypeindex].w;
                }

                sendmessage("Click <span style='cursor:pointer' onclick='reset();'><b>here</b></span> to begin");
            }


Now run your code!

Your board should have a start tile (the arrow flight) and an end tile (the heart)



There should be a viable path between them. But you won't see it unless you do this...

            function reset()
            {
                var start_id,end_id;
                var tiletypeindex;

                do
                {
                    resetboard();
                    start_id=generaterandomno(0,3) * 6;
                    objtiles[start_id].n=objtiletype[8].n;
                    objtiles[start_id].s=objtiletype[8].s;
                    objtiles[start_id].e=objtiletype[8].e;
                    objtiles[start_id].w=objtiletype[8].w;
                    end_id=(generaterandomno(1,4) * 6) - 1;
                    objtiles[end_id].n=objtiletype[9].n;
                    objtiles[end_id].s=objtiletype[9].s;
                    objtiles[end_id].e=objtiletype[9].e;
                    objtiles[end_id].w=objtiletype[9].w;

                    document.getElementById("tile"+start_id).style.backgroundPosition=objtiletype[8].pos;
                    document.getElementById("tile"+end_id).style.backgroundPosition=objtiletype[9].pos;

                    createpath(start_id,end_id);
                }
                while (path[path.length-1]==-1);

                for (var i=1;i<path.length-1;i++)
                {
                    tiletypeindex=generaterandomno(0,3);

                    if (objtiles[path[i-1]].x==objtiles[path[i]].x&&objtiles[path[i+1]].x==objtiles[path[i]].x)
                    {
                        tiletypeindex=generaterandomno(4,7);
                    }

                    if (objtiles[path[i-1]].y==objtiles[path[i]].y&&objtiles[path[i+1]].y==objtiles[path[i]].y)
                    {
                        tiletypeindex=generaterandomno(4,7);
                    }

                    document.getElementById("tile"+path[i]).style.backgroundPosition=objtiletype[tiletypeindex].pos;

                    objtiles[path[i]].n=objtiletype[tiletypeindex].n;
                    objtiles[path[i]].e=objtiletype[tiletypeindex].e;
                    objtiles[path[i]].s=objtiletype[tiletypeindex].s;
                    objtiles[path[i]].w=objtiletype[tiletypeindex].w;

                    document.getElementById("tile"+path[i]).style.backgroundColor="#FF4400";
                }

                sendmessage("Click <span style='cursor:pointer' onclick='reset();'><b>here</b></span> to begin");
            }


Refresh! That line changed the background color of all the tiles in the paths array, less the first and last to orange.



Stil don't see the path? Well, if the tiles were properly rotated, like so...



There it is - a way for the player to win this game. There might be more than one solution, of course, but the important thing was to ensure that there is at least one viable solution. Keep the background change for now. It may come in useful in the next part.

Next

We cause the tiles to rotate upon being clicked, and determine when the player wins the game.

No comments:

Post a Comment