Talking about zNumbers game, Game development, HTML5, Javascript and Phaser.
Are you enjoying znumberz game, the remake of zNumbers? It’s free with no ads both for iOS and Android and you should definitively download and install it, or at least play online on triqui.com. One of the most interesting features in my opinion is the random level generator, which allows you to generate new levels and add them to the 10 levels of the original zNumbers game. Have a look at it: Check previous posts of the series to know how to play, and let’s focus on level generation. Just hit “Restart” button to generate a new level, and look at the console to see the solution. The core is ongenerateRandomLevel(maxAttempts)
method which has an argument called maxAttempts
. The higher maxAttempts
, the harder the level.
Basically it works this way:
* Start with an empty level
* Pick a start position in a randomly choosen tile
* There is a loop which is executed n
times, where n
is the number of tiles in the game
* From start position, try maxAttempts
times to generate a valid move – which means is landing on an empty tile – going in a random direction for a random (1 to 4) number of steps
* If you find a valid move before maxAttempts
attempts, set start position to the destination of valid move.
* No matter if you found or not the valid move, execute the loop again.
It’s easy to see The higher maxAttempts
, the harder the level, and the more tiles already placed, the harder to find a valid move in less than maxAttempts
attempts.
The interesting thing is you can also write down the moves to solve the level.
Look at the source code:
<pre class="wp-block-syntaxhighlighter-code">var game;
var gameOptions = {
gameWidth: 700,
gameHeight: 800,
tileSize: 100,
fieldSize: {
rows: 6,
cols: 6
},
colors: [0x999999, 0xffcb97, 0xffaeae, 0xa8ffa8, 0x9fcfff],
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)
]
}
window.onload = function() {
game = new Phaser.Game(gameOptions.gameWidth, gameOptions.gameHeight);
game.state.add("TheGame", TheGame);
game.state.start("TheGame");
}
var TheGame = function(){};
TheGame.prototype = {
preload: function(){
game.stage.backgroundColor = 0xf5f5f5;
game.load.image("tiles", "assets/sprites/tile.png");
game.load.image("restart", "assets/sprites/restart.png");
},
create: function(){
game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
game.scale.pageAlignHorizontally = true;
game.scale.pageAlignVertically = true;
this.generateRandomLevel(60);
this.createLevel();
},
// function to generate a random playable level
generateRandomLevel: function(maxAttempts){
console.log("A POSSIBLE SOLUTION");
// we will store the generated level here
this.level = []
// initializing the array
for(var i = 0; i < gameOptions.fieldSize.rows; i++){
this.level[i] = [];
for(var j = 0; j < gameOptions.fieldSize.cols; j++){
this.level[i][j] = 0;
}
}
// choosing a random start position
var startPosition = new Phaser.Point(game.rnd.integerInRange(0, gameOptions.fieldSize.rows - 1), game.rnd.integerInRange(0, gameOptions.fieldSize.cols - 1));
// here we'll store the solution
var solution = "";
// we'll execute this process once for each tile in the game
for(i = 0; i <= gameOptions.fieldSize.rows * gameOptions.fieldSize.cols; i++){
// keeping count of how many attempts we are doing to place a tile
var attempts = 0;
// we repeat this loop...
do{
// choosing a random tile value from 1 to 4
var randomTileValue = game.rnd.integerInRange(1, 4);
// choosing a random direction
var randomDirection = game.rnd.integerInRange(0, gameOptions.directions.length - 1);
// given the start position and the tile value, we can determine the destination
var randomDestination = new Phaser.Point(startPosition.x + randomTileValue * gameOptions.directions[randomDirection].x, startPosition.y + randomTileValue * gameOptions.directions[randomDirection].y);
// we made one more attempt
attempts ++;
// until we find a legal destination or we made too many attempts
} while(!this.isLegalDestination(randomDestination) && attempts < maxAttempts);
// if we did not make too many attempts...
if(attempts < maxAttempts){
// updating solution string
solution = "(" + startPosition.x + "," + startPosition.y + ") => (" + randomDestination.x + "," + randomDestination.y + ")\n" + solution;
// inserting the tile in the field
this.level[startPosition.x][startPosition.y] = randomTileValue;
// start position now is the position of the last placed tile
startPosition = new Phaser.Point(randomDestination.x, randomDestination.y);
}
}
// these rows just display the solution in the Chrome console
console.log(this.level);
console.log(solution);
},
// function to check if a destination is legal
isLegalDestination: function(p){
// it's not legal if it's outside the game field
if(p.x < 0 || p.y < 0 || p.x >= gameOptions.fieldSize.rows || p.y >= gameOptions.fieldSize.cols){
return false;
}
// it's not legal if there's already a tile
if(this.level[p.x][p.y]!=0){
return false;
}
// ok, it's legal
return true
},
createLevel: function(){
this.tilesArray = [];
this.tileGroup = game.add.group();
this.tileGroup.x = (game.width - gameOptions.tileSize * gameOptions.fieldSize.cols) / 2;
this.tileGroup.y = this.tileGroup.x;
for(var i = 0; i < gameOptions.fieldSize.rows; i++){
this.tilesArray[i] = [];
for(var j = 0; j < gameOptions.fieldSize.cols; j++){
this.addTile(i, j);
}
}
game.input.onDown.add(this.pickTile, this);
game.add.button(game.width / 2, game.height - this.tileGroup.y, "restart", function(){
game.state.start("TheGame");
}, this).anchor.set(0.5, 1);
},
addTile: function(row, col){
var tileXPos = col * gameOptions.tileSize + gameOptions.tileSize / 2;
var tileYPos = row * gameOptions.tileSize + gameOptions.tileSize / 2;
var theTile = game.add.sprite(tileXPos, tileYPos, "tiles");
theTile.anchor.set(0.5);
theTile.width = gameOptions.tileSize;
theTile.height = gameOptions.tileSize;
var tileValue = this.level[row][col];
theTile.tint = gameOptions.colors[tileValue];
var tileText = game.add.text(0, 0, tileValue.toString(), {
font: (gameOptions.tileSize / 2).toString() + "px Arial",
fontWeight: "bold"
});
tileText.anchor.set(0.5);
tileText.alpha = (tileValue > 0) ? 0.5 : 0
theTile.addChild(tileText);
this.tilesArray[row][col] = {
tileSprite: theTile,
value: tileValue,
text: tileText
};
this.tileGroup.add(theTile);
},
pickTile: function(e){
this.resetTileTweens();
var posX = e.x - this.tileGroup.x;
var posY = e.y - this.tileGroup.y;
var pickedRow = Math.floor(posY / gameOptions.tileSize);
var pickedCol = Math.floor(posX / gameOptions.tileSize);
if(pickedRow >= 0 && pickedCol >= 0 && pickedRow < gameOptions.fieldSize.rows && pickedCol < gameOptions.fieldSize.cols){
var pickedTile = this.tilesArray[pickedRow][pickedCol];
var pickedValue = pickedTile.value;
if(pickedValue > 0){
this.saveTile = new Phaser.Point(pickedRow, pickedCol);
this.possibleLanding = [];
this.possibleLanding.length = 0;
this.setTileTweens(pickedTile.tileSprite);
for(var i = 0; i < gameOptions.directions.length; i++){
var newRow = pickedRow + pickedValue * gameOptions.directions[i].x;
var newCol = pickedCol + pickedValue * gameOptions.directions[i].y;
if(newRow < gameOptions.fieldSize.rows && newRow >= 0 && newCol < gameOptions.fieldSize.cols && newCol >=0 && this.tilesArray[newRow][newCol].value == 0){
this.setTileTweens(this.tilesArray[newRow][newCol].tileSprite);
this.possibleLanding.push(new Phaser.Point(newRow, newCol));
}
}
}
else{
if(this.pointInArray(new Phaser.Point(pickedRow, pickedCol))){
this.tilesArray[pickedRow][pickedCol].value = -1;
this.tilesArray[pickedRow][pickedCol].text.alpha = 0.5;
this.tilesArray[pickedRow][pickedCol].text.text = this.tilesArray[this.saveTile.x][this.saveTile.y].value.toString();
this.tilesArray[this.saveTile.x][this.saveTile.y].value = 0;
this.tilesArray[this.saveTile.x][this.saveTile.y].tileSprite.tint = gameOptions.colors[0];
this.tilesArray[this.saveTile.x][this.saveTile.y].text.alpha = 0;
}
this.possibleLanding = [];
this.possibleLanding.length = 0;
}
}
},
setTileTweens: function(tile){
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);
},
resetTileTweens: function(){
var activeTweens = game.tweens.getAll();
for(var i = 0; i < activeTweens.length; i++){
activeTweens[i].target.width = gameOptions.tileSize;
activeTweens[i].target.height = gameOptions.tileSize;
}
game.tweens.removeAll();
},
pointInArray: function(p){
for(var i = 0; i < this.possibleLanding.length; i++){
if(this.possibleLanding[i].x == p.x && this.possibleLanding[i].y == p.y){
return true;
}
}
return false;
}
}</pre>
maxAttemtps
ranging from 40 to 80.
And don’t forget to get znumberz for iOS or Android. Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.