Daily challenges are one of the simplest engagement features you can add to a games site — Wordle made the pattern famous. On Classroom Games, a collection of free learning games for teachers (math, typing, spelling, reading comprehension, geography, fractions, multiplication, sight words, brain breaks and more), the homepage shows one "Today's Pick" with a points goal. It resets at midnight, and there's one pick per day.
The interesting part: you can build this without a server. Here's the approach.
1. Derive "today" from the local date, not a timestamp
A daily feature should flip at the player's midnight, not UTC midnight. A class in California and a class in Paris should each get a fresh challenge when their day starts.
function localDateKey(d = new Date()) {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`; // "2026-09-25"
}
Using getFullYear/getMonth/getDate (not the UTC variants) is the whole trick.
2. Pick deterministically from the date
Everyone on the same local date should see the same pick, and refreshing shouldn't reroll it. Hash the date string into an index:
// FNV-1a: tiny, fast, good enough for picking an item
function hash(str) {
let h = 0x811c9dc5;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = Math.imul(h, 0x01000193);
}
return h >>> 0;
}
const CATEGORIES = [
"math", "typing", "reading", "spelling", "puzzles", "memory",
"logic", "trivia", "geography", "science", "words", "vocabulary",
"grammar", "sight-words", "multiplication", "fractions",
"counting", "time-money", "brain-breaks",
];
export function todaysPick(date = new Date()) {
return CATEGORIES[hash(localDateKey(date)) % CATEGORIES.length];
}
No database, no cron job, no API. The "schedule" is a pure function of the date.
3. Avoid back-to-back repeats
Pure hashing can land on the same category two days in a row, which feels broken to users even though it's random. A small fix is to check yesterday and step forward on a collision:
export function todaysPickNoRepeat(date = new Date()) {
const y = new Date(date); y.setDate(y.getDate() - 1);
let i = hash(localDateKey(date)) % CATEGORIES.length;
if (CATEGORIES[i] === todaysPick(y)) i = (i + 1) % CATEGORIES.length;
return CATEGORIES[i];
}
4. Store progress locally, keyed by date
The status ("Not started", in progress, goal reached) only matters for today, so it lives in localStorage under the date key. Old keys are naturally ignored.
function loadStatus() {
try {
const saved = JSON.parse(localStorage.getItem("daily") || "{}");
return saved.date === localDateKey() ? saved : { date: localDateKey(), points: 0 };
} catch {
return { date: localDateKey(), points: 0 }; // private mode, blocked storage, etc.
}
}
Wrapping storage in try/catch matters in schools: managed browsers sometimes block or wipe site storage, and the page should still work.
javascript,webdev,gamedev,education,
5. Handle the tab left open overnight
Classroom machines often stay on with the same tab open. Re-check the date key when the page becomes visible again:
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible" && loadStatus().points === 0) {
render(todaysPickNoRepeat());
}
});
Why this works well for classrooms
- No accounts — nothing to sign up for, nothing to get approved.
- Same pick for the whole class on the same day, which makes it easy to use as a warm-up.
- A clear goal (e.g. 50+ points) gives students a target without a leaderboard.
You can try today's pick at classroomgames.app. Have you built daily features without a backend? I'd like to hear how you handled time zones.
Top comments (0)