Building a word search generator in TypeScript (where every word appears exactly once)
A word search looks like the easiest puzzle in the world to generate. Put the words in a grid, fill the gaps with random letters, done. I wrote that version in an hour. Then I found the bug that version always has, and the generator in Word Search grew into something more careful.
The stack
A quick note on the tech, since every game in this blog shares it:
- TypeScript and Vite, with no UI framework. Plain DOM for menus, a canvas for the board.
- Capacitor to wrap it as an Android app, with plugins for AdMob, purchases (RevenueCat), Firebase Analytics, local notifications, haptics and the in-app review prompt.
- Playwright to drive the game in a headless browser for tests and store screenshots.
A web stack on Android has a reputation for feeling cheap. In my experience it's fine for puzzle games, as long as you respect the phone (more on battery in a much later post). The upside is that every game also runs in a browser, which makes testing on a real phone trivial.
Seeded and deterministic
Every puzzle comes from a string seed:
j:es:42is level 42 of the Spanish journey,d:en:2026-04-28is the English daily puzzle for a date,t:fr:animals:hard:3is round three of the French animals theme on hard.
The same seed builds the same grid on every phone. That gives me a shared daily puzzle with no server, levels that survive reinstalling, and bug reports I can reproduce exactly.
Placing words
The generator places the longest words first, because they're hardest to fit. For each word it tries candidate positions in all allowed directions and scores them, with a bias towards spots that overlap letters already on the grid. Overlaps make a grid feel hand-made and dense rather than words floating in noise.
It also skips word pairs where one contains the other. If the list has both CAT and CATERPILLAR, finding CAT inside CATERPILLAR is ambiguous, so one of them goes.
If a layout can't fit the words, it retries with a new sub-seed (seed#attempt), up to 40 times, and only then drops a word.
Filler letters that look like the language
Uniform random filler looks wrong. A grid full of Q, X and Z is visibly fake, and it makes the real words jump out. So filler letters are drawn by letter frequency for that language. Spanish grids get lots of E, A and O. Each language also has a fold() function for accents, so a player's swipe across "É" matches an "E" in the word list where that language treats them as the same letter.
The bug every simple generator has
Here it is. You place SEAL. Then you fill the remaining cells with random letters, and somewhere else in the grid the filler happens to spell S-E-A-L diagonally. Now the word is in the grid twice. A player finds the "wrong" one, swipes it, and the game says no. To the player the game is broken, and they're right.
With short words and dense grids this isn't rare. Three- and four-letter words turn up by accident all the time, backwards as well.
The fix is a repair pass. After filling, the generator scans the grid in all eight directions for every placed word. If it finds an extra occurrence that uses at least one filler cell, it redraws those filler cells and checks again. If the extra occurrence is made entirely of letters from placed words (it happens with crossings), filler can't fix it, so the whole layout is rerolled. Palindromes like LEVEL need special handling, because the forward and backward readings are the same word.
The guarantee is simple to state: every word appears exactly once, in any direction. The test suite proves it by building about 4,700 puzzles across all languages and difficulties and checking each one.
The difficulty curve
Generation is only half the job. The other half is deciding what to generate. The journey starts tiny and generous so the first minute is all success:
- levels 1–2: 6×6 grids, four words, horizontal and vertical only,
- diagonals arrive between levels 6 and 12,
- backwards words from around level 46,
- all eight directions in the endless phase, with grid sizes cycling in waves so it never becomes a grind.
The biggest grid is 12×12. There's also a "big letters" setting that caps grids at 9×9 for older eyes. It turned out to be one of the most useful things in the game, and I didn't mention it in the store listing for months. Lesson noted.
Content rating starts in the word list
One thing I didn't expect: the content rating depends on the word lists. Google Play's IARC questionnaire asks about alcohol, sexual content and violence. A grid that can contain BEER, or a "body" theme with the wrong words, changes the honest answers. So before the first submission I went through every theme in every language and took those words out. It's a small job, and it's easier than explaining a mature rating on a word search.



Next: Spider Solitaire, the first game forked from this engine, and the first card game.