Let's create a "Sliding map tiles" puzzle web game

Aim

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.

How should the game work?

Implementation steps

0. Creating tiles

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.

1. Defining the containing HTML document and the game board

<!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>
1. The empty HTML document with the game board

2. Data storage, loading tiles

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
}
2. The game board with the tiles loaded and displayed

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';
2b. The game board with the tiles; tile edges are emphasized.

3. Event handler

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;
}
3. Event handler

4. Initial shuffle

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);
  }
4. Initial shuffle

5. Animated move

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.
...
5. Animated move

6. Final touches

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 board 
element 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:

	    ...
        if (finished()) // Display a message if finished.
	      alert('Congratulations!\n You did it!');
	    ...
6. Final touches

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 to true during animation, and modify the mouseclick event handler to do nothing if anim is true:

...
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
...
7. "Doubleclick bug" eliminated