Inspiration
Much like Wordle, Spelling Bee, and the crossword, I had often played NYT’s Letter Boxed game. I had often thought of building my own Norwegian version of it, but it wasn’t immediately obvious how such a system would work.
I let it percolate in my brain for a while until I sat down and just started writing code to see what it would take. There were a lot of false-starts, but I eventually wound up somewhere that I was happy with.
Process
At a high level, the way the app works on page load is:
- Construct a board
- Solve the board
- Present the board to the user for them to play
This rather circuitous approach is necessary because there are no guarantees that:
- a random board of 12 letters can be solved; or
- a board constructed from two words cannot be solved with one word
As a result, it was necessary to do the extra steps.
Generating a board
To generate a board, the general flow is outlined below. For the purposes of this text, I’m going to represent a given square like this:
_ _ _ | _ _ _ | _ _ _ | _ _ _
top | right |bottom | left
Each block of three _
characters is a side of the square, in clockwise order.
Initially, there are no letters, so each side is empty.
Pick a random1 word:
ABCDEF...
Take
A
and place it on the top side of the squareA _ _ | _ _ _ | _ _ _ | _ _ _
Take
B
and place it on the right side of the squareA _ _ | B _ _ | _ _ _ | _ _ _
Take
C
and:Create three parallel universes
In the first one, place it on the top side
A C _ | B _ _ | _ _ _ | _ _ _
In the second one, place it on the bottom side
A _ _ | B _ _ | C _ _ | _ _ _
In the fourth one, place it on the left side
A _ _ | B _ _ | _ _ _ | C _ _
In short, we’re putting
C
in all possible positions.Take D and:
For each of our parallel universes, create three more universes
Add
D
into all possible positionsA C _ | B D _ | _ _ _ | _ _ _ A C _ | B _ _ | D _ _ | _ _ _ A C _ | B _ _ | _ _ _ | D _ _ A D _ | B _ _ | C _ _ | _ _ _ A _ _ | B D _ | C _ _ | _ _ _ A _ _ | B _ _ | C _ _ | D _ _ A D _ | B _ _ | _ _ _ | C _ _ A _ _ | B D _ | _ _ _ | C _ _ A _ _ | B _ _ | D _ _ | C _ _
Continue like this for the rest of the letters in the word
If all 12 positions are filled, then we have constructed a puzzle with a known solution (even if suboptimal)
If we ran out of letters in the word, pick another word at random1 – but one which starts with the ending letter of the previous word.
GOTO 1
In theory, this could take a very long time. It’s probably possible to construct pathological inputs to make this take an infinite amount of time – any time you’re looping over randomly-chosen data, it’s a bit suspicious.
However, I built in an escape-hatch: if it’s ever exceeding a certain number of loops, just throw the whole thing away and start fresh. This isn’t perfect, but it’s at least a way to hopefully avoid thrashing the user’s computer.
(Remember: all of this happens in the user’s browser)
Solving a puzzle
Finding the optimal solution to the puzzle is the next challenge. The puzzle was generated from many randomly-chosen words, but it’s probable that there exists a more-optimal solution to the puzzle.
At a high level, the process looks like this:
- Filter the dictionary to words which are playable on a given puzzle
- Words which consist of letters on the square
- Words which do not have consecutive letters on the same side of the square
- Sort the playable words from “best” to “worst”
- If a word has more of the 12 letters than another word, it is better than another word which has fewer.
- If a word has equal numbers of the 12 letters than another word, the shorter word is better than another word which is longer.
- Play all of the words on their own boards
- If any boards are solved, you have found a solution
- The shortest word is the best solution
- For each board, re-sort the words from “best” to “worst”:
- If a word has more of the remaining letters than another word, it is better than another word which has fewer
- If a word is shorter than other word, then it is better than a longer word.
- For each board, create new boards for all of their second-word options
GOTO 4
Just like with the generation process, this has the possibility to end badly. For example, if a board took N words to construct2, the combinatorial explosion of boards being solved in parallel would be terrible for performance.
The escape-hatch for this step is different: I gave the user a “cancel” button and they can just generate a new board.
I’m not happy about this and I am sure that there exists a more efficient solving method, but fixing it hasn’t become a priority for me. In my testing, I see more than 3 word solutions extremely rarely.
Building the app
With those breakthroughs out of the way, all that was left was the app. This was relatively straightforward, though I did learn quite a bit about workers and background processing on the web along the way.
Reflections
This project was exceedingly fun for me. I really enjoyed the process of discovery for generating the board, solving the board, and then showing it to the user. This was an entirely different beast than [Ordle] and [Stavehumle] which were much simpler.
I also really enjoy the style of gameplay: there is no longer a single “correct” answer which you must find. Instead, players can find many valid solutions, and strive to find better ones. If a player has a limited vocabulary (🙋), they won’t be unable to solve it – they just might have a different solution than someone else.