Talym Myler

Full Stack Engineer

A Software Engineer passionate about building clean, impactful web apps

Home

Cheatle

A fast-paced daily game combining the gameplay of Boggle with elements of Scrabble in a 10-minute experience. Find the 5-highest scoring words before time runs out.

Jump to links

Key features:

  • Game refreshes every midnight with a new randomly generated board
  • User progress is saved on every exit to allow breaks
  • Uses a prefix tree to efficiently store the dictionary of valid words
  • REST API backend hosted on a self-managed linux VPS
Homepage preview of Cheatle

Why this project?

My motivation for this project was to remake a daily game I had been playing but with improved mechanics,especially related to the hint system hence CHEAT-le. This was mostly a passion project so I just used familiar tools but of course I also took the chance to practice some new technologies, and coding patterns. I could honestly still be developing the project to this day but I don't want to get neglect other project for this one so I'm leaving it as is.

How did you do it?

    Express.js logo

    Express

    Node.js logo

    Node.JS

    Digital Ocean logo

    Digital Ocean

    TypeScript logo

    TypeScript

    React logo

    React

    CSS logo

    CSS

Creating the Dictionary

Cheatle is a word game, and like all word games the list of valid words you choose to accept will have an impact on how fun the game is to play. Make the list too complex and players will feel frustration at never getting a chance. Make the list too easy and players won't feel the satisfaction of winning, so finding the right dictionary was something I spent a lot of time on. My eventual solution was to take the Scrabble dictionary and filter it to comply with the rules of Boggle:

  • Words have a minimum length of 3
  • Words have a maximum length of 16 (or 17 if using 'QU')
  • Words with a 'Q' must be followed by a 'U'

I then needed a way to efficiently store this list of valid words, I used a prefix tree representation so that words sharing the same prefix are stored as efficiently as possible. Every word in the list of valid words is parsed character by character, and inserted into the tree as a child node of the letter before it (or the root node for the first character). The node of the last letter in the word is marked as terminal to indicate a valid word is spelt at that point.

The graph below stores "arc", "art", "arch", "arcs", "arts", and "arty".

Prefix tree diagram

Originally I used a more expansive dictionary but the top words were often complex, or non-english. I then tried fixing this problem by creating my own dictionary from the top 20,000 most used english words, then leminizing, adding synonyms, and then comparing to a full dictionary to expand it to include related words. This did solve the problem of solution complexity but there would normally be a few valid words a game that were not present which was very noticeable as a player.

In the end using a Scrabble dictionary seemed like the best balance for the least work.

Generating Game Data

Cheatle is played on a 4x4 grid, with a set of 16 dice which are "rolled" each day to make the game board. The letters on the dice are arranged so that your chance of getting each letter is roughly similar to the distribution of letters in the English language. Tiles like "E", and "A" are more likely to appear, while "Z" and "QU" are rarer.

Graph showing distributions of each tile occurrence on the Cheatle dice

Once the board is generated, we need to find which words can be spelt on that board. This is done by iterating over the board and performing a Depth First Search starting from each tile. At each step, the prefix tree of valid words is traversed using the tile’s letter. If the current node is a valid word, it is added to a list of possible words.

Once the list of possible words has been collected, quicksort is run on it to find the 5th highest scoring element, and it's score: x. A second linear run is then performed on the list to collect all words with a score greater than or equal to x. A heap would likely perform better here but there are usually only around 100-200 words anyway.

Once we have the board, the list of possible words, and the list of highest scoring words, we are ready to serve this data on GET requests to the server.

Saving User Progress

Cheatle is a 10-minute game, but people don't always have a block of 10 uninterrupted minutes to play, so I thought it would be beneficial to add progress saving.

Every time the board is fetched from the server, a key is generated from that day’s letters. This key is then used to query localStorage. If a match is found, the stored game state is loaded, allowing the user to continue where they left off. If the key doesn’t match, the local storage data is cleared to avoid stale data.

Whenever the user leaves the page (detected via beforeunload and visibilitychange event listeners) the progress saving hook collects the game state from various sources using observer pattern callbacks, combines them, and then adds them into local storage.

This hook in particular highlighted the weakness of my design which uses multiple nested hooks to manage game state. If I was remaking the project today I would likely use a single gameState hook to mange the timer, hint system, and gameData.

Deployment

The backend is deployed on a DigitalOcean VPS, running under pm2, with a dedicated user to manage responsibilities. The frontend is deployed via GitHub pages using a GitHub Actions to manage auto-updates.

Where can I see it?

Back to top