Talking about Wordle game, Game development, HTML5, Javascript, Phaser and TypeScript.
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.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.