Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about Dungeon Raid game, Game development, HTML5, Javascript and Phaser.

Let’s release a new step of Dungeon Raid engine series, adding a couple of features, let me show you a quick recap:

Step 1: very basic prototype with only the capability of selecting adjacent tiles and backtrack if needed.

Step 2: making selected tiles fall and be replaced by new tiles.

Step 3: the engine works with any number of rows and columns, does not destroy tiles (allowing to save memory) and hides falling tiles with a mask, allowing you to easily build a GUI around the game field.

Now, we are going to add colors to tiles so you can only pick tiles of the same color, and set an option to decide whether to allow diagonal movement or not.

Have a look at the result:

Draw to select circles, you can also backtrack and this time diagonals aren’t allowed.

Have a look at the source code, the capability to move diagonally is set at line 12 while possible tile colors are defined at line 13.

var game;

var gameOptions = {
	gameWidth: 800, 
	gameHeight: 1400, 
	tileSize: 140, 
	fieldSize: {
          rows: 6,
          cols: 5
     },
     fallSpeed: 250,
     diagonal: false,
     colors: [0xff0000, 0x00ff00, 0x0000ff, 0xffff00]
}

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 = 0x444444;
          game.load.image("tiles", "assets/sprites/tiles.png");
          game.load.spritesheet("arrows", "assets/sprites/arrows.png", 420, 420);     
     },
  	create: function(){
          game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
		game.scale.pageAlignHorizontally = true;
		game.scale.pageAlignVertically = true;   
  		this.createLevel();
          game.input.onDown.add(this.pickTile, this);
  	}, 
	createLevel: function(){
          this.tilesArray = [];
          this.arrowsArray = [];
          // group creation and placement to stay in the center of the canvas
		this.tileGroup = game.add.group();
          this.arrowsGroup = game.add.group();
          this.tileGroup.x = (game.width - gameOptions.tileSize * gameOptions.fieldSize.cols) / 2;
          this.tileGroup.y = (game.height -  gameOptions.tileSize * gameOptions.fieldSize.rows) / 2;
          this.arrowsGroup.x = this.tileGroup.x;
          this.arrowsGroup.y = this.tileGroup.y;
          tileMask = game.add.graphics(this.tileGroup.x, this.tileGroup.y);
          tileMask.beginFill(0xffffff);
          tileMask.drawRect(0, 0, gameOptions.tileSize * gameOptions.fieldSize.cols, gameOptions.tileSize * gameOptions.fieldSize.rows);
          this.tileGroup.mask = tileMask;
          // tile creation
  		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);
			}
		}
          this.removedTiles = [];
	},
	addTile: function(row, col){
          // adding a new tile
		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.picked = false;
          theTile.coordinate = new Phaser.Point(col, row);
          theTile.value = Phaser.ArrayUtils.getRandomItem(gameOptions.colors);
          theTile.tint = theTile.value;
          this.tilesArray[row][col] = theTile;
          var text = game.add.text(-gameOptions.tileSize / 4, 0, "R" + theTile.coordinate.y.toString() + ", C" + theTile.coordinate.x.toString(), {fill: "#000", font:"bold 24px Arial"});
          theTile.addChild(text);
	     this.tileGroup.add(theTile);	
	},
     pickTile: function(e){
          // picking the first tile
          this.visitedTiles = [];
          this.visitedTiles.length = 0;
          if(this.tileGroup.getBounds().contains(e.position.x, e.position.y)){
               var col = Math.floor((e.position.x - this.tileGroup.x) / gameOptions.tileSize);
               var row = Math.floor((e.position.y - this.tileGroup.y) / gameOptions.tileSize);
               this.tilesArray[row][col].alpha = 0.5;
               this.tilesArray[row][col].picked = true;
               this.pickedColor = this.tilesArray[row][col].value
               game.input.onDown.remove(this.pickTile, this);
	     	game.input.onUp.add(this.releaseTile, this);
	     	game.input.addMoveCallback(this.moveTile, this);
               this.visitedTiles.push(this.tilesArray[row][col].coordinate);
               console.log("Picked tile at R" + row + ", C" + col);
          }  
     },
     moveTile: function(e){
          // we are over a tile
          if(this.tileGroup.getBounds().contains(e.position.x, e.position.y)){
               var col = Math.floor((e.position.x - this.tileGroup.x) / gameOptions.tileSize);
               var row = Math.floor((e.position.y - this.tileGroup.y) / gameOptions.tileSize);
               var distance = new Phaser.Point(e.position.x - this.tileGroup.x, e.position.y - this.tileGroup.y).distance(this.tilesArray[row][col]);
               // we are inside enough a tile
               if(distance < gameOptions.tileSize * 0.4 && this.tilesArray[row][col].value == this.pickedColor){
                    // a new, adjacent tile
                    if(!this.tilesArray[row][col].picked && this.checkAdjacent(new Phaser.Point(col, row), this.visitedTiles[this.visitedTiles.length - 1])){
                         this.tilesArray[row][col].picked = true;
                         this.tilesArray[row][col].alpha = 0.5;
                         this.visitedTiles.push(this.tilesArray[row][col].coordinate);
                         this.addArrow();
                         console.log("Adding tile at R" + row + ", C" + col);
                    }
                    // backtrack
                    else{
                         if(this.visitedTiles.length > 1 && row == this.visitedTiles[this.visitedTiles.length - 2].y && col == this.visitedTiles[this.visitedTiles.length - 2].x){
                              this.tilesArray[this.visitedTiles[this.visitedTiles.length - 1].y][this.visitedTiles[this.visitedTiles.length - 1].x].picked = false;
                              this.tilesArray[this.visitedTiles[this.visitedTiles.length - 1].y][this.visitedTiles[this.visitedTiles.length - 1].x].alpha = 1;
                              this.visitedTiles.pop();
                              this.arrowsArray[this.arrowsArray.length - 1].destroy();
                              this.arrowsArray.pop();
                              console.log("Back to tile at R" + row + ",C" + col);
                         }
                    }
               }
          }
     },
     releaseTile: function(){
          game.input.onUp.remove(this.releaseTile, this);
		game.input.deleteMoveCallback(this.moveTile, this);  
          // clear the path
          this.clearPath();
          // make tiles fall down
          this.tilesFallDown();
          // create new tiles
          this.placeNewTiles();
          console.log("----------------------------------------------------------"); 
     },
     checkAdjacent: function(p1, p2){
          if(gameOptions.diagonal){
               return (Math.abs(p1.x - p2.x) <= 1) && (Math.abs(p1.y - p2.y) <= 1);
          }
          else{
               return (Math.abs(p1.x - p2.x) == 1 && p1.y - p2.y == 0) || (Math.abs(p1.y - p2.y) == 1 && p1.x - p2.x == 0);   
          }
     },
     addArrow: function(){   
          // adding the arrows
          var fromTile = this.visitedTiles[this.visitedTiles.length - 2];
          var arrow = game.add.sprite(this.tilesArray[fromTile.y][fromTile.x].x, this.tilesArray[fromTile.y][fromTile.x].y, "arrows");
          this.arrowsGroup.add(arrow);
          arrow.anchor.set(0.5);
          // this routine handles arrow frame and angle according to its direction
          var tileDiff = new Phaser.Point(this.visitedTiles[this.visitedTiles.length - 1].x, this.visitedTiles[this.visitedTiles.length - 1].y)       
          tileDiff.subtract(this.visitedTiles[this.visitedTiles.length - 2].x, this.visitedTiles[this.visitedTiles.length - 2].y);          
          if(tileDiff.x == 0){
               arrow.angle = -90 * tileDiff.y;     
          }
          else{
               arrow.angle = 90 * (tileDiff.x + 1); 
               if(tileDiff.y != 0){
                    arrow.frame = 1;
                    if(tileDiff.y + tileDiff.x == 0){
                         arrow.angle -= 90;
                    }
               }   
          } 
          this.arrowsArray.push(arrow);        
     },
     clearPath: function(){
          this.arrowsGroup.removeAll(true);
          for(var i = 0; i < this.visitedTiles.length; i++){
               console.log("Removed tile R" + this.visitedTiles[i].y + ", C" + this.visitedTiles[i].x);
			this.tilesArray[this.visitedTiles[i].y][this.visitedTiles[i].x].visible = false;
               this.removedTiles.push(this.tilesArray[this.visitedTiles[i].y][this.visitedTiles[i].x]);
               this.tilesArray[this.visitedTiles[i].y][this.visitedTiles[i].x] = null;
               console.log("Removed tilesArray entry [" + this.visitedTiles[i].y + "][" + this.visitedTiles[i].x + "]");
		}
     },
     tilesFallDown: function(){
          for(var i = gameOptions.fieldSize.cols - 1; i >= 0; i--){
			for(var j = 0; j < gameOptions.fieldSize.rows; j++){
				if(this.tilesArray[i][j] != null){
                         var holes = this.holesBelow(i, j);
                         if(holes > 0){
                              var coordinate = new Phaser.Point(this.tilesArray[i][j].coordinate.x, this.tilesArray[i][j].coordinate.y);
                              var destination = new Phaser.Point(j, i + holes);
                              console.log("Tile at R" + coordinate.y + ", C" + coordinate.x + " moves to R" + destination.y + ", C" + destination.x)
                              var tween = game.add.tween(this.tilesArray[i][j]).to({
                                   y: this.tilesArray[i][j].y + holes * gameOptions.tileSize 
                              }, gameOptions.fallSpeed, Phaser.Easing.Linear.None, true);
                              tween.onComplete.add(this.nextPick, this)
                              this.tilesArray[destination.y][destination.x] = this.tilesArray[i][j]
                              console.log("Replenished tilesArray entry [" + destination.y + "][" + destination.x + "]");
                              this.tilesArray[coordinate.y][coordinate.x] = null;
                              console.log("Removed tilesArray entry [" + coordinate.y + "][" + coordinate.x + "]");
                              this.tilesArray[destination.y][destination.x].coordinate = new Phaser.Point(destination.x, destination.y)
                              this.tilesArray[destination.y][destination.x].children[0].text = "R" + destination.y + ", C" + destination.x; 
                         }
                    }
			}
		}
     },
     placeNewTiles: function(){
          for(var i = 0; i < gameOptions.fieldSize.cols; i++){
               var holes = this.holesInCol(i);
               if(holes > 0){
                    for(var j = 1; j <= holes; j++){
                         var tileXPos = i * gameOptions.tileSize + gameOptions.tileSize / 2;
               		var tileYPos = -j * gameOptions.tileSize + gameOptions.tileSize / 2;
                         var theTile = this.removedTiles.pop();
                         theTile.position = new Phaser.Point(tileXPos, tileYPos);
                         theTile.visible = true;
                         theTile.alpha = 1;
                         theTile.picked = false;
                         theTile.value = Phaser.ArrayUtils.getRandomItem(gameOptions.colors);
                         theTile.tint = theTile.value;
                         var tween = game.add.tween(theTile).to({
                              y: theTile.y + holes * gameOptions.tileSize
                         }, gameOptions.fallSpeed, Phaser.Easing.Linear.None, true)
                         tween.onComplete.add(this.nextPick, this)
                         theTile.coordinate = new Phaser.Point(i, holes - j);
                         this.tilesArray[holes - j][i] = theTile;
                         theTile.children[0].text = "R" + theTile.coordinate.y + ", C" + theTile.coordinate.x; 
                         console.log("Created a new tile at R" + theTile.coordinate.y.toString() + ", C" + theTile.coordinate.x.toString());
                         console.log("Added tilesArray entry [" + (holes - j).toString() + "][" + i + "]");
                    }
               }
          } 
     },
     nextPick: function(){
          if(!game.input.onDown.has(this.pickTile, this)){
               game.input.onDown.add(this.pickTile, this);
               console.log("added");                         
          }
     },
     holesBelow: function(row, col){
          var result = 0;
          for(var i = row + 1; i < gameOptions.fieldSize.rows; i++){
               if(this.tilesArray[i][col] == null){
                    result ++;          
               }
          }
          return result;
     },
     holesInCol: function(col){
          var result = 0;
          for(var i = 0; i < gameOptions.fieldSize.rows; i++){
               if(this.tilesArray[i][col] == null){
                    result ++;          
               }
          }
          return result;     
     }
}

The engine is getting more and more complete as I publish new posts, but you won’t guess what iOS blockbuster I am going to create just changing a few lines. You’ll see it next week, meanwhile download the source code.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.