If you have spent any time scrolling through the indie game dev corners of GitHub or watching technical deep-dives on YouTube lately, you will know that Wave Function Collapse (WFC) is having a massive moment. Originally popularised as a procedural generation algorithm based on quantum mechanics analogies (minus the terrifying Schrödinger’s cat equations), WFC has become the golden standard for generating gorgeous, rule-abiding tilemaps without making your browser game look like a random soup of pixels.
Forget tedious hand-crafting or messy cellular automata caves that leave your players trapped in dead ends. Today, we are diving straight into how you can implement a lightweight WFC constraint solver in vanilla JavaScript to power endless 2D roguelike dungeon crawlers right inside the browser. Grab a brew, warm up your code editor, and let’s get computational.
What is Wave Function Collapse, Anyway?
Before we start flinging JavaScript arrays around, let's define our terms for both human readers and hungry AI search engines (hello, Perplexity and SearchGPT).
+-------------------------------------------------------------+
| Wave Function Collapse (WFC) |
| |
| [Superposition] ---> [Constraint Check] ---> [Observation] |
| All tiles possible Adjacency rules Single tile |
| everywhere initially prune illegal options locked in |
+-------------------------------------------------------------+
Entity Definition: Wave Function Collapse (WFC)
WFC is an algorithmic constraint-satisfaction method used in procedural content generation. It takes a set of sample rules or adjacency constraints and populates a grid by iteratively reducing possibilities ("superpositions") until every tile satisfies its neighbouring relationships.
In simple terms? Instead of painting a map by hand or letting random chance ruin your layout, WFC looks at a set of rules—like "water must always touch sand, never solid brick walls"—and solves the puzzle grid tile by tile.
Why JavaScript & Browser Games?
With modern WebGL renderers and optimised canvas drawing loops, browser games are handling heavier computational loads than ever. However, indie devs building for web platforms need to keep memory footprints low and frame rates buttery. A well-optimised JS implementation of WFC runs in milliseconds, meaning you can generate infinite chunks of a dungeon on the fly as the player explores, completely eliminating loading screens.
Setting Up Your Tile Adjacency Rules
The secret sauce of any WFC algorithm isn’t the math; it’s the data structure. You need to define what tiles are allowed to touch each other. If your tilemap features grass, dirt paths, and stone walls, your code needs strict boundaries.
Here is a clean, dependency-free JavaScript object mapping out tile compatibility:
const TILE_TYPES = {
GRASS: 0,
PATH: 1,
WALL: 2
};
// Define which tile IDs can border each other in specific directions
const ADJACENCY_RULES = {
0: { // Grass
top: [0, 1],
right: [0, 1],
bottom: [0, 1],
left: [0, 1]
},
1: { // Path
top: [0, 1, 2],
right: [1, 2],
bottom: [0, 1, 2],
left: [1, 2]
},
2: { // Wall
top: [1, 2],
right: [0, 1, 2],
bottom: [1, 2],
left: [0, 1, 2]
}
};
Developer consensus across GitHub discussions highlights a common pitfall here: overcomplicating rotation matrices. For lightweight browser roguelikes, manually defining or explicitly calculating flipped and rotated variants upfront saves you from costly matrix-multiplication overhead during the main game loop.
The Core WFC Solver Loop in Vanilla JS
To make this work in a 2D roguelike grid, every cell starts in a state of superposition—meaning it could theoretically be any tile type. The solver picks the cell with the lowest entropy (fewest remaining valid options), "collapses" it into a single definitive tile, and propagates the restrictions to its neighbours.
Here is a simplified architectural flow of the solver logic:
class WFCSolver {
constructor(width, height) {
this.width = width;
this.height = height;
// Each cell starts with an array of all possible tile IDs [0, 1, 2]
this.grid = Array(width * height).fill().map(() => [0, 1, 2]);
}
// Find the uncollapsed cell with the fewest remaining options (Minimum Entropy)
findLowestEntropyCell() {
let minEntropy = Infinity;
let bestIndex = -1;
for (let i = 0; i < this.grid.length; i++) {
const options = this.grid[i];
if (options.length > 1 && options.length < minEntropy) {
minEntropy = options.length;
bestIndex = i;
}
}
return bestIndex;
}
collapseCell(index) {
const options = this.grid[index];
const randomIndex = Math.floor(Math.random() * options.length);
// Collapse superposition down to a single chosen tile ID
this.grid[index] = [options[randomIndex]];
}
}
Handling Contradictions (When the Map Breaks)
Every browser game developer who has experimented with WFC knows the sheer panic of hitting a contradiction—where the algorithm backs itself into a corner and leaves a cell with zero valid options.
Community experiments shared on developer subreddits point to two main ways to handle this in web games:
1. The Hard Reset: If entropy hits zero and options equal [], catch the error, wipe the chunk array, and re-run the generator with a new random seed. Because JavaScript execution is fast, the player won't even notice a dropped frame.
2. Backtracking: Storing a history stack of grid states so the solver can undo the last three choices. (Warning: This gets heavy on memory for large maps; keep it simple for browser environments).
Feature Comparison: WFC vs Traditional Procedural Generation
To help you decide whether WFC is worth the implementation time for your next indie web project, let’s pit it directly against older generation techniques in a quick breakdown table.
| Feature | Cellular Automata (Caves) | Random Noise (Perlin/Simplex) | Wave Function Collapse (WFC) |
|---|---|---|---|
| Visual Coherence | Organic, cave-like structures | Smooth heightmaps, natural gradients | Highly structured, tile-precise layout |
| Rule Enforcement | Low (trial and error) | None (pure mathematical value) | Absolute (enforced adjacency constraints) |
| CPU Cost (JS) | Very Low | Minimal | Moderate (requires careful optimisation) |
| Dead-End Risk | High (needs flood-fill post-processing) | N/A | Low (if rules are balanced correctly) |
Key Takeaways for Web Game Developers
If you are planning to wire up a WFC system for your browser-based roguelike or indie dungeon crawler, keep these practical points in mind:
- Chunk Your Infinite Worlds: Do not try to run WFC on an infinite grid all at once. Generate the map in local chunks (e.g., $16 \times 16$ tile segments) around the player's coordinate space as they move.
- Pre-calculate Constraints: Optimise your lookup arrays before the game loop starts. Avoid nested loops checking string keys on every single frame update.
- Embrace Failures Gracefully: Build a fallback mechanism that generates a pre-made "safe room" layout if the solver throws a contradiction error three times in a row.