Do you like my tutorials?

Then consider supporting me on Ko-fi

Talking about Dungeon Raid game, Game development, HTML5, Javascript, Phaser and TypeScript.

If you enjoyed the pure TypeScript class with no depencencies to handle Drag and Match games, you will like this TypeScript class to manage Draw and Match games like Dungeon Raid.

I already published a pure JavaScript class to manage them, but this one is way better because it allows more room for customization and requires way less methods.

In a few lines you will be able to build a game like this one:

Draw to select at least 3 symbols with the same color. You can move horizontally, vertically and diagonally, and you can also backtrack.

This is the complete source code, that is fully commented, with all DrawAndMatch class main methods explained.

The projects consists in one HTML file, one CSS file and four TypeScript files.

index.html

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

<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="initial-scale=1, maximum-scale=1">
        <link rel="stylesheet" href="style.css">
        <script src="main.js"></script> 
    </head>
    <body>   
        <div id = "thegame"></div>
    </body>
</html>

style.css

The cascading style sheets of the main web page.

* {
    padding : 0;
    margin : 0;
}

body {
    background-color: #000000;    
}

canvas {
    touch-action : none;
    -ms-touch-action : none;
}

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 : 800,
    height : 600
}

// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
    type : Phaser.AUTO,
    backgroundColor : 0x444444,
    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 called during class preloading
    preload() : void {

        this.load.spritesheet('items', 'assets/sprites/items.png', {
            frameWidth : 100,
            frameHeight : 100
        });
        this.load.spritesheet('arrows', 'assets/sprites/arrows.png', {
            frameWidth : 300,
            frameHeight : 300
        });
    }
 
    // method to be called once the instance has been created
    create() : void {

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

playGame.ts

Main game file, all game logic is stored here.

// THE GAME ITSELF

import { DrawAndMatch, DrawAndMatchItem, DrawAndMatchDirection } from './drawAndMatch';

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

// each game item has two sprites: the item itself and the arrow.
enum itemData {
    ITEM,
    ARROW
}

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

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

    // draw and match instance
    drawAndMatch : DrawAndMatch;

    // current game state
    currentState : gameState;

    // the player
    player : Phaser.Physics.Arcade.Sprite;

    // lookup table to save boring stuff like arrow frame and angle
    arrowLookupTable : any[];

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

        // method to populate the lookup table
        this.populateLookupTable();

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

        // create a draw and match board with default settings
        this.drawAndMatch = new DrawAndMatch();

        // get all game items
        this.drawAndMatch.items.map((item : DrawAndMatchItem) => {
 
            // create the gem sprite and place it to posX, posY, with a "value" frame
            let sprite : Phaser.GameObjects.Sprite = this.add.sprite(item.centerX, item.centerY, 'items', item.value);
            sprite.setDepth(0);

            // create the arrow sprite and place it to posX, posY
            let arrowSprite : Phaser.GameObjects.Sprite = this.add.sprite(item.centerX, item.centerY, 'arrows');
            arrowSprite.setDepth(1);
            arrowSprite.setVisible(false);
 
            // save sprite information into data custom property
            item.setData([sprite, arrowSprite]);
        });

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

    }

    // 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) { 

            // check if the chain can start at screen coordinate x, y
            let chainStarted : DrawAndMatchItem | null = this.drawAndMatch.startChain(pointer.position.x, pointer.position.y);
            
            // if the chain is not null...
            if (chainStarted !== null) {

                // set picked item alpha to 0.5
                chainStarted.data[itemData.ITEM].setAlpha(0.5);

                // now we are 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 according to x and y input position, and loop through updated items, if any
            this.drawAndMatch.handleInputMovement(pointer.position.x, pointer.position.y).map((item : DrawAndMatchItem) => {
                
                // set item alpha to highlight selected item
                item.data[itemData.ITEM].setAlpha(item.selected ? 0.5 : 1);

                // show item arrow, if allowed
                item.data[itemData.ARROW].setVisible(item.arrowVisible);

                // if we should show the arrow...
                if (item.arrowVisible) {

                    // find arrow sprite frame and angle into arrow lookup table
                    let lookupResult : any = this.arrowLookupTable[item.arrowDirection];

                    // set the proper frame
                    item.data[itemData.ARROW].setFrame(lookupResult.frame);

                    // set the proper angle
                    item.data[itemData.ARROW].setAngle(lookupResult.angle);
                }  
            })        
        } 
    }
 
    // input stop callback
    inputStop() : void {
         
        // is the player dragging?
        if (this.currentState == gameState.DRAGGING) {
 
            // game state changes to "arranging the board"
            this.currentState = gameState.ARRANGING;

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

            // call removeItems method and loop through items to be removed
            this.drawAndMatch.removeItems().map((item : DrawAndMatchItem) => {

                // should we remove the item?
                if (item.toBeRemoved) {

                    // add the sprite to spritesToRemove array
                    spritesToRemove.push(item.data[itemData.ITEM]);
                }

                // set item alpha to 1, fully opaque
                item.data[itemData.ITEM].setAlpha(1);

                // hide the arrow
                item.data[itemData.ARROW].setVisible(false);   
            });

            // this tween will make all spritesToRemove children fade out
            this.tweens.add({

                // tween target
                targets : spritesToRemove,

                // destination alpha
                alpha : 0,

                // tween duration, in milliseconds
                duration : 250,

                // scope of the callback function
                callbackScope : this,

                // function to be executed once the tween is completed
                onComplete : this.arrangeBoard
            });    
        }
    }

    // method to arrange the board
    arrangeBoard() : void {
        
        // array where to place the sprites to move
        let spritesToMove : Phaser.GameObjects.Sprite[] = [];
        
        // this is the maximum delta row traveled by a sprite
        let maxDeltaRow : number = 0;

        // call arrangeBoard method to get all items to move, and loop through them
        this.drawAndMatch.arrangeBoard().map((item : DrawAndMatchItem) => {

            // get item sprite, stored in data property
            let sprite : Phaser.GameObjects.Sprite = item.data[itemData.ITEM] as Phaser.GameObjects.Sprite;

            // set sprite alpha to 1, fully opaque
            sprite.setAlpha(1);

            // set proper sprite fame according to item value
            sprite.setFrame(item.value);

            // show the sprite
            sprite.setVisible(true);

            // place sprite at the start of its moving position
            sprite.setPosition(item.movement.startCenterX, item.movement.startCenterY);

            // place arrow sprite directly at the destination position
            item.data[itemData.ARROW].setPosition(item.movement.endCenterX, item.movement.endCenterY); 

            // set some sprite custom data
            sprite.setData({

                // start movement positin
                startY : item.movement.startCenterY,

                // total y movement
                totalMovement : item.movement.endCenterY - item.movement.startCenterY,

                // delta row
                deltaRow : item.movement.deltaRow
            });  

            // add the sprite to spritesToMove array
            spritesToMove.push(sprite);

            // update maximum delta row
            maxDeltaRow = Math.max(maxDeltaRow, item.movement.deltaRow);
        });
       
        // add a numeric tween to move all sprites at once
        this.tweens.addCounter({
            
            // start from zero
            from : 0,

            // end to 1
            to : 1,

            // set tween duration according to delta row
            duration : maxDeltaRow * 100,

            // scope of the callback function
            callbackScope : this,

            // function to be executed at each tween update
            onUpdate : (tween : Phaser.Tweens.Tween) => {

                // iterate through all spritesToMove items
                spritesToMove.forEach((item : Phaser.GameObjects.Sprite) => {

                    // calculate delta time
                    let delta : number = Math.min(1, tween.getValue() / item.getData('deltaRow') * maxDeltaRow);
                    
                    // change y position according to delta time, start y position and vertical movement
                    item.setY(item.getData('startY') + item.getData('totalMovement') * delta);
                })
            },
            
            // function to be executed when the tween ends
            onComplete : () => {

                // start waiting again for player input
                this.currentState = gameState.WAITING;
            }
        });
    }

    // method to populate the lookup table with all frames and angles needed to properly display arrows
    populateLookupTable() : void {
        this.arrowLookupTable = [];
        this.arrowLookupTable[DrawAndMatchDirection.NONE] = {
            frame : 0,
            angle : 0
        }
        this.arrowLookupTable[DrawAndMatchDirection.UP] = {
            frame : 0,
            angle : -90
        }
        this.arrowLookupTable[DrawAndMatchDirection.UP + DrawAndMatchDirection.RIGHT] = {
            frame : 1,
            angle : -90
        }
        this.arrowLookupTable[DrawAndMatchDirection.RIGHT] = {
            frame : 0,
            angle : 0
        }
        this.arrowLookupTable[DrawAndMatchDirection.RIGHT + DrawAndMatchDirection.DOWN] = {
            frame : 1,
            angle : 0
        }
        this.arrowLookupTable[DrawAndMatchDirection.DOWN] = {
            frame : 0,
            angle : 90
        }
        this.arrowLookupTable[DrawAndMatchDirection.DOWN + DrawAndMatchDirection.LEFT] = {
            frame : 1,
            angle : 90
        }
        this.arrowLookupTable[DrawAndMatchDirection.LEFT] = {
            frame : 0,
            angle : 180
        }
        this.arrowLookupTable[DrawAndMatchDirection.LEFT + DrawAndMatchDirection.UP] = {
            frame : 1,
            angle : 180
        }
    }  
}

You can start with these imports:

DrawAndMatch: the class defining the draw and match game itself.

DrawAndMatchItem: the class definining a draw and match item.

DrawAndMatchDirection: an enum with all possible arrow directions.

Then in your game all you have to do is call the constructor:

constructor(options): all options are optional, and I provided a default value for each option. Here they are:

rows: the amount of rows, default 6.

columns: the amount of columns, default 8.

items: the amount of different items, default 6.

tileSize: tile size, in pixels, default 100.

startX: x coordinate of the upper left corner of game table, default 0.

startY: y coordinate of the upper left corner of game table, default 0.

insideEnough: this value from 0 to 1 determines how inside the pointer should be inside an item to select it, default 0.4, that means the pointer should be 40% inside an item to select it.

minChain: minimum chain length to be considered a valid pick, default 3.

You will need only one property:

items: an array of all DrawAndMatchItem elements in game. DrawAndMatchItem has a setData(data) method to store any arbitrary information, such as the sprite used to represent it.

Each DrawAndMatchItem also have these properties:

row: row position in the board.

column: column position in the board.

 value: item value.

 data: it can be anything, use it to store your custom information.

 posX: horizontal position of the top left pixel of the item.

 posY: vertical position of the top left pixel of the item.

 size: item size, in pixels.

 centerX: horizontal position of the center pixel of the item.

 centerY: vertical position of the center pixel of the item.

 selected: true if the item is selected.

 arrowVisible: true if the arrow is visibe.

 arrowDirection: arrow direction, in DrawAndMatchDirection format.

 movement: item movement, a DrawAndMatchMovement class containing movement information.

 toBeRemoved: true if the item should be removed.

Once you click somewhere on the canvas, you just have to call startChain method.

startChain(x, y): returns a DrawAndMatchItem if coordinate (x, y) is a valid coordinate to start a chain, or null.

When you move the pointer, just call handleInputMovement method.

handleInputMovement(x, y): returns an array of DrawAndMatchItem representing the items you need to update when you moved the pointer at coordinate (x, y).

When you stop moving the pointer, call removeItems method.

removeItems(): returns the items to be removed after a draw, if any.

If you removed items, then call arrangeBoard method.

arrangeBoard(): returns all items to be moved, according to DrawAndMatchMovement class.

DrawAndMatchMovement class has these properties:

startX: x coordinate of the top left pixel of the start position.

startY: y coordinate of the top left pixel of the start position.

startCenterX: x coordinate of the center pixel of the start position.

startCenterY: y coordinate of the center pixel of the start position.;

endX: x coordinate of the top left pixel of the end position.

endY: y coordinate of the top left pixel of the end position.

endCenterX: x coordinate of the center pixel of the end position.

endCenterY: y coordinate of the center pixel of the end position.

deltaRow: amount of rows the item will travel.

drawAndMatch.ts

The class to handle draw and match games, ready to be used in your own projects.

interface DrawAndMatchConfig {
    rows? : number;
    columns? : number;
    items? : number;
    tileSize? : number;
    startX? : number;
    startY? : number;
    insideEnough? : number;
    minChain? : number;
}

interface GameConfig {
    rows : number;
    columns : number;
    items : number;
    tileSize : number;
    startX : number;
    startY : number;
    insideEnough : number;
    minChain : number;
}

interface DrawAndMatchTile {
    empty : boolean;
    value : number;
    item : DrawAndMatchItem;
}

interface DrawAndMatchCoordinate {
    row : number;
    column : number;
}

export enum DrawAndMatchDirection {
    NONE = 0,
    RIGHT = 1,
    DOWN = 2,
    LEFT = 4,
    UP = 8
}

export class DrawAndMatch {
    
    static readonly DEFALUT_VALUES : GameConfig = {
        rows : 6,
        columns : 8,
        items : 6,
        tileSize : 100,
        startX : 0,
        startY : 0,
        insideEnough : 0.4,
        minChain : 3
    }

    config : GameConfig;
    gameArray : DrawAndMatchTile[][];  
    chain : DrawAndMatchCoordinate[];
    itemPool : DrawAndMatchItem[];
    chainValue : number;
    previousChainLenght : number;

    constructor(options? : DrawAndMatchConfig) {

        this.config = {
            rows : (options === undefined || options.rows === undefined) ? DrawAndMatch.DEFALUT_VALUES.rows : options.rows,
            columns : (options === undefined || options.columns === undefined) ? DrawAndMatch.DEFALUT_VALUES.columns : options.columns,
            items : (options === undefined || options.items === undefined) ? DrawAndMatch.DEFALUT_VALUES.items : options.items,
            tileSize : (options === undefined || options.tileSize === undefined) ? DrawAndMatch.DEFALUT_VALUES.tileSize : options.tileSize,
            startX : (options === undefined || options.startX === undefined) ? DrawAndMatch.DEFALUT_VALUES.startX : options.startX,
            startY : (options === undefined || options.startY === undefined) ? DrawAndMatch.DEFALUT_VALUES.startY : options.startY,
            insideEnough : (options === undefined || options.insideEnough === undefined) ? DrawAndMatch.DEFALUT_VALUES.insideEnough : options.insideEnough, 
            minChain : (options === undefined || options.minChain === undefined) ? DrawAndMatch.DEFALUT_VALUES.minChain : options.minChain  
        }
        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 = Math.floor(Math.random() * this.config.items);
                this.gameArray[i][j] = {
                    empty : false,
                    value : randomValue,
                    item : new DrawAndMatchItem(i, j, randomValue, this.config.startX + j * this.config.tileSize, this.config.startY + i * this.config.tileSize, this.config.tileSize)
                }  
            }
        }
        this.chain = [];
        this.previousChainLenght = 0;
    }

    
    /* get all game items */
    get items() : DrawAndMatchItem[] {
        let items : DrawAndMatchItem[] = [];
        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);
            }
        }
        return items;
    }

    /* output the chain in the console */
    logChain() : void {
        let output : string = ''
        this.chain.forEach((coordinate : DrawAndMatchCoordinate, index : number) => {
            output += '[' + coordinate.row + ', ' + coordinate.column + ']';
            if (index < this.chain.length - 1) {
                output += ' -> ';
            }
        });
        console.log(output);
    }
    
    /* output board values in console */
    logBoard() : 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 a pick is in a valid row and column */
    private validPick(row : number, column : number) : boolean {
        return row >= 0 && row < this.config.rows && column >= 0 && column < this.config.columns && this.gameArray[row] != undefined && this.gameArray[row][column] != undefined;
    }

    /* check if input is inside game board */
    private 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(x : number, y : number) : DrawAndMatchItem[] {
        let items : DrawAndMatchItem[] = [];
        if (this.isInputInsideBoard(x, y)) {
            if (this.isInsideEnough(x, y)) {
                if (this.isCoordinateInChain(x, y)) {
                    if (this.isBacktrack(x, y)) {
                        let removedTile : DrawAndMatchCoordinate = this.chain.pop() as DrawAndMatchCoordinate;
                        let removedItem : DrawAndMatchItem = this.gameArray[removedTile.row][removedTile.column].item;  
                        removedItem.selected = false;
                        removedItem.arrowVisible = false;
                        items.push(removedItem);  
                        let lastTile : DrawAndMatchCoordinate = this.chain[this.chain.length - 1];
                        let lastItem : DrawAndMatchItem = this.gameArray[lastTile.row][lastTile.column].item;
                        lastItem.selected = true;
                        lastItem.arrowVisible = false;
                        items.push(lastItem);  
                    }        
                }
                else {
                    if (this.canContinueChain(x, y) && this.isChainValue(x, y)) {
                        this.chain.push(this.getCoordinateAt(x, y));
                        let addedTile : DrawAndMatchCoordinate = this.chain[this.chain.length - 1];
                        let addedItem : DrawAndMatchItem = this.gameArray[addedTile.row][addedTile.column].item;
                        addedItem.selected = true;
                        addedItem.arrowVisible = false;
                        items.push(addedItem);
                        let formerLastTile : DrawAndMatchCoordinate = this.chain[this.chain.length - 2];
                        let formerLastItem : DrawAndMatchItem = this.gameArray[formerLastTile.row][formerLastTile.column].item;
                        formerLastItem.selected = false;
                        formerLastItem.arrowVisible = true;
                        formerLastItem.arrowDirection = this.getArrowDirection(this.chain.length - 2);
                        items.push(formerLastItem);  
                    }
                }
            }
        }
        return items;
    }

    /* get arrow direction */
    private getArrowDirection(index : number) : number {
        let coordinate1 : DrawAndMatchCoordinate = this.chain[index + 1];
        let coordinate2 : DrawAndMatchCoordinate = this.chain[index];
        let deltaRow : number = coordinate1.row - coordinate2.row;
        let deltaColumn : number = coordinate1.column - coordinate2.column;
        let direction : number = 0;
        direction += (deltaColumn < 0) ? DrawAndMatchDirection.LEFT : ((deltaColumn > 0) ? DrawAndMatchDirection.RIGHT : 0);
        direction += (deltaRow < 0) ? DrawAndMatchDirection.UP : ((deltaRow > 0) ? DrawAndMatchDirection.DOWN : 0);  
        return direction;
    }

    /* check if a movement coordinate is a backtrack */
    private isBacktrack(x : number, y : number) : boolean {
        if (this.chain.length > 1) {
            let currentCoordinate : DrawAndMatchCoordinate = this.getCoordinateAt(x, y);
            let backtrackCoordinate : DrawAndMatchCoordinate = this.getChainBacktrack(); 
            return currentCoordinate.row == backtrackCoordinate.row && currentCoordinate.column == backtrackCoordinate.column;
        }
        return false;
    }

    /* get the chain head */
    private getChainHead() : DrawAndMatchCoordinate {
        return this.chain[this.chain.length - 1];
    }

    /* get chain backtrack item */
    private getChainBacktrack() : DrawAndMatchCoordinate {
        return this.chain[this.chain.length - 2];
    }

    /* check if a coordinate can continue the chain */
    private canContinueChain(x : number, y : number) : boolean {
        let currentCoordinate : DrawAndMatchCoordinate = this.getCoordinateAt(x, y);
        let previusCoordinate : DrawAndMatchCoordinate = this.getChainHead();
        let rowDifference : number = Math.abs(currentCoordinate.row - previusCoordinate.row);
        let columnDifference : number = Math.abs(currentCoordinate.column - previusCoordinate.column);
        return (rowDifference + columnDifference == 1) || (rowDifference == 1 && columnDifference == 1);
    }

    /* check if a coordinate matches chain value */
    private isChainValue(x : number, y : number) : boolean {
        let coordinate : DrawAndMatchCoordinate = this.getCoordinateAt(x, y);
        return this.gameArray[coordinate.row][coordinate.column].value == this.chainValue;
    }

    /* start the chain */
    startChain(x : number, y : number) : DrawAndMatchItem | null {
        if (this.isInputInsideBoard(x, y)) {
            let coordinate : DrawAndMatchCoordinate = this.getCoordinateAt(x, y);
            this.chain.push(coordinate);
            this.chainValue = this.gameArray[coordinate.row][coordinate.column].value;
            return this.gameArray[coordinate.row][coordinate.column].item;
        }
        return null;
    }

    /* check if a coordinate is in the chain */
    private isCoordinateInChain(x : number, y : number) : boolean {
        let result : boolean = false;
        let currentCoordinate : DrawAndMatchCoordinate = this.getCoordinateAt(x, y);
        this.chain.forEach((coordinate : DrawAndMatchCoordinate) => {
            if (currentCoordinate.row == coordinate.row && currentCoordinate.column == coordinate.column) {
                result = true;
            }
        });
        return result;
    }

    /* check if a coordinate is inside enough to be considered an actual item selection */
    private isInsideEnough(x : number, y : number) : boolean {
        let coordinate : DrawAndMatchCoordinate = this.getCoordinateAt(x, y);
        let centerX : number = coordinate.column * this.config.tileSize + this.config.startX + this.config.tileSize / 2;
        let centerY : number = coordinate.row * this.config.tileSize + this.config.startY + this.config.tileSize / 2;
        let distanceX : number = centerX - x;
        let distanceY : number = centerY - y;
        let maxDistance : number = this.config.insideEnough * this.config.tileSize;
        return distanceX * distanceX + distanceY * distanceY <= maxDistance * maxDistance;
    }

    /* get the screen coordinate, given a draw and match coordinate */
    private getCoordinateAt(x : number, y : number) : DrawAndMatchCoordinate {
        return {
            row : Math.floor((y - this.config.startY) / this.config.tileSize),
            column : Math.floor((x - this.config.startX) / this.config.tileSize)
        }; 
    }

    /* remove items */
    removeItems() : DrawAndMatchItem[] {
        this.itemPool = [];
        let items : DrawAndMatchItem[] = [];
        this.chain.forEach((coordinate : DrawAndMatchCoordinate) => {
            this.gameArray[coordinate.row][coordinate.column].item.toBeRemoved = this.chain.length >= this.config.minChain;
            items.push(this.gameArray[coordinate.row][coordinate.column].item);
            if (this.chain.length >= this.config.minChain) {
                this.gameArray[coordinate.row][coordinate.column].empty = true;
                this.itemPool.push(this.gameArray[coordinate.row][coordinate.column].item);
            }
        });
        this.chain = [];
        return items;
    }

    /* 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;
    }

    /* arrange the board after a draw */
    arrangeBoard() : DrawAndMatchItem[] {
        let result : DrawAndMatchItem[] = [];
        for (let i : number = this.config.rows - 2; i >= 0; i --) {
            for (let j : number = 0; j < this.config.columns; j ++) {
                let emptySpaces : number = this.emptySpacesBelow(i, j);  
                if (!this.gameArray[i][j].empty && emptySpaces > 0) {  
                    let item : DrawAndMatchItem = this.gameArray[i][j].item;
                    item.row += emptySpaces;
                    let movement : DrawAndMatchMovement = new DrawAndMatchMovement(item.posX, item.posY, item.posX, item.posY + emptySpaces * this.config.tileSize, emptySpaces, this.config.tileSize);
                    item.posY += emptySpaces * this.config.tileSize;
                    item.movement = movement;
                    result.push(item);
                    let tempTile : DrawAndMatchTile = 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 : DrawAndMatchItem = this.itemPool.shift() as DrawAndMatchItem; 
                    item.value = randomValue;   
                    item.row = j; 
                    item.column = i;
                    let movement : DrawAndMatchMovement = new DrawAndMatchMovement(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, this.config.tileSize);
                    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;
    }
}

export class DrawAndMatchItem {
 
    row : number;
    column : number;
    value : number;
    data : any;
    posX : number;
    posY : number;
    size : number
    centerX : number;
    centerY : number;
    selected : boolean;
    arrowVisible : boolean;
    arrowDirection : number;
    movement : DrawAndMatchMovement;
    toBeRemoved : boolean;
 
    constructor(row : number, column : number, value : number, posX : number, posY : number, size : number) {
        this.row = row;
        this.column = column;
        this.value = value;
        this.posX = posX;
        this.posY = posY;
        this.size = size;
        this.centerX = posX + size / 2;
        this.centerY = posY + size / 2;
        this.selected = false;
        this.arrowVisible = false;
        this.arrowDirection = DrawAndMatchDirection.NONE;
    }

    setData(data : any) : void {
        this.data = data;
    }
}

class DrawAndMatchMovement {
    startX : number;
    startY : number;
    startCenterX : number;
    startCenterY : number;
    endX : number;
    endY : number;
    endCenterX : number;
    endCenterY : number;
    deltaRow : number;
 
    constructor(startX: number, startY : number, endX : number, endY : number, deltaRow : number, size : number) {
        this.startX = startX;
        this.startY = startY;
        this.startCenterX = startX + size / 2;
        this.startCenterY = startY + size / 2;
        this.endX = endX;
        this.endY = endY;
        this.endCenterX = endX + size / 2;
        this.endCenterY = endY + size / 2;
        this.deltaRow = deltaRow;        
    }
}

Thanks to this class, building Draw and Match games will be a lot easier, this time I provided a Phaser example, but next time I will be using another framework to show you how easy is to build these kind of games no matter the tools you are using. Download the source code of the entire project.

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