View  Edit  Attributes  History  Attach  Print  Search

Part1-Basic

In the tutorial, we are going to implement an n-puzzle.

Design

The design is pretty simple. We will have a Puzzle class and PuzzlePiece class. Puzzle class stores the state and the logics of the game while PuzzliePiece render the pieces of the puzzle.

HTML

The HTML is pretty simple. We only need a container to represent the puzzle board.

<div style="width:400px; height:400px" id="board">

</div>

Puzzle

The Puzzle has two parameters for the constructor. div is the HTML container and dim is the dimension of the puzzle. If dim is 3, it will be a 8-puzzle. If dim is 4, it will be a 15-puzzle.

The constructor initializes the variables and create the pieces according to the dim.

function Puzzle(div, dim) {
        this.div = div;
        this.cellSize = div.offsetWidth / dim;
        this.empty = dim * dim - 1;
        var item = {};
        for (var i = 0, n = dim * dim; i < n; i++) {
                var newItem = new PuzzlePiece(this, i, dim, i == n - 1);

                item[i] = newItem;
                div.appendChild(newItem.div);
        }
}

appendChild

The appendChild() method adds a node after the last child node of the specified element node. So the resulting HTML is equivalent to

<div style="width:400px; height:400px" id="board">
  <div>1</div>
  <div>2</div>
  .
  .
  .
</div>

PuzzlePiece

The PuzzlePiece creates a div element as the physical presentation of a piece.

function PuzzlePiece(board, i, dim, isEmpty) {
        this.div = document.createElement("div");
        var current = i;
        if (!isEmpty) {
                this.div.innerHTML = i + 1;
        }
        this.div.style.position = 'absolute';
};

element.style.position

Each HTML element has a style property which represents the style object of the element's style attribute. Setting the position to absolute is the same as the following HTML

<div style="position: absolute">1</div>

absolute means the element will be positioned by the CSS properties namely left and top.

Initialization

Finally, we initialize the puzzle by getting the element from DOM and creating a new 15-puzzle with 4x4 dimension.

var board = document.getElementById("board");
var puzzle = new Puzzle(board, 4);

document.getElementById

document.getElementById() method returns a reference to the first object with the specified ID. Since it is used frequently, some JavaScript libraries shortcut the method to $().

Positioning the Pieces

The pieces are now clustered. update() function make sure the pieces are being in-place.

function PuzzlePiece(board, i, dim, isEmpty) {
        this.div = document.createElement("div");
        var size = board.cellSize;
        var current = i;
        if (!isEmpty) {
                this.div.innerHTML = i + 1;
        }
        this.div.style.position = 'absolute';

        this.update = function(i) {
                current = i;
                var y = Math.floor(i / dim);
                var x = i % dim;

                var pos = findPos(board.div);
                this.div.style.left = (pos.left + x * size) + "px";
                this.div.style.top = (pos.top + y * size) + "px";
        }

        this.update(i);
};

findPos

We have a utility functions to find the top and left value of the board (or any element when necessary).

function findPos(obj) {
        var curleft = curtop = 0;
        if (obj.offsetParent) {
                do {
                        curleft += obj.offsetLeft;
                        curtop += obj.offsetTop;
                } while (obj = obj.offsetParent);
        }
        return {left: curleft, top: curtop};
}

Refining the UI

Position the pieces correctly is not appealing enough. formatDiv() is added to

function PuzzlePiece(board, i, dim, isEmpty) {
        .
        .
        .
        function formatDiv(div) {
                div.style.textAlign = 'center';
                div.style.fontFamily = 'Arial';
                div.style.border = 'solid 1px black';
                div.style.fontSize = Math.floor(size * 0.9) + 'px';
                div.style.width = size + 'px';
                div.style.height = size + 'px';
        }

        this.update(i);
        formatDiv(this.div);
};

Function Declaration

You can define a JavaScript function in two ways:

function fn(param){}

or

fn = function(param){}

However, you can only define a public function in one way:

this.fn = function(param){}

The scope of this will only belongs to the public function. this in a private function means the private function is a constructor. The following code will not work for PuzzlePiece because this referring the object created by formatDiv:

function formatDiv() {
                this.div.style.textAlign = 'center';
                this.div.style.fontFamily = 'Arial';
                this.div.style.border = 'solid 1px black';
                this.div.style.fontSize = Math.floor(size * 0.9) + 'px';
                this.div.style.width = size + 'px';
                this.div.style.height = size + 'px';
        }

Implementing the Logics

PuzzlePiece

onclick event is defined so the pieces will react to the mouse click.

this.div.onclick = function() {
                board.move(current);
        }

Puzzle

move

Validates the move before swapping the piece with the empty block

this.move = function(a) {
                if (a == this.empty) return;

                var ay = Math.floor(a / dim);
                var ax = a % dim;

                var by = Math.floor(this.empty / dim);
                var bx = this.empty % dim;

                if ((ax == bx && Math.abs(ay - by) == 1) ||
                    (ay == by && Math.abs(ax - bx) == 1)) {
                        this.swap(a, this.empty);
                }
        }

swap

Swaps two pieces

this.swap = function(a, b) {
                var x = item[a];
                item[a] = item[b];
                item[b] = x;
                item[a].update(a);
                item[b].update(b);
                this.empty = a;
        }
 

randomize

The game is meaningless if all pieces are in-place

this.randomize = function() {
                var n = dim * dim;
                for (var i = 0; i < n * 2; i++) {
                        var a = Math.floor(Math.random() * n);
                        this.swap(a, this.empty);
                }
        }

Final Code

  1. <html>
  2. <head>
  3.         <title>Puzzle</title>
  4. <script>
  5. window.onload=function(){
  6.         var board = document.getElementById("board");
  7.         var puzzle = new Puzzle(board, 4);
  8. }
  9.  
  10. function Puzzle(div, dim) {
  11.         this.div = div;
  12.         this.cellSize = div.offsetWidth / dim;
  13.         this.empty = dim * dim - 1;
  14.         var item = {};
  15.         for (var i = 0, n = dim * dim; i < n; i++) {
  16.                 var newItem = new PuzzlePiece(this, i, dim, i == n - 1);
  17.  
  18.                 item[i] = newItem;
  19.                 div.appendChild(newItem.div);
  20.         }
  21.  
  22.         this.move = function(a) {
  23.                 if (a == this.empty) return;
  24.  
  25.                 var ay = Math.floor(a / dim);
  26.                 var ax = a % dim;
  27.  
  28.                 var by = Math.floor(this.empty / dim);
  29.                 var bx = this.empty % dim;
  30.  
  31.                 if ((ax == bx && Math.abs(ay - by) == 1) ||
  32.                     (ay == by && Math.abs(ax - bx) == 1)) {
  33.                         this.swap(a, this.empty);
  34.                 }
  35.         }
  36.  
  37.         this.swap = function(a, b) {
  38.                 var x = item[a];
  39.                 item[a] = item[b];
  40.                 item[b] = x;
  41.                 item[a].update(a);
  42.                 item[b].update(b);
  43.                 this.empty = a;
  44.         }
  45.  
  46.         this.randomize = function() {
  47.                 var n = dim * dim;
  48.                 for (var i = 0; i < n * 2; i++) {
  49.                         var a = Math.floor(Math.random() * n);
  50.                         this.swap(a, this.empty);
  51.                 }
  52.         }
  53.  
  54.         this.randomize();
  55.  
  56. }
  57.  
  58. function PuzzlePiece(board, i, dim, isEmpty) {
  59.         this.div = document.createElement("div");
  60.         var size = board.cellSize;
  61.         var current = i;
  62.         if (!isEmpty) {
  63.                 this.div.innerHTML = i + 1;
  64.         }
  65.         this.div.style.position = 'absolute';
  66.  
  67.         function formatDiv(div) {
  68.                 div.style.textAlign = 'center';
  69.                 div.style.fontFamily = 'Arial';
  70.                 div.style.border = 'solid 1px black';
  71.                 div.style.fontSize = Math.floor(size * 0.9) + 'px';
  72.                 div.style.width = size + 'px';
  73.                 div.style.height = size + 'px';
  74.         }
  75.  
  76.         this.div.onclick = function() {
  77.                 board.move(current);
  78.         }
  79.  
  80.         this.update = function(i) {
  81.  
  82.                 current = i;
  83.                 var y = Math.floor(i / dim);
  84.                 var x = i % dim;
  85.  
  86.                 var pos = findPos(board.div);
  87.                 this.div.style.left = (pos.left + x * size) + "px";
  88.                 this.div.style.top = (pos.top + y * size) + "px";
  89.  
  90. //              debug(this.div.innerHTML + ": " + i + " - " + x + ", " + y);
  91.         }
  92.  
  93.         this.update(i);
  94.         formatDiv(this.div);
  95. };
  96.  
  97. /* Utility functions */
  98. function debug(message) {
  99.         var pre = document.getElementById('debug');
  100.         pre.appendChild(document.createTextNode(message + '\n'));
  101. }
  102.  
  103. function findPos(obj) {
  104.         var curleft = curtop = 0;
  105.         if (obj.offsetParent) {
  106.                 do {
  107.                         curleft += obj.offsetLeft;
  108.                         curtop += obj.offsetTop;
  109.                 } while (obj = obj.offsetParent);
  110.         }
  111.         return {left: curleft, top: curtop};
  112. }
  113.  
  114. </script>
  115. </head>
  116. <body>
  117. <div style="width:300px; height:300px" id="board">
  118.  
  119. </div>
  120. <pre id="debug"></pre>
  121. </body>
  122. </html>