HTML5 Drag and Match engine updated to Phaser 3 using my pure Javascript class to handle Match3 games

Read all posts about "" game

My pure Javascript class to handle Match3 games is being downloaded a lot of times, and it’s easy to figure out why:

Match3 class handles everything happening under the hood and tells the framework how to move items on the screen, so you only need to manage input and animations.

Moreover, the class has been written in pure JavaScript, so it can be used together with any HTML5 framework and follows ECMAScript6 syntax.

I already built a Bejeweled and a Turnellio prototype using the class, so today I extended it a bit to manage Drag and Match games.

Drag and Match games are a little more complex to handle than Match3 games because you move an entire row or an entire column each time, but thanks to my class, I reduced the number of lines from almost 500 of the original prototype to about 250.

Have a look at the game:

Drag rows or columns to match 3 or more gems of the same color.

The source code is uncommented but the class has each method commented for you to understand how it works:

let game;
let gameOptions = {
    gemSize: 100,
    fallSpeed: 200,
    destroySpeed: 400,
    movementOffset: 10
}
window.onload = function() {
    let gameConfig = {
        type: Phaser.AUTO,
        scale: {
            mode: Phaser.Scale.FIT,
            autoCenter: Phaser.Scale.CENTER_BOTH,
            parent: "thegame",
            width: 800,
            height: 600
        },
        scene: playGame
    }
    game = new Phaser.Game(gameConfig);
    window.focus();
}
class playGame extends Phaser.Scene{
    constructor(){
        super("PlayGame");
    }
    preload(){
        this.load.spritesheet("gems", "assets/sprites/gems.png", {
            frameWidth: gameOptions.gemSize,
            frameHeight: gameOptions.gemSize
        });
    }
    create(){
        this.match3 = new Match3({
            rows: 6,
            columns: 8,
            items: 6
        });
        this.match3.generateField();
        this.canPick = true;
        this.canDrag = false;
        this.drawField();
        this.input.on("pointerdown", this.startMoving, this);
        this.input.on("pointermove", this.keepMoving, this);
        this.input.on("pointerup", this.stopMoving, this);
        this.tempGem = this.add.sprite(0, 0, "gems");
        this.tempGem.visible = false;
        this.tempGem.setOrigin(0, 0);
    }
    drawField(){
        this.poolArray = [];
        for(let i = 0; i < this.match3.getRows(); i ++){
            for(let j = 0; j < this.match3.getColumns(); j ++){
                let gemX = gameOptions.gemSize * j;
                let gemY = gameOptions.gemSize * i;
                let gem = this.add.sprite(gemX, gemY, "gems", this.match3.valueAt(i, j));
                gem.setOrigin(0, 0);
                this.match3.setCustomData(i, j, gem);
            }
        }
    }
    startMoving(pointer){
        if(this.canPick){
            this.movingRow = false;
            this.movingCol = false;
            this.canPick = false;
            let row = Math.floor(pointer.y / gameOptions.gemSize);
            let col = Math.floor(pointer.x / gameOptions.gemSize);
            if(this.match3.validPick(row, col)){
                this.match3.setSelectedItem(row, col)
                this.canDrag = true;
            }
            else{
                this.canPick = true;
            }
        }
    }
    keepMoving(pointer){
        if(this.canDrag){
            let vector = new Phaser.Math.Vector2(pointer.x - pointer.downX, pointer.y - pointer.downY);
            if(this.movingRow === false &amp;&amp; this.movingCol === false){
                if(vector.length() > gameOptions.movementOffset){
                    let angle = vector.angle();
                    if((angle >= Math.PI / 4 &amp;&amp; angle <= Math.PI * 3 / 4) || (angle >= Math.PI * 5 / 4 &amp;&amp; angle <= Math.PI * 7 / 4)){
                        this.movingCol = this.match3.getSelectedItem().column;
                    }
                    else{
                        this.movingRow = this.match3.getSelectedItem().row;
                    }
                }
            }
            else{
                this.tempGem.visible = true;
                for(let i = 0; i < this.match3.getRows(); i++){
                    for(let j = 0; j < this.match3.getColumns(); j++){
                        if(i === this.movingRow){
                            this.match3.customDataOf(i, j).x = (j * gameOptions.gemSize + vector.x) % (gameOptions.gemSize * this.match3.getColumns());
                            this.tempGem.y = this.match3.customDataOf(i, j).y;
                            let offset = Math.floor(Math.abs(vector.x) / gameOptions.gemSize);
                            if(vector.x > 0){
                                offset = offset * -1 - 1;
                                this.tempGem.x = vector.x % gameOptions.gemSize - gameOptions.gemSize;
                            }
                            else{
                                this.tempGem.x = vector.x % gameOptions.gemSize;
                            }
                            this.tempGem.setFrame(this.match3.valueAtDelta(this.match3.getSelectedItem().row, 0, 0, offset))
                            if(this.match3.customDataOf(i, j).x < 0){
                                this.match3.customDataOf(i, j).x += gameOptions.gemSize * this.match3.getColumns();
                            }
                        }
                        if(j === this.movingCol){
                            this.match3.customDataOf(i, j).y = (i * gameOptions.gemSize + vector.y) % (gameOptions.gemSize * this.match3.getRows());
                            this.tempGem.x = this.match3.customDataOf(i, j).x;
                            let offset = Math.floor(Math.abs(vector.y) / gameOptions.gemSize);
                            if(vector.y > 0){
                                offset = offset * -1 - 1;
                                this.tempGem.y = vector.y % gameOptions.gemSize - gameOptions.gemSize;
                            }
                            else{
                                this.tempGem.y = vector.y % gameOptions.gemSize;
                            }
                            this.tempGem.setFrame(this.match3.valueAtDelta(0, this.match3.getSelectedItem().column, offset, 0))
                            if(this.match3.customDataOf(i, j).y < 0){
                                this.match3.customDataOf(i, j).y += gameOptions.gemSize * this.match3.getRows();
                            }
                        }
                    }
                }
            }
        }
    }
    stopMoving(pointer){
        if(this.canDrag){
            this.canDrag = false;
            let vector = new Phaser.Math.Vector2(pointer.upX - pointer.downX, pointer.upY - pointer.downY);
            this.gemsToMove = [];
            let movement = new Phaser.Math.Vector2(0, 0);
            if(this.movingCol !== false){
                let offset = Math.round(vector.y / gameOptions.gemSize);
                let gemsToMove = this.match3.shiftColumnBy(this.movingCol, offset);
                gemsToMove.forEach(function(gem){
                    if(Math.abs(this.match3.customDataOf(gem.row, gem.column).y - gem.row * gameOptions.gemSize) > gameOptions.gemSize){
                        let temp = this.match3.customDataOf(gem.row, gem.column).y;
                        this.match3.customDataOf(gem.row, gem.column).y = this.tempGem.y;
                        this.tempGem.y = temp;
                    }
                    this.gemsToMove.push(this.match3.customDataOf(gem.row, gem.column));
                }.bind(this));
                let destination = (this.tempGem.y < 0) ? -gameOptions.gemSize : this.match3.getRows() * gameOptions.gemSize;
                movement.y = destination - this.tempGem.y;
            }
            if(this.movingRow !== false){
                let offset = Math.round(vector.x / gameOptions.gemSize);
                let gemsToMove = this.match3.shiftRowBy(this.movingRow, offset);
                gemsToMove.forEach(function(gem){
                    if(Math.abs(this.match3.customDataOf(gem.row, gem.column).x - gem.column * gameOptions.gemSize) > gameOptions.gemSize){
                        let temp = this.match3.customDataOf(gem.row, gem.column).x;
                        this.match3.customDataOf(gem.row, gem.column).x = this.tempGem.x;
                        this.tempGem.x = temp;
                    }
                    this.gemsToMove.push(this.match3.customDataOf(gem.row, gem.column));
                }.bind(this));
                let destination = (this.tempGem.x < 0) ? -gameOptions.gemSize : this.match3.getColumns() * gameOptions.gemSize;
                movement.x = destination - this.tempGem.x;
            }
            this.gemsToMove.push(this.tempGem);
            this.tweens.add({
                targets: this.gemsToMove,
                props: {
                    y: {
                        value: "+=" + movement.y
                    },
                    x: {
                        value: "+=" + movement.x
                    }
                },
                duration: Math.abs(gameOptions.destroySpeed / gameOptions.gemSize * (movement.x + movement.y)),
                callbackScope: this,
                onComplete: function(event, sprite){
                    this.tempGem.visible = false;
                    this.handleMatches();
                }
            });
        }
    }
    handleMatches(){
        if(this.match3.matchInBoard()){
            let gemsToRemove = this.match3.getMatchList();
            let destroyed = 0;
            gemsToRemove.forEach(function(gem){
                this.poolArray.push(this.match3.customDataOf(gem.row, gem.column))
                destroyed ++;
                this.tweens.add({
                    targets: this.match3.customDataOf(gem.row, gem.column),
                    alpha: 0,
                    duration: gameOptions.destroySpeed,
                    callbackScope: this,
                    onComplete: function(event, sprite){
                        destroyed --;
                        if(destroyed == 0){
                            this.makeGemsFall();
                        }
                    }
                });
            }.bind(this));
        }
        else{
            this.canPick = true;
        }
    }
    makeGemsFall(){
        let moved = 0;
        this.match3.removeMatches();
        let fallingMovements = this.match3.arrangeBoardAfterMatch();
        fallingMovements.forEach(function(movement){
            moved ++;
            this.tweens.add({
                targets: this.match3.customDataOf(movement.row, movement.column),
                y: this.match3.customDataOf(movement.row, movement.column).y + movement.deltaRow * gameOptions.gemSize,
                duration: gameOptions.fallSpeed * Math.abs(movement.deltaRow),
                callbackScope: this,
                onComplete: function(){
                    moved --;
                    if(moved == 0){
                        this.endOfMove()
                    }
                }
            })
        }.bind(this));
        let replenishMovements = this.match3.replenishBoard();
        replenishMovements.forEach(function(movement){
            moved ++;
            let sprite = this.poolArray.pop();
            sprite.alpha = 1;
            sprite.y = gameOptions.gemSize * (movement.row - movement.deltaRow);
            sprite.x = gameOptions.gemSize * movement.column,
            sprite.setFrame(this.match3.valueAt(movement.row, movement.column));
            this.match3.setCustomData(movement.row, movement.column, sprite);
            this.tweens.add({
                targets: sprite,
                y: gameOptions.gemSize * movement.row,
                duration: gameOptions.fallSpeed * movement.deltaRow,
                callbackScope: this,
                onComplete: function(){
                    moved --;
                    if(moved == 0){
                        this.endOfMove()
                    }
                }
            });
        }.bind(this))
    }
    endOfMove(){
        if(this.match3.matchInBoard()){
            this.time.addEvent({
                delay: 250,
                callback: this.handleMatches()
            });
        }
        else{
            this.canPick = true;
            this.selectedGem = null;
        }
    }
}

class Match3{

    // constructor, simply turns obj information into class properties
    constructor(obj){
        if(obj == undefined){
            obj = {}
        }
        this.rows = (obj.rows != undefined) ? obj.rows : 8;
        this.columns = (obj.columns != undefined) ? obj.columns : 7;
        this.items = (obj.items != undefined) ? obj.items : 6;
    }

    // generates the game field
    generateField(){
        this.gameArray = [];
        this.selectedItem = false;
        for(let i = 0; i < this.rows; i ++){
            this.gameArray[i] = [];
            for(let j = 0; j < this.columns; j ++){
                do{
                    let randomValue = Math.floor(Math.random() * this.items);
                    this.gameArray[i][j] = {
                        value: randomValue,
                        isLocked: false,
                        isEmpty: false,
                        row: i,
                        column: j
                    }
                } while(this.isPartOfMatch(i, j));
            }
        }
    }

    // locks a random Item and returns item coordinates, or false
    lockRandomItem(){
        let unlockedItems = [];
        for(let i = 0; i < this.rows; i ++){
            for(let j = 0; j < this.columns; j ++){
                if(!this.isLocked(i, j)){
                    unlockedItems.push({
                        row: i,
                        column: j
                    })
                }
            }
        }
        if(unlockedItems.length > 0){
            let item = unlockedItems[Math.floor(Math.random() * unlockedItems.length)];
            this.lockAt(item.row, item.column)
            return item;
        }
        return false;
    }

    // returns a random row number
    randomRow(){
        return Math.floor(Math.random() * this.rows);
    }

    // returns a random column number
    randomColumn(){
        return Math.floor(Math.random() * this.columns);
    }

    // locks the item at row, column
    lockAt(row, column){
        this.gameArray[row][column].isLocked = true;
    }

    // returns true if item at row, column is locked
    isLocked(row, column){
        return this.gameArray[row][column].isLocked;
    }

    // returns true if there is a match in the board
    matchInBoard(){
        for(let i = 0; i < this.rows; i ++){
            for(let j = 0; j < this.columns; j ++){
                if(this.isPartOfMatch(i, j)){
                    return true;
                }
            }
        }
        return false;
    }

    // returns true if the item at (row, column) is part of a match
    isPartOfMatch(row, column){
        return this.isPartOfHorizontalMatch(row, column) || this.isPartOfVerticalMatch(row, column);
    }

    // returns true if the item at (row, column) is part of an horizontal match
    isPartOfHorizontalMatch(row, column){
        return this.valueAt(row, column) === this.valueAt(row, column - 1) &amp;&amp; this.valueAt(row, column) === this.valueAt(row, column - 2) ||
                this.valueAt(row, column) === this.valueAt(row, column + 1) &amp;&amp; this.valueAt(row, column) === this.valueAt(row, column + 2) ||
                this.valueAt(row, column) === this.valueAt(row, column - 1) &amp;&amp; this.valueAt(row, column) === this.valueAt(row, column + 1);
    }

    // returns true if the item at (row, column) is part of an horizontal match
    isPartOfVerticalMatch(row, column){
        return this.valueAt(row, column) === this.valueAt(row - 1, column) &amp;&amp; this.valueAt(row, column) === this.valueAt(row - 2, column) ||
                this.valueAt(row, column) === this.valueAt(row + 1, column) &amp;&amp; this.valueAt(row, column) === this.valueAt(row + 2, column) ||
                this.valueAt(row, column) === this.valueAt(row - 1, column) &amp;&amp; this.valueAt(row, column) === this.valueAt(row + 1, column)
    }

    // increments the value of the item
    incValueAt(row, column){
        this.gameArray[row][column].value = (this.gameArray[row][column].value + 1) % this.items
    }

    // returns the value of the item at (row, column), or false if it's not a valid pick
    valueAt(row, column){
        if(!this.validPick(row, column)){
            return false;
        }
        return this.gameArray[row][column].value;
    }

    // returns the value of the item at (row + deltaRow, column + deltaColumn), wrapping around the array if necessary, or false if (row, column) is not a valid pick
    valueAtDelta(row, column, deltaRow, deltaColumn){
        if(!this.validPick(row, column)){
            return false;
        }
        let destinationRow = ((row + deltaRow) % this.getRows() + this.getRows()) % this.getRows();
        let destinationColumn = ((column + deltaColumn) % this.getColumns() + this.getColumns()) % this.getColumns();
        return this.valueAt(destinationRow, destinationColumn);
    }

    // returns true if the item at (row, column) is a valid pick
    validPick(row, column){
        return row >= 0 &amp;&amp; row < this.rows &amp;&amp; column >= 0 &amp;&amp; column < this.columns &amp;&amp; this.gameArray[row] != undefined &amp;&amp; this.gameArray[row][column] != undefined;
    }

    // outputs the values to console, useful for debugging
    logValues(){
        let output = "";
        for(let i = 0; i < this.getRows(); i++){
            for(let j = 0; j < this.getColumns(); j++){
                output += this.valueAt(i, j);
                output += " ";
            }
            output += "\n";
        }
        console.log(output);
    }

    // shifts a column by "offset" amount, returns an array with new gems position
    shiftColumnBy(column, offset){
        let resultArray = [];
        let tempArray = [];
        let moveArray = [];
        for(let i = 0; i < this.getRows(); i++){
            tempArray[i] = Object.assign(this.gameArray[i][column]);
        }
        for(let i = 0; i < this.getRows(); i++){
            let actualShift = ((i - offset) % this.getRows() + this.getRows()) % this.getRows();
            this.gameArray[i][column] = Object.assign(tempArray[actualShift]);
            moveArray.push({
                row: i,
                column: column,
                deltaRow: offset,
                deltaColumn: 0
            });
        }
        return moveArray;
    }

    // shifts a column by "offset" amount, returns an array with new gems position
    shiftRowBy(row, offset){
        let resultArray = [];
        let tempArray = [];
        let moveArray = [];
        for(let i = 0; i < this.getColumns(); i++){
            tempArray[i] = Object.assign(this.gameArray[row][i]);
        }
        for(let i = 0; i < this.getColumns(); i++){
            let actualShift = ((i - offset) % this.getColumns() + this.getColumns()) % this.getColumns();
            this.gameArray[row][i] = Object.assign(tempArray[actualShift]);
            moveArray.push({
                row: row,
                column: i,
                deltaRow: 0,
                deltaColumn: offset
            });
        }
        return moveArray;
    }

    // returns the number of board rows
    getRows(){
        return this.rows;
    }

    // returns the number of board columns
    getColumns(){
        return this.columns;
    }

    // sets a custom data on the item at (row, column)
    setCustomData(row, column, customData){
        this.gameArray[row][column].customData = customData;
    }

    // returns the custom data of the item at (row, column)
    customDataOf(row, column){
        return this.gameArray[row][column].customData;
    }

    // returns the selected item
    getSelectedItem(){
        return this.selectedItem;
    }

    // set the selected item as a {row, column} object
    setSelectedItem(row, column){
        this.selectedItem = {
            row: row,
            column: column
        }
    }

    // deleselects any item
    deleselectItem(){
        this.selectedItem = false;
    }

    // checks if the item at (row, column) is the same as the item at (row2, column2)
    areTheSame(row, column, row2, column2){
        return row == row2 &amp;&amp; column == column2;
    }

    // returns true if two items at (row, column) and (row2, column2) are next to each other horizontally or vertically
    areNext(row, column, row2, column2){
        return Math.abs(row - row2) + Math.abs(column - column2) == 1;
    }

    // swap the items at (row, column) and (row2, column2) and returns an object with movement information
    swapItems(row, column, row2, column2){
        let tempObject = Object.assign(this.gameArray[row][column]);
        this.gameArray[row][column] = Object.assign(this.gameArray[row2][column2]);
        this.gameArray[row2][column2] = Object.assign(tempObject);
        return [{
            row: row,
            column: column,
            deltaRow: row - row2,
            deltaColumn: column - column2
        },
        {
            row: row2,
            column: column2,
            deltaRow: row2 - row,
            deltaColumn: column2 - column
        }]
    }

    // return the items part of a match in the board as an array of {row, column} object
    getMatchList(){
        let matches = [];
        for(let i = 0; i < this.rows; i ++){
            for(let j = 0; j < this.columns; j ++){
                if(this.isPartOfMatch(i, j)){
                    matches.push({
                        row: i,
                        column: j
                    });
                }
            }
        }
        return matches;
    }

    // removes all items forming a match
    removeMatches(){
        let matches = this.getMatchList();
        matches.forEach(function(item){
            this.setEmpty(item.row, item.column)
        }.bind(this))
    }

    // set the item at (row, column) as empty
    setEmpty(row, column){
        this.gameArray[row][column].isEmpty = true;
    }

    // returns true if the item at (row, column) is empty
    isEmpty(row, column){
        return this.gameArray[row][column].isEmpty;
    }

    // returns the amount of empty spaces below the item at (row, column)
    emptySpacesBelow(row, column){
        let result = 0;
        if(row != this.getRows()){
            for(let i = row + 1; i < this.getRows(); i ++){
                if(this.isEmpty(i, column)){
                    result ++;
                }
            }
        }
        return result;
    }

    // arranges the board after a match, making items fall down. Returns an object with movement information
    arrangeBoardAfterMatch(){
        let result = []
        for(let i = this.getRows() - 2; i >= 0; i --){
            for(let j = 0; j < this.getColumns(); j ++){
                let emptySpaces = this.emptySpacesBelow(i, j);
                if(!this.isEmpty(i, j) &amp;&amp; emptySpaces > 0){
                    this.swapItems(i, j, i + emptySpaces, j)
                    result.push({
                        row: i + emptySpaces,
                        column: j,
                        deltaRow: emptySpaces,
                        deltaColumn: 0
                    });
                }
            }
        }
        return result;
    }

    // replenished the board and returns an object with movement information
    replenishBoard(){
        let result = [];
        for(let i = 0; i < this.getColumns(); i ++){
            if(this.isEmpty(0, i)){
                let emptySpaces = this.emptySpacesBelow(0, i) + 1;
                for(let j = 0; j < emptySpaces; j ++){
                    let randomValue = Math.floor(Math.random() * this.items);
                    result.push({
                        row: j,
                        column: i,
                        deltaRow: emptySpaces,
                        deltaColumn: 0
                    });
                    this.gameArray[j][i].value = randomValue;
                    this.gameArray[j][i].isEmpty = false;
                    this.gameArray[j][i].isLocked = false;
                }
            }
        }
        return result;
    }
}

I will keep adding features to the class, so download the source code and stay tuned.

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