5 Lines of Code That Made My Roguelike Worth Playing Every Day
Source: Dev.to
Background
- 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.
- Golf: the course is fixed. Players compete on the same 18 holes, so the comparison is valid because the challenge is identical.
- Wordle: the word is fixed. Every player gets the same puzzle; the only variable is the number of guesses.
- 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())
Below is the code that sets a deterministic daily seed before loading the game scene:
func _on_start_daily_challenge() -> 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 scene – Seed setup on load (game_main.gd)
# 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")The seed() call sets Godot’s global RNG state. Every subsequent randi(), randf(), and array.shuffle() 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,577Every player who clicks “Daily Challenge 02/21” that day gets:
- The same first three enemies
- The same upgrade choices at levels 2, 3, 4…
- The same elite enemy spawns
- The same boss‑trigger timing
What differs is their reaction time, decision making, and skill execution. 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, but 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 because other players experienced the exact same 8:30 splitter wave. They can compare directly, using a shared reference point for that day.
The philosophy of daily challenges
Daily challenges solve a social coordination problem.
Without a shared seed, comparing roguelike runs requires trusting 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.
Daily challenges also create a natural reason to return: not “I want to get better” (vague) but “I haven’t done today’s challenge 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.
Neither of these was obvious to me initially.
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: the right daily reset time
Right now it resets at local midnight. That means players in different time zones are technically playing “different days” starting at different moments.
- Argument for UTC midnight: a single universal reset.
- Problem: UTC midnight is 9 am JST—awkward for Japanese players.
I haven’t solved this. For now it’s local date. 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:
Is your Claude Code setu… (the original text cuts off here).
Is your code 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?