Case study: Delve roguelike
Context
I wanted a quick-run roguelike that respected my time — 10 floors, 30–60 minute runs, replayability from modifiers and class variety rather than grinding for unlocks. The genre is well-trodden enough that the design space is known; the interesting part was figuring out which features add depth and which just add bloat, and getting the balance tight enough that deaths feel deserved rather than arbitrary.
The game has since grown a sibling: Ashes of the Pale Flame, a Slay-the-Spire-style deckbuilder set in the same world. Same lore, same monsters, different combat grammar.
Problem
Build a roguelike that feels classic — ASCII rendering, turn-based, permadeath, procedural dungeons — but runs in a browser with no install friction. Runs should be completable in an evening. Deaths should sting but not feel cheap. Classes should play differently enough that replaying as a different one isn’t just “same run, different numbers.”
The fairness constraint is harder than it sounds. Procedural generation can produce unwinnable situations: a floor with no health on it when you’re at 2 HP, a boss fight that spawns you adjacent to it. Every one of those moments breaks trust with the player in a way that ten good runs won’t undo.
Approach
1. Core loop first, systems layered in
The loop is standard: create character, descend, fight, loot, level up, die or win, start over. The interesting design question is what makes each run feel different. The answer ended up being the combination of class abilities + run modifiers + item randomisation — three independent axes of variance that interact in ways that are hard to predict.
Seven classes (Warrior, Rogue, Mage, Ranger, Necromancer, Paladin, Bard), each with a distinct ability, passive traits, and a perk tree with three milestone unlocks. Not every class is equally forgiving — Mage starts with 18 HP and hits like a freight train; Warrior starts with 26 HP and grinds through.
2. Run modifiers as the replayability engine
19 modifiers — 6 curses, 4 blessings, 9 mixed — assigned at character creation, rerollable once. Each one meaningfully warps the run:
- Forsaken: no shop between floors
- Haunted: +3 enemies per floor
- Glass Cannon: max HP halved, base ATK doubled
- Bloodthirst: kills restore 3 HP, healing potions do nothing
- Bloodprice: the shop accepts HP instead of gold
The mixed modifiers are the most interesting design space — they give something and take something, and the interaction with your class determines whether it’s net positive. Glass Cannon + Mage is terrifying in a good way.
3. Procedural generation with fairness constraints
Dungeon generation: place rooms, connect with corridors, add special rooms (shrines, libraries, vaults, challenge rooms) by floor depth, populate with enemies and items, add stairs. Standard stuff.
The non-standard part is the fairness layer on top:
- Starting room is always clear
- No enemy spawns within 3 tiles of the player start position
- Every floor guarantees at least one health potion
- Every floor has a valid path from entrance to exit (checked after generation)
- Shop is always reachable unless Forsaken is active
Floor events fire at 65% chance per floor — Hunting Pack, Blood Bounty, Silent Floor, Gold Rush, Undying — adding variance that isn’t tied to room layout.
4. Performance in a browser
ROT.js handles ASCII rendering, FOV, and dungeon generation. The bottleneck is pathfinding for 20+ enemies per turn. Fixes: A* with early exit when the player is unreachable, cached paths for static enemies that only recalculate on player movement, and batched DOM updates (one repaint per turn, not per enemy move). Keeps it feeling instant even with a full floor of enemies.
5. Balance, the part that never ends
The code stabilised in a few weeks. The next several months were tuning numbers. Problems that took the longest to fix:
- Mage was overpowered: Arcane Bolt on a cooldown still trivialised combat. Added a per-floor cast limit; that finally made the glass-cannon tradeoff real.
- Boss fights were tanks: Stack potions and outlast. Fixed by giving each boss a mechanic — Brood Mother spawns minions, the Warlord cleaves, the Pale King has two phases.
- Late-game armor made combat trivial: Added armor-bypassing enemies (wraiths, draining attacks) so stacking DEF isn’t a solved strategy.
- Haunted was unplayable: 3 extra enemies per floor sounds manageable until floor 7. Tuned enemy density curve rather than the modifier value.
Win rate for experienced players sits around 35%. First-time: ~5%. Most common cause of death: surrounded by orcs. Classic.
6. Daily Challenge
A shared seed per UTC date gives everyone the same dungeon layout, same item spawns, same modifier. Seed = hash of YYYY-MM-DD → deterministic RNG → reproducible run. No leaderboard yet, but the infrastructure is there.
7. World
The lore exists independently of the mechanics. Scroll fragments and faction notes (Order of the Pale Flame, the Thornwatch, Aldric’s chronicle) are scattered through the dungeon. The deckbuilder picks this up and runs with it — same monsters, same biome names, new framing.
What’s missing
- Meta-progression: nothing persists between runs. No unlocks, no reason to try every class beyond curiosity. The deckbuilder has the same gap.
- Better mobile UX: touch controls work but feel like an afterthought. Swipe-to-move would be cleaner than the current virtual d-pad.
- Encounter curation: enemies are drawn from a biome pool, so odd combinations (two Iron Golems, three pack leaders) are possible. Curated encounter sets would make fights feel more intentional.
- A Druid class — shapeshifting, terrain interaction — has been on the list for a while. The mechanic space is interesting enough that it keeps getting deferred rather than cut.