All Devlog Posts
How We Generate Dungeons cover

The question I get most from other engineers at game jams is not "what engine are you using" but "how do you keep the dungeon from feeling random." It is a good question, and the honest answer is that it took us several failed attempts before we arrived at something we were not embarrassed by. This post describes where we landed and why.

The short version: Ruinveil generates dungeons by applying a grammar, not by selecting templates. What that means in practice is more nuanced than the phrase suggests.

Why We Did Not Use Room Templates

Template-based generation is the path of least resistance and it has real virtues. You hand-author a set of rooms, tag them with metadata (combat density, traversal type, hazard level), and at runtime the system selects and chains them according to some criteria. It is reliable, it lets artists control exactly how every room looks, and the cognitive overhead on the engineering side is low.

The problem is that template libraries have a ceiling. Once a player has seen enough runs to internalize the library, the rooms feel familiar even when the arrangement changes. The dungeon starts to pattern-match against memory rather than against the present run. For a game where the loop depends on the player engaging with each run as a fresh problem, that familiarity is a design failure.

We could have built a very large template library. But as a three-person studio, maintaining a large hand-authored content set at runtime quality is a bet we could not afford. The grammar approach scales differently: we write rules, and the rules compose to produce content we did not directly author. The library grows by adding constraints, not by adding rooms.

What a Level Grammar Actually Is

A level grammar, in our implementation, is a collection of production rules that govern how a spatial graph can be constructed. The graph represents the dungeon: nodes are rooms, edges are connections (corridors, doors, drops). The grammar specifies what kinds of nodes can follow what other kinds of nodes, under what conditions, and with what spatial constraints.

A simple rule looks like this: a safe zone (rest room, shrine) must not connect directly to a high-density encounter room. There must be at least one transitional room between them. Another rule: before any boss chamber, the corridor approach must narrow (reduce the number of exit paths available to the player) and cannot contain resource pickups. A third rule: no dungeon can have two vault rooms in the same spatial quadrant.

These rules are not random placement guides. They encode specific spatial logic about how tension builds and releases across a dungeon. The encounter-before-boss rule is there because we want players to arrive at a boss with a sense of compression, not relaxation. The resource-exclusion zone before bosses forces a decision point: whatever the player has at that threshold is what they fight with.

The Generation Pipeline

When Ruinveil generates a dungeon, it runs a constrained graph construction algorithm seeded by the run's entropy value. The algorithm builds the room graph in several passes:

First pass: place the structural landmarks. Entry point, exit point, boss chamber location. These are determined by the dungeon's intended spatial length (measured in required traversal steps, not physical tile count) and the seed.

Second pass: fill the critical path. The chain of rooms connecting entry to boss to exit must satisfy every ordering constraint in the grammar. The system tries to place rooms that fulfill their required roles in the narrative arc: buildup zones, tension escalations, the pre-boss compression corridor. Each room placement is checked against all active constraints. If a placement fails all available options, the system backtracks and reroutes.

Third pass: branch the non-critical path. Side rooms, optional vaults, dead ends, secret areas. These follow a looser version of the grammar: the structural rules are still active, but the narrative-arc rules are scoped to the critical path.

Fourth pass: assign content. Enemies, hazards, items. This is where the enemy ecology system hands off from the level grammar; the dungeon knows what kind of encounter belongs in each room based on its role and position, and the enemy system assembles the specific set from that specification.

Total generation time on a mid-range desktop is in the 70-90ms range. That includes the enemy assembly pass. The player sees a loading screen of approximately 1.5 seconds (mostly asset streaming), so the generation itself is not the bottleneck.

What the Grammar Does Not Solve

I want to be direct about the current limitations because we have had to think hard about them.

The grammar is good at structural properties: ensuring a dungeon has the right tension arc, that certain room types do not appear in combinations that produce absurd or unfair situations, that the navigation graph has the right shape. It is less good at local visual and spatial coherence. A corridor that turns five times in a tight cluster is technically valid under our constraints, but it feels maze-like rather than dungeon-like. We have spatial density rules that are supposed to catch this, but they are coarser than we would like. Getting the grammar to encode more fine-grained spatial aesthetics without blowing up the search space is an open problem we are still working through.

There is also the question of player-space feedback. The grammar produces dungeons that satisfy structural properties, but it does not yet model how a player will perceive and reason about the space as they move through it. A layout that looks correct in the graph might feel disorienting to navigate because the spatial relationship between rooms does not map intuitively to the player's mental model. We are collecting data on this from playtests, but we do not yet have a clean way to encode navigability as a constraint.

The Constraint-as-Design-Intent Pattern

The thing that surprised me most when we started writing grammar rules was how much design intent they encode. The rule that prevents resource pickups in the pre-boss zone is not just a technical constraint. It is a statement about what we want the player to feel at that moment: committed, with no remaining optimization moves, just the kit they have accumulated. Writing that rule is a design decision, not an engineering one.

That has changed how Priya and I collaborate. Level design in Ruinveil is not a matter of placing rooms; it is a matter of writing rules that express the spatial logic we want the dungeon to embody. When we want a dungeon to feel more claustrophobic in its second half, we tighten the branch ratio rule for post-midpoint rooms. When we want the final approach to a boss to feel like a commitment, we extend the resource-exclusion zone. The grammar becomes a kind of specification language for design intent.

That said, there is a real craft gap between writing a rule and understanding how it will interact with the other rules under adversarial seed conditions. Some of our most interesting debugging sessions have been cases where two individually correct rules composed into a local deadlock that forced the generator into a valid-but-ugly backtrack path. Finding those interaction cases requires generating hundreds of dungeons and looking for structural anomalies, which is part of why we built our run-variety metrics system. That will be a separate post.