prerequisite
Graphs and grids
A tile grid is a graph whose vertices are cells and whose edges are neighbor relations, which is why every 2D generator talks about neighborhoods and connected regions.
Before this
Nothing beyond first-year college math. This is a starting page.
Why you need this
Every 2D technique in this cluster says some version of "a tile depends on its neighbors". Cellular automata count wall neighbors. Wave Function Collapse removes tile options from a cell when a neighbor changes. Markov random fields define a whole probability model out of neighbor pairs. To read any of those pages you need a precise word for "neighbor", a fixed coordinate convention, and one algorithm, flood fill, that finds which cells are reachable from which. All three come from graphs.
The idea
A graph is a set of vertices (also called nodes) and a set of edges, where each edge joins two vertices. That is the whole definition. Vertices are the things; edges are the "is next to" or "can reach" relation between things.
Edges come in two flavors.
- Undirected: the edge A-B means the same as B-A. Two rooms joined by an open doorway.
- Directed: the edge A to B does not imply B to A. A one-way drop from a ledge to the floor below. Directed edges are drawn as arrows.
A path is a sequence of vertices where each consecutive pair is joined by an edge. A cycle is a path that starts and ends at the same vertex without repeating an edge. A connected component is a largest group of vertices that can all reach each other by some path. A cave with two separate chambers and no tunnel between them has two connected components.
Storing a graph
Take four vertices A, B, C, D with undirected edges A-B, B-C, C-A, and C-D. There are two standard ways to store this.
An adjacency list keeps, for each vertex, the list of vertices it touches.
| Vertex | Neighbors |
|---|---|
| A | B, C |
| B | A, C |
| C | A, B, D |
| D | C |
An adjacency matrix is a square table with one row and one column per vertex. Entry (row, column) is 1 when the two vertices share an edge and 0 otherwise. For an undirected graph the matrix is symmetric across its diagonal.
| A | B | C | D | |
|---|---|---|---|---|
| A | 0 | 1 | 1 | 0 |
| B | 1 | 0 | 1 | 0 |
| C | 1 | 1 | 0 | 1 |
| D | 0 | 0 | 1 | 0 |
The list is compact when most vertices have few neighbors. The matrix answers "are A and D adjacent?" in one lookup but stores a 0 for every pair that is not adjacent. In this graph A-B-C-A is a cycle, A-B-C-D is a path, and all four vertices are one connected component. Add vertices E and F with a single edge E-F and the graph now has two components.
Worked example
A grid is a graph
A tilemap that is tiles wide and tiles tall has cells. Name each cell by its coordinates , where counts columns from 0 at the left and increases to the right, and counts rows from 0 at the top and increases downward. The top-left cell is and the bottom-right cell is . On a grid the cells are:
| x=0 | x=1 | x=2 | x=3 | |
|---|---|---|---|---|
| y=0 | (0,0) | (1,0) | (2,0) | (3,0) |
| y=1 | (0,1) | (1,1) | (2,1) | (3,1) |
| y=2 | (0,2) | (1,2) | (2,2) | (3,2) |
Now treat every cell as a vertex and every "shares a side" relation as an undirected edge. That is a graph. You never store the adjacency list, because it is implied by the coordinates: the neighbors of are for a fixed table of offsets .
Two neighborhoods
Say which one you mean every time. Generators behave very differently under the two.
The von Neumann neighborhood has 4 neighbors, the cells sharing a side.
| Name | dx | dy |
|---|---|---|
| Up | 0 | -1 |
| Right | 1 | 0 |
| Down | 0 | 1 |
| Left | -1 | 0 |
The Moore neighborhood has 8 neighbors, the cells sharing a side or a corner. It is the von Neumann table plus the four diagonals.
| Name | dx | dy |
|---|---|---|
| Up-left | -1 | -1 |
| Up | 0 | -1 |
| Up-right | 1 | -1 |
| Left | -1 | 0 |
| Right | 1 | 0 |
| Down-left | -1 | 1 |
| Down | 0 | 1 |
| Down-right | 1 | 1 |
For cell on the grid above, the von Neumann neighbors are , , , . The Moore neighbors add , , , .
What happens at the edge of the map
Cell has no cell above it. Something has to be decided for the offsets that point off the grid, and each choice changes what the map looks like.
| Option | Rule | What you see |
|---|---|---|
| Clamp | Replace an out-of-range coordinate with the nearest valid one, so becomes . | Edge cells see themselves as a neighbor. Patterns smear outward along the border and edges look thicker than the interior. |
| Wrap (torus) | Take coordinates modulo the size, so becomes . | The map tiles seamlessly. A cave that runs off the right side continues on the left. Good for repeating backgrounds, wrong for a bounded level. |
| Outside is wall | Any off-grid cell counts as a fixed value, usually wall or water. | The border fills in solid, so the playable area is naturally enclosed. This is the usual choice for caves. |
| Outside is missing | Skip the neighbor entirely, so edge cells have fewer neighbors. | Rules that count neighbors behave differently at the border than inside. A corner cell has only 3 neighbors, so under a "5 of 8" rule it can never become wall and the corners always open up. |
Flood fill: finding one connected region
Flood fill starts at one cell and visits every cell of the same value that can be reached through neighbors. It is breadth-first search on the grid graph. This version uses the von Neumann neighborhood and stores the grid as a flat array where cell lives at index .
function floodFill(grid, W, H, sx, sy) {
const target = grid[sy * W + sx];
const seen = new Uint8Array(W * H);
const queue = [[sx, sy]], region = [];
seen[sy * W + sx] = 1;
for (let head = 0; head < queue.length; head++) {
const [x, y] = queue[head];
region.push([x, y]);
for (const [dx, dy] of [[0, -1], [1, 0], [0, 1], [-1, 0]]) {
const nx = x + dx, ny = y + dy;
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
const i = ny * W + nx;
if (seen[i] || grid[i] !== target) continue;
seen[i] = 1;
queue.push([nx, ny]);
}
}
return region;
}
Run it on a 5-wide, 3-tall grid where 1 is wall and 0 is floor:
1 0 0 1 0
1 1 0 1 0
0 0 0 1 1
Starting at it returns the six floor cells on the left: , , , , , , and nothing else, because the column of walls at blocks the way. The two floor cells at and are a second connected component. To find all components, loop over every cell and start a fill at each one not yet in seen.
In a map generator
Graphs show up at every stage of the pipeline described on the hub page.
- Catalog: the tile catalog is itself a graph. Vertices are tile ids, and a directed edge from tile to tile labelled "right" means may sit to the right of . Wave Function Collapse consumes exactly this.
- Layout: a dungeon plan is a graph of rooms before it is a grid of tiles. Locks, keys, and critical paths are path and cycle statements.
- Fill: cellular automata count neighbors in a Moore neighborhood with outside treated as wall. Flood fill then keeps the largest floor component so the player cannot spawn in a sealed pocket.
- Regime over time: regions that share a border are neighbors in a coarse graph, and a flood or fire regime spreads along its edges.
- Export: the flat index is exactly how a tile layer is stored in the JSON manifest.
"A tile depends on its neighbors" is a graph statement: the value at a vertex is a function of the values at the vertices it shares an edge with, and nothing else. Change the edge set (switch von Neumann for Moore, or wrap the borders) and you change the generator without touching the rule.
Common mistakes
- Mixing up and when indexing. The flat index is , not . Get it backwards and a map renders as a shredded diagonal smear.
- Forgetting to say which neighborhood. A rule tuned for 8 neighbors applied to 4 produces a map that is almost entirely one value, because the counts never reach the thresholds.
- Clamping when you meant "outside is wall". The border of a cave grows a thick rind two or three cells deep instead of a clean one-cell wall.
- Wrapping by accident. A negative coordinate fed into a JavaScript modulo stays negative, so
(-1) % Wis-1, notW - 1. Islands appear cut off at one edge and continued on the other, or the code readsundefined. - Recursive flood fill on a big map. A recursive version calls itself once per cell and overflows the call stack somewhere around a few thousand cells. Use the queue version above.
- Treating diagonal contact as connected for movement. Two floor cells that touch only at a corner are Moore neighbors but a player who moves in four directions cannot pass between them. Use von Neumann for reachability and Moore for smoothing.
Cost
For a general graph with vertices and edges, an adjacency list uses memory and an adjacency matrix uses . A grid needs neither, since neighbors are computed from coordinates in constant time. Flood fill visits each cell at most once and checks each of its neighbors, so the time is with or , and the seen array is memory. On a map that is about 65 thousand cells and a quarter million neighbor checks, which runs in a few milliseconds. It starts to hurt when you flood fill every chunk every frame; fill once per generation instead and cache the component labels.
Going further
- Breadth-first search and depth-first search in any introductory algorithms text; flood fill is the former on a grid.
- Dijkstra's algorithm, which is breadth-first search with weighted edges, for path costs across terrain.
- Union-find (disjoint set union), a faster way to label connected components when you only need labels and not the cells in order.
- Cellular automata, the first technique page that uses neighborhoods and flood fill together.
- Tilemaps and autotiling, where neighbor bits pick which tile art to draw.
Leads to
- techniqueCellular automataFill a grid with random noise, apply one neighbor-counting rule to every cell a few times, and the noise organizes itself into caves with rounded walls and no straight lines.
- prerequisiteConstraint satisfactionVariables with shrinking sets of allowed values, rules between neighbors, and a search that backtracks or restarts when a set runs empty, which is the machinery under Wave Function Collapse.
- techniqueGraph grammars and L-systemsGrow the structure of a level, its rooms, locks, keys, and branches, by rewriting symbols and graphs with rules, before any tile is placed.
- techniqueMarkov random fieldsWhat "each cell depends only on its neighbors" means on a grid, why you sample it with Gibbs sweeps instead of computing it, and why the HMM tricks do not survive the move from a chain to a grid.
- prerequisiteTilemaps and autotilingTile sheets, tile ids, layered 2D arrays, and the neighbor bitmasks that pick the right edge art, which together form the data every generator in this cluster reads and writes.