← All posts

Wave Function Collapse in JavaScript: Infinite 2D Roguelike Tilemaps

Master Wave Function Collapse (WFC) in JavaScript to build infinite, constraint-solving 2D roguelike tilemaps for your indie browser games.

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.

FeatureCellular Automata (Caves)Random Noise (Perlin/Simplex)Wave Function Collapse (WFC)
Visual CoherenceOrganic, cave-like structuresSmooth heightmaps, natural gradientsHighly structured, tile-precise layout
Rule EnforcementLow (trial and error)None (pure mathematical value)Absolute (enforced adjacency constraints)
CPU Cost (JS)Very LowMinimalModerate (requires careful optimisation)
Dead-End RiskHigh (needs flood-fill post-processing)N/ALow (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.

Thanks for reading. Browse more from the Wobblox blog, or jump straight into all 100 free games.