“zNumbers” game level generator commented source code released
Talking about zNumbers game, Game development, HTML5, Javascript and Phaser.
Do you like my tutorials?
Then consider supporting me on Ko-fi.
generateRandomLevel(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.