My game has a daily leaderboard. Players compare scores in the comments. Streaks track who keeps coming back.
None of it has a server.
Here's how it works — and why I think more indie games should do this.
The problem with leaderboards
You ship a game. You want players to compete. The natural answer is a leaderboard.
Then you realize what a leaderboard actually needs:
- A backend server
- A database
- Authentication (so scores can't be faked)
- API rate limiting
- Maintenance when it goes down at 2am
- Money
For an indie game making $0, this is the wrong trade-off. You're building infrastructure instead of game.
There's a simpler way.
Step 1: Shared seeds (same experience for everyone)
The core insight: if everyone plays the same RNG seed, their scores are directly comparable without any server.
In my game (Spell Cascade, built in Godot), the Daily Challenge seed is derived from the date:
var date: Dictionary = Time.get_date_dict_from_system()
var seed_base: int = (int(date.year) * 10000) + (int(date.month) * 100) + int(date.day)
var daily_seed: int = seed_base * 31337 # prime multiplier for better distribution
seed(daily_seed)
Everyone who plays on February 21 gets the same enemies, the same upgrade choices, the same wave timing. The run is deterministic.
This means a score of "42 kills in 5:23" means exactly the same thing for you and for me — we fought the same enemies with the same RNG.
No authentication needed. The seed is the proof.
Step 2: itch.io comments as the social layer
itch.io gives every game a free comment section. I didn't need to build one.
The friction was: how do players share a score without typing it manually?
I added a "📋 Copy Score" button to the result screen. One click copies this to the clipboard:
[Spell Cascade Daily 02/21] 🗡️ Fire Wizard ★★★ | 5:23 | 42 kills
yurukusa.itch.io/spell-cascade
Then a "💬 Post to itch.io comments" button opens the comment section in a new tab. Paste. Done.
The comment section becomes a leaderboard. Players reply to each other: "how did you get ★★★ with Fire Wizard? I kept dying at wave 15." That's the social engagement I wanted.
Implementation in GDScript (web export):
copy_btn.pressed.connect(func():
if OS.has_feature("web"):
var escaped := share_text.replace("'", "\\'").replace("\n", "\\n")
JavaScriptBridge.eval("navigator.clipboard.writeText('" + escaped + "').catch(()=>{})")
else:
DisplayServer.clipboard_set(share_text)
)
comment_btn.pressed.connect(func():
JavaScriptBridge.eval("window.open('https://yurukusa.itch.io/spell-cascade#comments', '_blank')")
)
Cost: ~20 lines of code. No backend.
Step 3: Yesterday's Challenge (24-hour window)
Problem: a player in Japan starts playing at 10pm, crosses midnight, and the Daily Challenge has reset. They can't participate in "yesterday's" discussion.
Fix: add a second button for yesterday's seed.
var unix_yesterday := int(Time.get_unix_time_from_system()) - 86400
var ydate: Dictionary = Time.get_date_dict_from_unix_time(unix_yesterday)
var seed_base: int = (int(ydate.year) * 10000) + (int(ydate.month) * 100) + int(ydate.day)
var yesterday_seed: int = seed_base * 31337
Engine.set_meta("daily_challenge_seed", yesterday_seed)
Engine.set_meta("daily_challenge_date_str", "%02d/%02d" % [int(ydate.month), int(ydate.day)])
The result screen shows "★ PAST CHALLENGE 02/20 ★" and the copy-paste score says [Spell Cascade Past 02/20], so past scores are distinguishable in comments.
This extends the community discussion window by 24 hours. The comments section for February 20 stays active through February 21.
Cost: ~25 lines of code. No backend.
Step 4: Streak tracking (localStorage)
The last piece: a reason to come back every day.
The game tracks your consecutive Daily Challenge days using localStorage — two keys, no server:
var last = localStorage.getItem('sc_daily_last') || '';
var streak = parseInt(localStorage.getItem('sc_daily_streak') || '0');
var today = '2026-02-21';
if (last === today) {
return streak; // already counted today
}
var yesterday = new Date(today + 'T00:00:00');
yesterday.setDate(yesterday.getDate() - 1);
var yStr = yesterday.toISOString().slice(0, 10);
streak = (last === yStr) ? streak + 1 : 1;
localStorage.setItem('sc_daily_last', today);
localStorage.setItem('sc_daily_streak', String(streak));
return streak;
The result screen shows:
- Day 1: "🔥 First daily! Come back tomorrow to start a streak"
- Day 2-6: "🔥 3-day streak!"
- Day 7+: "🔥 8-day streak! 🏆"
Playing Yesterday's Challenge doesn't count — only today's Daily increments the streak.
Cost: ~35 lines of GDScript + inline JS. No backend.
What this gives you
The full system:
| Feature | Infrastructure | Cost |
|---|---|---|
| Same experience for all players | Seed-based RNG | 5 lines |
| Score sharing | Clipboard API | 10 lines |
| Comment section | itch.io (free) | 0 lines |
| Past challenge access | Unix timestamp math | 25 lines |
| Streak counter | localStorage | 35 lines |
| Total | Zero backend | ~75 lines |
This isn't a compromise. It's a different architecture.
The seed is authentication. The comment section is the leaderboard. localStorage is the player profile.
The only thing you can't do: global rankings across all players in real-time. But for most indie games at early stage — you don't need that yet. You need players talking to each other.
This gives them a reason to talk.
Built by Claude Code
I didn't design all of this upfront. My AI agent (Claude Code) looked at what existed — the Daily Challenge seed, the result screen, the Copy Score button — and autonomously decided: "there's no reason to come back tomorrow. Let me fix that."
It implemented Yesterday's Challenge and the streak counter in the same session, without being asked. Then wrote this article.
The game is Spell Cascade — a browser-based auto-battler with spells that cascade off each other. Daily Challenge seed changes every 24 hours.
If you want to compare scores: comment section is open.
More tools: Dev Toolkit — 200 free browser-based tools for developers. JSON, regex, colors, CSS, SQL, and more. All single HTML files, no signup.
Top comments (0)