Building the Game - Creating the Basic Game - HTML5 Games: Creating Fun with HTML5, CSS3, and WebGL (2012)

HTML5 Games: Creating Fun with HTML5, CSS3, and WebGL (2012)

part 2

Creating the Basic Game

Chapter 4 Building the Game

Chapter 5 Delegating Tasks to Web Workers

Chapter 6 Creating Graphics with Canvas

Chapter 7 Creating the Game Display

Chapter 8 Interacting with the Game

Chapter 9 Animating Game Graphics

Chapter 4

Building the Game

in this chapter

• Creating a module for the game board

• Encapsulating the game state

• Setting up the jewel board

• Implementing the game rules

• Reacting to jewel swaps

In this chapter, I show you how to implement the rules and game mechanics that control the jewel board. I walk you through all the necessary game logic and create a representation of the game board with which the rest of the application can interact. I show you how to encapsulate the state of the board in an isolated module that allows modifications to the board only when all the game rules are followed. In addition, I show you how to make sure that the board automatically reacts to the swaps and rearranges the remaining jewels accordingly.

I also discuss some of the issues that arise when the game must be able to use two different sources that control the board. The single-player experience relies on the local, client-side game logic described in this chapter, but the game must also work with a server-side implementation of the same rules.

Creating the Game Board Module

The core of the game mechanics is isolated from the display and input elements of the game. The board module I walk you through in this chapter is a data model of the game state, specifically the current layout of jewels. The module exposes methods that the other modules, mainly the game screen, can use to interact with the game state. Because the board will serve as a back-end to the game display, the code in this chapter will not add any visual elements to the game.

Boiled down, the board module is just a representation of the jewel board. The only real functionality it exposes to the outside world is a query function for accessing the jewels on the board and a function for swapping jewels. The swapping function attempts to swap only a pair of jewels because jewels are swappable only if certain conditions are met. Swapping jewels also has consequences: Jewels are removed, new ones fall down, and so on. The board module handles all these conditions and reactions automatically. The module lives in the JavaScript file board.js in thescripts folder. Listing 4.1 shows the initial module.

Listing 4.1 The Board Module

jewel.board = (function() {

/* game functions go here */

return {

/* exposed functions go here */

};

})();

Add the board.js file to the second stage of the loader in loader.js as shown in Listing 4.2.

Listing 4.2 Loading the Board Module

// loading stage 2

if (Modernizr.standalone) {

Modernizr.load([

{

load : [

“scripts/screen.main-menu.js”,

“scripts/board.js”

]

}

]);

}

So far, the barebones board module has no functionality at all. Let’s move on to the first method and start fleshing out the module.

Initializing the game state

The board logic needs a few settings defined, such as the number of rows and columns, the number of different jewel types, and so on. Settings like these are best kept separate from the actual game code so it’s easy to change the values without having to go through all the code. Listing 4.3 shows a new settings object added to the jewel namespace in loader.js.

Listing 4.3 Adding a Settings Object

var jewel = {

screens : {},

settings : {

rows : 8,

cols : 8,

baseScore : 100,

numJewelTypes : 7

}

};

The rows and cols settings define the size of the board in terms of rows and columns. Jewel Warrior uses an 8x8 grid, a size that works well on small screens. The baseScore setting determines the number of points awarded per jewel that takes part in a chain. As you will see later, the score is further multiplied for chains involving more than three jewels. The base score is set to 100 points per jewel, but you can change this number if you want to inflate or deflate the scores. The last of the new settings, numJewelTypes, indicates the number of different jewel types. This number also corresponds to the number of jewel sprites.

The new settings are now accessible to the rest of the game and most importantly, at least for now, to the board module.

Initializing the board

To flesh out the module, you first need a function for setting up and initializing the game board. When the game starts, the board is already filled with random jewels. Listing 4.4 shows the initialize() function added to board.js.

Listing 4.4 The Initializing Function

jewel.board = (function() {

var settings,

jewels,

cols,

rows,

baseScore,

numJewelTypes;

function initialize() {

settings = jewel.settings;

numJewelTypes = settings.numJewelTypes,

baseScore = settings.baseScore,

cols = settings.cols;

rows = settings.rows;

fillBoard();

}

function print() {

var str = “”;

for (var y = 0; y < rows; y++) {

for (var x = 0; x < cols; x++) {

str += getJewel(x, y) + ” ”;

}

str += ”\r\n”;

}

console.log(str);

}

return {

initialize : initialize,

print : print

};

})();

The first thing you see in Listing 4.4 is a group of variable declarations. The first variable just imports the settings module because you need to access the settings in a bit. The second variable, jewels, is an array of arrays, essentially a two-dimensional array, representing the current state of the jewel board. In the jewels array, each jewel is represented by an integer value that indicates the type of the jewel. Using this array structure makes it easy to access the individual jewels by their coordinates. For example, the following snippet retrieves the jewel type found at position (x=3, y=2) on the board:

var type = jewels[3][2];

The listing also contains a number of variables whose values come from the settings module. The values are described in the next section. The print() function outputs the board data the the JavaScript console and is merely there to aid you in debugging. You can initialize the board module by typing the following into the console:

jewel.board.initialize()

Throughout this chapter, whenever you want to inspect the board data, you can simply enter the following command to print the data to the console:

jewel.board.print()

Using asynchronous functions

Before moving on to the next function, I want to share a small modification you can make to the initialize() function to prepare for the future. Chapter 5 shows you how to use Web Workers to move the board logic to a separate thread by creating a Web Worker-based board module that exposes the same methods as the one created in this chapter. Web Workers communicate with the rest of the application via an asynchronous messaging API, which means that all the methods exposed by the board modules must be able to function asynchronously.

Similarly, if you were to add a board module that used a server-side backend to verify and validate the board data, you would also need to send asynchronous Ajax requests to the server. Every function that modifies the game state would require a trip to the server and back before the response could be given to the caller. In other words, just because the function call returns, doesn’t mean the result is ready.

You can solve this problem of deferred response in several ways, including using full-fledged custom event dispatching systems or the concept of promise objects, as known from, for example, CommonJS and Node.js. The simplest solution, though, is just to provide a callback function as an argument to the relevant method and have that method call the callback function when it is done. You probably already know this pattern from JavaScript functions such as window.setTimeout() or the addEventListener() method on DOM elements. These functions both take another function as one of the parameters and call that function at some point in the future. Listing 4.5 shows the modified initialize() function in board.js.

Listing 4.5 Initializing with the Callback Function

jewel.board = (function() {

...

function initialize(callback) {

numJewelTypes = settings.numJewelTypes;

baseScore = settings.baseScore;

cols = settings.cols;

rows = settings.rows;

fillBoard();

callback();

}

....

})();

If you want to initialize the board via the JavaScript console, you can just pass an empty function:

jewel.board.initialize(function(){})

Now, all the action in the initialize() function happens immediately, so the result is pretty much the same as it was without the callback function. The main difference is that when you add the Web Worker module, it can use the same function signature, making the integration of that module a lot easier.

Filling the initial board

The fillBoard() function used in Listing 4.5 must initialize a cols x rows grid and fill it with random jewels. Listing 4.6 shows this function added to board.js.

Listing 4.6 Filling the Jewel Board

jewel.board = (function() {

...

function fillBoard() {

var x, y;

jewels = [];

for (x = 0; x < cols; x++) {

jewels[x] = [];

for (y = 0; y < rows; y++) {

jewels[x][y] = randomJewel();

}

}

}

...

})();

The jewel type is picked using the helper function randomJewel(), which simply returns an integer value between 0 and (numJewelTypes - 1). Listing 4.7 shows the randomJewel() function.

Listing 4.7 Creating a Random Jewel

jewel.board = (function() {

...

function randomJewel() {

return Math.floor(Math.random() * numJewelTypes);

}

...

})();

The basic algorithm for randomly filling the board is now in place. The current solution is a bit naive, though, and doesn’t guarantee that the resulting board is any good. Because the jewels are picked at random, there is a good chance that at least one or two chains are already on the board. The starting state of the game shouldn’t have any chains because that would lead to the player immediately getting points without lifting a finger. To ensure that this situation never occurs, the fillBoard() function must pick the jewels in a way that doesn’t form chains of more than two identical jewels.

The fill algorithm fills the board starting from the top-left corner and finishing in the bottom-right corner. This means that there will only ever be jewels above and to the left of the position currently being filled. A chain takes at least three identical jewels, which means that, for there to be a chain, the randomly picked jewel must be the same type as either the two jewels to the left or the two jewels above. For a relatively small board like the one Jewel Warrior uses, a simple brute-force solution is good enough. Listing 4.8 shows the changes to the fill routine.

Listing 4.8 Removing Initial Chains

jewel.board = (function() {

...

function fillBoard() {

var x, y,

type;

jewels = [];

for (x = 0; x < cols; x++) {

jewels[x] = [];

for (y = 0; y < rows; y++) {

type = randomJewel();

while ((type === getJewel(x-1, y) &&

type === getJewel(x-2, y)) ||

(type === getJewel(x, y-1) &&

type === getJewel(x, y-2))) {

type = randomJewel();

}

jewels[x][y] = type;

}

}

}

...

})();

The algorithm now includes a loop that keeps picking a random jewel type until the chain condition is not met. In most cases, the randomly picked jewel cannot form a chain at all, and in the few cases in which it does, an alternative is quickly found.

Without some form of bounds checking, however, a loop like the one in Listing 4.8 would try to access positions outside the board, resulting in errors. Instead of accessing the jewels array directly, the fillBoard() routine uses a helper function, getJewel(), that guards against these out-of-bounds errors. Listing 4.9 shows this function.

Listing 4.9 Getting Jewel Type from Coordinates

jewel.board = (function() {

...

function getJewel(x, y) {

if (x < 0 || x > cols-1 || y < 0 || y > rows-1) {

return -1;

} else {

return jewels[x][y];

}

}

...

})();

The getJewel() function returns -1 if either of the coordinates is out of bounds, that is, if x or y is negative or greater than (cols - 1) or (rows - 1), respectively. Because valid jewel types are in the range [0, numTypes - 1], this ensures that the return value is never equal to a real jewel type and therefore is not able to take part in a chain.

Implementing the Rules

Now that the initial board is ready, you can turn your attention to jewel swapping. The module exposes a swap() method that takes two sets of coordinates and tries to swap the jewels at those positions. Only valid swaps are allowed, and those that don’t meet the requirements are rejected. You can start by implementing the validation mechanism.

Validating swaps

A swap is valid only if it results in at least one chain of three or more identical jewels. To perform this check, you can use a function, checkChain(), that tests whether a jewel at a specified position is part of a chain. It determines this outcome by noting the jewel type at the specified position and then looking to the left and to the right and counting the number of jewels of the same type found in those directions. The same search is performed for the up and down directions. If the sum of identical jewels in either the horizontal or vertical search is greater than two (or three if you include the center jewel), there is a chain. Listing 4.10 shows the checkChain() function in board.js.

Listing 4.10 Checking for Chains

jewel.board = (function() {

...

// returns the number jewels in the longest chain

// that includes (x,y)

function checkChain(x, y) {

var type = getJewel(x, y),

left = 0, right = 0,

down = 0, up = 0;

// look right

while (type === getJewel(x + right + 1, y)) {

right++;

}

// look left

while (type === getJewel(x - left - 1, y)) {

left++;

}

// look up

while (type === getJewel(x, y + up + 1)) {

up++;

}

// look down

while (type === getJewel(x, y - down - 1)) {

down++;

}

return Math.max(left + 1 + right, up + 1 + down);

}

...

})();

Note that checkChain() doesn’t return a boolean value but instead returns the number of jewels found in the largest chain. This result gives a bit of extra information about the jewel that you can use later when the score is calculated. Now that you can detect chains at a given position, determining whether a swap is valid is relatively easy. The first condition is that the two positions must be adjacent. Only neighboring jewels can be swapped. If they are neighbors, assume that the swap is valid and switch the jewels types around. Now, if the swap was actually good,checkChain() should return a number larger than 2 for one of the two positions. Swap the jewels back and return the result from the checkChain() calls. Listing 4.11 shows the canSwap() function in board.js that implements this validation.

Listing 4.11 Validating a Swap

jewel.board = (function() {

...

// returns true if (x1,y1) can be swapped with (x2,y2)

// to form a new match

function canSwap(x1, y1, x2, y2) {

var type1 = getJewel(x1,y1),

type2 = getJewel(x2,y2),

chain;

if (!isAdjacent(x1, y1, x2, y2)) {

return false;

}

// temporarily swap jewels

jewels[x1][y1] = type2;

jewels[x2][y2] = type1;

chain = (checkChain(x2, y2) > 2

|| checkChain(x1, y1) > 2);

// swap back

jewels[x1][y1] = type1;

jewels[x2][y2] = type2;

return chain;

}

return {

canSwap : canSwap,

...

}

})();

Another helper function, isAdjacent(), is introduced in the canSwap() validation function. This function returns true if the two sets of coordinates are neighbors and false if they are not. The function easily determines whether they are neighbors by looking at the distance between the positions along both axes, also called the Manhattan distance. The sum of the two distances must be exactly 1 if the positions are adjacent. Listing 4.12 shows the isAdjacent() function in board.js.

Listing 4.12 Testing Adjacency

jewel.board = (function() {

...

// returns true if (x1,y1) is adjacent to (x2,y2)

function isAdjacent(x1, y1, x2, y2) {

var dx = Math.abs(x1 - x2),

dy = Math.abs(y1 - y2);

return (dx + dy === 1);

}

...

}

You can test the canSwap() function in the JavaScript console after you have initialized the board module. Use the print() function to inspect the board data and then test different positions by entering, for example, jewel.board.canSwap(4,3,4,2).

Detecting chains

After performing a swap, the game must search the board looking for chains of jewels to remove. Immediately following a swap, relatively few jewels are candidates for removal. There can only be the chains involving the two swapped jewels. However, when those jewels are removed, other jewels fall down and more jewels enter the board from the top. This means that the board must be checked again, and now the situation is not so simple anymore. The only way to be sure all chains are detected is to be thorough and search the whole board. When you use thecheckChain() function, this task is not so complicated. Listing 4.13 shows the getChains() function that loops across the board, looking for chains.

Listing 4.13 Searching the Board for Chains

jewel.board = (function() {

...

// returns a two-dimensional map of chain-lengths

function getChains() {

var x, y,

chains = [];

for (x = 0; x < cols; x++) {

chains[x] = [];

for (y = 0; y < rows; y++) {

chains[x][y] = checkChain(x, y);

}

}

return chains;

}

...

})();

The variable chains returned at the end of getChains() is a two-dimensional map of the board. Instead of jewel types, this map holds information about the chains in which the jewels take part. Each position on the board is checked by calling checkChain() and the corresponding position in the chains map is assigned the return value.

Removing chained jewels

Identifying the chains is not enough, however. The game must also act on that information. Specifically, the chains must be removed, and the jewels above should fall down. The chain map is processed in the check() function shown in Listing 4.14.

Listing 4.14 Processing Chains

jewel.board = (function() {

...

function check() {

var chains = getChains(),

hadChains = false, score = 0,

removed = [], moved = [], gaps = [];

for (var x = 0; x < cols; x++) {

gaps[x] = 0;

for (var y = rows-1; y >= 0; y--) {

if (chains[x][y] > 2) {

hadChains = true;

gaps[x]++;

removed.push({

x : x, y : y,

type : getJewel(x, y)

});

} else if (gaps[x] > 0) {

moved.push({

toX : x, toY : y + gaps[x],

fromX : x, fromY : y,

type : getJewel(x, y)

});

jewels[x][y + gaps[x]] = getJewel(x, y);

}

}

}

}

...

})();

This function removes jewels from the board and brings in new ones where necessary. Besides modifying the game board, the check() function also collects information about all the removed and repositioned jewels in two arrays, removed and moved. This data is important because you need it later for, for example, animating the changes on the screen.

Using two nested loops, the check() function visits every position on the board. If the position is marked in a chains map with a value greater than two, information about the position and jewel type is recorded in the array removed using a simple object literal. Because a falling jewel will simply overwrite this position later, you do not need to modify the actual jewels array yet.

Notice that the inner loop examines the rows from the bottom up instead of the usual top-down approach. This approach lets you immediately start moving the other jewels down. The algorithm also maintains a gaps array that contains a counter for each column. Before the algorithm processes a column, it sets the counter for that column to zero. Every time a jewel is removed, the counter is incremented. Whenever a jewel is allowed to stay, the gap counter determines whether the jewel should be moved down. If the counter is positive, the jewel must be moved down an equal number of rows. This information is recorded in a second array, moved, using a similar object literal, but this time recording both the start and end positions. You also need to update the jewels array now because this position is not touched again.

Creating new jewels

The check() function is not finished; it has a few loose ends. By moving existing jewels down, you fill the gaps below but leave new gaps at the top of the board. So, after processing all the jewels in a column, you need to create new jewels and have them come down from the top. Listing 4.15 shows this modification.

Listing 4.15 Adding New Jewels

jewel.board = (function() {

...

function check() {

...

for (var x = 0; x < cols; x++) {

gaps[x] = 0;

for (var y = rows-1; y >= 0; y--) {

...

}

// fill from top

for (y = 0; y < gaps[x]; y++) {

jewels[x][y] = randomJewel();

moved.push({

toX : x, toY : y,

fromX : x, fromY : y - gaps[x],

type : jewels[x][y]

});

}

}

}

...

})();

The number of new jewels you need to create in a column is equal to the number of gaps found in that column. The positions they need to occupy are easy to calculate because new jewels always enter the board from the top. Information about the new jewels is also added to the moved array alongside any existing jewels that might have moved down. Because the new jewels don’t have an actual starting position, an imaginary position outside the board is invented as if the jewels were already up there, waiting to fall down into the board.

Awarding points

In the initialize() function, I introduced a baseScore value for calculating the number of points given. Use that value to calculate the total rewarded across the board. Listing 4.16 shows the scoring added to the check() function.

Listing 4.16 Awarding Points for Chains

jewel.board = (function() {

...

function check() {

...

for (var x = 0; x < cols; x++) {

gaps[x] = 0;

for (var y = rows-1; y >= 0; y--) {

if (chains[x][y] > 2) {

hadChains = true;

gaps[x]++;

removed.push({

x : x, y : y,

type : getJewel(x, y)

});

// add points to score

score += baseScore

* Math.pow(2, (chains[x][y] - 3));

} else if (gaps[x] > 0) {

...

}

...

}

}

}

...

})();

For every jewel that is part of a chain, a number of points are added to score. The number of points depends on the length of the chain. For every extra jewel in the chain, the multiplier is doubled.

remember.eps

The score variable is not the player’s total score; it is just the score accumulated during this check() call. The board module has no concept of players; it simply calculates how many points should be awarded for a particular swap.

The chains are now gone, and the gaps are filled with new jewels. However, it is possible for the new jewels to create new chains, so you are not done yet. The check() function must call itself again recursively until there are no chains left at all. The function should also return the recorded changes. Listing 4.17 shows the changes to the check() function.

Listing 4.17 Checking the Board Recursively

jewel.board = (function() {

...

function check(events) {

...

events = events || [];

if (hadChains) {

events.push({

type : “remove”,

data : removed

}, {

type : “score”,

data : score

}, {

type : “move”,

data : moved

});

return check(events);

} else {

return events;

}

}

...

}

You need to join the data collected in removed, moved, and score with whatever data the recursive calls collect. To do this, add an optional events argument to the check() function. This argument is used only in the recursive calls. If no value is passed in this argument, initialize events to an empty array. After the board is processed, add the accumulated score, and the board changes to the events array using the simple event object format shown in Listing 4.16. Each event object has just a type and a data property. If no chains are found, the board is not modified and you don’t need to call check() again. Just return the events array that will then bubble up to the first call and be returned to the caller. This way, the caller gets a complete list of every change that happened between the last swap and the final board.

Refilling the grid

If the game goes on long enough, the player inevitably faces a board that has no moves. The game needs to register this fact so the board can be refilled with fresh jewels and the game can continue. For this purpose, you need a function that can tell whether any moves are available. Listing 4.18 shows the hasMoves() function.

Listing 4.18 Checking for Available Moves

jewel.board = (function() {

...

// returns true if at least one match can be made

function hasMoves() {

for (var x = 0; x < cols; x++) {

for (var y = 0; y < rows; y++) {

if (canJewelMove(x, y)) {

return true;

}

}

}

return false;

}

...

})();

The hasMoves() function returns true if at least one jewel can be moved to form a chain; otherwise, it returns false. The canJewelMove() helper function, which does the actual task of checking a position for moves, is shown in Listing 4.19.

Listing 4.19 Checking Whether a Jewel Can Move

jewel.board = (function() {

...

// returns true if (x,y) is a valid position and if

// the jewel at (x,y) can be swapped with a neighbor

function canJewelMove(x, y) {

return ((x > 0 && canSwap(x, y, x-1 , y)) ||

(x < cols-1 && canSwap(x, y, x+1 , y)) ||

(y > 0 && canSwap(x, y, x , y-1)) ||

(y < rows-1 && canSwap(x, y, x , y+1)));

}

...

})();

To check whether a given jewel can be moved to form a new chain, the canJewelMove() function uses canSwap() to test whether the jewel can be swapped with one of its four neighbors. Each canSwap() call is performed only if the neighbor is within the bounds of the board; that is,canSwap()tries to swap the jewel with its left neighbor only if the jewel’s x coordinate is at least 1 and less than (cols - 1), and so on.

If a time comes when the player cannot swap any jewels and hasMoves() therefore returns false, the board must be automatically refilled. The refill is triggered in the check() function. After the board is checked for chains, the jewels are removed, and new ones are brought in, add a call to the hasMoves() function to test whether the new board allows for further swaps and, if necessary, refill the board. Listing 4.20 shows the changes.

Listing 4.20 Triggering a Refill

jewel.board = (function() {

...

function check(events) {

...

if (hadChains) {

...

// refill if no more moves

if (!hasMoves()) {

fillBoard();

events.push({

type : “refill”,

data : getBoard()

});

}

return check(events);

} else {

return events;

}

}

})();

In addition to calling fillBoard(), this listing adds a refill event to the events array. This event carries with it a copy of the jewel board, created by the getBoard() function shown in Listing 4.21.

Listing 4.21 Copying the Board Data

jewel.board = (function() {

...

// create a copy of the jewel board

function getBoard() {

var copy = [],

x;

for (x = 0; x < cols; x++) {

copy[x] = jewels[x].slice(0);

}

return copy;

}

return {

...

getBoard : getBoard

};

})();

Simply calling fillBoard() doesn’t guarantee that the new board has available moves, though. There’s a slight chance that the randomly picked jewels just produce another locked board. A locked board, then, should trigger another, silent refill, without the player knowing. The best place to put this check is in the fillBoard() function itself. A call to hasMoves() can determine whether the board is usable, and if it is not, fillBoard() calls itself recursively before returning. This way, the board keeps refilling until it has at least one pair of movable jewels. Listing 4.22 shows the refill check added to the fillBoard() function.

Listing 4.22 Refilling the Board Recursively

jewel.board = (function() {

...

function fillBoard() {

...

// recursive fill if new board has no moves

if (!hasMoves()) {

fillBoard();

}

}

...

})();

This refill check also takes care of the special case in which the starting board has no jewels that can be swapped to form a chain. Just as probability dictates that the starting board sometimes has chains from the get-go, it is, of course, also possible that there are no moves at all. The recursive call fixes that problem.

Swapping jewels

All the functions that govern the state of the board are now in place. The only thing missing is a function that actually swaps jewels. This task is relatively straightforward. You already have the canSwap() function that tells whether the player is allowed to swap a given pair of jewels, and you have the check() function that takes care of all the board modifications following the swap. Listing 4.23 shows the swap() function.

Listing 4.23 The Swap Function

jewel.board = (function() {

...

// if possible, swaps (x1,y1) and (x2,y2) and

// calls the callback function with list of board events

function swap(x1, y1, x2, y2, callback) {

var tmp,

events;

if (canSwap(x1, y1, x2, y2)) {

// swap the jewels

tmp = getJewel(x1, y1);

jewels[x1][y1] = getJewel(x2, y2);

jewels[x2][y2] = tmp;

// check the board and get list of events

events = check();

callback(events);

} else {

callback(false);

}

}

return {

...

swap : swap

};

})();

Because the swap() function needs to be exposed to the rest of the game and because it potentially modifies the board, it must work in the same asynchronous fashion as the initialize() function. Besides the two sets of coordinates, swap() takes a callback function as its fifth argument. Depending on whether the swap succeeded, this callback function is called with either a list of events that happened after the swap or, in case of an invalid swap, with false. Listing 4.24 shows the functions that are now exposed via the board module.

Listing 4.24 Returning the Public Methods

jewel.board = (function() {

...

return {

initialize : initialize,

swap : swap,

canSwap : canSwap,

getBoard : getBoard,

print : print

};

})();

That’s it. With those functions exposed, the only way to alter the game state is to set up a fresh board or call the swap() method. All the rules of the game are enforced by the swap() method, so the integrity of the board is guaranteed. In addition, the only entry point to the jewel data is via thegetBoard() function, which doesn’t allow write access to the data, minimizing the risk that the rest of the code can inadvertently “break” the board.

You can test the swap() function by calling it directly from the JavaScript console. Initialize the board module by entering:

jewel.board.initialize(function(){})

Use jewel.board.print() to locate suitable positions and then enter, for example:

jewel.board.swap(4,3,4,2, function(e){console.log(e)})

Remember that the swap() functions needs a callback function. Just give it a function that outputs the events array to the console.

Summary

This chapter showed you how to implement the core of the game mechanics. You walked through the implementation of all the basic game rules concerning jewel swapping, chains, and falling jewels. The game board is now neatly encapsulated in an isolated module and allows access to the data only through a few access points, ensuring that all modifications happen in accordance with the rules of the game.

This chapter also showed you how to prepare for the future multiplayer functionality by shaping the module such that the game can use both the single-player, local game logic as well as the server-bound multiplayer module. Using callback functions for some of the key methods allows the two modules to share the same interface, making it easier to add an asynchronous, server-bound board module at a later time.