Blog

Procedural Generation Only Works When Something Checks It

7 min read

A generator that can produce an unplayable level will produce one, on someone else, on launch day. The rules that reject bad output are not plumbing. They are the design, written down.

The generator is the easy half

Anyone can write a dungeon generator in an afternoon. Scatter some rooms, connect them with corridors, drop in enemies and treasure. It runs, it produces dungeons, and for about ten minutes it feels like magic.

Then you play forty of them. Two have the exit sealed behind a locked door whose key spawned inside the locked area. One puts the player in a room with six enemies at point-blank range on frame one. Several are boring in a way you cannot immediately articulate. One is genuinely great, which is worse than it sounds, because now you know the good level was luck and you have no way to make it happen again.

That gap between a generator that runs and a generator you would ship is the actual work, and it is almost entirely about constraints and checking. The generation is the cheap part. This is true whether the generator is a program you wrote or a model producing output, and it is the most commonly skipped stage in the whole subject.

Unwinnable is the default state

Left to itself, a random arrangement of things is usually broken. Not aesthetically. Functionally.

Spelunky's generator handles this in the most direct way available: before it does anything else, it carves a guaranteed path from the entrance to the exit, choosing a route through the grid of rooms and marking those cells as part of the path. Everything else gets filled in around a route that is known to exist. The guarantee is structural. It is not checked afterwards, it is built first, and nothing later is allowed to break it.

Minesweeper solves a smaller version of the same problem by making the first click always safe, generating the board after you have clicked. A game where you can lose before making a decision is not a game, and the fix is to generate under a constraint rather than hope.

Key-and-door dependencies are where this bites hardest, because the failure is silent. Everything looks fine. The player wanders for twenty minutes before concluding the game is broken, and they are right. Any generator that places gated content needs to reason about ordering, which usually means generating the dependency graph first and the geometry second, not the other way round.

A validator is a design document you can run

The useful reframe is that the checks are not quality control bolted on at the end. They are the clearest statement you will ever write of what your levels are supposed to be.

When you sit down to write a validator you are forced into sentences like: the player must never start within three metres of an enemy, there must be at least one route to the exit that does not require the double jump, no room may have more than two exits on the same wall, the first treasure must be visible from the entrance. Every one of those is a design decision that was previously living vaguely in your head.

Writing them as code does two things. It makes them true for every level instead of the ones you happened to look at, and it makes them arguable. A rule you can point at is a rule you can decide to remove, and half of tuning a generator is discovering which of your rules were superstitions.

The satisfying part is that a validator gets better in a way a prompt does not. Every bad level you find becomes a rule, and that rule is permanent. After a few weeks the failures you are catching are subtle ones, because the obvious ones can no longer be produced.

Seeds are how you debug something that changes every time

You cannot fix what you cannot reproduce, and a generator without seeding is a machine for producing bugs that vanish when you look at them.

Make the seed visible from the start. Print it, log it with every crash, and make it settable. The workflow this unlocks is the whole point: a tester reports a broken level, you type the seed, and you are standing in the identical broken level.

The second thing seeds unlock is volume. Once generation is reproducible and validation is code, you can run ten thousand seeds without a human present and count the failures. This is the moment a generator stops being a toy. You are no longer asking whether this level is good. You are asking what fraction of all levels violate a rule, which is a question with an answer.

Budget for the rejects. Generate-check-discard-retry is a completely normal loop, and a generator that throws away eight out of ten attempts is fine if generating is cheap. What is not fine is shipping the eight.

Variety in the data is not variety in the play

The other way generators disappoint has nothing to do with correctness. Every level is valid, playable and different, and the game is still monotonous.

Kate Compton's phrase for this is exact: ten thousand bowls of oatmeal. Each bowl has a mathematically unique arrangement of oats. To a person eating them, there is one bowl of oatmeal.

The reason is that variety is only perceived when it changes what the player does. A generator that varies wall texture, room size and enemy count produces levels that look different and play identically, because none of those change your plan. A generator that varies whether the room has cover, whether the enemies are between you and the exit, or whether you can reach the high ground first produces levels that feel different with far less variation, because each of those changes a decision.

So the sharpest question to ask of any generated variation is: what does the player do differently because of this? If there is no answer, that dimension is decoration, and adding more of it will not help.

What to generate and what to place by hand

Generation is wrong for the parts of a game that need to happen in a particular order to a person who has never played it before.

The first hour especially. Teaching depends on sequence: show the mechanic in safety, then under mild pressure, then in combination. A generator cannot guarantee that sequence because it does not know what the player has already understood. Nearly every roguelike with a good opening has a handcrafted one.

Boss fights, story beats and any moment you want people to talk about afterwards. The value of those is that they are specific, and specific is the opposite of what a generator does.

What generation is right for is the middle: the material, the arrangement, the run-to-run variation that keeps a game alive past the tenth attempt. And it is right for anything you need at a volume no human would produce, which is the honest reason most terrain and loot systems are procedural.

The mix that recurs across good examples is authored units in generated arrangements. Hades generates the sequence of hand-built rooms. Diablo assembles authored tiles. The human makes the small thing well and the machine makes the combinations, because judgement is expensive per square metre and cheap per rule.

Where this lands in a desktop tool of this kind

A runtime generator is code, so in a desktop tool of this kind it is code that the assistant writes. You describe the generator you want and the GDScript lands in your project, which is a real Godot project, so the generator is a file you can open, read and tune. That matters more here than almost anywhere else, because a generator is the definition of something you will adjust for the rest of the project.

Ask for the validator at the same time as the generator. It is the same size of job and it is the half that pays off, and describing your rules out loud is useful even before any code exists.

The starting material is generated too. Terrain comes from generation, including real elevation data behind a landmass, and individual assets are produced one at a time from a description. Building needs a connection because that all comes over the network. Playing what you built does not: the game runs locally on the copy of Godot the app bundles.

Then you do the thing no automated check can do. You play thirty runs and notice that the levels are technically valid and slightly dull, which is a judgement about experience that no validator will ever make for you. That is the creator's call, and it is the one that decides whether the generator was worth building.

The short version

Write the constraint before the generator. Guarantee the critical path structurally instead of checking for it afterwards. Seed everything from day one. Run thousands of levels headless and count failures rather than eyeballing five. Hand-build the opening and the moments that matter. Ask of every varied dimension what the player does differently because of it.

Do those and a modest generator will carry a game for years. Skip them and the most sophisticated generator you can build will produce infinite oatmeal, correctly, forever.