1. The Starting State — A Fully-Walled Grid
Before a single line of carving code runs, every cell in the maze exists and every wall between every pair of adjacent cells is closed. Think of it as a sheet of graph paper where every single grid line is solid. You can see all the cells, but there are no passages between any of them.
Key design choice: walls are stored as booleans on
each cell, not as separate wall objects.
Every wall is shared between two cells — the north wall of cell
(1, 0) is the same physical wall as the south wall of
(0, 0). When we carve it, we set
both booleans to false.
Wall = false means the wall has been carved away (open passage).
Carving is a one-way operation — once a wall is removed it stays removed.
The outer boundary walls are never carved — the maze always has a solid perimeter. Only interior walls between two valid cells can be removed.
2. What "Carving" Actually Means
"Carving a wall" is the heart of maze generation. All it means is: set two booleans to false.
The direction table in SWMaze stores both sides of each wall
so the code never has to figure this out manually:
So the actual carving code is just:
cell.walls[wall] = false and
neighbor.walls[opposite] = false.
3. The DFS Algorithm — How It Decides Where to Carve
Maze generation uses recursive Depth-First Search (DFS).
Here is the complete real code from swMaze.js with annotations:
generate() calls
_clearVisited() to reset all the flags to false.
The solver then reuses the same flag for its own pass.
The flag is scratch space — it gets reused, not accumulated.
4. What the Colors Mean During the Build
The demo shows the carving as an animation. Each cell changes color as the algorithm visits it. Here is what each color means:
| Color | State name | What it means | When it changes |
|---|---|---|---|
| Light blue-gray | 'floor' |
Cell has not been touched by the DFS yet. All four walls still up. | Immediately when carved for the first time. |
| Cyan / teal | 'frontier' |
Where the DFS pencil tip is right now.
This is the most recently carved destination — the cell
that _generateDFS is currently running inside.
|
Set to frontier when a BUILD_CARVE step points to it. Cleared to 'carved' when the next BUILD_CARVE step fires from it. |
| Medium gray | 'carved' |
Cell has been carved and explored. One or more passages open. The DFS has been here and moved on to a neighbor (or will shortly). | Set when a BUILD_CARVE fires from this cell (it was frontier, now it moves the frontier forward). |
| Dark dim blue | 'buildback' |
The DFS backtracked through this cell after fully exhausting all its unvisited neighbors. It is a "dead end from above" — all forward paths from here led back. | Set when a BUILD_BACKTRACK step fires and the cell is still the frontier (no further carves came from it). |
| Green | 'start' |
Cell S — always fixed top-left. Never changes color during build. | Never changes during build. |
| Amber | 'treasure' |
Cell T — always fixed bottom-right. Never changes color during build. | Never changes during build. |
5. The Big Question — How Does It Know Where to Pick Back Up?
This is the core of recursion. The answer is: the JavaScript call stack remembers exactly where every function call came from and what it was doing. You never have to track it manually — the language does it for you.
Every time _generateDFS(r, c) calls
_generateDFS(nr, nc), JavaScript:
- Pauses the current function at exactly that line.
-
Saves everything about the current state: the values
of
r,c,dirs, and which iteration of the for-loop we're on. -
Starts a brand-new call of
_generateDFSwith the neighbor's coordinates. - When that new call returns, JavaScript automatically restores everything it saved and continues the paused for-loop from exactly where it left off — trying the next direction.
Visualizing the call stack growing and shrinking:
6. A Traced Example — a 3×3 Grid
Let's walk through a tiny 3×3 grid manually. Directions are chosen as E, S, W, N at every cell (fixed for this trace — normally they'd be shuffled). S = start at (0,0), T = treasure at (2,2).
| # | Event | Call stack (bottom→top) | What happens visually | Walls opened |
|---|---|---|---|---|
| — | generate(0,0) called | empty | All 9 cells are light gray (floor). S=(0,0) is green, T=(2,2) is amber. | — |
| 1 | _generateDFS(0,0) starts. dirs=[E,S,W,N]. Try E → (0,1) is unvisited. |
[DFS(0,0)] |
(0,1) turns teal. | (0,0).E = false (0,1).W = false |
| 2 | Recurse → _generateDFS(0,1). dirs=[E,S,W,N]. Try E → (0,2) unvisited. |
[DFS(0,0), DFS(0,1)] |
(0,1) turns gray (carved). (0,2) turns teal. | (0,1).E = false (0,2).W = false |
| 3 | Recurse → _generateDFS(0,2). dirs=[E,S,W,N]. E → OOB. Try S → (1,2) unvisited. |
[DFS(0,0), DFS(0,1), DFS(0,2)] |
(0,2) turns gray. (1,2) turns teal. | (0,2).S = false (1,2).N = false |
| 4 | Recurse → _generateDFS(1,2). dirs=[E,S,W,N]. E→OOB. Try S → (2,2) unvisited. |
[…, DFS(0,2), DFS(1,2)] |
(1,2) turns gray. (2,2) turns teal. | (1,2).S = false (2,2).N = false |
| 5 | Recurse → _generateDFS(2,2). E→OOB. S→OOB. W→(2,1) unvisited. Try W. |
[…, DFS(1,2), DFS(2,2)] |
(2,2)→T stays amber. (2,1) turns teal. | (2,2).W = false (2,1).E = false |
| 6 | Recurse → _generateDFS(2,1). E→visited. S→OOB. W→(2,0) unvisited. Try W. |
[…, DFS(2,2), DFS(2,1)] |
(2,1)→gray. (2,0) turns teal. | (2,1).W = false (2,0).E = false |
| 7 | Recurse → _generateDFS(2,0). E→visited. S→OOB. W→OOB. N→(1,0) unvisited. Try N. |
[…, DFS(2,1), DFS(2,0)] |
(2,0)→gray. (1,0) turns teal. | (2,0).N = false (1,0).S = false |
| 8 | Recurse → _generateDFS(1,0). E→(1,1) unvisited. Try E. |
[…, DFS(2,0), DFS(1,0)] |
(1,0)→gray. (1,1) turns teal. | (1,0).E = false (1,1).W = false |
| 9 | Recurse → _generateDFS(1,1). E→(1,2) visited. S→(2,1) visited. W→(1,0) visited. N→(0,1) unvisited. Try N. |
[…, DFS(1,0), DFS(1,1)] |
(1,1)→gray. (0,1) is already carved — passage added. | (1,1).N = false (0,1).S = false |
| 10 | _generateDFS(0,1): already visited! Wait — (0,1) IS visited. So DFS(1,1) tries all 4 dirs — all neighbors visited/OOB. Returns. |
[…, DFS(1,0)] ← stack shrinks! |
(1,1) dims to dark blue (buildback). DFS is now back inside DFS(1,0)'s for-loop. | — |
| 11 | DFS(1,0) tries next dirs: S→visited, W→OOB, N→(0,0) visited. All done. Returns. | […, DFS(2,0)] |
DFS resumes at DFS(2,0)'s for-loop. Stack shrinks further. | — |
| 12–… | Each remaining call finds all neighbors visited and returns. Stack unwinds all the way back to DFS(0,0). | [ ] (empty) |
The stack fully unwinds. All 9 cells visited. Maze complete! | — |
7. The "Perfect Maze" Guarantee
After the DFS finishes, the maze is guaranteed to be perfect:
visited flag prevents any cell from being
entered from two different directions. There is no second path in.
if (visited) continue; check prevents this absolutely.
These three properties together mean: there is exactly one path between any two cells in the finished maze. Because of this, the solver that comes later will always reach T from S — and it will hit dead ends on the way, because wrong corridors have no alternative exit.
8. Why the Walls Don't All Disappear at Once
Here is a subtlety worth understanding. When you press Build Maze, the generation code runs synchronously — meaning the entire maze is computed in a single JavaScript call, all at once, before the browser draws even one frame. The actual wall-opening happens invisibly fast.
So how does the animation work? There are two separate systems:
Every time the DFS carves a wall or backtracks, it pushes a plain
object onto a list (_buildStepQueue):
The algorithm doesn't draw anything. It only records what happened.
There is a second wall array called animWalls.
It starts fully closed (all walls true) and is updated
one step at a time as the animation plays back.
The draw loop processes one step from the queue per frame,
opens the corresponding wall in animWalls,
and redraws — so you see exactly one wall disappear per frame.
Without this, the very first frame would show the fully-finished
maze with all walls already open, because
maze.getCell() already has the final state.
animWalls is the "slow motion replay" layer.
Phase 1 —
maze.generate() runs instantly: computes the entire maze,
records every decision as a step object. No drawing.Phase 2 — The p5.js
draw() loop plays back one step per frame:
updates cell colors, opens one wall in animWalls, redraws.
This is the animation you watch.
9. Putting It All Together — One Frame of Animation
Here is what happens inside the sketch during a single animation frame when a
BUILD_CARVE step fires:
And for a BUILD_BACKTRACK step — the DFS returned from a neighbor,
and now the current cell goes dim to show the algorithm retreated through here:
maze.getCell().walls) is not touched at all by backtrack events.
10. Things to Watch For in the App
- The teal cell carves a long corridor without backtracking. This happens when the DFS gets lucky and finds a chain of unvisited neighbors — it dives straight forward, turning each cell teal then gray in sequence. The call stack is growing one frame at a time.
- The teal cell suddenly jumps backward. A dead end: all four neighbors of the current cell were visited or out of bounds. The function returned, and the call stack popped back to the parent cell — which may immediately try a new direction and jump the teal forward again.
- Long dim-blue streaks appear near the end. By the time most cells are visited, new carves become rarer and the DFS spends most of its time backtracking through already-explored territory. This is why the cost display shows more backtracks as the maze nears completion.
- The outer border never disappears. Boundary cells can never carve outward (out of bounds check), so their perimeter walls are never opened. The maze always has a solid outer frame.
-
S and T stay their colors throughout.
The start and treasure cells are seeded into
cellStatebefore the animation begins. The carve handlers only set states on non-start, non-treasure cells, so S and T always display their own colors regardless of what carving does around them.
Continue Exploring
Watch the Build phase closely now. Set speed to 3 or 4 so each step is slow enough to see the teal cell jump and the walls disappear one at a time.
Open Maze 1 →The island saga uses the same DFS skeleton, but the "carve" equivalent is adding a cell to a list, and backtracking removes it. The maze never removes anything — every carve is permanent.
Islands vs. Mazes →Return to the index to explore all stages of the island saga, both maze variants, and all companion documents.
Saga Index →