Do you like my tutorials?

Then consider supporting me on Ko-fi

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 on 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>
Download the source code and play with it, in my opinion the best levels are generated with 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.