Pure TypeScript class with no depencencies to handle Drag and Match games in just a few lines – Full Phaser example

Read all posts about "" game

When building the pure TypeScript class with no dependencies to handle Drag and Match games, my aim was to have the class manage as much as possible of these kind of games.

Now, I finally have a class which only requires you to handle input and animations, taking care about all the rest, object pooling included.

It also features multiple combos. Have a look at this example:

Drag rows and columns and try match some items.

The red rectangle represents the game area, while the green square highlights the dummy item which is used to make items wrap around rows and columns without actually wrapping them.

Everything is managed by the class, whose constructor allows you to create a game with a lot of options:

rows: amount of rows.
columns: amount of columns.
items: amount of different items.
match: amount of equal consecutive items to define a match. Normally we define a match when we have 3 or more consecutive items, but it’s up to you.
tileSize: tile size, in pixels. Useful to let the class calculate items movements and positions for you.
startX: horizontal coordinate of the top left item, in pixels.
startY: vertical coordinate of the top left item, in pixels.
minDragDistance: minimum input movement to say the player is trying to drag a row or a column.

You don’t have to set all values, in this case the class will use default values but obviously it’s recommended to set all values.

Once you created your Drag and Match instance, with items property you get an array of objects with item positions, ready to be placed to the canvas.

Each item has a data property you can use to store your custom information, such as a Phaser sprite or anything else you may want to bind to such item.

isInputInsideBoard method, given an input coordinate, returns true if the input is inside the board, false otherwise.

handleInputMovement method, given start input coordinate an current input coordinate, calculates item movements and returns all information you need to place both the actual items and the dummy item used to fake the wraparound effect.

handleInputStop method, given start input coordinate an current input coordinate, calculate item final destination and updated the game board, returning all item information.

items property returns an array with all game items.

matches property returns an array with all board matches.

thereAreMatches property returns true if there are matches in the board, false otherwise.

removeMatches method removes the matches, if any, from the game field and returns an array with all removed items. Internally, removed items are inserted into an object pool, so you don’t have to worry about recycling items.

arrangeboard method arranges the board after a match, making items fall down and replenishing the board with new items.

logValues method is a simple method to output game board values on the console, for a debugging purpose.

Let’s see the source code, which is made of one html file and 4 TypeScript files:

index.html

The web page which hosts the game, to be run inside thegame element.

<!DOCTYPE html>
<html>
	<head>
        <style type = "text/css">
            * {
                padding: 0;
                margin: 0;
            }
            body{
                background: #000;
            }
            canvas {
                touch-action: none;
                -ms-touch-action: none;
            }
        </style>
        <script src = "main.js"></script>
    </head>
	<body>
        <div id = "thegame"></div>
	</body>
</html>

main.ts

This is where the game is created, with all Phaser related options.

// MAIN GAME FILE

// modules to import
import Phaser from 'phaser';
import { PreloadAssets } from './preloadAssets';
import { PlayGame } from './playGame';

// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
    mode : Phaser.Scale.FIT,
    autoCenter : Phaser.Scale.CENTER_BOTH,
    parent : 'thegame',
    width : 1000,
    height : 800
}

// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
    type : Phaser.AUTO,
    backgroundColor : 0x222222,
    scale : scaleObject,
    scene : [PreloadAssets, PlayGame]
}

// the game itself
new Phaser.Game(configObject);

preloadAssets.ts

Here we preload all assets to be used in the game.

// CLASS TO PRELOAD ASSETS

// this class extends Scene class
export class PreloadAssets extends Phaser.Scene {

    // constructor    
    constructor() {
        super({
            key : 'PreloadAssets'
        });
    }

    // method to be execute during class preloading
    preload(): void {

        this.load.spritesheet('items', 'assets/items.png', {
            frameWidth : 100,
            frameHeight : 100
        });
	}

    // method to be called once the instance has been created
	create(): void {

        // call PlayGame class
        this.scene.start('PlayGame');
	}
}

playGame.ts

Main game file, where we handle input and visual effects. I highlighted the lines of code that use my class. As you can see, the whole game logic is managed in only 27 lines.

// THE GAME ITSELF

// we have three possible game states: waiting (for input), dragging (the board) and arranging (the board, after a match) 
enum gameState {
    WAITING,
    DRAGGING,
    ARRANGING  
}

// modules to import
import { DragAndMatch } from './dragAndMatch';

// this class extends Scene class
export class PlayGame extends Phaser.Scene {

    // drag and match instance
    dragAndMatch : DragAndMatch;

    // graphics object to highlight dummy item
    dummyHighlight : Phaser.GameObjects.Graphics; 

    currentState : gameState;

    // constructor
    constructor() {
        super({
            key: 'PlayGame'
        });
    }

    create() : void {

        // current game state is "waiting for input"
        this.currentState = gameState.WAITING;

        // setting up a drag and match game with only some options
        this.dragAndMatch = new DragAndMatch({
            startX : 100,
            startY : 100
        });

        // get all game items
        this.dragAndMatch.items.map((item) => {

            // create a sprite and place it to posX, posY, with a "value" frame
            let sprite : Phaser.GameObjects.Sprite = this.add.sprite(item.posX, item.posY, 'items', item.value);
            
            // set sprite registration point
            sprite.setOrigin(0);

            // is this the dummy item?
            if (item.isDummy) {

                // hide dummy item
                sprite.setVisible(false);
            }

            // save sprite information into data custom property
            item.data = sprite;
        });

        // input listeners
        this.input.on('pointerdown', this.inputStart, this);
        this.input.on('pointermove', this.inputMove, this);
        this.input.on('pointerup', this.inputStop, this);

        // just some graphics to show game area and highlight dummy item
        // values are hardcoded so if you change game configuration, you need to change these values too.
        // Anyway, this does not affect the game itself
        let graphics : Phaser.GameObjects.Graphics = this.add.graphics();
        graphics.lineStyle(4, 0xff0000, 1);
        graphics.strokeRect(100, 100, 800, 600);
        this.dummyHighlight = this.add.graphics();
        this.dummyHighlight.lineStyle(4, 0x00ff00, 1);
        this.dummyHighlight.strokeRect(0, 0, 100, 100); 
        this.dummyHighlight.setVisible(false);
    }       

    // input start callback
    inputStart(pointer : Phaser.Input.Pointer) : void {

        // if current state is "waiting for player input" and the input is inside the board...
        if (this.currentState == gameState.WAITING &amp;&amp; this.dragAndMatch.isInputInsideBoard(pointer.position.x, pointer.position.y)) {
            
            // let's start dragging
            this.currentState = gameState.DRAGGING;
        }
    }

    // input move callback
    inputMove(pointer : Phaser.Input.Pointer) : void {

        // is the player dragging?
        if (this.currentState == gameState.DRAGGING) {

            // handle input movement using only starting and current coordinates 
            this.dragAndMatch.handleInputMovement(pointer.downX, pointer.downY, pointer.position.x, pointer.position.y).map((item) => {
                
                // set sprite position                
                item.data.setPosition(item.posX, item.posY)
                
                // set sprite proper frame according to item value
                item.data.setFrame(item.value);
                
                // is this the dumm item?
                if (item.isDummy) {

                    // place dummy highlight graphic object
                    this.dummyHighlight.setPosition(item.data.x, item.data.y);

                    // show dummy highlight graphic object
                    this.dummyHighlight.setVisible(true);

                    // show dummy item
                    item.data.setVisible(true);
                }
            });
        }
    }

    // input stop callback
    inputStop(pointer : Phaser.Input.Pointer) : void {
        
        // is the player dragging?
        if (this.currentState == gameState.DRAGGING) {

            // game state changes to "arranging the board"
            this.currentState = gameState.ARRANGING;

            // handle input stop using only starting and current coordinates 
            this.dragAndMatch.handleInputStop(pointer.downX, pointer.downY, pointer.position.x, pointer.position.y).map((item) => {
                
                // set item position
                item.data.setPosition(item.posX, item.posY);
                
                // is this the dummy item?
                if (item.isDummy) {

                    // hide dummy item
                    item.data.setVisible(false);

                    // hide dummy highlight graphic object
                    this.dummyHighlight.setVisible(false);
                }
            });

            // call a method to remove the matches
            this.removeMatches();
        }
    }

    // method to remove the matches
    removeMatches() : void {

        if (this.dragAndMatch.thereAreMatches) {

            // here we place the sprites to remove
            let spritesToRemove : Phaser.GameObjects.Sprite[] = [];

            // removeMatches method gives you a list of  items to remove, and we loop through them
            this.dragAndMatch.removeMatches().map((item) => {

                // insert item data (the sprite) in spritesToRemove array
                spritesToRemove.push(item.data);
            });

            // let's fade them out, the call arrangeBoard method
            this.tweens.add({
                targets : spritesToRemove,
                alpha : 0,
                duration : 150,
                callbackScope : this,
                onComplete : this.arrangeBoard
            });
        }

        // no sprites to remove?
        else {

            // set game state to "waiting for player input"
            this.currentState = gameState.WAITING;
        }
    }

    // method to arrange the board
    arrangeBoard() : void {

        // here we place the sprites to be moved
        let spritesToMove : Phaser.GameObjects.Sprite[] = [];

        // arrangeBoard gives you a list of items to be moved
        this.dragAndMatch.arrangeBoard().map((item) => {

            // get item sprite, stored in data property
            let sprite : Phaser.GameObjects.Sprite = item.data as Phaser.GameObjects.Sprite;
            
            // make it visible
            sprite.alpha = 1;

            // set sprite position
            sprite.setPosition(item.movement.startX, item.movement.startY);
          
            // set sprite frame
            sprite.setFrame(item.value);

            // set some custom data, such as new vertical position and tween duration
            sprite.setData({
                newY : item.movement.endY,
                tweenDuration : 150 * item.movement.deltaRow
            });
            spritesToMove.push(sprite);
        });

        // move the sprites with a tween
        this.tweens.add({
            targets : spritesToMove,
            props : {
                y : {
                    getEnd: function(target : Phaser.GameObjects.Sprite) {
                        return target.getData('newY');
                    },
                    duration : function(target : Phaser.GameObjects.Sprite) {
                        return target.getData('tweenDuration');
                    }
                },
            },
            callbackScope : this,

            // once the movement is complete, let's see if there are new matches to remove
            onComplete : this.removeMatches
        });
    }
}

dragAndMatch.ts

The class responsible of everything, the one you can use in your projects

interface DragAndMatchConfig {
    rows? : number;
    columns? : number;
    items? : number;
    match? : number;
    tileSize? : number;
    startX? : number;
    startY? : number;
    minDragDistance? : number;
    [otherOptions : string] : unknown;
}

interface GameConfig {
    rows : number;
    columns : number;
    items : number;
    match : number;
    tileSize : number;
    startX : number;
    startY : number;
    minDragDistance : number;
}

enum directionType {
    NONE,
    HORIZONTAL,
    VERTICAL  
}

interface DragAndMatchTile {
    empty : boolean;
    value : number;
    item : DragAndMatchItem;
}

interface DragAndMatchMatch {
    startRow : number;
    startColumn : number;
    endRow : number;
    endColumn : number;
    direction : directionType;
    length : number;
    value : number;
}

export class DragAndMatch {

    static readonly DEFALUT_VALUES : GameConfig = {
        rows : 6,
        columns : 8,
        items : 6,
        match : 3,
        tileSize : 100,
        startX : 0,
        startY : 0,
        minDragDistance : 30
    }

    config : GameConfig;
    gameArray : DragAndMatchTile[][];
    dragDirection : directionType;
    dummyItem : DragAndMatchItem;
    itemPool : DragAndMatchItem[];
    
    constructor(options? : DragAndMatchConfig) {
       this.config = {
            rows : (options === undefined || options.rows === undefined) ? DragAndMatch.DEFALUT_VALUES.rows : options.rows,
            columns : (options === undefined || options.columns === undefined) ? DragAndMatch.DEFALUT_VALUES.columns : options.columns,
            items : (options === undefined || options.items === undefined) ? DragAndMatch.DEFALUT_VALUES.items : options.items,
            match : (options === undefined || options.match === undefined) ? DragAndMatch.DEFALUT_VALUES.match : options.match,
            tileSize : (options === undefined || options.tileSize === undefined) ? DragAndMatch.DEFALUT_VALUES.tileSize : options.tileSize,
            startX : (options === undefined || options.startX === undefined) ? DragAndMatch.DEFALUT_VALUES.startX : options.startX,
            startY : (options === undefined || options.startY === undefined) ? DragAndMatch.DEFALUT_VALUES.startY : options.startY,
            minDragDistance : (options === undefined || options.minDragDistance === undefined) ? DragAndMatch.DEFALUT_VALUES.minDragDistance : options.minDragDistance,    
        }
        this.dragDirection = directionType.NONE;
        this.gameArray = [];
        for (let i : number = 0; i < this.config.rows; i ++) {
            this.gameArray[i] = [];
            for (let j : number = 0; j < this.config.columns; j ++) {
                let randomValue : number = this.safeValue(i, j);
                this.gameArray[i][j] = {
                    empty : false,
                    value : randomValue,
                    item : new DragAndMatchItem(i, j, randomValue, this.config.startX + j * this.config.tileSize, this.config.startY + i * this.config.tileSize, false)
                }  
            }
        }
        this.dummyItem = new DragAndMatchItem(0, 0, 0, 0, 0, true);
    }

    /* get all game items */
    get items() : DragAndMatchItem[] {
        let items : DragAndMatchItem[] = [];
        for (let i : number = 0; i < this.config.rows; i ++) {
            for (let j : number = 0; j < this.config.columns; j ++) {
                items.push(this.gameArray[i][j].item);
            }
        }
        items.push(this.dummyItem);
        return items;
    }

    /* remove the matches */
    removeMatches() : DragAndMatchItem[] {
        this.itemPool = [];
        let items : DragAndMatchItem[] = [];
        let matches : DragAndMatchMatch[] = this.matches;
        matches.forEach((match) => {
            switch (match.direction) {
                case directionType.VERTICAL :
                    for (let i : number = match.startRow; i <= match.endRow; i ++) {
                        if (!this.gameArray[i][match.startColumn].empty) {
                            this.gameArray[i][match.startColumn].empty = true;
                            items.push(this.gameArray[i][match.startColumn].item);
                            this.itemPool.push(this.gameArray[i][match.startColumn].item);
                        }
                    }
                    break;
                case directionType.HORIZONTAL :
                    for (let i : number = match.startColumn; i <= match.endColumn; i ++) {
                        if (!this.gameArray[match.startRow][i].empty) {
                            this.gameArray[match.startRow][i].empty = true;
                            items.push(this.gameArray[match.startRow][i].item);
                            this.itemPool.push(this.gameArray[match.startRow][i].item);
                        }
                    }
                    break;
            }
        });
        return items;
    }

    /* arrange the board after a match */
    arrangeBoard() : DragAndMatchItem[] {
        let result : DragAndMatchItem[] = [];
        for (let i = this.config.rows - 2; i >= 0; i --) {
            for (let j = 0; j < this.config.columns; j ++) {
                let emptySpaces = this.emptySpacesBelow(i, j);  
                if (!this.gameArray[i][j].empty &amp;&amp; emptySpaces > 0) {  
                    let item : DragAndMatchItem = this.gameArray[i][j].item;
                    item.row += emptySpaces;
                    let movement : DragAndMatchMovement = new DragAndMatchMovement(item.posX, item.posY, item.posX, item.posY + emptySpaces * this.config.tileSize, emptySpaces);
                    item.posY += emptySpaces * this.config.tileSize;
                    item.movement = movement;
                    result.push(item);
                    let tempTile : DragAndMatchTile = this.gameArray[i + emptySpaces][j];
                    this.gameArray[i + emptySpaces][j] = this.gameArray[i][j];
                    this.gameArray[i][j] = tempTile;
                }
            }
        }
        for (let i : number = 0; i < this.config.columns; i ++) {
            if (this.gameArray[0][i].empty) { 
                let emptySpaces : number = this.emptySpacesBelow(0, i) + 1;
                for (let j : number = emptySpaces - 1; j >=0; j --) {
                    let randomValue : number = Math.floor(Math.random() * this.config.items);       
                    let item : DragAndMatchItem = this.itemPool.shift() as DragAndMatchItem; 
                    item.value = randomValue;   
                    item.row = j; 
                    item.column = i;
                    let movement : DragAndMatchMovement = new DragAndMatchMovement(this.config.startX + i * this.config.tileSize, this.config.startY - (emptySpaces - j) * this.config.tileSize, this.config.startX + i * this.config.tileSize, this.config.startY + j * this.config.tileSize, emptySpaces);
                    item.posY = this.config.startY + j * this.config.tileSize;
                    item.posX = this.config.startX + i * this.config.tileSize;
                    item.movement = movement;   
                    result.push(item);       
                    this.gameArray[j][i].item = item;
                    this.gameArray[j][i].value = randomValue;
                    this.gameArray[j][i].empty = false;
                }
            }
        }
        return result;
    }
    
    /* output board values in console */
    logValues() : void {
        let output : string = '';
        for (let i : number = 0; i < this.config.rows; i ++) {    
            for (let j : number = 0; j < this.config.columns; j ++) {
                output += (this.gameArray[i][j].empty ? '.' : this.gameArray[i][j].value) + ' ';
            }   
            output += '\n';
        }
        console.log(output);
    }

    /* check if input is inside game board */
    isInputInsideBoard(x : number, y : number) : boolean {
        let column : number = Math.floor((x - this.config.startX) / this.config.tileSize);
        let row : number = Math.floor((y - this.config.startY) / this.config.tileSize);
        return this.validPick(row, column);
    }

    /* handle input movement */
    handleInputMovement(startX : number, startY : number, currentX : number, currentY : number) : DragAndMatchItem[] {
        let distanceX : number = currentX - startX;
        if (Math.abs(distanceX % this.config.tileSize) == 0) {
            distanceX += 0.001;
        }
        let distanceY : number = currentY - startY;
        if (Math.abs(distanceY % this.config.tileSize) == 0) {
            distanceY += 0.001;
        }
        if (this.dragDirection == directionType.NONE &amp;&amp; Math.abs(distanceX) + Math.abs(distanceY) > this.config.minDragDistance) {
            this.dragDirection = (Math.abs(distanceX) > Math.abs(distanceY)) ? directionType.HORIZONTAL : directionType.VERTICAL;
        }
        let items : DragAndMatchItem[] = [];
        switch (this.dragDirection) {
            case directionType.HORIZONTAL :  
                let row : number = Math.floor((startY - this.config.startY) / this.config.tileSize);
                items = this.getItemsAtRow(row);
                items.forEach((item) => {
                    let newPosX : number = item.column * this.config.tileSize + distanceX;
                    let limitX : number = this.config.columns * this.config.tileSize;
                    newPosX = newPosX >= 0 ? newPosX % limitX : (newPosX % limitX + limitX) % limitX;
                    item.posX = this.config.startX + newPosX; 
                });
                this.dummyItem.posY = this.config.startY + row * this.config.tileSize;
                this.dummyItem.posX = distanceX >= 0 ? (this.config.startX + Math.abs(distanceX) % this.config.tileSize - this.config.tileSize) : (this.config.startX - Math.abs(distanceX) % this.config.tileSize);
                let columnOffset : number = Math.floor(distanceX / this.config.tileSize);
                let newColumnReference : number = columnOffset >= 0 ? (this.config.columns - 1 - columnOffset % this.config.columns) : (((1 + columnOffset) * -1) % this.config.columns);
                this.dummyItem.value = this.gameArray[row][newColumnReference].value;
                items.push(this.dummyItem);
                break;
            case directionType.VERTICAL :
                let column : number = Math.floor((startX - this.config.startX) / this.config.tileSize);
                items = this.getItemsAtColumn(column);
                items.forEach((item) => {
                    let newPosY : number = item.row * this.config.tileSize + distanceY;                  
                    let limitY : number = this.config.rows * this.config.tileSize;
                    newPosY = newPosY >= 0 ? newPosY % limitY : (newPosY % limitY + limitY) % limitY;
                    item.posY = this.config.startY + newPosY;                  
                });
                this.dummyItem.posX = this.config.startX + column * this.config.tileSize;
                this.dummyItem.posY = distanceY >= 0 ? (this.config.startY + Math.abs(distanceY) % this.config.tileSize - this.config.tileSize) : (this.config.startY - Math.abs(distanceY) % this.config.tileSize);  
                let rowOffset : number = Math.floor(distanceY / this.config.tileSize);
                let newRowReference : number = rowOffset >= 0 ? (this.config.rows - 1 - rowOffset % this.config.rows) : (((1 + rowOffset) * -1) % this.config.rows);
                this.dummyItem.value = this.gameArray[newRowReference][column].value;
                items.push(this.dummyItem);
                break;
        }
        return items;
    }

    /* handle stop input movement */
    handleInputStop(startX : number, startY : number, currentX : number, currentY : number) : DragAndMatchItem[] {
        let items: DragAndMatchItem[] = [];
        let tempItemArray : DragAndMatchItem[];
        let tempValueArray : number[];
        switch (this.dragDirection) {
            case directionType.HORIZONTAL : 
                let row : number = Math.floor((startY - this.config.startY) / this.config.tileSize);
                let distanceX : number = currentX - startX;
                let columnOffset : number = Math.round(distanceX / this.config.tileSize);
                tempItemArray = this.getItemsAtRow(row);
                tempValueArray = this.getValuesAtRow(row);
                for (let i : number = 0; i < this.config.columns; i ++) {
                    let destinationColumn : number = i - columnOffset;    
                    let wrappedDestinationColumn : number = this.wrapValues(destinationColumn, this.config.columns);
                    let item : DragAndMatchItem = tempItemArray[wrappedDestinationColumn];
                    item.column = i;
                    item.posX = this.config.startX + i * this.config.tileSize;
                    this.gameArray[row][i].item = item;
                    this.gameArray[row][i].value = tempValueArray[wrappedDestinationColumn];
                }
                items = this.getItemsAtRow(row);
                break;
            case directionType.VERTICAL :  
                let column : number = Math.floor((startX - this.config.startX) / this.config.tileSize);
                let distanceY : number = currentY - startY;
                let rowOffset : number = Math.round(distanceY / this.config.tileSize);
                tempItemArray = this.getItemsAtColumn(column);
                tempValueArray = this.getValuesAtColumn(column);
                for (let i : number = 0; i < this.config.rows; i ++) {
                    let destinationRow : number = i - rowOffset;    
                    let wrappedDestinationRow : number = this.wrapValues(destinationRow, this.config.rows);
                    let item : DragAndMatchItem = tempItemArray[wrappedDestinationRow];
                    item.row = i;
                    item.posY = this.config.startY + i * this.config.tileSize;
                    this.gameArray[i][column].item = item;
                    this.gameArray[i][column].value = tempValueArray[wrappedDestinationRow];
                }
                items = this.getItemsAtColumn(column);
                break; 
        }
        items.push(this.dummyItem);
        this.dragDirection = directionType.NONE;
        return items;
    }

    /* get all matches */
    get matches() : DragAndMatchMatch[] {
        let matches : DragAndMatchMatch[] = [];
        for (let i : number = 0; i < this.config.rows; i ++) {
            matches = matches.concat(this.getHorizontalMatches(i));
        }
        for (let i : number = 0; i < this.config.columns; i ++) {
            matches = matches.concat(this.getVerticalMatches(i));    
        }
        return matches;
    }

    /* are there matches? */
    get thereAreMatches() : boolean {
        return this.matches.length > 0;
    }

    /* get all matches in a row */
    private getHorizontalMatches(row : number) : DragAndMatchMatch[] {
        let matches : DragAndMatchMatch[] = [];
        let combo : number = 1;
        let currentValue : number = this.gameArray[row][0].value;
        let comboStart : number = 0;
        for (let i : number = 1; i < this.config.columns; i ++) {
            let value : number = this.gameArray[row][i].value;
            if (value == currentValue) {
                combo ++;
            }
            if (value != currentValue || i == this.config.columns - 1) {
                if (combo >= this.config.match) {
                    matches.push({
                        startRow : row,
                        startColumn : comboStart,
                        endRow : row,
                        endColumn : comboStart + combo - 1,
                        direction : directionType.HORIZONTAL,
                        length : combo,
                        value : currentValue
                    });
                }
                combo = 1;
                comboStart = i;
                currentValue = value;
            }
        }   
        return matches;
    }

    /* just a function to wap values */
    private wrapValues(value : number, limit : number) : number {
        return value >= 0 ? value % limit : (value % limit + limit) % limit;
    }

    /* get all items in a row */
    private getItemsAtRow(row : number) : DragAndMatchItem[] {
        let items : DragAndMatchItem[] = [];
        for (let i : number = 0; i < this.config.columns; i ++) {
            items.push(this.gameArray[row][i].item as DragAndMatchItem);
        }
        return items;
    }

    /* get all values in a row */
    private getValuesAtRow(row : number) : number[] {
        let values : number[] = [];
        for (let i : number = 0; i < this.config.columns; i ++) {
            values.push(this.gameArray[row][i].value);
        }
        return values;
    }

    /* get all items in a column */
    private getItemsAtColumn(column : number) : DragAndMatchItem[] {
        let items : DragAndMatchItem[] = [];
        for (let i : number = 0; i < this.config.rows; i ++) {
            items.push(this.gameArray[i][column].item as DragAndMatchItem);
        }
        return items;
    }

    /* get all values in a column */
    private getValuesAtColumn(column : number) : number[] {
        let values : number[] = [];
        for (let i : number = 0; i < this.config.rows; i ++) {
            values.push(this.gameArray[i][column].value);
        }
        return values;
    }

    /* check a pick is in a valid row and column */
    private validPick(row : number, column : number) : boolean {
        return row >= 0 &amp;&amp; row < this.config.rows &amp;&amp; column >= 0 &amp;&amp; column < this.config.columns &amp;&amp; this.gameArray[row] != undefined &amp;&amp; this.gameArray[row][column] != undefined;
    }

    /* get all matches in a column */
    private getVerticalMatches(column : number) : DragAndMatchMatch[] {
        let matches : DragAndMatchMatch[] = [];
        let combo : number = 1;
        let currentValue : number = this.gameArray[0][column].value;
        let comboStart : number = 0;
        for (let i : number = 1; i < this.config.rows; i ++) {
            let value : number = this.gameArray[i][column].value;
            if (value == currentValue) {
                combo ++;
            }
            if (value != currentValue || i == this.config.rows - 1) {
                if (combo >= this.config.match) {
                    matches.push({
                        startRow : comboStart,
                        startColumn : column,
                        endRow : comboStart + combo - 1,
                        endColumn : column,
                        direction : directionType.VERTICAL,
                        length : combo,
                        value : currentValue
                    })
                }
                combo = 1;
                comboStart = i;
                currentValue = value;
            }
        }   
        return matches;
    }

    /* calculate how many empty spaces there are below (row, column) */
    private emptySpacesBelow(row : number, column : number) : number {
        let result : number = 0;
        if (row != this.config.rows) {
            for (let i : number = row + 1; i < this.config.rows; i ++) {
                if (this.gameArray[i][column].empty) {
                    result ++;
                }
            }
        }
        return result;
    }

    /* generate a safe value, which can't return a match in the game */
    private safeValue(row : number, column : number) : number {
        let safeValues : number[] = Array.from(Array(this.config.items).keys());
        if (row >= this.config.match - 1) {
            let possibleMatch : boolean = true;
            let prevValue : number = -1;
            let value : number = -1;
            for (let i : number = row - 1; i > row - this.config.match; i --) {
                value = this.gameArray[i][column].value;
                possibleMatch = possibleMatch &amp;&amp; (value == prevValue || prevValue == -1);
                prevValue = value;
            }
            if (possibleMatch) {
                let index = safeValues.indexOf(value);
                if (index > -1) {
                    safeValues.splice(index, 1);
                }
            }
        }
        if (column >= this.config.match - 1) {
            let possibleMatch : boolean = true;
            let prevValue : number = -1;
            let value : number = -1;
            for (let i : number = column - 1; i > column - this.config.match; i --) {
                value = this.gameArray[row][i].value;
                possibleMatch = possibleMatch &amp;&amp; (value == prevValue || prevValue == -1);
                prevValue = value;
            }
            if (possibleMatch) {
                let index = safeValues.indexOf(value);
                if (index > -1) {
                    safeValues.splice(index, 1);
                }
            }
        }
        return safeValues[Math.floor(Math.random() * safeValues.length)]
    }
}

class DragAndMatchItem {

    row : number;
    column : number;
    value : number;
    data : any;
    posX : number;
    posY : number;
    isDummy : boolean;
    movement : DragAndMatchMovement;

    constructor(row : number, column : number, value: number, posX: number, posY: number, isDummy : boolean) {
        this.row = row;
        this.column = column;
        this.value = value;
        this.posX = posX;
        this.posY = posY;
        this.isDummy = isDummy;
    }
}

class DragAndMatchMovement {
    startX : number;
    startY : number;
    endX : number;
    endY : number;
    deltaRow : number;

    constructor(startX: number, startY : number, endX : number, endY : number, deltaRow : number) {
        this.startX = startX;
        this.startY = startY;
        this.endX = endX;
        this.endY = endY;
        this.deltaRow = deltaRow;        
    }
}

And now you can finally have your Drag and Match game running in just a few lines. Now, I am working on a little hyper casual game which will show you how easy is to build Drag and Match games with this class. Download the source code of the entire project.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

214 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers