A daily puzzle is a promise: everyone gets the same board today, and it had better be worth their
time. Most of the work went into keeping that promise.
Dialword is six wrap-around dials, twelve letters each, all of them visible and all of them freely spinnable. You
turn the reels to bring letters into the illuminated gate across the middle, tap them in order to spell a word, and
tap the last one again to submit it. Letters you use are consumed — the dial they came from gets shorter, and
a dial you empty collapses out of the puzzle entirely. Clear all six and you have taken the board apart.
Everyone plays the same board on the same day. That single sentence caused most of the interesting problems.
The dud problem
A daily puzzle generated from the date is easy to build and easy to get wrong. Seed a random board from today's date,
ship it, and roughly one day in six you hand every player on the site a board with almost nothing in it. In a puzzle
you generate on demand that is a shrug and a reroll. In a daily, it is everyone's turn ruined at once, and they all
find out together.
The obvious fix is calibration: work out what makes a board good, encode it as thresholds, tune the generator until
it stops producing bad ones. I did not want to do that, because the thresholds would need retuning every time
anything else changed — the letter distribution, the vowel rule, the word list.
What it does instead is cheap and needs no theory. For a given date it generates six candidate boards, counts how
many opening words each one supports, and keeps the richest. That is it. There is no model of what makes a board
good; there is only the observation that the best of six is reliably much better than one taken blind, and that the
truly barren boards simply lose. It costs about fifty milliseconds at load, which is invisible, and it has no
numbers in it that go stale.
Every dial is also guaranteed at least two vowels. That one is a hard rule rather than a preference, because
a dial of consonants is not a difficult dial, it is a dead one, and no amount of best-of-six will rescue a board
that contains it.
Midnight, but whose?
The daily rolls over at midnight UTC, not at midnight wherever you happen to be.
Local rollover is friendlier in the obvious way — your new puzzle arrives when your day starts. It also quietly
breaks the thing that makes a daily worth having. If the board is keyed to local midnight then two people talking
about "today's puzzle" may be talking about two different boards, a streak means something different depending on
which way you have flown recently, and anyone who wants a second go at today only has to change their clock.
UTC costs some players a strange rollover hour and buys a single shared answer to "what is today's board". For a
puzzle whose whole appeal is that it is the same one everybody else is doing, that is the right trade.
Saving a game by not saving it
Half-finished games have to survive a reload, and the ordinary way to do that is to write the state out — every
dial, every remaining letter, every position, the score — and read it back.
Dialword does not store the board at all. It stores the list of words you have played and which dial each letter came
from, and when you come back it rebuilds the board from the date seed and replays your moves into it.
The reason is that a snapshot is a second source of truth, and second sources of truth drift. Change how a dial
collapses, or how a word scores, and every saved game out there is now a description of a board your code no longer
produces — and you find out through bug reports from people whose scores changed overnight. A replay cannot
drift, because it is not a description of the game, it is the game's own inputs. Whatever the rules do today, they
do to your moves today.
It is smaller too, which is a pleasant accident rather than the point.
The correction I had to make twice
Selecting letters goes in one direction: once you start spelling, each next letter has to come from a dial further
along than the last, forward or backward but never doubling back. That constraint is what makes a word a route
across the board rather than a free-for-all, and it was right from the start.
What was wrong was undoing. The first version treated tapping an already-selected letter as a toggle, which meant
touching something in the middle of a five-letter word tore a hole in it. Now, tapping a letter you have already
selected keeps that letter and drops everything after it — you are not removing a letter, you are rewinding to
a point.
That is a tiny change and it took a second pass to see, because the toggle behaviour is not wrong in the abstract. It
is wrong here, where a selection is a path and not a set. A word you are building has an order, and the
natural gesture on an ordered thing is to go back to somewhere in it, not to pluck a piece out of the middle.
Two smaller fixes came from the same direction. A word that does not validate now clears the whole selection rather
than leaving you to dismantle a failed attempt by hand — and it still tells you why. And the tray shows
whether what you currently have spelled is a real word, in red or green, live, as you build it. That last one
changes the game more than it looks like it should: it turns speculative tapping into something you can learn from,
because you can watch the moment a nonsense string becomes a word.
The reels had to settle properly
The dials snap to detents — each letter has a resting position under the gate, and a spin that ends between two
of them has to travel to one. The first implementation rounded to the nearest, which is correct and felt broken.
The failure case was a letter left a hair past halfway. The maths said "that one", the reel moved a couple of pixels
to seat it, and the eye read it as the reel not responding at all. Fixing the animation so it always visibly travels
to the detent it has chosen — rather than nudging when the distance is short — is a few lines of code
that sit somewhere between physics and manners.
What it looks like, and what it does not
Dialword is machined metal: gunmetal in the dark theme, brushed steel in the light one, with an amber-lit gate across
the middle as the one warm thing on screen. The score is set in tabular monospace so the digits do not shuffle
sideways as it climbs.
This deserves a mention because the default was something else entirely. Ask for a "clean, warm, friendly" puzzle
game and what arrives is cream backgrounds, a serif headline and a terracotta accent — a look that has become
the house style of machine-made design, pleasant and instantly forgettable. It is the visual equivalent of a stock
photo.
Six numbered dials that click into detents are not a warm cream object. They are a machine. Leaning into that —
metal, precision, one illuminated indicator — gave the game a face of its own, and made the amber gate read as
what it is: the place where the work happens.
Statistics that are not flattering you
Dailies are tracked: games played, current and longest streak, best score, average, how many boards you have cleared
completely, and how each game actually ended. Sixty days of history are kept.
Two rules keep those numbers honest, and both exist because the naive version lies.
Recording happens when a game ends, not when the summary screen appears. Those sound like the same moment
and are not: the summary also shows on reload, so tying the record to it means a finished game re-recorded every
time someone came back to look. On top of that, applying a day's result is idempotent — the same date cannot
be counted twice however many ways the code finds to ask.
And you can play a random board any time you like, which resumes if you reload it, and which never touches the
statistics at all. A practice board that counted toward your streak would not be practice. The daily is the record;
everything else is a sandbox.
What it took
The core is a pure module that knows nothing about the screen — boards, dials, selection, scoring, collapse
— with the renderer as a thin layer on top. Scoring is letter values drawn from letter frequency on a
one-to-ten scale, multiplied by the length of the word minus two, so that a long word is worth considerably more
than the same letters split up. Emptying a dial is worth a flat twenty-five; emptying all six pays two hundred and
fifty.
Most of it exists twice over in tests — a hundred and ten on the core alone, plus separate suites for the
statistics, the word-list parser and loader, and a jsdom pass that drives the real interface and checks that what
the screen says agrees with what the core believes. That last one is the only test that would have caught the class
of bug I am most afraid of: a game that is correct underneath and shows you something else.