Wordle clone

16 min. read

I guess we are all sad from the news that the NYT has bought Wordle...

Since then a lot of people have created clones of it, some of them with some twist: like heardle, in which you have to guess the song that is playing, or colordle, where you have to guess the HEX representation of a color. There is even a custom wordle that allows you to choose your own word to send to your friends. And you also have Absurdle, Dordle, Quordle, Octordle, Sedecordle, Lewdle, Primel, Sweardle, Queerdle, Star Wordle, Worldle, Globe...

I wonder how many of these will still stand 20 years from now 😄.

A lot has been said about its success being due to it being a plain web app, with no trackers, no adds, no popups, no bullshit. Just like the web should be. That resonates with me, so I thought I'll give it a try and implement my own wordle, just for fun.

Screenshot of the Wordle clone

You can check out the details of this project in the dedicated portfolio page.

Set up

For the frontend, I used my HTML scafold repository as a starting point. I keep updating it and changing things here and there, but basically it sets up a vanilla JavaScript webapp using Jasmine standalone as the testing framework, and gulp as a task runner. It also uses sass for the styles.

It has tasks for concatenating, minimizing and watching files, as well as creating useful sourcemaps. For the CSS it also runs autoprefixer to add only the vendor prefixes that are needed.

Finally, there is a task that runs a browser sync server, to serve the page that runs the tests.

I am really happy that I manage to keep the JavaScript below 10kB, and the CSS too. In total, 11 requests, 112.53 kB transferred, and takes 1.65s to load. This can be improved, but is not bad for a webapp.

Deploy

Since this is mostly a JavaScript app, I thought I could use GitHub pages to serve it. GitHub will automatically serve anything you put in an orphan gh-pages branch.

This is ideal for a project where you have dev code that you want to track with version control but is not your site, and code generated by your dev code that you want to track and is your site. You can keep track of changes to both by keeping the dev code in the main branch and the generated code in the gh-pages branch.

How to do this? I tell gulp to generate the final site in a folder called site. Before I build the site I go inside of this folder and clone the main branch there, then I create the orphan branch:


cd site
git clone git@github.com:octopusinvitro/wordle-js.git .
git checkout --orphan gh-pages

The files from the main branch will be there so they need to be removed. That's dev code. This folder is for site code.

Then we get out of the folder and build the site normally. This will generate the site files inside of the site folder. We can enter the folder again, commit all the site files and push.

The site folder will always be stuck in the gh-pages branch, forever. Every time a file changes, the site is regenerated, and we enter the site folder to commit and push to gh-pages. GitHub will then serve the updated site.

Meanwhile, the root directory of the app is never in the gh-pages branch, so that you can track changes to the dev code that generates the site. You will commit and push to the main branch from here, or to other branches.

This is all explained in the README.

The game

The first thing I wanted to solve was to find an API that would allow me to ask for a 5 letter word everyday. I spent some time on this and couldn't find a good solution. So I decided to leave this for later and start building something with a hardcoded set of words.

I first focused on the basic functionality of the game. I ended up with these classes:

  • Board: The class to represent the board and operations in the board like getting the word from a row, checking if a row is full, or is the current row, etc.

  • Game: Checks for win or game-over, receives a word and checks which letters are present, absent or correct, etc.

  • GameUI: Deals with DOM operations on the elements that are the visual representation of the board and the keyboard, sets event listeners, etc.

I also created a Selectors class, I often do this to keep my selectors organized and so that I can use them both in tests and the code.

For the words, since I am still searching for a good API, I just hardcoded a bunch of words into a words.js file, and the game selects one randomly on every refresh.

I didn't add any logic yet to constrain the game to one word a day, so if you want to play again, you just have to refresh the page.

The tests all use a board with just two rows, as that is enough to test the logic.

Working with the board I learnt that I can use slice() to get the last element of an array:


array.slice(-1);

And string | 0 to easily convert something to an integer. I like to keep learning these little things after so many years coding in JavaScript!

The UI

The wordle page not only has the game, it also has a help section, stats, and settings. At the moment of writing this, I finished the help and stats section, but not the settings.

I ended up with this classes:

  • Modal: Deals with DOM operations related with the upper menu buttons, which change the visibility and position of specific sections in the page.

  • ModalStats: Extends the modal class for the needs of the modal that shows the stats, since that one is a bit different. It uses the clipboard API to copy the shareable board to the user's clipboard.

  • State: JavaScript doesn't have enums, which would be amazing for representing the state of a tile, so I tried to implement an enum here. I didn't want to have lots of conditional code like if (state === 'correct') { return '🟩'}, so I put that knowledge in this class.

  • Stats: Calculates the stats that you see when you click the stats button. It uses the localStorage API to read and store the stats in the browser. It defines a default stats structure, for the first time the game is played, then it reads from localStorage and does a naive validation of the fields.

I haven't worked much with the localStorage or clipboard APIs before, so it was nice to have an app where I can play with them. They seem pretty straight-forward to use, and since I am running the Jasmine standalone library, the tests related with localStorage can be easily run directly against the browser, which is great.

The clipboard API has been changing though, so some browsers may not support the new method signatures yet.

Old clipboard syntax:


document.execCommand('copy');
document.execCommand('paste');

New clipboard syntax:


navigator.clipboard.writeText();
navigator.clipboard.readText();

In order to copy things to the clipboard, you also need a manifest file in your root directory with the relevant permissions:


{
  "permissions": [
    "clipboardRead",
    "clipboardWrite"
  ]
}

MDN has an article that explains it in detail. I decided to use the new syntax as the old one may be deprecated soon.

Since the Game is the one that holds the knowledge of which tiles in the board where correct, absent or present, it made sense that the method to generate the shareable board lived there.

For my attempt at creating an enum, I have the State class define three constants that store instances of State, so I can define methods on them:


static CORRECT = new State('correct');

That way I can pretend I have an enum like State.CORRECT, but can also call State.CORRECT.tile or State.CORRECT.name, which is neat.

The first time you play, or if the user has deleted the local storage, there are no stats to read, so the default structure is returned. In the Stats class, I would have used JSON transformations to get a deep independent copy (including child objects):


let deepIndependentCopy = JSON.parse(JSON.stringify(original));
However I decided to use the spread operator, for a cleaner (and maybe more performant) solution:

let stats = JSON.parse(localStorage.getItem(Stats.KEY));

if (!stats) {
  stats = { ...Stats.DEFAULT };
  stats.guesses = { ...Stats.DEFAULT.guesses };
}

This creates an independent copy of the default structure and the guesses object inside that structure before returning it, so that we don't accidentally modify the default structure. Another clean option would be Object.assign(copy, original);, but the child objects also need to be copied separately. If I had a more complicated structure I would use JSON.

In the ModalStats class there is a share button that when clicked, generates a shareable version of the board. But because clicking on the modal closes the modal, we need to handle the button click like this:


this.shareButton.addEventListener('click', (event) => {
  event.preventDefault();
  event.stopPropagation();
  this.copyToClipboard();
});

This avoids closing the modal when clicking the share button.

Comments