Puzzle(W) Sample Overview

The Puzzle sample application demonstrates how you can create a puzzle game.

The following figure illustrates the main screen of the Puzzle.

Figure: Puzzle screen

Puzzle screen Puzzle screen Puzzle screen

Background

The application opens with the main screen that shows a randomly selected image divided into 16 mixed-up pieces. Originally, the piece from the bottom right corner is removed to allow shifting the other pieces.

When the user taps a piece, it is moved to the location of the empty piece. The purpose of the game is to put all pieces in their right locations; when the game is solved, the full image is displayed.

Source Files

You can create and view the sample application project including the source files in the IDE.

File name Description
config.xml This file contains the application information for the platform to install and launch the application, including the view mode and the icon to be used in the device menu.
css/style.css This file contains the CSS styling for the application UI.
img/ This directory contains the application images used as the puzzle image and application icon.
index.html This is a starting file from which the application starts loading. It contains the layout of the application screens.
js/main.js This file contains the code for the main application module used for initialization.
js/systeminfo.js This file contains the battery state handling code.

Implementation

Defining the Application Layout

To define the application layout and initialize the game screen:

  1. The application has only 1 page, and the static page structure is defined in the index.html file. The selected image is placed in the game element. The element styles are specified in the css/style.css file.

    <!DOCTYPE html>
    <html>
       <head>
          <meta charset="utf-8" />
          <meta name="viewport" content="width=device-width,height=device-height,user-scalable=no">
          <title>Puzzle</title>
          <link rel="stylesheet" type="text/css" href="css/style.css"/>
       </head>
       <body>
          <div id="transparent"></div>
          <div id="game">
             <div id="background"></div>
             <div id="pieces"></div>
          </div>
          <script src="js/systeminfo.js"></script>
          <script src="js/main.js"></script>
       </body>
    </html>
    
  2. On a circular screen, a faded copy of selected image is also used as a background, as defined in the css/style.css file:

    #transparent
    {
       position: absolute;
       left: 0;
       right: 0;
       top: 0;
       bottom: 0;
       opacity: .5;
    }
    
    #background
    {
       position: absolute;
       left: 0;
       top: 0;
       display: none;
       background-position: center center;
       width: 100%;
       height: 100%;
       opacity: 0.1;
       -webkit-filter: invert(100%);
    }
    
  3. The main module is responsible for the puzzle logic and for managing the UI. It updates the UI, listens to events, and initializes the systeminfo module responsible for checking the battery level. This module sets a random image to the puzzle and splits it to pieces, shuffles the pieces, and handles the tap event on puzzle pieces and animations.

    After initializing the main module, the application is ready for use and waits for user actions.

    The init() method initializes the game and binds events for the back key presses and tapping on a puzzle piece:

    /* js/main.js */
    function init()
    {
       adjustGameElement();
       holdRestart();
    
       /* Add event listener for the back key */
       document.addEventListener('tizenhwkey', function onTizenhwkey(e)
       {
          if (e.keyName === 'back' && !!systeminfo)
          {
             systeminfo.closeApplication();
          }
       });
    
       document.getElementById('pieces').addEventListener('click', function movePiece(event)
       {
          if (event.target.classList.contains('piece'))
          {
             changePiecePosition(event.target);
          }
       });
    }
    
  4. The adjustGameElement() method sets the piece size and position:

    /* js/main.js */
    function adjustGameElement()
    {
       if (isScreenCircular)
       {
          adjustGameElementForCircularScreen();
       }
       else
       {
          adjustGameElementForRectangularScreen();
       }
    }
    

    It is necessary to determine the screen type (circular or rectangle), and calculate the piece size and update game board element size accordingly. On a circular screen, the puzzle board is adjusted to be a square inscribed in a circle.

    /* js/main.js */
    function adjustGameElementForRectangularScreen()
    {
       gameEl.style.height = window.innerHeight + 'px';
       pieceWidth = Math.floor(resolution.width / grid.x);
       pieceHeight = Math.floor(resolution.height / grid.y);
    }
    
    function adjustGameElementForCircularScreen()
    {
       var sideLength = Math.floor(window.innerHeight / Math.sqrt(2));
       gameElOffset = Math.floor((window.innerWidth - sideLength) / 2);
    
       pieceWidth = Math.floor(sideLength / grid.x);
       pieceHeight = Math.floor(sideLength / grid.y);
    
       gameEl.style.width = pieceWidth * grid.x + 1 + 'px';
       gameEl.style.height = pieceHeight * grid.y + 1 + 'px';
       gameEl.style.marginLeft = gameElOffset + 'px';
       gameEl.style.marginTop = gameElOffset + 'px';
    }
    
  5. The user can restart the game by tapping on the game board and holding the tap for a time that is longer than the HOLD_TIME constant value:

    /* js/main.js */
    function holdRestart()
    {
       gameEl.addEventListener('touchstart', onHoldStart);
    }
    
    function onHoldStart(event)
    {
       var startTime = new Date().getTime(),
           touchEnd = function touchEnd()
           {
              gameEl.removeEventListener('touchend', touchEnd);
              onHoldEnd(startTime);
           },
           touches = event.touches.length;
    
       /* Prevent reset during a single-touch hold */
       singleTouch = touches < 2;
    
       gameEl.addEventListener('touchend', touchEnd);
    }
    
    function onHoldEnd(startTime)
    {
       var endTime = new Date().getTime();
       if ((endTime - startTime) > HOLD_TIME)
       {
          if (interval !== null || singleTouch === true)
          {
             /* Do not restart if shuffling in progress */
             /* Single-touch hold must not cause restart (2nd condition) */
             return;
          }
          start();
       }
    }
    
  6. Start the game with the start() method:

    /* js/main.js */
    function start(firstStart)
    {
       transparentEl.classList.remove('opaque');
       clearGame();
       setFile();
       setBackground();
       setFree(grid.x - 1, grid.y - 1);
       createPieces();
       shufflePieces();
       if (firstStart === true)
       {
          systeminfo.init();
       }
    }
    
  7. At the end, clear the game board (remove all pieces) with the clearGame() method:

    /* js/main.js */
    function clearGame()
    {
       lock = false;
       clearInterval(interval);
       var pieces = document.getElementsByClassName('piece'),
           i = pieces.length;
    
       while (i)
       {
          i -= 1;
          pieces[i].remove();
       }
    }
    

Selecting the Puzzle Image

The setFile() method gets a random file from the images/ folder and sets the file path to the file variable:

/* js/main.js */
function setFile()
{
   file = 'images/' + resolution.width + 'x' + resolution.height + '/' +
   randomFile();
}

function randomFile()
{
   var random = Math.floor(Math.random() * images.length);

   if (images.length === 1)
   {
      return images[0];
   }

   if (random === lastRandom)
   {
      return randomFile();
   }

   lastRandom = random;

   return images[random];
}

The selected image is set as a background and its opacity is changed dynamically with the animateElement() method. The move of the puzzle piece is animated with the requestAnimationFrame() method.

/* js/main.js */
function setBackground()
{
   backgroundEl.style.backgroundImage = 'url(' + file + ')';
   backgroundEl.style.display = 'block';
   backgroundEl.style.opacity = 1;
   animateElement(backgroundEl, 'opacity', 0.4, '');
   backgroundEl.style.webkitFilter = 'invert(100%)';
   transparentEl.style.backgroundImage = 'url(' + file + ')';
}

function animateElement(el, propertyName, targetValue, unit)
{
   var animationStartTime = +new Date(),
       startValue = parseFloat(el.style[propertyName], 10),
       diff = targetValue - startValue,
       tick;

   el.style[propertyName] = startValue + unit;

   tick = function tick()
   {
      var /* Time passed since the start (in milliseconds) */
          currentAnimationTime = new Date() - animationStartTime,
          /* Current progress (amount of animation which must be completed) */
          currentProgress = currentAnimationTime / ANIMATION_TIME;

      if (currentProgress >= 1)
      {
         el.style[propertyName] = targetValue + unit;

         return true;
      }

      el.style[propertyName] = (startValue + currentProgress * diff) + unit;

      requestAnimationFrame(tick);

      return false;
   };

   tick();
}

Splitting the Image into Puzzle Pieces

The setFree() method defines which field is the empty one. During the game initialization process, the coordinates for the right bottom piece are passed to this method.

/* js/main.js */
function setFree(x, y)
{
   free.x = x;
   free.y = y;
}

The other fields are filled with the shuffled image pieces:

/* js/main.js */
function createPieces()
{
   var i = 0, j = 0, piece, position;

   for (i; i < grid.y; i += 1)
   {
      for (j = 0; j < grid.x; j += 1)
      {
         if (i === (grid.y - 1) && j === (grid.x - 1))
         {
            /* Leave the last piece empty */
            break;
         }

         piece = document.createElement('div');
         piece.className = 'piece match';
         piece.x = j;
         piece.y = i;
         piece.startX = j;
         piece.startY = i;
         piece.number = i * grid.x + j;
         piece.setAttribute('id', 'piece_' + j + '_' + i);
         piece.step = 0;
         piece.style.width = (pieceWidth - 1) + 'px';
         piece.style.height = (pieceHeight - 1) + 'px';

         position = getPosition(piece.x, piece.y);
         piece.style.left = position.left + 'px';
         piece.style.top = position.top + 'px';
         piece.style.position = 'absolute';
         piece.style.opacity = 1;
         piece.style.backgroundImage = 'url(' + file + ')';
         piece.style.backgroundPosition = '-' + (gameElOffset + piece.x * (pieceWidth - 1) + j + 1)
                                          + 'px -' + (gameElOffset + piece.y * (pieceHeight - 1) + i + 1) + 'px';
         document.getElementById('pieces').appendChild(piece);
      }
   }
}

Moving and Mixing Up the Puzzle Pieces

After the board is ready, all puzzle pieces are randomly shuffled. A random piece moves into the free field.

When the user taps a piece, the following actions occur:

  1. Piece position is updated to the empty field.

  2. Step counter value is incremented.

  3. Coordinates of the free field are updated.

  4. Application checks whether the user wins the game. If so, the final animation is started.

/* js/main.js */
function shufflePieces()
{
   interval = setInterval(changePiecePosition, LONG_DELAY);
   setTimeout(function stopChangingPiecePosition()
   {
      clearInterval(interval);
      interval = null;
   }, grid.x * grid.y * SHUFFLE_TIME);
}

function changePiecePosition(piece)
{
   var x = 0,
       y = 0,
       position = null;

   piece = piece || pickRandomNearest();
   x = piece.x;
   y = piece.y;
   if (!lock && canMove(x, y))
   {
      lock = true;
      piece.classList.toggle('match', false);

      position = getPosition(free.x, free.y);

      /* Piece goes into the free field */
      piece.x = free.x;
      piece.y = free.y;
      animateElement(piece, 'left', position.left, 'px');
      animateElement(piece, 'top', position.top, 'px');

      onChangePiecePosition(piece, x, y);
   }
}

function onChangePiecePosition(piece, left, top)
{
   matchPosition(piece);
   setStepCount(piece);
   setFree(left, top);
   checkWin();
   lock = false;
}

function matchPosition(piece)
{
   if (piece.x === piece.startX &&
       piece.y === piece.startY)
   {
      piece.classList.add('match');

      setTimeout(function animate()
      {
         var position = getPosition(piece.x, piece.y);
         /* Do not blink if the piece was already moved away */
         if (position.left !== parseInt(piece.style.left, 10) ||
             position.top !== parseInt(piece.style.top, 10))
         {
            return;
         }
         piece.style.opacity = 0;
         animateElement(piece, 'opacity', 1, '');
      }, ANIMATION_TIME + 50); /* Wait for the animation to end */
   }
}

function setStepCount(piece)
{
   piece.step += 1;
}

function checkWin()
{
   if (document.querySelectorAll('.piece:not(.match)').length === 0)
   {
      finalAnimation();
      backgroundEl.style.webkitFilter = 'none';
      animateElement(backgroundEl, 'opacity', 1, '');
   }
}

function finalAnimation()
{
   var pieces = document.getElementsByClassName('piece'),
       piecesLen = pieces.length;

   backgroundEl.style.display = 'none';
   transparentEl.classList.add('opaque');

   while (piecesLen)
   {
      piecesLen -= 1;
      scheduleFinalPieceAnimation(pieces[piecesLen], piecesLen * SHORT_DELAY,
                                  piecesLen === 0);
   }
}

Managing the Battery State

The application uses the systeminfo module to monitor the battery state:

/* js/systeminfo.js */
init: function init()
{
   'use strict';
   if (typeof tizen === 'object' &&
       typeof tizen.systeminfo === 'object')
   {
      this.systeminfo = tizen.systeminfo;
      this.checkBatteryLowState();
      this.listenBatteryLowState();
   }
   else
   {
      console.warn('tizen.systeminfo not available');
   }
}

If the battery state becomes low (less than 4%) and the charger is not connected, the application is notified and it closes:

/* js/systeminfo.js */
closeApplication: function closeCurrentApplication()
{
   'use strict';
   try
   {
      tizen.application.getCurrentApplication().exit();
   }
   catch (err)
   {
      console.error('Error: ', err.message);
   }
},

/* Add listener for battery change to low */
listenBatteryLowState: function listenBatteryLowState()
{
   'use strict';
   try
   {
      this.systeminfo.addPropertyValueChangeListener('BATTERY', function change(battery)
      {
         if (!battery.isCharging)
         {
            this.closeApplication();
         }
      },
      {lowThreshold: 0.04},
      function onError(err)
      {
         console.error('Error: ', err.message);
      });
   }
   catch (err)
   {
      console.error('Error: ', err.message);
   }
},

/* Check low battery state */
checkBatteryLowState: function checkBatteryLowState()
{
   'use strict';
   try
   {
      this.systeminfo.getPropertyValue('BATTERY', function onGetBatteryInfo(battery)
      {
         if (battery.level < 0.04 && !battery.isCharging)
         {
            this.closeApplication();
         }
      }, null);
   }
   catch (err)
   {
      console.error('Error: ', err.message);
   }
},