Tilemap & World Gen
Four modules for building procedural 2D worlds. The core design principle: MapWalkerA guarantees a connected, walkable path between every area. Noise is great for terrain shapes but can’t promise connectivity — use walkers when a player must always be able to reach the destination.
Include
<script src="/js/dn/tilemap/mapWalkers.js"></script>
<script src="/js/dn/tilemap/noiseMapGen.js"></script>
<script src="/js/dn/tilemap/worldGenUtils.js"></script>
<script src="/js/dn/tilemap/tileMapBitmask.js"></script>
MapWalkerA — mapWalkers.js
A grid-based walker that claims tiles as it moves. Three modes:
| Mode | Behavior |
|---|---|
'num' | Walk until N tiles are claimed. Use for area blobs (lakes, rooms, forests). |
'target' | Bias toward a target tile. Use for corridors connecting two areas. |
'numtarget' | Claim N tiles while biasing toward a target (hybrid). |
Constructor
new MapWalkerA({
mapData: [], // flat tile array (writes startTileValue to claimed tiles)
mapWidth: 64,
mapHeight: 64,
startRow: 0,
startCol: 0,
startTileValue: 1, // tile value written to every claimed tile
// num / numtarget modes
numTiles: 200, // total tiles to claim
// target / numtarget modes
targetRow: 32,
targetCol: 32,
likelihoodFactor: 0.7, // 0–1: how strongly the walker biases toward target
// optional
radiusConstraint: 15, // max distance from start (num mode only)
onTileDiscovered: (row, col) => {}, // callback after each step
})
Methods
| Method | Description |
|---|---|
walk() | Run synchronously. Returns when complete. |
generateAsync(stepCallback) | Step one tile at a time, calling stepCallback each step. Use for animated generation. |
Stuck Backtracking
When a walker is boxed in with no free neighbors, it backtracks along its own discoveredTiles stack (DFS unwind) until it finds a tile with a free neighbor. Never teleports — teleporting would break the path guarantee. Treat map edges as walls.
Key Rule: No Teleporting
The walker’s core value is a guaranteed connected path. If you need path-guaranteed corridors between two areas, use target mode. If connectivity doesn’t matter, use NoiseMapGen instead (simpler and faster).
NoiseMapGen — noiseMapGen.js
Seeded, layered noise for organic terrain shapes. Three noise types:
| Type | Output range | Best for |
|---|---|---|
ValueNoise | [0, 1] | Elevation, moisture, cave density |
PerlinNoise | ~[-1, 1] | Smooth gradients, wind, fog |
SimplexNoise | ~[-1, 1] | Faster than Perlin, similar quality |
Constructor
new NoiseMapGen({
seed: 'world-a', // string or number — same seed = same map
type: 'simplex', // 'value' | 'perlin' | 'simplex'
octaves: 4, // layering passes (more = more detail)
scale: 0.05, // zoom level (smaller = zoomed out, more features)
persistence: 0.5, // amplitude decay per octave (0–1)
lacunarity: 2.0, // frequency multiplier per octave
amplitude: 1.0, // overall output scale
threshold: 0.5, // values above this become land (1), below become ocean (0)
})
Methods
| Method | Description |
|---|---|
generate(width, height) | Returns a flat tile array (thresholded to 0/1 by default). |
generate(width, height, { raw: true }) | Returns raw float array — useful for layering. |
applyToArea(area, mapData, w, h, opts) | Drop-in for walkArea() — populates mapData in-place. |
NoiseMapGen.combine(generators, w, h) | Sum raw float layers, then threshold. |
When to Use Noise vs Walkers
- Noise → organic terrain shapes (islands, cave density, elevation). Fast. No path guarantee.
- Walkers → guaranteed connectivity. Use for corridors between named locations.
- Together → use noise for base terrain, then walkers for feature placement and connections on top.
worldGenUtils — worldGenUtils.js
Layer-2 utilities that sit on top of MapWalkerA.
connectAreas(mapData, mapWidth, mapHeight, areaA, areaB, opts)
Runs a targeted walker from areaA.spawnCol/spawnRow to areaB.spawnCol/spawnRow. Optionally applies zone-coloring after: tiles closer to areaA get areaA.tileValue, tiles closer to areaB get areaB.tileValue. Used to carve corridors between two area blobs.
connectAreas(mapData, 64, 64,
{ spawnRow: 10, spawnCol: 10, tileValue: TILE.FOREST },
{ spawnRow: 50, spawnCol: 50, tileValue: TILE.DESERT },
{ connectorTileValue: TILE.PATH, likelihoodFactor: 0.75 }
);
walkArea(mapData, mapWidth, mapHeight, opts)
Convenience wrapper for a num-mode MapWalkerA. Place a terrain blob around a center point.
sealDeadEnds(mapData, mapWidth, mapHeight, opts)
Finds corridor entry tiles with only one open cardinal neighbor (dead-end stubs) and fills them with a wall tile. Cleans up generation artifacts.
identifyCorridors(mapData, mapWidth, mapHeight, opts)
Returns land tiles with fewer than threshold cardinal land neighbors — i.e., narrow 1-tile-wide paths. Use to detect and optionally widen natural chokepoints.
tileMapBitmask — tileMapBitmask.js
Resolves the correct sprite-sheet index for a tile based on its 8 neighbors.
How It Works
Each tile has 8 neighbors (cardinal + diagonal). The bitmask key is an 8-bit string describing which neighbors are “matching” terrain. The resolver looks up that key in a bmKey table you provide, returning the sprite-sheet index.
const bmKey = {
'00000000': 46, // isolated
'01010101': 12, // fully surrounded
// ... 256 possible combinations
};
setupMapBitmaskCSV(mapData, mapWidth, mapHeight, bmKey, targetTileValue);
This is how FF1-style tilesets achieve smooth terrain borders — each grass tile knows whether it’s next to water on each side and picks the right corner/edge/center sprite.