dot-procedural-generation generates a level as a document — a grid, a list
of rooms, a list of links and a list of markers — and leaves building it to the
game.
Requires dot-core.
Why a document rather than a scene tree
A generator that instantiates nodes as it goes cannot be compared between two
machines, asserted on by a suite, shared by a 2D and a 3D game, or paused
half-way. Working on DotProcGenDoc instead buys four things, in order of how
much they matter:
- Two peers agree by comparing a hash. A seed and a
fingerprint(), not a map. - “Is it finishable?” is a question you can ask. It is a flood fill over a grid.
- One generator, two dimensions. A top-down game reads the grid as tiles; a first-person game extrudes it.
- It can be generated over several frames without anybody walking into a half-built world.
Usage
var p := DotProcGenPipeline.new()
p.width = 64
p.height = 64
p.add(rooms).add(corridors).add(spawns).add(validate)
p.seed_source = rng # optional; takes the session's seed
var res := p.generate(seed)
if res.ok:
build(res.value as DotProcGenDoc)
Or sliced across frames, one step at a time on the main thread, because a web template without threads is most of them. The sliced and the blocking runs produce exactly the same map, which is what makes slicing an implementation detail.
Example: the sliced runner
runner.pipeline = p
runner.finished.connect(_build)
runner.begin(seed)The validator
Generation is easy. Generation that cannot produce an unplayable round is the problem. A cellular-automata cave has sealed pockets by construction; a room placer can wall off a start it carved earlier; a scattered objective lands in a chamber with no door about one seed in thirty. Every one of those is invisible to every other kind of check — the map is well formed, the counts are right, the rooms are the size they should be, and a player walks in and cannot finish.
var check := DotProcGenValidate.new()
check.from_kind = &"spawn"
check.min_connected_fraction = 0.6 # connected AND not mostly wasted
check.fill_unreachable = true # usually better than failing
A failure retries on a mixed seed, not the next one
seed + 1 is somebody else’s world, so a failure on seed 41 would quietly hand
a player the map seed 42 should have produced.
Each step draws from its own stream
With one shared generator, inserting a step shifts every later random number, so
adding a “scatter some rubble” pass changes the room layout. DotProcGenRandom
derives a child stream by hashing a name, and two children never disturb each
other.
It is a second implementation of what
dot-core also carries,
and it predates that module arriving there: DotProcGenRandom names nothing
outside this addon, and what a caller supplies is the seed, through a
duck-typed seed_source with one seed_for(name) method.
A spanning tree is a bad level
DotProcGenConnect builds a minimum spanning tree — the cheapest corridors that
guarantee one piece — and then adds extra_loops more. A tree has exactly one
route between any two rooms, so every fight is a corridor, every retreat is the
way you came, and the map reads as a sequence rather than as a place.
`to_ascii()` is worth more than it looks
Every generator bug this kind of code has is visible in ten lines of text and invisible in every assertion that does not print one. Print the map when a check fails.
