All Devlog Posts
The Seed System cover

Ruinveil's seed system was not a late addition to the design. It was an architectural decision made before we wrote the first line of the generation engine. The reasoning was simple: if we are building a procedural game, we should think carefully about what it means to share a run with another person. The seed is the answer to that question.

This post describes how the seed system works, why we built it the way we did, and where it has created constraints we are still navigating.

What a Seed Is and Is Not

A seed in Ruinveil is a compact string, currently twelve characters, that fully specifies a run. Given the same seed and the same game version, the dungeon generated will be identical in every meaningful way: room layout, room roles, enemy composition, enemy behavior trait draws, resource placement, vault locations, boss parameters. Two players running the same seed will have the same dungeon to navigate.

What the seed does not control: player choices. Routing decisions, item selection from loot pools, combat positioning. These are outside the seed's scope. The seed defines the space the player navigates; it does not define how they navigate it. This is the correct split: what the dungeon offers should be reproducible; what the player does with it should not be.

There is a design question embedded in that split that we have had to revisit. Some roguelites treat all randomness as seed-controlled, including item drop results from enemies. Our current design seeds item drops from a combination of the run seed and the player's action history at the moment of the drop. This means item drops are not fully reproducible from the seed alone: two players running the same seed but making different combat choices will see different item drop sequences. We made this tradeoff deliberately, but it creates edge cases in the speedrunning community that we need to communicate clearly.

The Generation Architecture That Makes Seeds Work

Deterministic procedural generation from a seed requires that every random decision in the pipeline use the seed as its entropy source, and that the order of those decisions is deterministic: no branching based on system time, frame counts, or any non-seed input. This sounds obvious but it is surprisingly easy to violate when the generation pipeline has multiple passes that were developed independently.

We had a six-week period in late 2024 where our seeds were unstable: the same seed would produce slightly different dungeons on consecutive runs. The root cause was a race condition in the enemy behavior assembly pass. The order in which enemy instances were initialized depended on the OS scheduler's thread dispatch order, which introduced non-determinism. The behavior trait draws, which happen per-enemy-instance in initialization order, were therefore non-deterministic even though each individual draw was seeded correctly.

The fix was to enforce a strictly sorted initialization order for enemy instances based on their room assignment and position within the room, using the seed value as a tiebreaker for any two instances with the same spatial position. After that fix, seeds became stable. This is the kind of architectural detail that matters enormously in a seeded system and that is easy to miss until it breaks.

Seed Format and Player Experience

The seed is displayed to the player at the start of each run and at the end of each run in the summary screen. Players can enter a specific seed before starting a run to replay a previously experienced dungeon or to play a seed they received from another player.

The twelve-character string format is a deliberate choice. It is short enough to remember and share verbally or in a chat message but long enough to encode the full entropy we need. The character set is alphanumeric uppercase, which avoids ambiguous characters like 0/O and 1/I/l. A player can say "try seed RUIN4VX9KM2" in a stream chat and their viewers can enter it accurately.

We chose not to use word-based seed notation (which some games use, generating seeds like "IRON-FIRE-TOMB") because it compresses the entropy space in ways that create unexpected collisions between similar-sounding seeds and complicates the backend representation. The alphanumeric approach is slightly less memorable but more robust.

Seeds and Speedrunning

The community case for seed support is clearest in speedrunning. A speedrunner who has found a fast routing strategy through a specific dungeon layout can record their run, share the seed, and let others verify, attempt to beat, or study the same layout. Without seeds, speedrunning a procedural game is either meaningless (every run is a different game so times are not comparable) or it requires any% style records that mix player skill with generation luck in ways that are hard to interpret.

Seeded categories allow the community to isolate player skill from generation luck when they want to, while keeping the uncontrolled random experience available for players who prefer it. We intend to support both in Ruinveil's leaderboard structure: a random-seed category where runs are categorized by dungeon length and difficulty, and a specific-seed category where players compete on known dungeons.

We do not yet have a fully developed leaderboard system. This is on our roadmap but it requires significantly more infrastructure than the seed system itself. The seed system was something we could build into the engine from the start; the competitive infrastructure is a later-stage investment.

Seed Stability Across Game Versions

One limitation we want to be transparent about: seeds are not version-stable across major updates. When we change the generation grammar, add new enemy types to the ecology pool, or modify the behavior trait library, previously valid seeds may produce different dungeons or fail to generate entirely. This is a known tradeoff and one we cannot fully avoid without freezing the game's content.

Our current approach is to tag seeds with the game version at the time they were generated and display a warning when a player attempts to load a seed from a different version. We preserve the old generation code as a legacy path for one major version cycle, then deprecate it. Players who want to preserve specific dungeons can export a full dungeon snapshot rather than relying on seed re-generation.

We are not satisfied with this solution but we have not found a better one that does not require either freezing content or maintaining an increasingly complex multi-version generation archive. This is a known open problem in seeded procedural design and we are watching how other studios in the space handle it.

What We Learned Building This First

The decision to build the seed system before writing the full generation pipeline shaped the architecture in ways that cost us some development speed early but saved us significant rework later. Every major system was built with determinism as a first-class requirement. We had to think carefully about evaluation order and entropy consumption at every step. That discipline is part of why our generation pipeline has relatively few non-determinism bugs: the constraint was present from the beginning, not bolted on afterward.

If we were starting over, we would keep the same approach. Build for determinism first, add features second. The alternative: build freely, then try to seed-stabilize a mature pipeline, is a path that has caused visible pain for other studios and we wanted to avoid it.