Build a HTML5 game like Wordle using Phaser and TypeScript – Step 2: add a virtual keyboard

Read all posts about "" game

Here we go with the second step of Wordle HTML5 prototype using Phaser and TypeScript.

In first step we covered keyboard input and result management, now it’s time to add a virtual keyboard to let people play from mobile devices.

This is what I built:

In yellow, the word you have to guess, randomly picked from the official list of Wordle words.

In cyan, the word you want to write. Use the keyboard to write the word. You can play with your physical keyboard, using Backspace to delete the last letter, Enter to submit the word or Space to restart the game with a new randomly picked word.

Or you can play with the brand new virtual keyboard added in this step.

I used two images for the keys, and a bitmap font for the letters printed on the keys.

I found SnowB BMF to be the best online bitmap font generator, but if you have suggestions, let me know.

Let me show you the completely commented source code, made of 5 TypeScript files and one HTML file:

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 : 700,
    height : 500
}

// 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, such as the JSON object with all words, the images and the bitmap font.

// 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 is how we preload a JSON object
        this.load.json('words', 'assets/words.json');

        // preload small and big key images
        this.load.image('key', 'assets/key.png');
        this.load.image('bigkey', 'assets/bigkey.png');

        // this is how we preload a bitmap font
        this.load.bitmapFont('font', 'assets/font.png', 'assets/font.fnt');
	}

    // 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 result management.

// THE GAME ITSELF

import KeyboardKey from "./keyboardKey";

// possible word states:
// perfect, when the letter is in the right position
// correct, when the letter exists but it's not in the right position
// wrong, when the letter does not exist
enum letterState {
    PERFECT,
    CORRECT,
    WRONG  
}

// keyboard layout, as a string array, each item is a row of keys
// > represents Enter
// < represents Backspace
const keyboardLayout : string[] = ['QWERTYUIOP','ASDFGHJKL','>ZXCVBNM<'];

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

    // array with all possible words
    words : string[];

    // text object to show the word we are writing
    wordText : Phaser.GameObjects.Text;

    // text object to display the result
    resultText : Phaser.GameObjects.Text;

    // string where to store the current word
    currentWord : string;

    // string where to store the word to guess
    wordToGuess : string;

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

    create() : void {

        // store JSON loaded words into words array
        this.words = this.cache.json.get('words');

        // at the beginning, current word is empty
        this.currentWord = '';

        // pick a random word to guess
        this.wordToGuess = this.words[Phaser.Math.Between(0, this.words.length - 1)].toUpperCase();

        // just a static text
        this.add.text(10, 10, 'Word to guess: ' + this.wordToGuess, {
            font : '32px arial',
            color : '#ffff00'
        }); 

        // another static text
        let staticText : Phaser.GameObjects.Text = this.add.text(10, 50, 'Your word: ', {
            font : '32px arial',
            color : '#00ffff'
        });

        // this is the text we are going to write
        this.wordText = this.add.text(staticText.getBounds().right, 50, '', {
            font : '32px arial'
        });

        // this is the text where to display the result
        this.resultText = this.add.text(10, 90, '', {
            font : '32px arial',
            color : '#ff00ff'
        });

        // loop through keyboardLayout array
        keyboardLayout.forEach((row : string, index : number) => {
            
            // determine position of key sprites
            // some values are still hardcoded, and need to be optimized
            let rowWidth : number = 64 * row.length;
            let firstKeyPosition : number = (this.game.config.width as number - rowWidth) / 2;

            // loop through string
            for (let i : number = 0; i < row.length; i ++) {

                // get the i-th character
                let letter : string = row.charAt(i);

                // add the keyboard key
                new KeyboardKey(this, firstKeyPosition + i * 64 - (letter == '>' ? 31 : 0), 300 + index * 64, row.charAt(i));
            }
        });

        // keydown linster to call onKeyDown method
        this.input.keyboard.on('keydown', this.onKeyDown, this);
    }
    
    // method to process a key pressed
    onKeyDown(e : KeyboardEvent) : void {

        // store key pressed in key variable
        var key : string = e.key;
        
        // if the key is space, restart the game
        if (key == ' ') {
            this.scene.start('PlayGame');
            return;
        }

        // backspace
        if (key == 'Backspace') {
            this.updateWord('<');
            return;
        }

        // regular expression saying "I want one letter"
        const regex = /^[a-zA-Z]{1}$/;

        // letter a-z or A-Z
        if (regex.test(key)) {
            this.updateWord(key);
            return;
        }

        // enter
        if (key == 'Enter') {
            this.updateWord('>');
        }
    }

    //method to be called each time we need to update a word
    updateWord(s : string) : void {
        switch(s) {

            // backsace
            case '<' :

                // if the word has at least one character, remove the last character
                if (this.currentWord.length > 0) {
                    this.currentWord = this.currentWord.slice(0, -1);
                    this.wordText.setText(this.currentWord);
                }
                break;

            // enter
            case '>' :
                
                // if the word is 5 characters long, proceed to verify the result
                if (this.currentWord.length == 5) {

                    // if the word is a valid word, proceed to verify the result
                    if (this.words.includes(this.currentWord.toLowerCase())) {

                        // at the beginning we se the result as a series of wrong characters
                        let result : number[] = Array(5).fill(letterState.WRONG);

                        // creation of a temp string with the word to guess
                        let tempWord : string = this.wordToGuess;

                        // loop through all word characters
                        for (let i : number = 0; i < 5; i ++) {

                            // do i-th char of the current word and i-th car of the word to guess match?
                            if (this.currentWord.charAt(i) == tempWord.charAt(i)) {

                                // this is a PERFECT result
                                result[i] = letterState.PERFECT;

                                // remove the i-th character from temp word
                                tempWord = this.removeChar(tempWord, i);
                            }

                            // i-th char of the current word and i-th car of the word to guess do not match
                            else {

                                // loop through all character of the word to guess
                                for (let j : number = 0; j < 5; j ++) {

                                    // do i-th char of the current word and j-th car of the word to guess match,
                                    // and don't j-th char of the current word and j-th car of the word to guess match?
                                    if (this.currentWord.charAt(i) == this.wordToGuess.charAt(j) && this.currentWord.charAt(j) != this.wordToGuess.charAt(j)) {
                                        
                                        // this is a correct result
                                        result[i] = letterState.CORRECT;

                                        // remove the i-th character from temp word
                                        tempWord = this.removeChar(tempWord, j);
                                        break;    
                                    }
                                }   
                            }    
                        }

                        // time to show the result
                        let resultString : string = '';

                        // loop through all result items and compose result string accordingly
                        result.forEach((element : number, index : number) => {
                            resultString += this.currentWord.charAt(index) + ' : ';
                            switch (element) {
                                case letterState.WRONG :
                                    resultString += 'wrong letter';
                                    break;
                                case letterState.CORRECT :
                                    resultString += 'right letter in wrong position';
                                    break;
                                case letterState.PERFECT : 
                                    resultString += 'PERFECT letter';
                            }
                            resultString += '\n';
                        });

                        // display result string
                        this.resultText.setText(resultString);
                    }

                    // if the word is not a valid word, display the error message
                    else {
                        this.resultText.setText('Not a valid word');    
                    }
                }

                // if the word is not a 5 letters word, display the error message
                else {
                    this.resultText.setText('Not a 5 letters word');
                }
                break;
            
            // a-z or A-Z
            default :

                // if the word is less than 5 characters long, remove last character
                if (this.currentWord.length < 5) {
                    this.currentWord += s.toUpperCase();
                    this.wordText.setText(this.currentWord);
                }                
        }
    }

    // simple method to change the index-th character of a string with '_'
    // just to have an unmatchable character
    removeChar(initialString : string, index : number) : string {
        return initialString.substring(0, index) + '_' + initialString.substring(index + 1);
    }
}

keyboardKey.ts

Custom class for the virtual keyboard key.

// KEYBOARD KEY CLASS

import { PlayGame } from "./playGame";
import KeyboardLetter from "./keyboardLetter";

// this class extends Sprite class
export default class KeyboardKey extends Phaser.GameObjects.Sprite {

    // letter bound to the key
    boundLetter : string;

    // parent scene
    parentScene : PlayGame;


    constructor(scene : PlayGame, x : number, y : number, letter : string) {

        // different image key according if it's a letter character or '<' or '>'
        super(scene, x, y, '<>'.includes(letter) ? 'bigkey' : 'key');

        // assign parent scene
        this.parentScene = scene;

        // assign bound letter
        this.boundLetter = letter;

        // set sprite registration point to top, left
        this.setOrigin(0);

        // add the sprite to the scene
        scene.add.existing(this); 
        
        // set the sprite interactive
        this.setInteractive();

        // listener for pointer down on the sprite, to call handlePointer callback
        this.on('pointerdown', this.handlePointer);

        // add a keyboard letter accoring to 'letter value
        switch(letter) {
            case '<' :
                new KeyboardLetter(scene, x + 10, y + 10, 'DELETE', 18);
                break;
            case '>' :
                new KeyboardLetter(scene, x + 10, y + 10, 'SUBMIT', 18);
                break;
            default :
                new KeyboardLetter(scene, x + 10, y + 10, letter, 36);
        }
    }

    // method to be called when the user clicks or taps the letter
    handlePointer() : void {

        // call 'updateWord' method on parent scene
        this.parentScene.updateWord(this.boundLetter);
    }
}

keyboardLetter.ts

Custom class for the letter to be printed on the virtual keyboard key.

// KEYBOARD LETTER

import { PlayGame } from "./playGame";

// this class extends BitmapText class
export default class KeyboardLetter extends Phaser.GameObjects.BitmapText {
    constructor(scene : PlayGame, x : number, y : number, text : string, size : number) {
        super(scene, x, y, 'font', text, size);
        scene.add.existing(this);
    }
}

Adding the virtual keyboard was easy, and ironically it’s the most complicated part of the development of this game.

Next time, we’ll see the final prototype, meanwhile download the source code.

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

215 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
// Stairs
// 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