In v0.10.0, I gave every run in my roguelike a name.
In v0.11.0, I gave every day a run.
The problem with Endless Mode
When I shipped Endless Mode (v0.9.9), I added this line to the result screen:
"Post your Endless score in itch.io comments!"
It was a bet that players would naturally turn the comment section into a leaderboard.
The problem: no two runs are the same. When one player posts "I survived Wave 27 as a Chain Annihilator," there's nothing for another player to compare against. They ran a different map, got different upgrade choices, faced different enemy patterns.
The itch.io comment section wasn't a leaderboard. It was just a list of unrelated accomplishments.
What makes a leaderboard work
A leaderboard needs a fixed variable.
In golf, the course is fixed. Players compete on the same 18 holes. The comparison is valid because the challenge is identical.
In Wordle, the word is fixed. Every player gets the same puzzle. The only variable is the number of guesses.
In my roguelike, nothing was fixed. Random seed on every run, random on every refresh.
The fix was obvious once I saw it: use the date as the seed.
The implementation
Every run in Spell Cascade uses Godot's global random number generator for:
- Enemy type selection (
randi() % pool) - Enemy spawn positions (
randf_range()) - Upgrade choices (
array.shuffle()) - Elite enemy probability (
randf() < elite_chance)
All of this feeds from the same global state. If you set that state at the start of a run, everything that follows is deterministic.
Here's the complete implementation:
# title.gd — Daily Challenge button handler
func _on_daily() -> void:
if _transitioning:
return
_transitioning = true
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 number distribution
Engine.set_meta("daily_challenge_seed", daily_seed)
var scene: PackedScene = load("res://scenes/game.tscn")
get_tree().change_scene_to_packed(scene)
# game_main.gd — seed setup on game load
func _ready() -> void:
if Engine.has_meta("daily_challenge_seed"):
var daily_seed: int = Engine.get_meta("daily_challenge_seed")
seed(daily_seed)
is_daily_challenge = true
Engine.remove_meta("daily_challenge_seed")
That's the core of it. The seed() call sets Godot's global RNG state. Every randi(), randf(), and array.shuffle() that follows is now deterministic.
The Engine metadata pattern handles the title-to-game scene transition without an autoload singleton: set the value before the scene change, read and delete it immediately on load.
What 2026-02-21 looks like
On February 21, 2026, the seed is:
20260221 × 31337 = 634,601,577
Every player who clicks "Daily Challenge 02/21" that day gets:
- The same first three enemies
- The same upgrade choices at level 2, 3, 4...
- The same elite enemy spawns
- The same boss trigger timing
What's different: their reaction time, their decision making, their skill execution.
That's the game. The "map" is shared. The score is earned.
The social mechanic
Within 24 hours of shipping this, the first daily challenge post appeared in the itch.io comments. Not just a score — a debrief.
"Got to Wave 19 as Chain Annihilator. Nearly had Endless but the splitter wave at 8:30 got me."
That post is meaningful to other players in a way that never was before. They played the same 8:30 splitter wave. They know exactly what it was like. They can compare.
One comment about a specific wave is now a shared reference point for everyone who played that day.
The philosophy of daily challenges
Daily challenges work because they solve a social coordination problem.
Without a shared seed, comparing roguelike runs requires trusting in statistical similarity — "we both played the same difficulty on average." That's a weak basis for competition.
With a shared seed, comparison is direct. "We both saw that splitter wave at 8:30" is a fact, not an approximation.
The other thing daily challenges do: they create a natural reason to come back. Not "I want to get better" (vague) but "I haven't done today's yet" (specific, actionable, expiring).
What the AI contributed
The idea came from reading about how Wordle drove its engagement loop. The implementation question — how to set a deterministic seed in Godot without breaking normal mode — was something I didn't know.
Claude Code confirmed: seed() sets the global state, so every downstream call inherits it. The Engine metadata pattern for passing data between scenes without a singleton was also its suggestion. Neither of these was obvious to me going in.
What I brought: the observation that my comment section wasn't working as a leaderboard, and the theory that shared seeds would fix it. What Claude contributed: the two-line implementation that made it work.
Open question
What's the right daily reset time?
Right now it resets at local midnight. That means players in different timezones are technically playing "different days" starting at different moments. An argument for UTC midnight. But UTC midnight is 9am JST — too awkward for Japanese players to treat as a day boundary.
I haven't solved this. Local date for now. If the player base ever gets large enough that timezone inconsistency matters, it'll become a real problem.
For now: same date, same seed. If you're playing Feb 21, you're playing the Feb 21 run.
Play it
Spell Cascade is free in the browser: yurukusa.itch.io/spell-cascade
Is your Claude Code setup actually safe? Run npx cc-health-check — a free 20-point diagnostic. Score below 80? The Claude Code Ops Kit fixes everything in one command.
What wave did you reach today?
Top comments (0)