When I launched my daily word game, three players emailed me on the same morning — from three different countries — each insisting they were playing a different "today's game." They were all right. And that was the bug.
I run a small family of browser word games — Dive Krillion (the daily one), Krillion Infinite (endless), and Noillirk (the backwards edition). Same prompt-and-rarity engine, three sites. The hardest problem in the whole project wasn't the game. It was midnight.
The naive version (don't do this)
My first reset logic was one line of thinking: the day changes when the player's clock says so. Device-local midnight. It failed in every direction at once:
- Players in different timezones were on different dives, so scores and the daily leaderboard compared people who had played different prompts.
- Anyone could change their phone's date and unlock tomorrow's dive early.
- "Today" on the share card didn't match "today" on the website.
The other naive option — server time (UTC) — is just as bad. Midnight UTC is 5 AM in Pakistan and the previous evening in New York. Half your players get the new daily game at a time that feels completely random.
Fix #1: one game, one canonical midnight
Dive Krillion is a shared daily ritual: everyone on Earth plays the same dive on the same day. That only works if "today" is defined in exactly one place. I picked a canonical timezone — midnight Eastern Time — and the whole game (prompt selection, streaks, leaderboard, the on-site countdown) runs on it.
The prompt itself is just an index into a 302-prompt bank: the number of calendar days since launch, computed in the canonical timezone. The trick that makes this reliable in JavaScript is to never do 24-hour arithmetic on timestamps. Days are not 86,400 seconds long — twice a year, in a DST-observing zone, they're 23 or 25 hours. Count calendar dates instead:
function diveIndex(date: Date, timeZone: string, epoch: string): number { const parts = new Intl.DateTimeFormat("en-US", { timeZone, year: "numeric", month: "2-digit", day: "2-digit", }).formatToParts(date); const get = (t: string) => Number(parts.find(p => p.type === t)!.value); const todayUtc = Date.UTC(get("year"), get("month") - 1, get("day")); const [ey, em, ed] = epoch.split("-").map(Number); const epochUtc = Date.UTC(ey, em - 1, ed); return Math.round((todayUtc - epochUtc) / 86_400_000); } Format the date in the target zone, rebuild it as a UTC calendar date, then subtract. DST can't hurt you, because you never measured hours — you counted dates.
Fix #2: the endless game gets the opposite rule
Krillion Infinite has no shared daily dive — it's personal, endless play. Forcing Eastern Time on it made no sense, so it resets at midnight, your local time. Same engine, opposite rule, chosen because the product is different: a shared ritual needs one clock; a personal game needs yours.
That split is written on both sites, in the FAQs, in plain language. Half of "timezone bugs" are actually expectation bugs — players forgive any rule except a hidden one.
The push-notification trap
The daily reminder push goes out once, globally, shortly after the canonical reset. That only works because Dive Krillion has one reset moment. Try to send a "your new dive is ready" push for a local-midnight game and you're either waking people at 3 AM in some zone or maintaining a per-timezone send queue. If you build a daily game, decide early which side of that trade you're on.
What I'd tell anyone building a daily game
- Pick your reset semantics before your schema. Streaks, leaderboards, archives ("play yesterday's dive"), and notifications all inherit the decision.
- Count calendar dates, never hours. The
Intl.DateTimeFormat+Date.UTCpattern above is boring and correct, including on DST days. - One shared day = one timezone. Personal cadence = local time. Mixing them in one product confuses everyone.
- Write the rule where players can see it. A countdown that says "Next dive in 04:12:33 (ET)" ends more support emails than any fix.
Try the three clocks
- Daily dive (midnight ET): divekrillion.com
- Endless (midnight, your time): krillioninfinite.com
- Backwards edition: krillioninfinite.online
All free, no download. If you've shipped a daily game, I'm curious: which clock did you pick — and what did it break first?