Complete HTML5 SameGame game for you to play and download, featuring “no more moves” check

Talking about SameGame game, Game development, HTML5, Javascript and Phaser.

Here we are with the final step of SameGame series, a complete playable SameGame with high scores and a “no more moves” check. The game features four colors and a 25×10 grid like in the original game, and the score is based on n * (n - 2) formula where n is the number of removed tiles. Your best score is saved in local storage, have a go with the game:
To check if there are no more moves, after each player move I just loop through the game field performing flood fill on each tile. When I find a flood fill with two or more tiles, I break the loop and allow the player to continue. Here is the source code:
var game;

var gameOptions = {
	gameWidth: 2200,
	gameHeight: 1400,
	tileSize: 100,
	fieldSize: {
          rows: 10,
          cols: 20
     },
     colors: [0xdb0a5b, 0x19b5fe, 0x00b16a, 0xf7ca18],
     localStorageName: "samegame"
}

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.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
     	game.scale.pageAlignHorizontally = true;
     	game.scale.pageAlignVertically = true;
          game.stage.disableVisibilityChange = true;
          game.load.image("tiles", "assets/sprites/tile.png");
          game.load.bitmapFont("font", "assets/fonts/font.png", "assets/fonts/font.fnt"); 
     },
  	create: function(){          
          this.score = 0;
          this.savedData = localStorage.getItem(gameOptions.localStorageName)==null?{score:0}:JSON.parse(localStorage.getItem(gameOptions.localStorageName));      
          this.scoreText = game.add.bitmapText(10, 10, "font", "", 72);
          var bestScoreText = game.add.bitmapText(game.width - 10, 10, "font", "Best score: " + this.savedData.score.toString(), 72);
          this.gameText = game.add.bitmapText(game.width / 2, game.height - 50, "font", "SAMEGAME", 90)
          this.gameText.anchor.set(0.5, 1);
          bestScoreText.anchor.set(1, 0);
          this.updateScore();
  		this.createLevel();
  	}, 
	createLevel: function(){
          this.canPick = true;   
          this.tilesArray = [];          
		this.tileGroup = game.add.group();
          var background = game.add.tileSprite(0, 0, gameOptions.tileSize * gameOptions.fieldSize.cols, gameOptions.tileSize * gameOptions.fieldSize.rows, "tiles"); 
          background.alpha = 0.2;
          this.tileGroup.add(background);
          this.tileGroup.x = (game.width - gameOptions.tileSize * gameOptions.fieldSize.cols) / 2;
          this.tileGroup.y = (game.height -  gameOptions.tileSize * gameOptions.fieldSize.rows) / 2;
  		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);
			}
		}
	},
	addTile: function(row, col){
		var tileXPos = col * gameOptions.tileSize + gameOptions.tileSize / 2;
		var tileYPos = row * gameOptions.tileSize + gameOptions.tileSize / 2;
          var theTile = game.add.button(tileXPos, tileYPos, "tiles", this.pickTile, this);
          theTile.anchor.set(0.5);
          theTile.width = gameOptions.tileSize;
          theTile.height = gameOptions.tileSize;
          theTile.value = game.rnd.integerInRange(0, gameOptions.colors.length - 1);
          theTile.tint = gameOptions.colors[theTile.value];
          theTile.coordinate = new Phaser.Point(col, row);
          this.tilesArray[row][col] = theTile;
	     this.tileGroup.add(theTile);	
	},
     pickTile: function(e){
          if(this.canPick){
               this.filled = [];
               this.filled.length = 0;
               this.floodFill(e.coordinate, e.value);
               if(this.filled.length > 1){
                    this.score += this.filled.length * (this.filled.length - 2);
                    this.updateScore();
                    this.canPick = false;
                    this.destroyTiles();
               }
          }
     },
     destroyTiles: function(){      
          for(var i = 0; i < this.filled.length; i++){
               var tween = game.add.tween(this.tilesArray[this.filled[i].y][this.filled[i].x]).to({
                    alpha: 0
               }, 150, Phaser.Easing.Linear.None, true);
               tween.onComplete.add(function(e){ 
                    e.destroy();  
                    if(tween.manager.getAll().length == 1){
                         this.fillVerticalHoles();     
                    }                                
               }, this);
               this.tilesArray[this.filled[i].y][this.filled[i].x] = null;
          }
     },
     fillVerticalHoles: function(){
          var filled = false;
          for(var i = gameOptions.fieldSize.rows - 2; i >= 0; i--){
               for(var j = 0; j < gameOptions.fieldSize.cols; j++){
                    if(this.tilesArray[i][j] != null){
                         var holesBelow = 0;
                         for(var z = i + 1; z < gameOptions.fieldSize.rows; z++){
                              if(this.tilesArray[z][j] == null){
                                   holesBelow ++;
                              }    
                         }
                         if(holesBelow){  
                              filled = true;
                              this.moveDownTile(i, j, i + holesBelow);                                                                   
                         }
                    }     
               }
          }
          if(!filled){
               this.fillHorizontalHoles();      
          }
     },
     moveDownTile: function(fromRow, fromCol, toRow){
          this.tilesArray[toRow][fromCol] = this.tilesArray[fromRow][fromCol];
          this.tilesArray[toRow][fromCol].coordinate = new Phaser.Point(fromCol, toRow);
          var tween = game.add.tween(this.tilesArray[toRow][fromCol]).to({
               y: toRow * gameOptions.tileSize + gameOptions.tileSize / 2         
          }, 125, Phaser.Easing.Linear.None, true);
          tween.onComplete.add(function(){
               if(tween.manager.getAll().length == 1){
                    this.fillHorizontalHoles();
               }
          }, this)
          this.tilesArray[fromRow][fromCol] = null;
     },
     fillHorizontalHoles: function(){
          var filled = false
          for(i = 0; i < gameOptions.fieldSize.cols - 1; i++){
               if(this.tilesInColumn(i) == 0){
                    for(j = i + 1; j < gameOptions.fieldSize.cols; j++){
                         if(this.tilesInColumn(j) != 0){
                              for(z = 0; z < gameOptions.fieldSize.rows; z++){
                                   if(this.tilesArray[z][j] != null){
                                        filled = true;
                                        this.moveLeftTile(z, j, i)
                                   }    
                              }
                              break;
                         }     
                    }
               }
          }
          if(!filled){
               this.canPick = true;
               this.checkForMoves();     
          }
     },
     moveLeftTile: function(fromRow, fromCol, toCol){
          this.tilesArray[fromRow][toCol] = this.tilesArray[fromRow][fromCol];
          this.tilesArray[fromRow][toCol].coordinate = new Phaser.Point(toCol, fromRow);
          var tween = game.add.tween(this.tilesArray[fromRow][toCol]).to({
               x: toCol * gameOptions.tileSize + gameOptions.tileSize / 2  
          }, 250, Phaser.Easing.Bounce.Out, true);
          tween.onComplete.add(function(){
               if(tween.manager.getAll().length == 1){
                    this.canPick = true;  
                    this.checkForMoves();     
               }
          }, this)
          this.tilesArray[fromRow][fromCol] = null;
     },     
     checkForMoves: function(){
          var noMoves = true;
          var boardCleared = true;
          for(var i = 0; i < gameOptions.fieldSize.rows; i++){
			for(var j = 0; j < gameOptions.fieldSize.cols; j++){
                    if(this.tilesArray[i][j] != null){
                         boardCleared = false;
                         this.filled = [];
                         this.filled.length = 0;
                         this.floodFill(this.tilesArray[i][j].coordinate, this.tilesArray[i][j].value);
                         if(this.filled.length > 1){
                              noMoves = false;
                              break;
                         }
                    }
			}
               if(!noMoves){
                    break;
               }
		}
          if(noMoves){
               if(boardCleared){
                    this.gameText.text = "Congratulations!!!";          
               }
               else{
                    this.gameText.text = "No more moves!!!";  
               } 
               var bestScore = Math.max(this.score, this.savedData.score); 
               localStorage.setItem(gameOptions.localStorageName,JSON.stringify({
                    score: bestScore
          	}));
               game.time.events.add(Phaser.Timer.SECOND * 5, function(){
                    game.state.start("TheGame");     
               }, this);   
          }     
     },     
     updateScore: function(){
          this.scoreText.text = "Score: " + this.score.toString();
     },
     tilesInColumn: function(col){
          var result = 0;
          for(var i = 0; i < gameOptions.fieldSize.rows; i++){
               if(this.tilesArray[i][col] != null){ 
                    result ++;
               }
          }
          return result;
     },
     floodFill: function(p, n){
          if(p.x < 0 || p.y < 0 || p.x >= gameOptions.fieldSize.cols || p.y >= gameOptions.fieldSize.rows){
               return;
          }
          if(this.tilesArray[p.y][p.x] != null && this.tilesArray[p.y][p.x].value == n && !this.pointInArray(p)){
               this.filled.push(p);
               this.floodFill(new Phaser.Point(p.x + 1, p.y), n);
               this.floodFill(new Phaser.Point(p.x - 1, p.y), n); 
               this.floodFill(new Phaser.Point(p.x, p.y + 1), n);
               this.floodFill(new Phaser.Point(p.x, p.y - 1), n);       
          }
     },
     pointInArray: function(p){
          for(var i = 0; i < this.filled.length; i++){
               if(this.filled[i].x == p.x && this.filled[i].y == p.y){
                    return true;
               }
          }
          return false;
     }
}
Now you have zero excuses not to develop one of the 10 successful games you can easily create starting from the SameGame engine I blogged about yesterday. Download the source code and start making games.