Talking about zNumbers game, Game development, HTML5, Javascript and Phaser.
In this second post of the series about zNumbers, I added all the original 10 levels as well as a restart button. I am also writing a level generator but I am not quite satisfied about the layout it returns, I think the best way to design levels for this game is mixing manual and algorithmic generation. I’ll sort it out, as usual, meanwhile have a look at the game with 10 levels: Tap/touch a number then tap/touch a destination. Will you be able to move all numbers once? If you have a mobile phone, have a go directly at this link. The source code keeps being completely commented so it should be easy for you to understand ohw it works:
// the game itself
var game;
// levels
var levels = [
[ // level 1
[0, 0, 0, 0, 0, 0],
[3, 2, 3, 2, 2, 2],
[0, 0, 2, 3, 2, 2],
[2, 0, 2, 2, 0, 0],
[0, 2, 3, 0, 2, 2],
[2, 3, 0, 2, 0, 4]
],
[ // level 2
[0, 2, 3, 3, 2, 1],
[2, 0, 3, 3, 0, 2],
[1, 4, 3, 3, 0, 1],
[1, 4, 3, 3, 0, 1],
[2, 0, 3, 3, 0, 2],
[0, 2, 3, 3, 2, 1]
],
[ // level 3
[0, 2, 2, 2, 0, 2],
[1, 1, 1, 1, 1, 1],
[1, 3, 0, 3, 3, 1],
[1, 3, 3, 3, 3, 1],
[1, 1, 1, 1, 1, 1],
[4, 4, 0, 4, 0, 1]
],
[ // level 4
[3, 4, 2, 2, 0, 2],
[1, 1, 1, 1, 1, 1],
[0, 1, 3, 0, 3, 1],
[1, 3, 1, 3, 1, 1],
[1, 1, 1, 1, 1, 1],
[3, 0, 0, 4, 0, 1]
],
[ // level 5
[4, 2, 1, 2, 1, 4],
[1, 2, 1, 1, 2, 1],
[1, 3, 2, 3, 3, 1],
[1, 1, 2, 2, 3, 1],
[1, 0, 1, 1, 2, 1],
[4, 0, 0, 4, 1, 4]
],
[ // level 6
[3, 2, 1, 1, 2, 1],
[1, 2, 1, 0, 2, 2],
[2, 1, 1, 1, 2, 1],
[1, 2, 1, 1, 2, 1],
[0, 2, 1, 1, 2, 4],
[3, 0, 0, 4, 0, 4]
],
[ // level 7
[2, 0, 2, 3, 0, 2],
[0, 2, 1, 3, 2, 1],
[2, 0, 1, 3, 0, 2],
[0, 2, 3, 3, 2, 1],
[2, 0, 1, 3, 0, 2],
[0, 2, 0, 3, 2, 1]
],
[ // level 8
[1, 3, 0, 1, 1, 2],
[0, 4, 0, 0, 2, 3],
[3, 1, 0, 0, 3, 1],
[1, 2, 0, 0, 1, 2],
[4, 3, 1, 0, 4, 3],
[2, 4, 1, 1, 2, 4]
],
[ // level 9
[4, 1, 0, 0, 2, 4],
[2, 4, 2, 4, 1, 0],
[0, 0, 2, 0, 4, 2],
[1, 1, 0, 0, 4, 2],
[2, 4, 1, 4, 0, 1],
[3, 0, 4, 1, 0, 4]
],
[ // level 10
[3, 2, 2, 0, 2, 0],
[2, 3, 2, 3, 2, 0],
[0, 1, 3, 1, 2, 2],
[1, 1, 0, 0, 0, 1],
[2, 2, 1, 3, 3, 1],
[3, 2, 1, 1, 2, 2]
]
];
// this object contains all customizable game options
// changing them will affect gameplay
var gameOptions = {
// game width, in pixels
gameWidth: 700,
// game height, in pixels
gameHeight: 800,
// tile size, in pixels
tileSize: 100,
// field size, an object
fieldSize: {
// rows in the field, in units
rows: 6,
// columns in the field, in units
cols: 6
},
// tile colors
colors: [0x999999, 0xffcb97, 0xffaeae, 0xa8ffa8, 0x9fcfff],
// array with various directions, you can make the game harder if you only use the first 4 (up, down, left, right)
directions: [
new Phaser.Point(0, 1),
new Phaser.Point(0, -1),
new Phaser.Point(1, 0),
new Phaser.Point(-1, 0),
new Phaser.Point(1, 1),
new Phaser.Point(-1, -1),
new Phaser.Point(1, -1),
new Phaser.Point(-1, 1)
]
}
// function to be execute once the page loads
window.onload = function() {
// creation of a new Phaser Game
game = new Phaser.Game(gameOptions.gameWidth, gameOptions.gameHeight);
// adding "TheGame" state
game.state.add("TheGame", TheGame);
// launching "TheGame" state
game.state.start("TheGame");
}
/* ****************** TheGame state ****************** */
var TheGame = function(){};
TheGame.prototype = {
// function executed when the state initializes, with level argument.
init: function(level){
// this.level is zero if level is undefined, or level argument value otherwise
this.level = (level != undefined) ? level : 0;
},
// function to be executed when the game preloads
preload: function(){
// setting background color to dark grey
game.stage.backgroundColor = 0xf5f5f5;
// load the white tile which will be tinted on the fly
game.load.image("tiles", "assets/sprites/tile.png");
// the restart button
game.load.image("restart", "assets/sprites/restart.png");
},
// function to be executed as soon as the game has completely loaded
create: function(){
// scaling the game to cover the entire screen, while keeping its ratio
game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
// horizontally centering the game
game.scale.pageAlignHorizontally = true;
// vertically centering the game
game.scale.pageAlignVertically = true;
// this function will create the level
this.createLevel();
},
createLevel: function(){
// tiles are saved in an array called tilesArray, so we can update tile layout without losing the initial configuration
this.tilesArray = [];
// this group will contain all tiles
this.tileGroup = game.add.group();
// we are centering the group horizontally and keeping the same margin from the top
this.tileGroup.x = (game.width - gameOptions.tileSize * gameOptions.fieldSize.cols) / 2;
this.tileGroup.y = this.tileGroup.x;
// two loops to create a grid made by "gameOptions.fieldSize.rows" x "gameOptions.fieldSize.cols" columns
for(var i = 0; i < gameOptions.fieldSize.rows; i++){
this.tilesArray[i] = [];
for(var j = 0; j < gameOptions.fieldSize.cols; j++){
// this function adds a tile at row "i" and column "j"
this.addTile(i, j);
}
}
// waiting for user input
game.input.onDown.add(this.pickTile, this);
// placing restart button
game.add.button(game.width / 2, game.height - this.tileGroup.y, "restart", function(){
// restart the level
game.state.start("TheGame", true, false, this.level);
}, this).anchor.set(0.5, 1);
},
// function to add a tile at "row" row and "col" column
addTile: function(row, col){
// determining x and y tile position according to tile size
var tileXPos = col * gameOptions.tileSize + gameOptions.tileSize / 2;
var tileYPos = row * gameOptions.tileSize + gameOptions.tileSize / 2;
// tile is added as an image
var theTile = game.add.sprite(tileXPos, tileYPos, "tiles");
// setting tile registration point to its center
theTile.anchor.set(0.5);
// adjusting tile width and height according to tile size
theTile.width = gameOptions.tileSize;
theTile.height = gameOptions.tileSize;
// time to assign the tile a random value, which is also a random color
var tileValue = levels[this.level][row][col];
// tinting the tile
theTile.tint = gameOptions.colors[tileValue];
// adding tile number
var tileText = game.add.text(0, 0, tileValue.toString(), {
font: (gameOptions.tileSize / 2).toString() + "px Arial",
fontWeight: "bold"
});
// setting tile number registration point to the center
tileText.anchor.set(0.5);
tileText.alpha = (tileValue > 0) ? 0.5 : 0
// now tile number is a child of tile itself
theTile.addChild(tileText);
// adding the image to "tilesArray" array, creating an object
this.tilesArray[row][col] = {
// tile image
tileSprite: theTile,
// the value of the tile
value: tileValue,
// the text of the tile
text: tileText
};
// also adding it to "tileGroup" group
this.tileGroup.add(theTile);
},
// this function is executed at each user input (click or touch)
pickTile: function(e){
// method to reset all tile tweens
this.resetTileTweens();
// determining x and y position of the input inside tileGroup
var posX = e.x - this.tileGroup.x;
var posY = e.y - this.tileGroup.y;
// transforming coordinates into actual rows and columns
var pickedRow = Math.floor(posY / gameOptions.tileSize);
var pickedCol = Math.floor(posX / gameOptions.tileSize);
// checking if row and column are inside the actual game field
if(pickedRow >= 0 && pickedCol >= 0 && pickedRow < gameOptions.fieldSize.rows && pickedCol < gameOptions.fieldSize.cols){
// this is the tile we picked
var pickedTile = this.tilesArray[pickedRow][pickedCol];
// getting tile value
var pickedValue = pickedTile.value;
// if it's a legal tile...
if(pickedValue > 0){
// saving picked tile coordinate
this.saveTile = new Phaser.Point(pickedRow, pickedCol);
// here we will place the possible landing tiles, if ones
this.possibleLanding = [];
this.possibleLanding.length = 0;
// tween the tile
this.setTileTweens(pickedTile.tileSprite);
// looping through all directions
for(var i = 0; i < gameOptions.directions.length; i++){
// determining new coordinates
var newRow = pickedRow + pickedValue * gameOptions.directions[i].x;
var newCol = pickedCol + pickedValue * gameOptions.directions[i].y;
// are we on a legal tile?
if(newRow < gameOptions.fieldSize.rows && newRow >= 0 && newCol < gameOptions.fieldSize.cols && newCol >=0 && this.tilesArray[newRow][newCol].value == 0){
// we tween the tile
this.setTileTweens(this.tilesArray[newRow][newCol].tileSprite);
// this tile is a possible landing tile
this.possibleLanding.push(new Phaser.Point(newRow, newCol));
}
}
}
// it's not a legal tile. Maybe a possible landing?
else{
// check if the picked tile is in the array of possible landings
if(this.pointInArray(new Phaser.Point(pickedRow, pickedCol))){
// this tile can't be picked anymore
this.tilesArray[pickedRow][pickedCol].value = -1;
// showing tile text
this.tilesArray[pickedRow][pickedCol].text.alpha = 0.5;
// setting destination tile text as source tile value
this.tilesArray[pickedRow][pickedCol].text.text = this.tilesArray[this.saveTile.x][this.saveTile.y].value.toString();
// empty source tile
this.tilesArray[this.saveTile.x][this.saveTile.y].value = 0;
// change source tile color
this.tilesArray[this.saveTile.x][this.saveTile.y].tileSprite.tint = gameOptions.colors[0];
// hiding tile text
this.tilesArray[this.saveTile.x][this.saveTile.y].text.alpha = 0;
// checkinf for solution
this.checkSolution();
}
// empty possibleLanding array
this.possibleLanding = [];
this.possibleLanding.length = 0;
}
}
},
// defines the tile tween
setTileTweens: function(tile){
// defining a pulse tween which changes width and height in 200 milliseconds, with a continuous loop and with a yoyo effect
this.pulseTween = game.add.tween(tile).to({
width: gameOptions.tileSize * 0.8,
height: gameOptions.tileSize * 0.8
}, 200, Phaser.Easing.Cubic.InOut, true, 0, -1, true);
},
// this function reset all tile tweens
resetTileTweens: function(){
// get all running tweens
var activeTweens = game.tweens.getAll();
// looping through all running tweens
for(var i = 0; i < activeTweens.length; i++){
// set tile width and height back to original size
activeTweens[i].target.width = gameOptions.tileSize;
activeTweens[i].target.height = gameOptions.tileSize;
}
// remove all tweens
game.tweens.removeAll();
},
// this function checks for solution
checkSolution: function(){
// looping through the entire game field
for(var i = 0; i < gameOptions.fieldSize.rows; i++){
for(var j = 0; j < gameOptions.fieldSize.cols; j++){
// if there's still a number tile on the field...
if(this.tilesArray[i][j].value > 0){
// level is not solved
return false;
}
}
}
// level is solved, wait one second then advance to next level
game.time.events.add(Phaser.Timer.SECOND, function(){
// start "TheGame" state, clearing World display list (true), without clearing the cache (false), and passing the new level
game.state.start("TheGame", true, false, (this.level + 1) % 10);
}, this);
},
// checks if a point is in possibleLanding array
pointInArray: function(p){
// looping through possibleLanding
for(var i = 0; i < this.possibleLanding.length; i++){
// do we find the point at i-th element?
if(this.possibleLanding[i].x == p.x && this.possibleLanding[i].y == p.y){
// yes we found it
return true;
}
}
// p is not inside possibleLanding
return false;
}
}
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.