Build a provably-fair daily challenge
4 min read
The daily challenge is one of the most powerful retention loops in games. Wordle built a phenomenon on it. Everyone gets the same puzzle today, so the score is comparable and the share ("4/6!") means something. But it hides two hard problems. First, every player must derive an identical board from the same input. Second, and this is the one people get wrong, that input must be unpredictable in advance. If a clever player can compute tomorrow's puzzle tonight, the whole thing is broken. Ship the seed in your client bundle and someone will datamine a month of answers by lunch.
Triggair's seeded RNG solves both. You get a seed that's identical for everyone today and impossible to predict ahead of time, because the secret that generates it never leaves the server.
One shared seed for everyone, today
tg.rng.seed with a shared scope returns the same value for every player in the same period. Ask for it and you get today's seed plus a period_key that names the day.
const { seed, period_key } = await tg.rng.seed('daily', {
period: 'daily',
scope: 'shared',
});
// seed: 'a3f19c8e...' (identical for all players on 2026-07-09)
// period_key: '2026-07-09'
scope: 'shared' is the "everyone gets the same board" guarantee. period: 'daily' is what rolls it over at the day boundary. Tomorrow you'll get a different seed, and there's no way to compute it today, because it's derived from a secret that stays server-side.
Derive today's content deterministically
The seed is a hex string, which is just a big number in disguise. Turn slices of it into indices and you can pick words, spawn layouts, or shuffle a deck, all deterministically, so every player builds the identical challenge from the identical seed.
const WORDS = ['PLANET', 'ROCKET', 'ORBITS', 'COMETS', /* ... */];
function pickWord(seed: string) {
// Read the first 8 hex chars as a number, map into the word list.
const n = parseInt(seed.slice(0, 8), 16);
return WORDS[n % WORDS.length];
}
function shuffle<T>(items: T[], seed: string): T[] {
const arr = [...items];
for (let i = arr.length - 1; i > 0; i--) {
// A fresh slice of the seed per step keeps the shuffle deterministic.
const j = parseInt(seed.slice((i * 4) % 56, ((i * 4) % 56) + 8), 16) % (i + 1);
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
const todaysWord = pickWord(seed);
const todaysDeck = shuffle(WORDS, seed);
Same seed in, same board out, on every device, with no content pushed from your server. You ship the rules; Triggair ships the unpredictable-but-shared randomness.
Per-player fairness for solo runs
Not every game wants a shared board. Roguelike daily runs often want each player to get their own seed: still tied to the day, still tamper-proof, just personal. Flip the scope.
const run = await tg.rng.seed('daily_run', {
period: 'daily',
scope: 'player',
});
// Unique to this player for today; unpredictable in advance.
scope: 'player' gives procedural-but-fair: the player can't scout their own run ahead of time or reroll for a lucky seed, but nobody's map matches anyone else's. Same anti-cheat property, different social shape.
Share the result
The daily loop is only a loop if results spread. tg.social.share packs a context into a short code you can drop into a link.
const { code } = await tg.social.share({
board: 'daily',
period_key, // stamps which day this result is for
guesses: 4,
});
// e.g. share URL: https://yourgame.com/?s=<code>
// On the landing page: resolve works BEFORE login, so a
// fresh visitor sees the challenge before you mint their account.
const { context } = await tg.social.resolveShare(code);
showDailyPreview(context); // { board: 'daily', period_key, guesses: 4 }
Because resolveShare runs before tg.login(), a friend who clicks the link sees "today's challenge, beat my 4/6" first, and gets their anonymous account only when they tap play.
Gotchas
- Never derive content from anything client-side. The security comes from the server-held secret behind the seed. Don't mix in a hardcoded constant thinking it adds safety; it just makes the board predictable.
- Stamp
period_keyon shares and scores. It's what ties a result to the exact board it was played on, so yesterday's brag doesn't masquerade as today's. - Same seed, same code path. If two players run different game versions, identical seeds can still yield different boards, so gate the challenge on a content version if that matters.
A shared seed that's fair, unpredictable, and secret-free on the client turns "play again tomorrow" into a habit. Derive the board from the seed, share the result with a code, and you've got the loop that built Wordle.