Talym Myler
Full Stack EngineerA Software Engineer passionate about building clean, impactful web apps
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 linksKey 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

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
Node.JS
Digital Ocean
TypeScript
React
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".
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.
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.