Wave Function Collapse
The mechanism (simple-tile)
The basic variant is simple-tile: you specify by hand which tiles may be adjacent (grass next to grass and to an edge; water next to water and an edge; the edge in between). The algorithm:
The WFC loop pseudocode
while undetermined cells remain:
cell = the cell with minimum entropy # fewest options
tile = pick from cell.options by weight # tile frequency
cell.collapse(tile) # observe
queue = [cell]
while queue is not empty: # propagate
c = queue.pop()
for neighbor n of cell c:
before = n.options
n.options ∩= compatible_with(c) # trim by adjacency
if n.options changed: queue.push(n)
if some cell has empty options:
contradiction → backtrack or restartThere's a second flavor too — overlapping / NxN-pattern: the adjacency rules aren't written by hand but extracted from an example image (all N×N windows). More powerful (it learns from the sample) but more expensive and more temperamental. The lab uses simple-tile so you can see the mechanics without the magic.
🕹 Games to play — and what to notice
WFC and its relatives are most visible where things are "varied but always coherent". Four cases, from "you pull the collapse yourself" to a contrast with a different approach to PCG — and what to notice by hand (simple to complex).
Oskar Stålberg's little-town sandbox: you place a voxel house and the engine builds out the roofs, walls, windows and arches so that everything fits together. Under the hood it's WFC on an irregular grid + marching cubes: each block "collapses" its geometry by adjacency rules with its neighbors. You're literally pulling observe with your hand, and propagate finishes the rest.
🎮 Play: place and remove a couple of blocks in a row — watch the roofs/windows/arches reassemble around the new neighbors. Put a block off on its own, then connect it to the house — you'll see the geometry "collapse" around the join. That's constraint propagation you steer with a finger (exactly like the anchor in the lab).
Same Stålberg, but here WFC generates island geometry (his EPC2018 talk is literally called "Wave Function Collapse in Bad North"). Every island is a connected, playable topology: no impossible cliffs, always somewhere to land and defend. The implementation is non-interactive (it generates before the battle), unlike Townscaper.
🎮 Play: run through the campaign map — every island is different but always coherent and traversable; notice that there are no "weird" cliffs or trap dead ends. That's adjacency constraints holding the shape.
The first commercial use of WFC (Brian Bucklew, Freehold Games; GDC 2019 / Roguelike Celebration talks). Generation is multi-pass: a coarse structure → middle passes fill in detail via WFC → final passes fix connectivity and populate the level. Bucklew discussed the pain points directly — overfitting/homogeneity and level connectivity — and how he treats them.
🎮 Play: wander the ruins and buildings — the wall and floor patterns are varied but locally coherent (WFC learned from examples), while the level is always connected (a final validation pass). The contrast of "varied but not garbage".
Not WFC but a neighboring approach: the level is assembled from hand-made room templates on a 4×4 grid, with a guaranteed path from entrance to exit carved on top. But the moral is straight out of the lesson: generation without constraints = garbage; "connected and traversable" is a set of hard constraints on top of randomness, and valid ≠ playable.
🎮 Play: run a dozen levels — each is different, but a path from entrance to exit always exists (that carved path). Try to "get stuck with no bombs" — you'll feel where the traversability constraint ends and design craft begins.
Deep end: WFC as a CSP, Shannon entropy and the undecidability of tilingskippable
WFC = a constraint satisfaction problem (CSP)
The variables are cells, the domains are sets of tiles, the constraints are adjacencies. The propagate() step is enforcement of arc consistency (AC-3): for every arc (cell → neighbor) you remove unsupported values from the neighbor's domain and re-queue the affected arcs. The WFC pseudocode is literally a specialized AC-3.
What "entropy" means precisely
"The cell with minimum entropy" is Shannon entropy over the weighted options:
(w_t is the tile's frequency weight; usually plus a small amount of noise to break ties). With equal weights it degenerates into "fewest remaining options" — the MRV (minimum remaining values) heuristic from classical CSP.
Termination and complexity
AC-3 is sound but not complete: arc consistency doesn't guarantee that a global solution exists. So WFC without backtracking is incomplete — it can hit a contradiction even when a solution exists. Completeness requires search with backtracking.
In the general case it's harsher: "tile the plane with a given set of Wang tiles" is an undecidable problem (Berger, 1966, the domino problem); the finite version (n×n) is NP-complete. WFC inherits that hardness; it works in practice because game tilesets are "loose" (there are many valid configurations) and a restart is cheap.
Deep end · design: controllability versus surpriseskippable
Pure procedural content quickly feels "samey" and soulless — that's its main design failure, not a technical one. The control levers:
- Anchors and set pieces: you pin key cells/rooms by hand and the generator fills in the rest (the boss arena and the entrance are fixed, the path between them is procedural).
- Handcrafted + procedural hybrid: Spelunky/Diablo assemble a level procedurally out of hand-made pieces — the best of both worlds.
- Generation QA: valid ≠ interesting and ≠ traversable. You need automatic checks for connectivity (flood-fill/A*), for dead ends, for dullness — otherwise the player gets "technically correct" garbage.
- Build-time vs runtime: generate at build time (control, curation) or on the fly (infinity, risk) — a design choice, not just a technical one.
General: scheduling, timetabling, layout, product configurators — all CSPs; SAT/SMT solvers under the hood.
ML / AI: constrained / grammar-guided decoding is WFC over tokens (generation strictly to a schema); structured output; diffusion with guidance = generation under constraints; synthetic data/augmentation = procgen for training.
Backend: dependency resolution (npm/cargo/apt) = constraint solving; the classic source of "version hell".
Principle: "generation = randomness squeezed by rules"; wherever there's "assemble a valid whole out of constrained pieces", there's a CSP.
wfc.py (simple-tile: extract_adjacency, propagate, weighted collapse, anchoring) — it runs as is. Add a log of "who collapsed whom" and watch the observe / propagate order in the console. Modifications in increasing order: add a mountain tile with a rare weight (you'll get more contradictions), then backtracking instead of a restart (completeness instead of "try again"), then overlapping mode — rules from a sample image rather than by hand. The folder is labs/lab-05-wfc-terrain/.Is "minimum entropy" the MRV heuristic from CSP?
If propagate = AC-3, why doesn't it guarantee a solution?
WFC inherits the NP-completeness of tiling — so why is it instant in games?
Why is the overlapping variant computationally more expensive than simple-tile?
Is it actually a "wave function" at all?
- Maxim Gumin, the original WaveFunctionCollapse repository (GitHub) — the source and the gallery.
- "WFC is constraint solving in the wild" (Karth & Smith) — an academic treatment of the link to CSP.
- Module 11, section 4.1 + Lab 05.