We run a small site of free daily browser games called PuzzleMint. One of the most-played is Scramble: five jumbled words a day, easy to hard, and you swap the letter tiles back into place. It's a single HTML page plus one generated data file. No framework, no backend, no accounts.
Here's how it's put together, and a few decisions that turned out to matter more than the code.
The word bank is built, not hand-typed
The game reads one file, words.js, generated by a Python script. Nobody edits it by hand. The inputs are:
- Hand-picked category lists, one text file per category (
animals.txt,cricket.txt,desi-food.txt…). Each has a name, a region (both,indiaorglobal), a clue line shown above the tiles, and easy / medium / hard lists. - Film titles reused from our party games' content (Bollywood, Hollywood, animated), where an existing 1–3 difficulty maps to easy / medium / hard.
- "Everyday words" pulled from
wordfreq: common English base words of 4 to 9 letters above a frequency threshold.
Clues come from WordNet, but only when the sense matches the category. "Crane" in Animals should get the bird definition, not the construction machine, so each category maps to allowed WordNet lexicographer files (noun.animal, noun.food, noun.location…). Films get a simple "Hindi film, 2001"-style clue instead.
The script also enforces a few rules:
- letters and spaces only, at least 4 letters, at most 12 letters per word (a phone row fits about 12 tiles)
- every entry appears once; the first category wins
- a blocklist keeps anything crude, grim or sensitive out of a family word game
Result: 6,000+ entries in 27 categories.
One answer per daily word
Anagrams are the classic trap. LISTEN, SILENT and TINSEL are all "correct", so which one did the game mean?
The build checks every single word for other real arrangements (again using wordfreq, with a lower frequency threshold). If it finds any, it stores them as alts:
- In Endless mode the game accepts any of them as a solve.
- Words with alts are never picked for the daily five, so the shared daily always has exactly one answer.
The daily is append-only
Everyone gets the same five words each day, so people can compare times. The schedule is precomputed: day 1 is a fixed date, and the build fills 365 days ahead.
The important rule: the daily list is append-only. Re-running the build keeps every existing day's words and only adds new days at the end. If a later edit removes a word that a past or upcoming daily already uses, the build fails loudly. Without that, fixing a typo in a category file could silently change yesterday's puzzle, and someone's streak would no longer match what they actually played.
The page works out the day number from the calendar date and looks up that day's five. Players in India get a slightly different daily with a few India-only words (desi food, cricket, Bollywood). A small region script picks the India or Global set before the page paints, and there's a visible switch to change it.
Tiles: tap to swap, or drag
On phones, drag-and-drop on small tiles is fiddly, so the primary interaction is tap a tile, then tap another to swap them. Drag is supported as well, using Pointer Events (pointerdown / pointermove / pointerup), so mouse, touch and pen go through one code path.
A few small touches made a difference:
- A short haptic buzz via
navigator.vibrateon phones that support it. - When every tile is in place, the row turns green, with no "submit" button.
- Hints are deliberately gentle: Clue (meaning), Letter (locks one tile in its correct place), Shuffle (re-jumbles the loose tiles), Show me (gives up).
Scoring that stays honest
The daily result is a row of five bars:
- green: solved on your own
- amber: solved with a clue or a letter
- grey: gave up
Combined with the time, that makes results shareable without a points system to explain. The share button uses the Web Share API (navigator.share) where it's available.
State lives in localStorage
There are no accounts, so everything is local:
- the current daily's progress (so a refresh doesn't lose your place)
- streak, played and solved counts
- which Endless words you've already seen, so repeats are rare across thousands of words
If you clear your browser data you lose your streak. For a two-minute game, that trade-off is fine, and it means there's no personal data to protect on a server.
Hosting
It's static files on S3 behind CloudFront: one index.html, words.js, a service worker and a web manifest, so it installs as a PWA and loads instantly on repeat visits. The whole page is about 55 KB of HTML before the word file.
What I'd tell anyone building a daily puzzle
- Generate your content, and make the generator strict. Most bugs were content bugs: an ambiguous anagram, a word that doesn't fit on a phone, a clue for the wrong sense.
- Freeze the past. An append-only daily schedule saves you from confusing players.
- Design for the thumb first. Tap-to-swap beat drag-and-drop for most players.
- Keep the end state obvious. Green tiles and five coloured bars explain themselves.
You can play today's five here. Feedback on the word lists is very welcome.
