// 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.
HTML5 Drag and Match engine updated to Phaser 2.6.2 new feature added: checking for valid matches (and undo the move otherwise)
Read all posts about "Drag and Match" game
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: