GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Maze Generator: 9 Carve-and-Solve Scenes

In this maze generator set, mazes are carved, not drawn — 02 reads carve order from a random frontier's ripple bands, 06 watches the A-star open list reach

All nine boards share one 31×31 grid of 961 cells and a fixed seed, so rebuilding a board brings back the very same maze.

Watch Nine Mazes Carve, Merge and Solve Themselves in the Grid Below

Auto-plays · click a tile to jump to its section · all nine in one zip

Maze Generator: 9 Carve-and-Solve Boards From Wall Removal, Ripple Growth and Shortest Paths

The nine boards split the life of one maze into stages. Boards 01 to 03 build a maze in three different ways by taking walls out, 04 edits a finished maze by reopening its dead ends, 05 to 08 solve the result four ways, and 09 raises the answer into a single hedge-garden scene. Each board paints onto one 2D <canvas>, the stylesheet only sizes the stage, the buttons and the focus ring, and there is no @keyframes behind the motion: every move comes from a requestAnimationFrame script.

Four walls per cell, packed into four bits

Each cell owns one bit per wall, north 1, east 2, south 4 and west 8, so every cell starts at 15 (ALL_WALLS) and loses a bit whenever a wall comes out. knock clears the matching bit on the cell across the wall in the same call, which rules out a wall that is open from one side and shut from the other. Headings are numbered clockwise, 0 north, 1 east, 2 south, 3 west, so adding 1 to the current heading turns right and adding 3 turns left; the wall follower in 07 runs on exactly those two additions.

A fixed seed rebuilds the same maze

The only random source is makeRng(seed), a mixer over 32-bit integers, with a default seed of 464 that the auto demo bumps by one for each new board. None of the nine index.html files contains Math.random, Date.now, fetch or localStorage. That is why counts in this post, such as 96 dead ends or a 320-cell shortest route, come back unchanged once you open the zip and run it at seed 464.

What a two-second capture of each board recorded

Every board was filmed in a 480×300 window for 2 seconds at 24 frames, and motion was read from the 23 gaps between neighboring frames, so the moved-frame column counts out of 23.

Board Cumulative area Mean intensity Moved frames
01 Depth-first carve 5.2% 105.4 23/23
02 Frontier ripples 16.9% 106.4 23/23
03 Island merging 25.4% 43.1 23/23
04 Reopened dead ends 19.1% 33.6 23/23
05 Distance flood heat map 41.1% 122.5 19/23
06 Estimated-distance search 12.8% 55.6 23/23
07 Left-hand walk 4.7% 70.1 23/23
08 Detour valley 40.3% 120.3 13/23
09 Hedge garden 28.2% 25.5 23/23

Boards 01 and 07 sit near 5% of the area because one step only repaints the rim of a 9px cell or two. Board 01 takes a step every 24ms and walks about 83 steps in the 2 seconds, backtracks included, and the pink of a stack cell fades by e^(−1/72), roughly 1.4%, each time the head digs one cell deeper. Board 07 steps every 16ms, about 125 steps in the window, yet its trail only loses 0.4% per step because every cell is multiplied by 0.996. Boards 05 and 08 fall short of 23 because each has a stretch where the screen holds still: once the wave has reached the last cell or the level has risen all the way, nothing moves until the next board starts.

Pages these boards suit

If you came looking for a classroom worksheet, the kind of thing people print as a grid maze for kids, board 01 is the base, and if you want the birth of a maze itself as a loading backdrop, the ripples of 02 fit better. Board 03 suits algorithm lessons and figures in technical posts, while 04 belongs wherever a puzzle needs several ways through, such as a children's activity page or an escape-room hint card. Board 05 works as a difficulty map, 06 as a route-guidance demo, 07 as a board-game rules explainer or a mini game during loading, 08 as the answer reveal, and 09 as the starting point for a hero section on a landing page. For a small maze game site of your own, pairing 01 to build the board with 07 or 08 to show the solution is a sensible first combination.

Why Does Every Maze Generator Algorithm Here Remove Walls Instead of Drawing Them?

A 31×31 grid has 2×31×30 = 1,860 inner wall candidates. Start with all of them standing, take out only walls between two cells that are not yet connected, and joining all 961 cells removes exactly 960 walls (cells minus one); at that moment every pair of cells is linked by one route only, which is what a perfect maze means. Boards 01, 02 and 03 choose their next wall in completely different ways, yet in measurement all three removed 960 walls and left the same 900 standing. Drawing walls onto an empty board would force a separate check for sealed-off rooms and accidental loops, while the removal approach replaces that check with a single condition: skip any wall whose two cells are already joined. So however different the methods look, the real difference narrows to one question, which wall to take out next. Board 04 breaks that condition on purpose to create loops.

01A Grid Maze That Digs Deeper First and Backs Out to Carve New Branches

From the cell on top of the stack it picks one uncarved neighbor with the seeded random source, removes the wall toward it and steps in, and when no neighbor is left it pops one cell and backs out. A 961-cell board finishes after 960 pushes and 961 pops, 1,921 steps whatever the seed, with stack cells fading from pink as e^(−k/72) at k cells below the head and freshly popped cells tinted sky blue in proportion to the backtrack run that left them. The auto demo resumes from a board already 640 steps in (stack depth 463), the Carve again button digs from step 0, and the render measured 5.2% cumulative area, 105.4 mean intensity and 23/23 moved frames.

A Grid Maze That Digs Deeper First and Backs Out to Carve New Branches
Open in a new tab

01 · A Grid Maze That Digs Deeper First and Backs Out to Carve New Branches — Measured area 5.212% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

GRID_NCARVE_STEP_MSBACKTRACK_TINT
        const opts = [];
        for (let d = 0; d < 4; d += 1) {
            const n = neighbor(cur, d);
            if (n >= 0 && visited[n] === 0) {
                opts.push(d);
            }
        }
        steps += 1;
        if (opts.length > 0) {
            const n = knock(walls, cur, opts[Math.floor(rnd() * opts.length)]);
            visited[n] = 1;
            stackPos[n] = top;
            stack[top] = n;
            top += 1;
            carved += 1;
            backRun = 0;
            return 0;
        }
        top -= 1;
        stackPos[cur] = -1;
        backRun += 1;
        popRun[cur] = backRun;
        backs += 1;
        return 1;

02A Maze Whose Frontier Ripples Outward as It Grows

Cells touching the carved area wait in a frontier list, and each round one of them is pulled out, joined to a carved neighbor through a single wall, and replaced in the list by its own uncarved neighbors. Every cell's join number is painted in bands of 26 (RIPPLE_W), and across the 37 bands the mean distance from the center cell grows from 2.1 cells in the first band to 14.4 in the last, which is why the bands read as ripples moving out from the middle. Growing from the center cell at one join per 8ms, the board completes its 960 joins in 7,680ms, and the render measured 16.9% cumulative area, 106.4 mean intensity and 23/23 moved frames.

A Maze Whose Frontier Ripples Outward as It Grows
Open in a new tab

02 · A Maze Whose Frontier Ripples Outward as It Grows — Measured area 16.87% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

FRONTIER_PICKRIPPLE_WRING_FADE
        let c = 0;
        if (mode === 'newest') {
            c = g.front.pop();
        }
        else if (mode === 'oldest') {
            c = g.front.shift();
        }
        else {
            const k = Math.floor(g.rnd() * len);
            c = g.front[k];
            g.front[k] = g.front[len - 1];
            g.front.pop();
        }
        g.inFront[c] = 0;
        const links = [];
        for (let d = 0; d < 4; d += 1) {
            const n = neighbor(c, d);
            if (n >= 0 && g.inMaze[n] === 1) {
                links.push(d);
            }
        }
        knock(g.walls, c, links[Math.floor(g.rnd() * links.length)]);

03A Maze Built From a Shuffled Wall List Where Islands Merge Into One

All 1,860 inner walls are shuffled by the seed and checked front to back with island merging (union-find): when the two cells belong to islands with different representatives, the wall comes out and the smaller island hangs under the larger one's representative, and when the representatives match, the wall stays. Finding a representative re-hangs every cell on the way straight onto it, which together with merging by size keeps islands from stretching into long chains, and each removed wall gives off a small light circle that grows and fades over 360ms (MERGE_FLASH_MS). At seed 464 the islands become one on check 1,738, so all 122 remaining walls stay, and the render measured 25.4% cumulative area, 43.1 mean intensity and 23/23 moved frames.

A Maze Built From a Shuffled Wall List Where Islands Merge Into One
Open in a new tab

03 · A Maze Built From a Shuffled Wall List Where Islands Merge Into One — Measured area 25.362% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

WALL_SHUFFLE_SEEDISLAND_HUE_NMERGE_FLASH_MS
    // Find the representative + path compression: hang every cell on the way up straight onto the representative so the next find is short
    function find(i) {
        let r = i;
        while (parent[r] !== r) {
            r = parent[r];
        }
        let c = i;
        while (parent[c] !== r) {
            const nx = parent[c];
            parent[c] = r;
            c = nx;
        }
        return r;
    }
        const code = order[wi];
        wi += 1;
        const a = code >> 1;
        const dir = (code & 1) === 0 ? 1 : 2;
        const b = neighbor(a, dir);
        let ra = find(a);
        let rb = find(b);
        if (ra === rb) {
            skipped += 1;
            return 1;
        }
        knock(walls, a, dir);
        if (size[ra] < size[rb]) {
            const t = ra;
            ra = rb;
            rb = t;
        }
        parent[rb] = ra;
        size[ra] += size[rb];
        islands -= 1;
        removed += 1;
        flashes.push({ cell: a, dir: dir, age: 0 });
        return 0;

04A Maze That Reopens Dead Ends to Leave Multiple Ways Through

The finished depth-first maze is scanned from cell 0, and each cell with exactly one open side is kept with probability BRAID_KEEP; otherwise one closed wall at its tip comes out and splices it into a side passage. When the cell behind that wall is a dead end too, that wall is chosen first so two alleys disappear in one splice, which is how the 96 dead ends of the seed-464 maze reach 0 after only 95 splices. At the default of 0 the entrance-to-exit distance shrinks from 320 cells to 90, and the render measured 19.1% cumulative area, 33.6 mean intensity and 23/23 moved frames.

A Maze That Reopens Dead Ends to Leave Multiple Ways Through
Open in a new tab

04 · A Maze That Reopens Dead Ends to Leave Multiple Ways Through — Measured area 19.073% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

BRAID_KEEPDEADEND_SCAN_MSSPLICE_GLOW
    // Remove one wall at a dead end's tip to join a side passage: if the neighbor is also a dead end, pick it first and clear two alleys at once
    function spliceAt(walls, i, rnd) {
        const shut = [];
        const dead = [];
        for (let d = 0; d < 4; d += 1) {
            const n = neighbor(i, d);
            if (n < 0 || (walls[i] & BIT[d]) === 0) {
                continue;
            }
            shut.push(d);
            if (isDeadEnd(walls, n)) {
                dead.push(d);
            }
        }
        const pool = dead.length > 0 ? dead : shut;
        if (pool.length === 0) {
            return -1;
        }
        const d = pool[Math.floor(rnd() * pool.length)];
        knock(walls, i, d);
        return d;
    }

05A Maze Where a Spreading Wave Computes the Distance to Every Cell

The entrance cell gets 0, and every 20ms each cell in the current ring writes its distance plus one into every open neighbor that is still empty, which forms the next ring of a breadth-first distance flood. Color divides each distance by the larger of the current ring and 60 (HEAT_MAX_D) and lays it on a sky, mint, peach and pink ramp; on seed 464 the wave covers all 961 cells in 66 rings (1,320ms), and cell 932 in the last ring surfaces as the farthest room. Pressing From farthest room floods again from that cell, the cell reached at the end of 69 rings marks the far end of the longest path (69 cells), and the render measured 41.1% cumulative area, 122.5 mean intensity and 19/23 moved frames.

A Maze Where a Spreading Wave Computes the Distance to Every Cell
Open in a new tab

05 · A Maze Where a Spreading Wave Computes the Distance to Every Cell — Measured area 41.073% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

FLOOD_STEP_MSHEAT_MAX_DRING_ALPHA
        const next = [];
        for (const c of layer) {
            for (let d = 0; d < 4; d += 1) {
                if (!isOpen(walls, c, d)) {
                    continue;
                }
                const n = neighbor(c, d);
                if (dist[n] >= 0) {
                    continue;
                }
                dist[n] = front + 1;
                next.push(n);
            }
        }
        if (next.length === 0) {
            layer = [];
            if (sweep === 2) {
                longest = front;
                longEnd = farCell;
            }
            return;
        }
        front += 1;
        layer = next;
        reached += next.length;

06A Path Search That Opens the Cell With the Smallest Estimated Distance to the Goal First

A min-heap hands over the cell with the smallest f, the distance walked so far g plus slope K times the horizontal and vertical cells left to the exit, and closes it; whenever an open neighbor's g drops, its parent switches to this cell and it goes back into the heap. This board alone runs on a maze that keeps 40% of its dead ends and splices the rest into loops (LOOP_KEEP 0.4), so several routes reach the exit, and once the exit closes, the path traced back through parents is drawn at one cell per 18ms. On the seed-464 maze K 1.0 closes 264 cells and returns the shortest 90-cell path, and the render measured 12.8% cumulative area, 55.6 mean intensity and 23/23 moved frames.

A Path Search That Opens the Cell With the Smallest Estimated Distance to the Goal First
Open in a new tab

06 · A Path Search That Opens the Cell With the Smallest Estimated Distance to the Goal First — Measured area 12.835% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ASTAR_H_KOPEN_TINTTRACE_MS
            for (let d = 0; d < 4; d += 1) {
                if (!isOpen(walls, c, d)) {
                    continue;
                }
                const n = neighbor(c, d);
                if (closed[n] === 1) {
                    continue;
                }
                const ng = g[c] + 1;
                if (ng >= g[n]) {
                    continue;
                }
                g[n] = ng;
                parent[n] = c;
                if (inOpen[n] === 0) {
                    inOpen[n] = 1;
                    openN += 1;
                }
                heapPush(ng + k * hEst(n), n);
            }

07A Walk That Finds the Exit by Keeping One Hand on the Left Wall

Every 16ms the walker applies three rules in order: if the left is open, turn left and step; otherwise, if the front is open, step ahead; if both are blocked, turn right in place. Each step multiplies every cell's trail by 0.996, a half-life of about 173 steps, so old footprints dim, and cells passed once are painted mint while cells passed twice or more turn lilac. On the seed-464 maze the left hand reaches the exit in 1,095 steps (810 cells forward, 570 turns); the auto demo continues a walk already 420 steps in, From the start walks again from step 0, and the render measured 4.7% cumulative area, 70.1 mean intensity and 23/23 moved frames.

A Walk That Finds the Exit by Keeping One Hand on the Left Wall
Open in a new tab

07 · A Walk That Finds the Exit by Keeping One Hand on the Left Wall — Measured area 4.677% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

WALK_STEP_MSHAND_SIDETRAIL_FADE
    // Three rules: (1) if the hand side is open, turn that way and step (2) otherwise, if the front is open, step (3) if both are blocked, turn the other way in place
    function walkStep() {
        if (pos === EXIT) {
            return false;
        }
        const side = hand === 'left' ? (dir + 3) % 4 : (dir + 1) % 4;
        const other = hand === 'left' ? (dir + 1) % 4 : (dir + 3) % 4;
        steps += 1;
        for (let i = 0; i < CELL_N; i += 1) {
            trail[i] *= TRAIL_FADE;
        }
        if (isOpen(walls, pos, side)) {
            dir = side;
            turns += 1;
            moveAhead();
        }
        else if (isOpen(walls, pos, dir)) {
            moveAhead();
        }
        else {
            dir = other;
            turns += 1;
        }
        return true;
    }

08A Board Where Distance Becomes Heat Color and the Answer Appears as a Valley

Breadth-first distance runs twice, once from the entrance and once from the goal, and for each cell the entrance distance plus the goal distance minus the shortest distance gives the extra cells a route through that cell costs, a value that is exactly 0 on the shortest path. That value goes onto a five-stop heat ramp and is pressed darker the nearer it is to 0, by up to 45% (VALLEY_DEPTH), while the level rises along a squared curve over 1.5 seconds so the lowest band surfaces first like the floor of a valley. At seed 464, 321 cells read 0, the cell count of the 320-step shortest path (shortest plus one on all 10 seeds tried), and the render measured 40.3% cumulative area, 120.3 mean intensity and 13/23 moved frames.

A Board Where Distance Becomes Heat Color and the Answer Appears as a Valley
Open in a new tab

08 · A Board Where Distance Becomes Heat Color and the Answer Appears as a Valley — Measured area 40.303% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

THERMO_STOPSVALLEY_DEPTHREVEAL_MS
    // Per cell: entrance-to-cell distance + cell-to-goal distance - shortest = extra cells a route through it costs. Exactly 0 on the shortest path
    function computeField() {
        const ds = bfsDist(walls, ENTRY);
        const dg = bfsDist(walls, goal);
        shortest = ds[goal];
        maxEx = 1;
        floorN = 0;
        for (let i = 0; i < CELL_N; i += 1) {
            excess[i] = ds[i] + dg[i] - shortest;
            if (excess[i] > maxEx) {
                maxEx = excess[i];
            }
            if (excess[i] === 0) {
                floorN += 1;
            }
        }
        revealT = 0;
        holdLeft = 0;
    }

09A Hedge-Garden Maze Seen From Above, With Fog From Center Distance and Shadows From Height and Sun Angle

The wall bits turn into 1,024 segments (seed 464), each pushed away from the sun into a parallelogram shadow that joins one path and gets filled once, and then side faces plus a top face lifted 4px (HEDGE_H_PX) stand the hedges up. Horizontal hedges brighten along −sin and vertical ones along −cos as they turn toward the sun, and the fog density 1 − e^(−0.0042·r) climbs to 0.443 at the midpoint of a board edge and 0.695 at a canvas corner, pulling the eye to the middle. The sun circles once every 5,712ms while a lantern follows the 321-cell answer path at one cell per 70ms, and the render measured 28.2% cumulative area, 25.5 mean intensity and 23/23 moved frames.

A Hedge-Garden Maze Seen From Above, With Fog From Center Distance and Shadows From Height and Sun Angle
Open in a new tab

09 · A Hedge-Garden Maze Seen From Above, With Fog From Center Distance and Shadows From Height and Sun Angle — Measured area 28.202% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

HEDGE_H_PXFOG_KSHADOW_LEN
        const len = SHADOW_LEN * HEDGE_H_PX;
        const sx = Math.cos(sun) * len;
        const sy = Math.sin(sun) * len;
        // Shadow: a parallelogram that pushes one segment away from the sun
        ctx.fillStyle = rgba(BG, 0.24);
        ctx.beginPath();
        for (let s = 0; s < segs.length; s += 4) {
            const x1 = segs[s];
            const y1 = segs[s + 1];
            const x2 = segs[s + 2];
            const y2 = segs[s + 3];
            const nx = y1 === y2 ? 0 : 1.6;
            const ny = y1 === y2 ? 1.6 : 0;
            ctx.moveTo(x1 - nx, y1 - ny);
            ctx.lineTo(x2 + nx, y2 + ny);
            ctx.lineTo(x2 + nx + sx, y2 + ny + sy);
            ctx.lineTo(x1 - nx + sx, y1 - ny + sy);
            ctx.closePath();
        }
        ctx.fill();

Pick Order, Step Delay, Heuristic Slope: Three Settings That Reshape the Corridors

The counts in this section come from a measuring script that loads each board's index.html functions and calls them directly at a fixed seed, not from the capture.

In 02, the pick rule alone moves the dead-end count between 266 and 326

With the same seed and the same center start, random gives 326 dead ends, a farthest cell 35 away from the center and a 62-cell entrance-to-exit route; newest gives 319, 487 and 426, and oldest gives 266, 30 and 60. The random rule swaps a seeded random index with the last element and pops it, so pulling from the middle of the list costs no shifting. Newest takes back the cell it just added, so long depth-first corridors form and the farthest cell drifts out to 487, while oldest serves the longest-waiting cell first, keeps the farthest cell at 30 and leaves the fewest dead ends. Enter or the Change pick rule button cycles through the three.

In 01, the step delay changes only how long the carve takes

Dropping CARVE_STEP_MS from 24 to 8 cuts the 1,921 steps from 46,104ms to 15,368ms, about 46.1 seconds down to 15.4. The random source is drawn only on the steps that dig one cell deeper, not on a backtrack, and it does not depend on time, so the same seed yields the same maze three times faster. Backtrack runs of up to 122 steps in a row shrink by the same ratio, from 2,928ms to 976ms (calculated, not measured).

In 06, a slope of 2.2 closes fewer than half the cells

On the seed-464 maze K 1.0 closes 264 cells and K 2.2 closes 126, both with a 90-cell route, while K 0, which ignores the remaining distance, closes 804. Widened to 20 seeds, the mean closed count falls from 556.85 to 249.45 and drops on every one of them; K 1.0 found the shortest route on all 20 mazes, but K 2.2 returned a longer route than the shortest on 12, with a widest gap of 14 cells.

In 04, the keep ratio decides how far a wall follower has to walk

Each row is one sweep over the seed-464 depth-first maze, which starts with 96 dead ends.

Keep ratio Splices Dead ends left Entrance to exit Left-hand walk Right-hand walk
0 (default) 95 0 90 219 253
0.3 69 27 112 251 271
0.6 37 59 112 209 451
1 0 96 320 1,095 1,484

Ratio 1 splices nothing, so the left-hand walk takes the original 1,095 steps; at 0 it falls to 219, exactly a fifth, and the right-hand walk falls from 1,484 to 253. At 0.6 the left hand needs 209 steps but the right hand 451, so the same board more than doubles depending on which hand touches the wall. One sweep takes 5,766ms.

Where the Boards Break: Deep Recursion, Thousands of Cells, Reopened Loops and a Hidden Tab

What follows was confirmed by recomputing the formulas in the source or by calling its functions through the measuring script. Frame times on specific devices were not measured.

Recursive carving turns stack depth into call depth

The stack of 01 reaches 499 cells at seed 464. Written as a recursive function, that walk would pile up as many nested calls, and a bigger board pushes the depth further until it can hit the call stack limit. That is why the source runs a loop that pushes and pops cell numbers on an Int32Array stack.

More cells means a step count of cells × 2 − 1

The step count of 01 is cells × 2 − 1, so a 61×61 board (3,721 cells) works out to 7,441 steps, and at 24ms per step that means about 178.6 seconds of waiting; both figures are calculated from the formula, not measured. Board 07 also outran its cell count, 1,095 steps against 961 cells at seed 464, and across seeds 464 to 473 it ranged from 496 to 1,641 steps. The oldest rule of 02 pulls with Array.shift, which slides the whole array forward each time, but this board's frontier peaks at 138 cells, so it does not slow down at the current size.

Reopened alleys can trap a wall follower on an island

A maze with loops like 04 loses the rule that two cells share one route. These boards place the entrance and exit on the outer wall, so the left-hand walk still gets out, but move the exit inside the board or start the walker on an island ringed by a loop and it can circle that island without end. The trick 05 uses, flooding twice to find the longest path, also depends on single-route mazes, so on looped mazes like 04 and 06 it gives no guarantee.

Reading the mint cells of 07 as the answer path is a trap

At seed 464 the mint cells passed exactly once number 350, while the shortest path crosses 321 cells. A cell at the tip of a dead end is entered, turned around twice in place and left, so it counts as passed once and stays mint, while a junction cell can be passed twice or more as the walker visits side branches and turns lilac. To paint only the answer path, measure distance from two ends the way 08 does.

The heap in 06 can outgrow the cell count

Stale heap entries are not deleted; a closed flag skips them when they come out, so a cell whose g dropped can sit in the heap several times. That is why the heap array is sized at four times the cell count, and shrinking that multiple while enlarging the board can overflow it.

Two islands in one color are not one island

Island color in 03 is the representative number modulo 6, so right after check 400 the 561 islands still standing share six colors. Two touching islands can match in color and still have different representatives, and pressing a cell lights its whole island for 1.4 seconds to show the true extent. Checking the wall list in creation order instead of shuffling it would remove same-direction walls row by row and leave long horizontal corridors.

Taller hedges bury the 9px corridors

Board 09 lifts the top face of each hedge by HEDGE_H_PX above a board whose cells are 9px wide. Raise the height and the top faces and shadows cover the corridors until the route vanishes. Filling each shadow separately would also mean 1,024 fills, which is why the parallelograms go into one path and get filled once.

A hidden tab does not unleash a burst of steps

While a tab is hidden requestAnimationFrame rests, and the time added on the first frame back is clipped at 100ms. The 16ms fixed step also runs at most six times per frame, so the board does not race to catch up on the hidden stretch and resumes walking where it stopped.

The zip that lets you change a seed or a pick rule and carve the boards again on your own machine opens with yh7mecs5. Each board comes as an index.html and style.scss pair, joined by React components that port the same math.

Re-Carving With Three Changed Values, and Accessibility for a Keyboard-Solvable Maze

Variant Value Result
Change the carving speed 01 CARVE_STEP_MS 24 → 8 The 1,921-step carve finishes in about 15.4 seconds instead of 46.1, three times faster, and the same seed keeps the maze shape unchanged.
Change the heuristic slope 06 ASTAR_H_K 1.0 → 2.2 At seed 464 closed cells fall from 264 to 126 with the route still 90 cells, and 12 of 20 seeds return a route longer than the shortest.
Change the alley-keeping ratio 04 BRAID_KEEP 0 → 0.6 Dead ends left rise from 0 to 59, splices fall from 95 to 37, and entrance to exit grows from 90 to 112 cells.

All nine boards can be handled without a mouse. Every canvas carries tabindex="0" and an aria-label describing what the board does, and every board has two real <button> elements; the pause buttons of 01 and 03, Use right hand in 07 and Stop the sun in 09 announce their state through aria-pressed. Keys: 01 takes Space to pause, → for 20 steps and Home or Enter to restart; 02 takes Enter to change the pick rule and → for 40 cells; 03 takes Space to pause and → for 60 walls; 04 uses ↑ and ↓ for the keep ratio; 05 takes Enter to flood from the farthest room and → for 5 rings; 06 uses ↑ and ↓ for the slope and Enter to solve again; 07 takes Space to pause, Enter to switch hands and → for 20 steps; 08 moves the goal cell with the arrow keys; 09 turns the sun 15° at a time with ← and →.

The status line under each board stays silent at aria-live="off" during the auto demo and switches to polite the moment a person first touches the pointer, a key or a button, so only changes after that are read aloud. A screen reader is spared a stream of announcements for every demo step.

When prefers-reduced-motion is on, none of the nine boards starts its frame loop; settle() runs the whole process to the end in one go and draw() is called once. Boards 01 to 03 show a fully carved maze, 05 a finished heat map, 07 the trail walked all the way to the exit and 08 a fully revealed valley, and 09 holds the garden with the sun and the lantern at their starting spots. Input still works in this mode, and each press makes after() compute straight through to the result and repaint.

Do Maze Generator Online Tools Do Anything a Browser Cannot?

Not for building and solving. All it takes is a cell array, wall bits per cell, a seeded random source and one stack or queue. Instead of handing back a finished picture the way a maze generator online does, these boards run every step in plain sight with no library, CDN or WebGL, using only fillRect, line strokes and requestAnimationFrame. If a single printable maze is the goal, ignore the demo pacing, run the board to the end in one go the way settle() does and draw the result; the depth-first carve of a 961-cell board takes only 1,921 steps.

If you came here looking for a css maze game, note that the stylesheet only frames the stage; the maze itself lives in the script. For other grid pathfinding scenes, 9 snake game autopilot boards steer a snake with the same breadth-first distance, and in the slime mold network simulation a slime mold finds the shortest route through a maze. For more small games you control by hand, continue to the CSS mini games collection.

Questions Readers Raise Before Putting a Maze Game on Their Own Page

Can I print a bigger maze, the way a maze generator with grid output does?

Change GRID_N and the cell size CS together. One side of the board is GRID_N × CS, and since the canvas works in 480×300 internal units, the current 31×9 = 279px is the size that fits inside a height of 300. More cells also raise the step count of 01 to cells × 2 − 1, so lower CARVE_STEP_MS at the same time to keep the wait short.

Which board should I read to pull out only the answer path?

The simplest place to look is the second half of buildSegs() in 09. It takes the distance array measured from the entrance, starts at the exit, and keeps moving to an open neighbor whose distance is exactly one less until it reaches the entrance, then reverses the order. If you only need to know whether a cell lies on the answer path, the zero-extra-cells test in 08 gives the same answer.

How do I let a player walk the maze with the arrow keys?

Reuse the isOpen(walls, pos, dir) check that walkStep() in 07 relies on. Instead of the three rules, pass the pressed arrow key as dir and call moveAhead() only when that side is open, and the player moves without passing through walls. Arrival at the exit is still decided by a single pos === EXIT test.

Enter the archive password

The password is inside this article. You will find it as you read.