Procedural Generation in JavaScript
Browser-based procedural generation has unique advantages over native implementations. Players can see results instantly without downloading or installing anything. Developers can iterate rapidly because browser developer tools provide real-time inspection, hot reloading, and visual debugging. The combination of procedural generation with web distribution makes it practical to build games that reach millions of players through a single URL, with every player experiencing a unique world generated on their own device.
Set Up a Seeded Random Number Generator
JavaScript's built-in Math.random() cannot be seeded, which means you cannot reproduce the same generation output from a specific seed value. For procedural generation, you need a seeded pseudorandom number generator (PRNG) that produces the same sequence of numbers from the same seed every time.
Several lightweight, high-quality seeded PRNGs are available for JavaScript. Mulberry32 is a simple 32-bit PRNG that fits in a few lines of code and produces good statistical randomness for game applications. SplitMix32 and xoshiro128** offer better statistical properties for applications that need higher-quality randomness. The seedrandom npm package wraps multiple PRNG algorithms behind a common interface and can replace Math.random globally.
A minimal seeded PRNG implementation takes a seed integer and returns a function that produces a new pseudorandom float between 0 and 1 each time it is called. You call this function wherever you would normally call Math.random. By passing the same seed at the start of generation, you guarantee the same level, terrain, or dungeon is produced regardless of platform, browser, or operating system.
For chunk-based worlds where different parts of the map generate independently, use a hash function instead of a sequential PRNG. A position-based hash takes the seed plus the chunk coordinates as input and returns a pseudorandom value. This approach is inherently parallel (any chunk can be generated without knowledge of other chunks) and deterministic (the same coordinates always produce the same value). MurmurHash3 and xxHash are popular choices for position-based hashing in JavaScript.
Implement or Import Noise Functions
Several npm packages provide ready-to-use noise implementations for JavaScript. The simplex-noise package is the most widely used, offering 2D, 3D, and 4D simplex noise with configurable seeding. The open-simplex-noise package provides the OpenSimplex algorithm, which avoids the directional artifacts of classic Perlin noise. For projects that need a minimal footprint, a standalone simplex noise implementation fits in about 200 lines of JavaScript.
Using a noise library is straightforward: create a noise generator with a seed, then sample it at any coordinates to get a smooth random value. For a 2D heightmap, you loop over each pixel of your output grid, call noise2D(x * scale, y * scale), and use the returned value as the height or color. The scale parameter controls the frequency of the noise, with smaller values producing larger, more spread-out features.
Fractal Brownian motion (fBm) layers multiple octaves of noise to create multi-scale detail. A JavaScript fBm function loops through the desired number of octaves, accumulating noise values at increasing frequencies and decreasing amplitudes. Four to six octaves produce convincing terrain detail for most browser games. Each octave multiplies the frequency by the lacunarity (typically 2.0) and multiplies the amplitude by the persistence (typically 0.5).
Performance matters for real-time generation. Generating a 512x512 heightmap by sampling noise at every pixel can take 50 to 200 milliseconds on a modern browser, depending on the number of octaves and the noise implementation. For interactive applications where terrain generates while the player watches, consider generating at a lower resolution and upscaling, or generating in chunks spread across multiple animation frames using requestAnimationFrame to avoid blocking the main thread.
Build a Grid-Based Dungeon Generator
A 2D array is the natural data structure for grid-based dungeon generation in JavaScript. Create an array of arrays where each cell stores a tile type (wall, floor, door, stairs, etc.). Initialize every cell as a wall, then carve out floor cells according to your chosen algorithm.
For a BSP dungeon generator in JavaScript, define a recursive split function that divides a rectangle into two smaller rectangles by choosing a random split position along either the horizontal or vertical axis. Recurse on both halves until each partition is below a minimum size. Then place a randomly sized room within each leaf partition and carve corridors connecting sibling rooms. The entire algorithm runs synchronously and completes in under a millisecond for typical dungeon sizes (80x50 cells).
For cellular automata caves, fill the grid randomly (about 45% walls, 55% floors), then run 4 to 6 iterations of the smoothing rule: for each cell, count its wall neighbors (including diagonals, for a maximum of 8), and set the cell to wall if it has 5 or more wall neighbors, or floor otherwise. After smoothing, run a flood fill from a central floor cell to identify the largest connected region. Carve tunnels to connect any disconnected floor regions to the main area.
JavaScript objects or Maps work well for storing room metadata alongside the spatial grid. Each room object records its bounds, center position, connections to other rooms, assigned enemy groups, and item placements. This metadata layer sits on top of the tile grid and enables gameplay logic to reference rooms by their properties rather than raw coordinates.
The rot.js library provides ready-made dungeon generators (BSP, Digger, Uniform, Cellular) along with field-of-view calculation, pathfinding, lighting, and input handling. For developers building traditional roguelikes in the browser, rot.js eliminates the need to implement these algorithms from scratch and provides a solid, tested foundation.
Render Procedural Output to Canvas or WebGL
The HTML Canvas element is the simplest way to render procedural generation output in the browser. Create a canvas element, get its 2D rendering context, and draw each tile as a colored rectangle. For a dungeon with 80x50 cells at 12 pixels per cell, the canvas is 960x600 pixels and renders in under a millisecond, fast enough to redraw every frame during gameplay.
For terrain heightmaps, use getImageData and putImageData to write pixel colors directly to the canvas buffer. Loop through each pixel, convert the noise value to an RGB color (using elevation thresholds for biome coloring), and write the color to the image data array. Put the image data back to the canvas in a single call. This approach is faster than drawing individual rectangles for pixel-level detail.
Tile-based rendering with sprite sheets uses drawImage to blit tile sprites from a tileset image onto the canvas at the appropriate grid positions. Each tile type maps to a specific rectangle within the sprite sheet, and the renderer draws only the tiles visible within the current viewport. This culling optimization keeps frame times constant regardless of the total map size.
For 3D terrain, Three.js provides a PlaneGeometry that you can modify by setting vertex heights from your noise function. Create a plane with the desired number of segments, iterate over the geometry's position attribute, set each vertex's Y value from the heightmap, recompute normals, and add the mesh to the scene. Texture the terrain using vertex colors or a multi-layer shader that blends textures based on elevation and slope.
WebGL shaders can generate noise entirely on the GPU, which is orders of magnitude faster than JavaScript noise for large terrains. A fragment shader that implements simplex noise can generate and render a terrain heightmap in real time at full screen resolution. This approach is ideal for terrain visualization tools and games where the terrain needs to update dynamically.
Move Heavy Generation to Web Workers
Web Workers run JavaScript in background threads, separate from the main thread that handles rendering and user input. Moving computationally expensive generation to a Web Worker prevents the interface from freezing during level creation, which is especially important for complex generators that take more than 16 milliseconds (one frame at 60fps) to complete.
The Worker communication pattern for procedural generation follows a simple request-response model. The main thread posts a message to the worker containing the seed and generation parameters. The worker runs the generation algorithm, then posts the result (the tile grid, heightmap, or room data) back to the main thread. The main thread receives the result and renders it. During generation, the main thread can show a loading animation or progress bar.
For chunk-based worlds that generate terrain continuously as the player moves, a pool of Web Workers provides parallelism. Each worker generates one chunk at a time, and the main thread dispatches chunk generation requests to idle workers. With four workers running in parallel, the generation throughput quadruples compared to single-threaded generation, enabling smoother exploration in games with large procedurally generated worlds.
Data transfer between the main thread and workers uses the structured clone algorithm by default, which copies the data. For large heightmaps or tile grids, this copying can be expensive. Transferable objects (ArrayBuffers) eliminate the copy by transferring ownership of the memory from one thread to the other. Using Float32Array for heightmaps and Uint8Array for tile grids, wrapped in transferable ArrayBuffers, makes worker communication nearly free for even large datasets.
SharedArrayBuffer, where available, enables multiple workers to read and write the same memory simultaneously. This is useful for generation algorithms that benefit from parallel processing, like noise evaluation across a large grid, where each worker computes a portion of the output in-place without any data transfer overhead.
JavaScript provides everything needed for sophisticated procedural generation: seeded PRNGs for determinism, noise libraries for terrain, straightforward grid algorithms for dungeons, Canvas and WebGL for rendering, and Web Workers for background generation. The browser's instant-access distribution model makes JavaScript procedural generation an especially powerful combination for reaching players worldwide.