If you don’t live on Mars, you probably know Wordle game. Developed in October 2021 by Josh Wardle, after a couple of months it was acquired by the New York Times “for an undisclosed price in the low-seven figures.”
The gameplay is very easy: Every day, a five-letter word is chosen which players aim to guess within six tries.
After every guess, each letter is marked as either green, yellow or gray: green indicates that letter is correct and in the correct position, yellow means it is in the answer but not in the right position, while gray indicates it is not in the answer at all.
Multiple instances of the same letter in a guess, such as the “o”s in “robot”, will be colored green or yellow only if the letter also appears multiple times in the answer; otherwise, excess repeating letters will be colored gray.
Around the web you can find a lot of Wordle clones and tutorials, so it’s time to publish my take on the game.
In this first step, I am going to show you how to manage user input and handle results.
First of all, let’s see the prototype:
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 use Backspace to delete the last letter, Enter to submit the word or Space to restart the game with a new randomly picked word.
The prototype handles the keyboard input and is able to recognize when a word is incomplete (shorter than five characters) or invalid (not included in the word list).
For each letter, I also say if it’s wrong, correct or perfect.
Let’s have a look at the source code, split into one html file and three 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 : 600, height : 280 } // 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, at the moment only the JSON object with all words.
// 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'); } // 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 keyboard input and result management.
// THE GAME ITSELF // 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 } // 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' }); // keydown linster to call updateWord method this.input.keyboard.on('keydown', this.updateWord, this); } // method to be called each time we need to update a word updateWord(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; } // if the key is backspace and the word has at least one character, // remove the last character if (key == 'Backspace' && this.currentWord.length > 0) { this.currentWord = this.currentWord.slice(0, -1); this.wordText.setText(this.currentWord); return; } // regular expression saying "I want one letter" const regex = /^[a-zA-Z]{1}$/; // if the key is a letter and the word has less than 5 characters, // add the character if (regex.test(key) && this.currentWord.length < 5) { this.currentWord += e.key.toUpperCase(); this.wordText.setText(this.currentWord); return; } // if the key is Enter, it's time to check the result if (key == 'Enter') { // 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'); } } } // 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); } }
Keyboard input and result management has been done in just a few lines, so in next step we can start building the virtual keyboard to be used with tap/click. Download the source code of the entire project, word list included.