The sliding tile puzzle is a puzzle game with a picture cut to m*n uniform rectangular tiles. One of the tiles is missing - leaving an empty place; the others are mixed up. Any tile next to the empty place can be slided to the empty place. The task is to slide the tiles while all of them get to their original place, restoring the original picture.
The tiles can be created with a graphical software. The file name of the tiles should contain the row and column number of the tile.
In this example the tiles are stored in the folder tiles/
, and named, t_{row}_{column}.jpg
, where {row} is the row number while {column} is the column number.
For example: tiles/t_2_3.jpg. The picture was cut into 4 rows and 7 columns. The size of the tiles is 100*110 pixels.
<!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <title>Sliding map tiles puzzle</title> </head> <body> <h1>Sliding map tiles puzzle</h1> <div id="board" style="position: relative; width: 700px; height: 440px; background: #d3d3d3;"></div> </body> </html>
We have to store which image tile is contained in each grid cell, and where currently the empty space is. Let's start by defining global variables:
<script> var m=4; // number of rows var n=7; // number of columns var xs=100; // tile width var ys=110; // tile height var ux=n-1; // x coordinate of the empty space var uy=m-1; // y coordinate of the empty space var tiles=[]; // this array stores the tile images. tiles[i][j] points to the image in the row i, column j. </script>
Tile images have to be loaded into the tiles[]
array and displayed on page load.
Therefore a function is assigned to the body
element's onload
event:
<body onload="init()">
Naturally the init()
function also has to be defined:
function init() // this function is called after page load { // finding the game board <div> element var board=document.getElementById('board'); // loading and storing images for(var i=0;i<m;i++) { tiles[i]=[]; // a two-dimensional array is an array of arrays in Javacript. for(var j=0;j<n;j++) if (i<(m-1)||j<(n-1)) // if it is not the right bottom (empty) tile... { tiles[i][j]=new Image(); // creating Image object with(tiles[i][j]) { src='tiles/t_'+i+'_'+j+'.jpg'; // assigning file name style.position='absolute'; // and position on the board style.top=ys*i+'px'; style.left=xs*j+'px'; } } } tiles[m-1][n-1]=null; // the empty space is designated by a null value // showing images for(i=0;i<m;i++) for(j=0;j<n;j++) if (tiles[i][j]!=null) board.appendChild(tiles[i][j]); // appending image objects to board object }
The edge of the tiles is not really visible this way, so let's give them a border. As the border increases the total size of the tiles the images also have to be resized. (Another solutinon would be to use a larger game board, and placing the images with respect of the larger tile size caused by the borders.) To gain a 3D effect, the left and top bordrs are lighter, while the others are darker gray:
style.width=(xs-4)+'px';style.height=(ys-4)+'px'; style.borderLeft=style.borderTop='2px solid lightgray'; style.borderRight=style.borderBottom='2px solid darkgray';
The following step is the "heart" of the game: clicking on the tiles should make them slide if the empty place is next to them. The event handler is defined on the game board. First, it calculates from the clicling coordinates, which tile was clicked. If the tile is next to the empty space, it is moved.
// event handler for clicking board.onclick=function(e) { var ev=e||window.event; // event object is passed in the function parameter in Firefox and Chrome while // in the window.event object in IE var ex=ev.pageX||ev.clientX+document.body.scrollLeft; // retrieving coordinates var ey=ev.pageY||ev.clientY+document.body.scrollTop; // depending on browser type ex-=board.offsetLeft; // subtracting board coordinate offset ey-=board.offsetTop; var mx=Math.floor(ex/xs); // calculating which tile it is var my=Math.floor(ey/ys); if (neighbour(mx,my)) // if (my,mx) tile is next to the empty space, move(mx,my); // then it is moved. return false; }
We still need the two functions that were clled in the code above: neighbour()
determines if a tile is next to the empty space and move()
moves a tile.
function neighbour(x,y) // determines whether, the (x,y) tile is next to (ux,uy) { // it is a neighbour if one of the coordinates match and the other differs exactly by 1. with (Math) return (abs(x-ux)==1&&(y==uy))||(abs(y-uy)==1&&(x==ux)); } function move(x,y) // moves the (x,y) tile to (ux,uy) { with(tiles[y][x]) { style.top=uy*ys+'px'; style.left=ux*xs+'px'; // setting new coordinates } tiles[uy][ux]=tiles[y][x]; // moveing the image in the matrix tiles[y][x]=null; // deleting it from the original place ux=x;uy=y; // (x,y) is the new empty space; }
To make the game interesting, let's shuffle the tiles on startup. As we don't want to have an initial tile configuration that is impossible to solve, the initial shuffle is realised by a numer of random tile slides. This has to be done after the loading of the images but before they are displayed. One step of the random tile slide is implemented by selecting a random direction (right, left, up, down), and checking if there is a tile in that direction from the empty space. If there is, it is slided; if not, then the tile in the opposite direction is slided.
... var mixCnt=30; // number of steps of the initial shuffle ... // shuffle - a series of random slides var sx,sy,r; for(i=0;i<mixCnt;i++) { r=Math.floor(Math.random()*4); // r determines the direction switch(r) { case 0: // right (or left) sx=(ux<n-1)?(ux+1):(ux-1);sy=uy; break; case 1: // left (or right) sx=(ux>0)?(ux-1):(ux+1);sy=uy; break; case 2: // down (or up) sy=(uy<m-1)?(uy+1):(uy-1);sx=ux; break; case 3: // up (or down) sy=(uy>0)?(uy-1):(uy+1);sx=ux; break; } move(sx,sy); }
Animating the sliding move (the tiles are not simply jump to their new place but visibly sliding) is a nice visual effect. This can be realised by slightly moving the tile in certain time intervals while it reaches its new place. Using a 10-step move provides a smooth motion.
To implement this, let's create a new function called animMove()
.
We also keep the original move()
function for the initial shuffle.
Naturally, the new animMove()
function has top be called when clicking on a tile.
... var animT=30; // animation delay in ms. One slide takes ten times this ... function animMove(x,y) // animated move. Moves the (x,y) tile to (ux,uy) in 10 steps { var animStep=0; // number of current step // setting up a timer that calls a function every "animT" ms. var tId=setInterval(function() { if (animStep<10) // if this is not the final step { animStep++; var nx=(x*(10-animStep)+ux*animStep)*xs/10; // calculating the new position as var ny=(y*(10-animStep)+uy*animStep)*ys/10; // the weighed average of the initial and the final position with(tiles[y][x]) { style.top=ny+'px'; style.left=nx+'px'; // updating coordinates } } else { clearInterval(tId); // deleting timer tiles[uy][ux]=tiles[y][x]; // moving tile in the matrix as well tiles[y][x]=null; // deleting it from the original place ux=x;uy=y; // (x,y) is the new empty space } },animT); } ... if (neighbour(mx,my)) // if (my,mx) tile is next to the empty space, animMove(mx,my); // then it is moved. ...
Let's now give the game its final touches: Let's place an "initial state" (==not shuffled) and a "shuffle and start" button; possibility to set the number of shuffle steps. Finaly the game should detect if all the tiles are at theis right place.
Buttons are simply defined in HTML:
<input type="button" value="Initial state" onclick="initialState()" /> <input type="button" value="Shuffle and start" onclick="start()" /> Number of shuffle steps: <input type="text" id="mixCnt" value="30" size="2" />
Once there is a shuffle button, shuffling should not happen automatically, only if the user clicks on that button.
So let's remove the shuffling code from the init()
function and insert them into the start()
function that is called on the button click.
function start() // initial shuffle { // shuffle - a series of random slides var sx,sy,r; mixCnt=document.getElementById('mixCnt').value; // read number of steps from text box for(i=0;i<mixCnt;i++) { r=Math.floor(Math.random()*4); // r determines the direction switch(r) { case 0: // right (or left) sx=(ux<n-1)?(ux+1):(ux-1);sy=uy; break; case 1: // left (or right) sx=(ux>0)?(ux-1):(ux+1);sy=uy; break; case 2: // down (or up) sy=(uy<m-1)?(uy+1):(uy-1);sx=ux; break; case 3: // up (or down) sy=(uy>0)?(uy-1):(uy+1);sx=ux; break; } move(sx,sy); } }
Setting the initial state in the initialState()
function is simply implemented by clearing the game board and the tiles[]
array,
and moving appropriate code (that loads and displays the tiles) from the init()
function to initialState()
.
function initialState() { // emptying the matrix and the game board tiles=[]; board.innerHTML=""; // loading and storing images for(var i=0;i<m;i++) { tiles[i]=[]; // a two-dimensional array is an array of arrays in Javacript. for(var j=0;j<n;j++) if (i<(m-1)||j<(n-1)) // if it is not the right bottom (empty) tile... { tiles[i][j]=new Image(); // creating Image object with(tiles[i][j]) { src='tiles/t_'+i+'_'+j+'.jpg'; // assigning file name style.position='absolute'; // and position on the board style.top=ys*i+'px'; style.left=xs*j+'px'; style.width=(xs-4)+'px';style.height=(ys-4+'px'); style.borderLeft=style.borderTop='2px solid lightgray'; style.borderRight=style.borderBottom='2px solid darkgray'; } } } tiles[m-1][n-1]=null; // the empty space is designated by a null value // showing images for(i=0;i<m;i++) for(j=0;j<n;j++) if (tiles[i][j]!=null) board.appendChild(tiles[i][j]); // appending image objects to board object } function init() // this function is called after page load { // finding the game boardelement var board=document.getElementById('board'); // setting initial state initialState(); ...Now let's add a function that checks if all the tiles are placed correctly...:
function finished() // checks whether all tiles are at their right place { for(var i=0;i<m;i++) for(var j=0;j<n;j++) // checking is the tile file name contains the appropriate row and column number if ((i!=uy||j!=ux)&&tiles[i][j].src.indexOf(i+'_'+j)<0) return false; // if a single tile is misplaced then it is not finished yet return true; // if all the tiles are OK, it is finished }... and call it after every animated move, and congratulate if it returns
true
:6. Final touches... if (finished()) // Display a message if finished. alert('Congratulations!\n You did it!'); ...One minor bug still remained in the game: the mouse click event handler works during the animation as well. Therefore if the user clicks quickly twice the same tile, strange things can happen. To eliminate this, let's create a nev global boolean variable called
anim
, set it totrue
during animation, and modify the mouseclick event handler to do nothing ifanim
is true:7. "Doubleclick bug" eliminated... var anim=false; // true while animation is in progress; mouseclick event handler should do nothing then. ... function animMove(x,y) // animated move. Moves the (x,y) tile to (ux,uy) in 10 steps { var animStep=0; // number of current step anim=true; // animation is in progress; mouseclick should not work now. ... clearInterval(tId); // deleting timer anim=false; // animation finised ... board.onclick=function(e) { if (anim) return false; // mouse click does nothing during animation ...