Talking about 2048 game, Game development, HTML5, Javascript and Phaser.
It’s a long time I do not update my “2048” tutorial series as you can find all kind of information about this game – and much more – in my book “HTML5 Cross Platform Game Development Using Phaser 3“, but some readers asked for a feature which is not included in the book, but which will be included soon: how to determine when there aren’t more moves?
Normally, people realize they got stuck when they aren’t able to scroll the table anymore, but I never built the part of the script which can tell you when there aren’t more moves.
In this example, built with old Phaser 3.2.1 and not updated to the latest version, I add the “no more moves” warning. It shoud be easy for you to port it to newer Phaser versions.
Have a look:
Use WASD keys, ARROW keys or swipe to move tiles. Once there are no more moves, the game will notify it.
In the source code, I highlighted the new lines to handle “no more moves” detection, and this is the concept:
It’s NOT game over when at least one of these three conditions is satisfied:
– There is an empty tile
– There are two adjacent horizontal tiles with the same value
– There are two adjacent vertical tiles with the same value
In all other cases, it’s game over.
Have a look:
var game;
var gameOptions = {
tileSize: 200,
tweenSpeed: 50,
tileSpacing: 20
}
window.onload = function() {
var gameConfig = {
type: Phaser.CANVAS,
width: gameOptions.tileSize * 4 + gameOptions.tileSpacing * 5,
height: gameOptions.tileSize * 4 + gameOptions.tileSpacing * 5,
backgroundColor: 0xecf0f1,
scene: [playGame]
};
game = new Phaser.Game(gameConfig);
window.focus()
resize();
window.addEventListener("resize", resize, false);
}
var playGame = new Phaser.Class({
Extends: Phaser.Scene,
initialize:
function playGame(){
Phaser.Scene.call(this, {key: "PlayGame"});
},
preload: function(){
this.load.image("tile", "tile.png");
this.load.spritesheet("tiles", "assets/sprites/tiles.png", {
frameWidth: gameOptions.tileSize,
frameHeight: gameOptions.tileSize
});
},
create: function(){
this.fieldArray = [];
this.fieldGroup = this.add.group();
for(var i = 0; i < 4; i++){
this.fieldArray[i] = [];
for(var j = 0; j < 4; j++){
var two = this.add.sprite(this.tileDestination(j), this.tileDestination(i), "tiles");
two.alpha = 0;
two.visible = 0;
this.fieldGroup.add(two);
this.fieldArray[i][j] = {
tileValue: 0,
tileSprite: two,
canUpgrade: true
}
}
}
this.input.keyboard.on("keydown", this.handleKey, this);
this.canMove = false;
this.addTwo();
this.addTwo();
this.input.on("pointerup", this.endSwipe, this);
},
endSwipe: function(e){
var swipeTime = e.upTime - e.downTime;
var swipe = new Phaser.Geom.Point(e.upX - e.downX, e.upY - e.downY);
var swipeMagnitude = Phaser.Geom.Point.GetMagnitude(swipe);
var swipeNormal = new Phaser.Geom.Point(swipe.x / swipeMagnitude, swipe.y / swipeMagnitude);
if(swipeMagnitude > 20 && swipeTime < 1000 && (Math.abs(swipeNormal.y) > 0.8 || Math.abs(swipeNormal.x) > 0.8)){
var children = this.fieldGroup.getChildren();
if(swipeNormal.x > 0.8) {
for (var i = 0; i < children.length; i++){
children[i].depth = game.config.width - children[i].x;
}
this.handleMove(0, 1);
}
if(swipeNormal.x < -0.8) {
for (var i = 0; i < children.length; i++){
children[i].depth = children[i].x;
}
this.handleMove(0, -1);
}
if(swipeNormal.y > 0.8) {
for (var i = 0; i < children.length; i++){
children[i].depth = game.config.height - children[i].y;
}
this.handleMove(1, 0);
}
if(swipeNormal.y < -0.8) {
for (var i = 0; i < children.length; i++){
children[i].depth = children[i].y;
}
this.handleMove(-1, 0);
}
}
},
addTwo: function(){
var emptyTiles = [];
for(var i = 0; i < 4; i++){
for(var j = 0; j < 4; j++){
if(this.fieldArray[i][j].tileValue == 0){
emptyTiles.push({
row: i,
col: j
})
}
}
}
var chosenTile = Phaser.Utils.Array.GetRandomElement(emptyTiles);
this.fieldArray[chosenTile.row][chosenTile.col].tileValue = 1;
this.fieldArray[chosenTile.row][chosenTile.col].tileSprite.visible = true;
this.fieldArray[chosenTile.row][chosenTile.col].tileSprite.setFrame(0);
this.tweens.add({
targets: [this.fieldArray[chosenTile.row][chosenTile.col].tileSprite],
alpha: 1,
duration: gameOptions.tweenSpeed,
onComplete: function(tween){
tween.parent.scene.canMove = true;
// when a move is completed, it's time to chek for game over
tween.parent.scene.checkGameOver();
},
});
},
handleKey: function(e){
if(this.canMove){
var children = this.fieldGroup.getChildren();
switch(e.code){
case "KeyA":
case "ArrowLeft":
for (var i = 0; i < children.length; i++){
children[i].depth = children[i].x;
}
this.handleMove(0, -1);
break;
case "KeyD":
case "ArrowRight":
for (var i = 0; i < children.length; i++){
children[i].depth = game.config.width - children[i].x;
}
this.handleMove(0, 1);
break;
case "KeyW":
case "ArrowUp":
for (var i = 0; i < children.length; i++){
children[i].depth = children[i].y;
}
this.handleMove(-1, 0);
break;
case "KeyS":
case "ArrowDown":
for (var i = 0; i < children.length; i++){
children[i].depth = game.config.height - children[i].y;
}
this.handleMove(1, 0);
break;
}
}
},
handleMove: function(deltaRow, deltaCol){
this.canMove = false;
var somethingMoved = false;
this.movingTiles = 0;
for(var i = 0; i < 4; i++){
for(var j = 0; j < 4; j++){
var colToWatch = deltaCol == 1 ? (4 - 1) - j : j;
var rowToWatch = deltaRow == 1 ? (4 - 1) - i : i;
var tileValue = this.fieldArray[rowToWatch][colToWatch].tileValue;
if(tileValue != 0){
var colSteps = deltaCol;
var rowSteps = deltaRow;
while(this.isInsideBoard(rowToWatch + rowSteps, colToWatch + colSteps) && this.fieldArray[rowToWatch + rowSteps][colToWatch + colSteps].tileValue == 0){
colSteps += deltaCol;
rowSteps += deltaRow;
}
if(this.isInsideBoard(rowToWatch + rowSteps, colToWatch + colSteps) && (this.fieldArray[rowToWatch + rowSteps][colToWatch + colSteps].tileValue == tileValue) && this.fieldArray[rowToWatch + rowSteps][colToWatch + colSteps].canUpgrade && this.fieldArray[rowToWatch][colToWatch].canUpgrade){
this.fieldArray[rowToWatch + rowSteps][colToWatch + colSteps].tileValue ++;
this.fieldArray[rowToWatch + rowSteps][colToWatch + colSteps].canUpgrade = false;
this.fieldArray[rowToWatch][colToWatch].tileValue = 0;
this.moveTile(this.fieldArray[rowToWatch][colToWatch], rowToWatch + rowSteps, colToWatch + colSteps, Math.abs(rowSteps + colSteps), true);
somethingMoved = true;
}
else{
colSteps = colSteps - deltaCol;
rowSteps = rowSteps - deltaRow;
if(colSteps != 0 || rowSteps != 0){
this.fieldArray[rowToWatch + rowSteps][colToWatch + colSteps].tileValue = tileValue;
this.fieldArray[rowToWatch][colToWatch].tileValue = 0;
this.moveTile(this.fieldArray[rowToWatch][colToWatch], rowToWatch + rowSteps, colToWatch + colSteps, Math.abs(rowSteps + colSteps), false);
somethingMoved = true;
}
}
}
}
}
if(!somethingMoved){
this.canMove = true;
}
},
moveTile: function(tile, row, col, distance, changeNumber){
this.movingTiles ++;
this.tweens.add({
targets: [tile.tileSprite],
x: this.tileDestination(col),
y: this.tileDestination(row),
duration: gameOptions.tweenSpeed * distance,
onComplete: function(tween){
tween.parent.scene.movingTiles --;
if(changeNumber){
tween.parent.scene.transformTile(tile, row, col);
}
if(tween.parent.scene.movingTiles == 0){
tween.parent.scene.resetTiles();
tween.parent.scene.addTwo();
}
}
})
},
transformTile: function(tile, row, col){
this.movingTiles ++;
tile.tileSprite.setFrame(this.fieldArray[row][col].tileValue - 1);
this.tweens.add({
targets: [tile.tileSprite],
scaleX: 1.1,
scaleY: 1.1,
duration: gameOptions.tweenSpeed,
yoyo: true,
repeat: 1,
onComplete: function(tween){
tween.parent.scene.movingTiles --;
if(tween.parent.scene.movingTiles == 0){
tween.parent.scene.resetTiles();
tween.parent.scene.addTwo();
}
}
})
},
resetTiles: function(){
for(var i = 0; i < 4; i++){
for(var j = 0; j < 4; j++){
this.fieldArray[i][j].canUpgrade = true;
this.fieldArray[i][j].tileSprite.x = this.tileDestination(j);
this.fieldArray[i][j].tileSprite.y = this.tileDestination(i);
if(this.fieldArray[i][j].tileValue > 0){
this.fieldArray[i][j].tileSprite.alpha = 1;
this.fieldArray[i][j].tileSprite.visible = true;
this.fieldArray[i][j].tileSprite.setFrame(this.fieldArray[i][j].tileValue - 1);
}
else{
this.fieldArray[i][j].tileSprite.alpha = 0;
this.fieldArray[i][j].tileSprite.visible = false;
}
}
}
},
isInsideBoard: function(row, col){
return (row >= 0) && (col >= 0) && (row < 4) && (col < 4);
},
tileDestination: function(pos){
return pos * (gameOptions.tileSize + gameOptions.tileSpacing) + gameOptions.tileSize / 2 + gameOptions.tileSpacing
},
checkGameOver: function(){
// loop through the entire board
for(var i = 0; i < 4; i++){
for(var j = 0; j < 4; j++){
// if there is an empty tile, it's not game over
if(this.fieldArray[i][j].tileValue == 0){
return;
}
// if there are two vertical adjacent tiles with the same value, it's not game over
if((i < 3) && this.fieldArray[i][j].tileValue == this.fieldArray[i + 1][j].tileValue){
return;
}
// if there are two horizontal adjacent tiles with the same value, it's not game over
if((j < 3) && this.fieldArray[i][j].tileValue == this.fieldArray[i][j + 1].tileValue){
return
}
}
}
// ok, it's definitively game over :(
alert("no more moves");
}
});
function resize() {
var canvas = document.querySelector("canvas");
var windowWidth = window.innerWidth;
var windowHeight = window.innerHeight;
var windowRatio = windowWidth / windowHeight;
var gameRatio = game.config.width / game.config.height;
if(windowRatio < gameRatio){
canvas.style.width = windowWidth + "px";
canvas.style.height = (windowWidth / gameRatio) + "px";
}
else{
canvas.style.width = (windowHeight * gameRatio) + "px";
canvas.style.height = windowHeight + "px";
}
}
This will allow you to check when there aren’t more moves and notify players it’s game over. Download the source code and play with it.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.