Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about Drag and Match game, Game development, HTML5, Javascript and Phaser.

Here we go with another important feature to add to our Drag and Match engine update to Phaser 2.6.2 Today’s feature checks for valid matches after the player ends a move, performing an “undo” if the move did not make any match. In a Drag and Match engine, a valid match is made by three or more colors in a row, horizontally or vertically. I borrowed some functions from my Bejeweled prototype to check for valid matches because there’s no need to reinvent the wheel. Here we go with the game: there aren’t three-in-a-row matches and that’s not due to the randomness…
Now try to make a move, dragging with the finger or with the mouse, which does not form a valid match, and you’ll see the tiles come back to their original places with a blink effect, made using a yoyo tween. If you have a mobile device, you can play it directly from this link. Have a look at the commented source code, which is growing bigger and bigger:
// the game itself
var game;

// global game options
var gameOptions = {

     // width of the game, in pixels
     gameWidth: 400,
     
     // height of the game, in pixels
     gameHeight: 400,

     // size of the sprite sheet, in pixels
     spritesheetSize: 50, 
     
     // size of each tile, in pixels
     tileSize: 50,
     
     // size of the field, in tiles
     fieldSize: 6,
     
     // different tile types
     tileTypes: 6,
     
     // distance from the left of the screen to the left of the board, in pixels 
     offsetX: 50,
     
     // distance from the top of the screen to the top of the board, in pixels 
     offsetY: 50,
     
     // duration of the tween to adjust tiles, in milliseconds
     tweenSpeed: 100
}

// some constants to be used in the game

// I am not dragging
var NO_DRAG = 0;

// I am dragging horizontally
var HORIZONTAL_DRAG = 1;

// I am dragging vertically
var VERTICAL_DRAG = 2;

// The game state is "doing nothing"
var GAME_STATE_IDLE = 0;

// When the player is dragging a row/column
var GAME_STATE_DRAG = 1;

// When the player stops dragging
var GAME_STATE_STOP = 2;

// when the window has been fully loaded
window.onload = function() {	        

     // creation of a Game instance
	game = new Phaser.Game(gameOptions.gameWidth, gameOptions.gameHeight);
     
     // adding "PlayGame" state
	game.state.add("PlayGame", playGame)
     
     // starting "PlayGame" state
	game.state.start("PlayGame");
}

var playGame = function(game){}
playGame.prototype = {

     // when the state preloads
	preload: function(){
     
          // loading the spritesheet with all tile images
          game.load.spritesheet("tiles", "tiles.png", gameOptions.spritesheetSize, gameOptions.spritesheetSize);
          
          // setting the game on maximum scale mode to cover the entire screen
          game.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
          game.scale.pageAlignHorizontally = true;
          game.scale.pageAlignVertically = true; 
	},
     
      // once the state has been created
	create: function(){
     
          // tileArray is the array which will contain all tiles
          this.tileArray = [];
          
          // creation of the group which will contain all tiles
          this.tileGroup = game.add.group();
          
          // adjusting group position according to offset
          this.tileGroup.x = gameOptions.offsetX;
          this.tileGroup.y = gameOptions.offsetY;
          
          // creation of a mask with the same size of the board to be placed in the same position of the group
          // this way we are hiding everything is outside the board  
          this.tileMask = game.add.graphics(this.tileGroup.x, this.tileGroup.y);
          this.tileMask.beginFill(0xffffff);
          this.tileMask.drawRect(0, 0, gameOptions.fieldSize * gameOptions.tileSize, gameOptions.fieldSize * gameOptions.tileSize);
          this.tileGroup.mask = this.tileMask;
          this.tileMask.visible = true;
          
          // filling the board with tiles thanks to "addTile" method
          for(var i = 0; i < gameOptions.fieldSize; i++){
			this.tileArray[i] = [];
			for(j = 0; j < gameOptions.fieldSize; j++){
				this.addTile(i, j);
			}
		}
          
          // adding the temporary tile thanks to addTempTile method
		this.addTempTile();
          
          // waiting for player input to call pickTile method
		game.input.onDown.add(this.pickTile, this);
          
          // the game has just been created, so we are doing nothing
          this.gameState = GAME_STATE_IDLE;
	},
     
     // function to add a tile at a given row and column
     addTile: function(row, col){
          
          // creation of the sprite in the proper position
		var theTile = game.add.sprite(col * gameOptions.tileSize, row * gameOptions.tileSize, "tiles");
		
          // setting tile width and height
          theTile.width = gameOptions.tileSize;
          theTile.height = gameOptions.tileSize;
          
          do{
               // choosing a random tile
               var randomTile = game.rnd.integerInRange(0, gameOptions.tileTypes - 1);
               
               // saving the value inside a custom property
     		theTile.tileValue = randomTile;
               
               // inserting the tile in tileArray array
     		this.tileArray[row][col] = theTile;
          }  while (this.isMatch(row, col));
          
          // showing the frame according to tile value
          theTile.frame = randomTile;
          
          // adding the sprite to tileGroup group
          this.tileGroup.add(theTile);	
     },
     
     // function to add the temporary tile
     addTempTile: function(){
     
          // creation of the sprite, no matter the position, we won't show it at the moment
          this.tempTile = game.add.sprite(0, 0, "tiles");
          
          // setting its width and height
          this.tempTile.width = gameOptions.tileSize;
          this.tempTile.height = gameOptions.tileSize;
          
          // setting the sprite to non visible
		this.tempTile.visible = false;
          
          // adding the sprite to tileGroup group
          this.tileGroup.add(this.tempTile);	        
     },
     
     // function to triggered when the player touches/clicks on the canvas
     pickTile: function(e){
          
          // determining row and column according to input position, tile size and offset
          this.movingRow = Math.floor((e.position.y - gameOptions.offsetY) / gameOptions.tileSize);
          this.movingCol = Math.floor((e.position.x - gameOptions.offsetX) / gameOptions.tileSize);
          
          // if row and column are actually inside game field...
          if(this.movingRow >= 0 && this.movingCol >= 0 && this.movingRow < gameOptions.fieldSize && this.movingCol < gameOptions.fieldSize){
               
               // at the moment we aren't dragging
               this.dragDirection = NO_DRAG;
               
               // removing the listener which waits for the input to begin
               game.input.onDown.remove(this.pickTile, this);
               
               // adding a listener which waits for the input to end then call releaseTile method
               game.input.onUp.add(this.releaseTile, this);
               
               // adding a listener which waits for the input to move then call moveTile method
               game.input.addMoveCallback(this.moveTile, this);
          }
     },
     
     // function to be executed at each frame
     update:function(){
     
          // checking game state to see what to do
          switch(this.gameState){
          
               // we are dragging
               case GAME_STATE_DRAG:
               
                    // call handleDrag method
                    this.handleDrag();
               break;
               
               // we just stopped dragging
               case GAME_STATE_STOP:
               
                    // call handleStop method
                    this.handleStop();                    
               break;
          }
          
          // at the end of the function, we set gameState again to idle
          this.gameState = GAME_STATE_IDLE;     
     
          
     },
     
     // function to handle - and draw on the canvas - the game when the player drags a row/column
     handleDrag:function(){
          
          // two different things to do according to drag direction
          switch(this.dragDirection){
               
               // horizontal drag
               case HORIZONTAL_DRAG:
               
                    // hiding temporary tile
     			this.tempTile.visible = false;
                    
                    // placing the temporary tile in the proper row
     			this.tempTile.y = this.movingRow * gameOptions.tileSize;
                    
                    // deltaX is the amount of tiles we are moving
     			var deltaX = (Math.floor(this.distX / gameOptions.tileSize) % gameOptions.fieldSize);
                    
     			// deltaX >= 0 means we are moving to the right (or not moving)
                    if (deltaX >= 0) {		
                    
                         // temporary tile frame is now the same as the rightmost visible tile							
     				this.tempTile.frame = this.tileArray[this.movingRow][gameOptions.fieldSize - 1 - deltaX].tileValue;
     			}
                    
                    // we are moving to the left
     			else{
                    
                         // temporary tile frame is now the same as the leftmost visible tile
     				deltaX = deltaX * -1 - 1;
     				this.tempTile.frame = this.tileArray[this.movingRow][deltaX].tileValue;		
     			}
                    
                    // looping through all the moving row
     			for(var i = 0; i < gameOptions.fieldSize; i++){
                    
                         // adjusting each tile horizontal position
     				this.tileArray[this.movingRow][i].x = (i * gameOptions.tileSize + this.distX) % (gameOptions.tileSize * gameOptions.fieldSize);
     				
                         // if tile position is less than zero...
                         if (this.tileArray[this.movingRow][i].x < 0) {
                         
                              // ... place it on the opposite side of the game field
        					this.tileArray[this.movingRow][i].x += gameOptions.tileSize * gameOptions.fieldSize;
     				}
     			}
                    // tileX is the amount of pixels we are moving, capped to gameOptions.tileSize
                      var tileX = this.distX % gameOptions.tileSize;
                    
                    // if the amount is greater than zero (moving to the right)
                    if(tileX > 0){
                         
                         // placing temporary tile before the leftmost tile
                         this.tempTile.x = tileX - gameOptions.tileSize;
                         
                         // showing temportary tile
                    	this.tempTile.visible = true;
                    }
                    
                    // if the amount is less than zero (moving to the left)
                    if(tileX < 0){
                         
                         // placing temporary tile before the leftmost tile
                         this.tempTile.x = tileX;      
                         
                         // showing temportary tile
                    	this.tempTile.visible = true;
                    } 
     		break;
               
               // vertical drag, same concept seen in horizontal drag, just applied to Y axis
               case VERTICAL_DRAG:
     			this.tempTile.visible = false;
     			this.tempTile.x = this.movingCol * gameOptions.tileSize;
     			var deltaY = (Math.floor(this.distY / gameOptions.tileSize) % gameOptions.fieldSize);
     			if (deltaY >= 0) {									
     				this.tempTile.frame = this.tileArray[gameOptions.fieldSize - 1 - deltaY][this.movingCol].tileValue;
     			}
     			else{
     				deltaY = deltaY * -1 - 1;
     				this.tempTile.frame = this.tileArray[deltaY][this.movingCol].tileValue;		
     			}
     			for(var i = 0; i < gameOptions.fieldSize; i++){
     				this.tileArray[i][this.movingCol].y = (i * gameOptions.tileSize + this.distY) % (gameOptions.tileSize * gameOptions.fieldSize);
     				if (this.tileArray[i][this.movingCol].y < 0) {
        					this.tileArray[i][this.movingCol].y += gameOptions.tileSize * gameOptions.fieldSize;
     				}  
     			}
                    var tileY = this.distY % gameOptions.tileSize;
                    if(tileY > 0){
                         this.tempTile.y = tileY - gameOptions.tileSize;
                         this.tempTile.visible = true;
                    }
                    if(tileY < 0){
                         this.tempTile.y = tileY;      
                         this.tempTile.visible = true;
                    } 
     		break;     
          }
     },
     
     // function to handle - and draw on the canvas - the game when the player stops dragging a row/column
     handleStop:function(){
     
          // two different things to do according to drag direction
          switch(this.dragDirection){
          
               // horizontal drag
     		case HORIZONTAL_DRAG:
               
                    // we have to find how many "half tiles" we dragged
     			var shiftAmount = Math.floor(this.distX / (gameOptions.tileSize / 2));
                    
                    // and now let's see how many tiles we dragged, with a modulo operation because the max amount is fieldSize - 1
     			shiftAmount = Math.ceil(shiftAmount / 2) % gameOptions.fieldSize;
                    
                    // creation of a temporary array
     			var tempArray = [];
                    
                    // now the idea is to insert in tempArray array the tileArray items in the order they are at the end of the drag
                    // when shiftAmount is greater than 0, we dragged to the right
     			if(shiftAmount > 0){
     				for(var i = 0; i < gameOptions.fieldSize; i++){
     					tempArray[(shiftAmount + i) % gameOptions.fieldSize] = this.tileArray[this.movingRow][i].tileValue;
     				}
     			}
                    
                    // when shiftAmount is less than zero, we dragged to the left 
     			else{
     				for(var i = 0; i < gameOptions.fieldSize; i++){
     					tempArray[i] = this.tileArray[this.movingRow][(Math.abs(shiftAmount) + i) % gameOptions.fieldSize].tileValue;
     				}
     			}
                    
                    // the offset is the amount of pixels we dragged, with tileSize as maximum
                    var offset = this.distX % gameOptions.tileSize;
                    
                    // if we dragged for more than half a tile...
                    if(Math.abs(offset) > gameOptions.tileSize / 2){
                    
                         // adjusting the offset according we dragged to the left (less than zero) or to the right
                         if(offset < 0){
                              offset = offset + gameOptions.tileSize;
                         }
                         else{
                              offset = offset - gameOptions.tileSize;
                         }
                    }
                    
                    // copying content from tempArray to tileArray and adjust tile position
     			for(i = 0; i < gameOptions.fieldSize; i++){
     				this.tileArray[this.movingRow][i].tileValue = tempArray[i];
     				this.tileArray[this.movingRow][i].frame = tempArray[i];
     				this.tileArray[this.movingRow][i].x = i * gameOptions.tileSize + offset;
                         
                         // tween to adjust tile position
                         game.add.tween(this.tileArray[this.movingRow][i]).to({
                              x: i * gameOptions.tileSize
                         }, gameOptions.tweenSpeed, Phaser.Easing.Cubic.Out, true);
     			}
                    
                    // tempdestination is the destination of the temporary tile, outside to the left
                    var tempDestination = -gameOptions.tileSize
                    
                    // if the offset is less than zero, then move temporary tile on the opposite side of the board
                    if(offset < 0){
                         this.tempTile.x += gameOptions.tileSize * gameOptions.fieldSize;
                         
                         // also set temporary tile destinatiokn outside to the right
                         tempDestination = gameOptions.fieldSize * gameOptions.tileSize;              
                    }
                    
                    // then adjusting temporary tile position with a tween
                    var tween = game.add.tween(this.tempTile).to({
                         x: tempDestination 
                    }, gameOptions.tweenSpeed, Phaser.Easing.Cubic.Out, true);
                    
                    // we add the listener for a new input only when the tween is completed
                    tween.onComplete.add(function(){ 
                         
                         // now we have to check if there is NO match in the board but we actually moved. If not, we have to move back tiles
                         // to their original position
                         if(!this.matchInBoard() && shiftAmount != 0){
                         
                              // remember shiftAmount? it was the amount of tiles we shifted. Let's reverse it.
                              shiftAmount *= -1;
                              
                              // our old temporary array now starts again as an empty array
                              tempArray = [];
                              
                              // filling again tempArray with the new (actually old) tiles positions
                              if(shiftAmount > 0){
               				for(var i = 0; i < gameOptions.fieldSize; i++){
               					tempArray[(shiftAmount + i) % gameOptions.fieldSize] = this.tileArray[this.movingRow][i].tileValue;
               				}
               			}
               			else{
               				for(var i = 0; i < gameOptions.fieldSize; i++){
               					tempArray[i] = this.tileArray[this.movingRow][(Math.abs(shiftAmount) + i) % gameOptions.fieldSize].tileValue;
               				}
               			}
                              
                              // updating again tileArray array
                              for(i = 0; i < gameOptions.fieldSize; i++){
               				this.tileArray[this.movingRow][i].tileValue = tempArray[i];
               				this.tileArray[this.movingRow][i].frame = tempArray[i];
               				this.tileArray[this.movingRow][i].x = i * gameOptions.tileSize;
                                   
                                   // this time the tween will be a quick blink
                                   game.add.tween(this.tileArray[this.movingRow][i]).to({
                                        alpha: 0.5
                                   }, gameOptions.tweenSpeed / 8, Phaser.Easing.Bounce.Out, true, 0, 8, true);
               			}                      
                         }
                         
                         game.input.onDown.add(this.pickTile, this);
                        
                    }, this)
     		break;
               
               // vertical drag follows the same concepts seen in horizontal drag
     		case VERTICAL_DRAG:
     			var shiftAmount = Math.floor(this.distY / (gameOptions.tileSize / 2));
				shiftAmount = Math.ceil(shiftAmount / 2) % gameOptions.fieldSize;
				var tempArray = [];
				if(shiftAmount > 0){
					for(var i = 0; i < gameOptions.fieldSize; i++){
						tempArray[(shiftAmount + i) % gameOptions.fieldSize] = this.tileArray[i][this.movingCol].tileValue;
					}
				}
				else{
					for(var i = 0; i < gameOptions.fieldSize; i++){
						tempArray[i] = this.tileArray[(Math.abs(shiftAmount) + i) % gameOptions.fieldSize][this.movingCol].tileValue;
					}
				}
                    var offset = this.distY % gameOptions.tileSize;
                    if(Math.abs(offset) > gameOptions.tileSize / 2){
                         if(offset < 0){
                              offset = offset + gameOptions.tileSize;
                         }
                         else{
                              offset = offset - gameOptions.tileSize;
                         }
                    }
				for(var i = 0; i < gameOptions.fieldSize; i++){
					this.tileArray[i][this.movingCol].tileValue = tempArray[i];
					this.tileArray[i][this.movingCol].frame = tempArray[i];
					this.tileArray[i][this.movingCol].y = i * gameOptions.tileSize + offset;
                         game.add.tween(this.tileArray[i][this.movingCol]).to({
                              y: i * gameOptions.tileSize
                         }, gameOptions.tweenSpeed, Phaser.Easing.Cubic.Out, true);
                    }
                    var tempDestination = -gameOptions.tileSize
                    if(offset < 0){
                         this.tempTile.y += gameOptions.tileSize * gameOptions.fieldSize;
                         tempDestination = gameOptions.fieldSize * gameOptions.tileSize;              
                    }              
                    var tween = game.add.tween(this.tempTile).to({
                         y: tempDestination     
                    }, gameOptions.tweenSpeed, Phaser.Easing.Cubic.Out, true);
                    tween.onComplete.add(function(){ 
                         if(!this.matchInBoard() && shiftAmount != 0){
                              shiftAmount *= -1;
                              tempArray = [];
                              if(shiftAmount > 0){
          					for(var i = 0; i < gameOptions.fieldSize; i++){
          						tempArray[(shiftAmount + i) % gameOptions.fieldSize] = this.tileArray[i][this.movingCol].tileValue;
          					}
          				}
          				else{
          					for(var i = 0; i < gameOptions.fieldSize; i++){
          						tempArray[i] = this.tileArray[(Math.abs(shiftAmount) + i) % gameOptions.fieldSize][this.movingCol].tileValue;
          					}
          				}
                              for(var i = 0; i < gameOptions.fieldSize; i++){
          					this.tileArray[i][this.movingCol].tileValue = tempArray[i];
          					this.tileArray[i][this.movingCol].frame = tempArray[i];
          					this.tileArray[i][this.movingCol].y = i * gameOptions.tileSize;
                                   game.add.tween(this.tileArray[i][this.movingCol]).to({
                                        alpha: 0.5
                                   }, gameOptions.tweenSpeed / 8, Phaser.Easing.Bounce.Out, true, 0, 8, true);
                              }
                         }
                         game.input.onDown.add(this.pickTile, this);
                    }, this)
     		break;	
     	}
          
          // we aren't dragging anymore
     	this.dragDirection = NO_DRAG;
     },
     
     // function to be triggered when the player moves the mouse/finger
     moveTile: function(e){
     
          // we are dragging
          this.gameState = GAME_STATE_DRAG;
     
          // determining horizontal and vertical distance between start and current input position
          this.distX = e.position.x - e.positionDown.x;
          this.distY = e.position.y - e.positionDown.y;
          
          // if we aren't dragging yet...
          if(this.dragDirection == NO_DRAG){
                         
               // how many pixels are we travelling with our finger/mouse?
     		var distance = e.position.distance(e.positionDown);
               
               // more than 5 pixels? ok, that's enough to determine the direction
     		if(distance > 5) {
               
                    // trigonometry to know drag angle
     			var dragAngle = Math.abs(Math.atan2(this.distY, this.distX));
                    
                    // if drag angle is between PI/4 and 3PI/4...
     			if((dragAngle > Math.PI / 4 && dragAngle < 3 * Math.PI / 4)) {
     				
                         // ... we can say it's a vertical drag
                         this.dragDirection = VERTICAL_DRAG;
     			}
     			else {
                    
                         // else it's an horizontal drag
     				this.dragDirection = HORIZONTAL_DRAG;
     			}
     		}
          }             
     },
     
     // function to be triggered when the player releases the mouse/finger
     releaseTile: function(){
     
          // we stopped dragging
          this.gameState = GAME_STATE_STOP;
          
          // removing listeners
          game.input.onUp.remove(this.releaseTile, this);
          game.input.deleteMoveCallback(this.moveTile, this); 
     },
     
     tileAt: function(row, col){
          if(row < 0 || row >= gameOptions.fieldSize || col < 0 || col >= gameOptions.fieldSize){
               return false;
          }
          return this.tileArray[row][col];
     },
     
     isHorizontalMatch: function(row, col){
          return this.tileAt(row, col).tileValue == this.tileAt(row, col - 1).tileValue && this.tileAt(row, col).tileValue == this.tileAt(row, col - 2).tileValue; 
     },
     
     isVerticalMatch: function(row, col){
          return this.tileAt(row, col).tileValue == this.tileAt(row - 1, col).tileValue && this.tileAt(row, col).tileValue == this.tileAt(row - 2, col).tileValue; 
     },
     
     isMatch: function(row, col){
          return this.isHorizontalMatch(row, col) || this.isVerticalMatch(row, col);
     },
     
     matchInBoard: function(){
          for(var i = 0; i < gameOptions.fieldSize; i++){
               for(var j = 0; j < gameOptions.fieldSize; j++){
                    if(this.isMatch(i, j)){
                         return true;
                    }
               }
          }
          return false;
     }
     
     
}
We are only a few steps away from having a complete working fully functional prototype, but you can download the source code and continue on your own. It would be great to see your results.

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