Skip to content

Commit c7a3ad4

Browse files
authored
Merge pull request #50 from healeycodes/post-generating-mazes
add generating mazes post
2 parents 1c4cd99 + a9a0cc2 commit c7a3ad4

File tree

6 files changed

+998
-0
lines changed

6 files changed

+998
-0
lines changed
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import React, { useEffect, useRef, useState } from 'react';
2+
import { Cell, Maze, solveMazeWithRandomDPS } from './maze';
3+
import { renderDebug, renderMaze, renderWhiteCell } from './render';
4+
import { findFurthestCells, randomMember, shuffle, sleep, timeoutWithCancel } from '.';
5+
6+
const FINISHED_MAZE_WAIT_FACTOR = 7;
7+
8+
async function aldousBroder(maze: Maze, ctx: CanvasRenderingContext2D, cancelSignal: AbortSignal, stepTime: number) {
9+
const visited = new Set();
10+
11+
let current = randomMember(maze.cells.flat());
12+
visited.add(current);
13+
14+
while (visited.size < maze.width * maze.height) {
15+
const next = shuffle(current.neighbors)[0];
16+
17+
if (!visited.has(next)) {
18+
current.carveEdge(next);
19+
visited.add(next);
20+
}
21+
22+
current = next;
23+
24+
renderMaze(maze, current, [], ctx);
25+
await timeoutWithCancel(stepTime, cancelSignal)
26+
}
27+
28+
const { cell1, cell2 } = await findFurthestCells(maze, ctx, stepTime, false);
29+
maze.start = cell1;
30+
maze.end = cell2;
31+
renderMaze(maze, null, [], ctx);
32+
await timeoutWithCancel(stepTime * FINISHED_MAZE_WAIT_FACTOR, cancelSignal)
33+
}
34+
35+
async function randomDFS(maze: Maze, ctx: CanvasRenderingContext2D, cancelSignal: AbortSignal, stepTime: number) {
36+
const visited = new Set<Cell>();
37+
38+
async function visit(last: Cell, next: Cell) {
39+
if (visited.has(next)) {
40+
return;
41+
}
42+
visited.add(next);
43+
44+
last.carveEdge(next);
45+
renderMaze(maze, next, [], ctx);
46+
await timeoutWithCancel(stepTime, cancelSignal)
47+
48+
const neighbors = shuffle(next.uncarvedEdges());
49+
for (const neighbor of neighbors) {
50+
await visit(next, neighbor);
51+
}
52+
}
53+
54+
const rndCell = randomMember(maze.cells.flat());
55+
await visit(rndCell, shuffle(rndCell.neighbors)[0]);
56+
57+
const { cell1, cell2 } = await findFurthestCells(maze, ctx, stepTime, false);
58+
maze.start = cell1;
59+
maze.end = cell2;
60+
renderMaze(maze, null, [], ctx);
61+
await timeoutWithCancel(stepTime * FINISHED_MAZE_WAIT_FACTOR, cancelSignal)
62+
}
63+
64+
async function wilsonsAlgorithm(maze: Maze, ctx: CanvasRenderingContext2D, cancelSignal: AbortSignal, stepTime: number) {
65+
const unvisited = new Set<Cell>(maze.cells.flat());
66+
const visited = new Set<Cell>();
67+
68+
// Choose one cell arbitrarily, add it to the maze, and mark it as visited
69+
const startCell = randomMember(maze.cells.flat())
70+
visited.add(startCell);
71+
unvisited.delete(startCell);
72+
73+
// Continue until all cells have been visited
74+
while (unvisited.size > 0) {
75+
let path = [];
76+
let current = randomMember(unvisited);
77+
78+
// Perform a random walk until reaching a cell already in the maze
79+
while (!visited.has(current)) {
80+
path.push(current);
81+
let next = randomMember(current.uncarvedEdges());
82+
83+
// If a loop is formed, erase that section of the path
84+
const loopIndex = path.indexOf(next);
85+
if (loopIndex !== -1) {
86+
path = path.slice(0, loopIndex + 1);
87+
} else {
88+
path.push(next);
89+
}
90+
91+
renderMaze(maze, next, path, ctx);
92+
if (visited.size === 1) {
93+
renderWhiteCell(maze, startCell, ctx)
94+
}
95+
await timeoutWithCancel(stepTime, cancelSignal)
96+
97+
current = next;
98+
}
99+
100+
// Add the path to the maze by carving edges and marking cells as visited
101+
for (let i = 0; i < path.length - 1; i++) {
102+
const cell = path[i];
103+
const nextCell = path[i + 1];
104+
cell.carveEdge(nextCell);
105+
visited.add(cell);
106+
unvisited.delete(cell);
107+
}
108+
}
109+
110+
const { cell1, cell2 } = await findFurthestCells(maze, ctx, stepTime, false);
111+
maze.start = cell1;
112+
maze.end = cell2;
113+
renderMaze(maze, null, [], ctx);
114+
await timeoutWithCancel(stepTime * FINISHED_MAZE_WAIT_FACTOR, cancelSignal)
115+
}
116+
117+
const componentForMaze = (mazeFunction: (maze: Maze, ctx: CanvasRenderingContext2D, cancelSignal: AbortSignal, stepTime: number) => void) => {
118+
return () => {
119+
const canvasRef = useRef<HTMLCanvasElement>(null);
120+
const [stepTime, setStepTime] = useState(150);
121+
const [mazeSize, setMazeSize] = useState(11);
122+
123+
useEffect(() => {
124+
const controller = new AbortController();
125+
const { signal: cancelSignal } = controller;
126+
const canvas = canvasRef.current;
127+
128+
if (!canvas) return;
129+
130+
const ctx = canvas.getContext('2d');
131+
if (!ctx) return;
132+
133+
const generateMaze = async () => {
134+
while (!cancelSignal.aborted) {
135+
const maze = new Maze(mazeSize, mazeSize);
136+
try {
137+
await mazeFunction(maze, ctx, cancelSignal, stepTime);
138+
} catch (e: unknown) {
139+
if (e instanceof Error && e.message.includes("aborted")) {
140+
continue;
141+
}
142+
throw e;
143+
}
144+
}
145+
};
146+
147+
generateMaze();
148+
return () => {
149+
controller.abort();
150+
};
151+
}, [stepTime, mazeSize]);
152+
153+
return (
154+
<center key={mazeFunction.name} className="no-select top-bot-padding">
155+
<canvas ref={canvasRef} width={330} height={330} />
156+
<div style={{ width: 330, textAlign: 'left' }}>
157+
<small> {mazeSize}x{mazeSize} (<a className="cursor" onClick={() => setMazeSize(mazeSize + 1)}>bigger</a>, <a className="cursor" onClick={() => setMazeSize(Math.max(mazeSize - 1, 2))}>smaller</a>) {stepTime}ms
158+
(<a className="cursor" onClick={() => setStepTime(Math.max(stepTime - 50, 0))}>faster</a>, <a className="cursor" onClick={() => setStepTime(stepTime + 50)}>slower</a>).
159+
</small>
160+
</div>
161+
<style jsx>{`
162+
.no-select {
163+
user-select: none;
164+
}
165+
.cursor {
166+
cursor: pointer;
167+
}
168+
.top-bot-padding {
169+
padding-top: 8px;
170+
padding-bottom: 8px;
171+
}
172+
`}</style>
173+
</center>
174+
);
175+
};
176+
}
177+
178+
export const RandomDFS = componentForMaze(randomDFS);
179+
export const AldousBroder = componentForMaze(aldousBroder);
180+
export const WilsonsAlgorithm = componentForMaze(wilsonsAlgorithm);
181+
182+
export const TreeDiameter = () => {
183+
const mazeCanvasRef = useRef<HTMLCanvasElement>(null);
184+
185+
useEffect(() => {
186+
const mazeCanvas = mazeCanvasRef.current;
187+
188+
if (!mazeCanvas) return;
189+
190+
const mazeCtx = mazeCanvas.getContext('2d');
191+
if (!mazeCtx) return;
192+
193+
(async () => {
194+
while (true) {
195+
const maze = new Maze(12, 12);
196+
solveMazeWithRandomDPS(maze)
197+
renderMaze(maze, null, [], mazeCtx)
198+
await sleep(1000);
199+
const { cell1, cell2 } = await findFurthestCells(maze, mazeCtx, 50, true)
200+
await sleep(2000);
201+
maze.start = cell1;
202+
maze.end = cell2;
203+
renderMaze(maze, null, [], mazeCtx);
204+
await sleep(2000);
205+
}
206+
})();
207+
}, []);
208+
209+
return (
210+
<center key="treeDiameter">
211+
<canvas ref={mazeCanvasRef} width={340} height={340} />
212+
<div style={{ width: 340, textAlign: 'left' }}><small>Finding the start and end cells via tree diameter.</small></div>
213+
</center>
214+
);
215+
}
216+
217+
export const IntroMaze = () => {
218+
const mazeCanvasRef = useRef<HTMLCanvasElement>(null);
219+
const debugCanvasRef = useRef<HTMLCanvasElement>(null);
220+
221+
useEffect(() => {
222+
const mazeCanvas = mazeCanvasRef.current;
223+
const debugCanvas = debugCanvasRef.current;
224+
225+
if (!mazeCanvas || !debugCanvas) return;
226+
227+
const mazeCtx = mazeCanvas.getContext('2d');
228+
const debugCtx = debugCanvas.getContext('2d')
229+
if (!mazeCtx || !debugCtx) return;
230+
231+
(async () => {
232+
while (true) {
233+
const maze = new Maze(2, 2);
234+
const a = maze.getCell(0, 0)
235+
const b = maze.getCell(1, 0)
236+
const c = maze.getCell(1, 1)
237+
const d = maze.getCell(0, 1)
238+
renderMaze(maze, null, [], mazeCtx)
239+
renderDebug(maze, null, [], debugCtx)
240+
await sleep(1000);
241+
a.carveEdge(b)
242+
renderMaze(maze, b, [], mazeCtx)
243+
renderDebug(maze, null, [], debugCtx)
244+
await sleep(1000);
245+
b.carveEdge(c)
246+
renderMaze(maze, c, [], mazeCtx)
247+
renderDebug(maze, null, [], debugCtx)
248+
await sleep(1000);
249+
c.carveEdge(d)
250+
renderMaze(maze, d, [], mazeCtx)
251+
renderDebug(maze, null, [], debugCtx)
252+
await sleep(1000);
253+
maze.start = a
254+
maze.end = d
255+
renderMaze(maze, null, [], mazeCtx)
256+
renderDebug(maze, null, [], debugCtx)
257+
await sleep(2000);
258+
}
259+
})();
260+
}, []);
261+
262+
return (
263+
<center key="intro" className="top-bot-padding">
264+
<canvas ref={mazeCanvasRef} width={170} height={170} />
265+
<canvas ref={debugCanvasRef} width={170} height={170} />
266+
<div style={{ width: 340, textAlign: 'left' }}><small>Left: user-facing maze view. Right: debug view.</small></div>
267+
<style jsx>{`
268+
.top-bot-padding {
269+
padding-top: 8px;
270+
padding-bottom: 8px;
271+
}
272+
`}</style>
273+
</center>
274+
);
275+
}

components/visuals/mazes/index.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { Cell, Maze } from "./maze";
2+
import { renderBFSOverlay, renderCellName, renderMaze } from "./render";
3+
4+
type Wall = { a: Cell; b: Cell };
5+
6+
export const shuffle = <T>(arr: T[]): T[] =>
7+
arr.sort(() => Math.random() - 0.5);
8+
9+
export const randomMember = <T>(group: Set<T> | T[]): T => {
10+
if (
11+
group instanceof Set && group.size === 0 ||
12+
group instanceof Array && group.length === 0
13+
) {
14+
throw new Error("empty group");
15+
}
16+
const items = group instanceof Set ? Array.from(group) : group;
17+
const randomIndex = Math.floor(Math.random() * items.length);
18+
return items[randomIndex];
19+
};
20+
21+
export function sleep(time: number) {
22+
return new Promise((resolve) => {
23+
setTimeout(resolve, time);
24+
});
25+
}
26+
27+
export function timeoutWithCancel(time: number, signal: AbortSignal) {
28+
return new Promise((resolve, reject) => {
29+
const timer = setTimeout(resolve, time);
30+
31+
signal.addEventListener("abort", () => {
32+
clearTimeout(timer);
33+
reject(new Error("aborted"));
34+
});
35+
});
36+
}
37+
38+
async function bfsFurthestCell(
39+
maze: Maze,
40+
start: Cell,
41+
ctx: CanvasRenderingContext2D,
42+
stepTime: number,
43+
withDraw: boolean,
44+
startCellName: false | string,
45+
endCellName: false | string,
46+
): Promise<{ furthestCell: Cell; distance: number }> {
47+
let maxDist = 0;
48+
let furthestCell = start;
49+
const queue: { cell: Cell; dist: number }[] = [{ cell: start, dist: 0 }];
50+
const visited = new Set<Cell>();
51+
visited.add(start);
52+
53+
while (queue.length > 0) {
54+
if (withDraw) {
55+
renderMaze(maze, null, [], ctx);
56+
renderBFSOverlay(maze, visited, ctx);
57+
58+
if (startCellName) {
59+
renderCellName(maze, start, startCellName, ctx);
60+
}
61+
62+
await new Promise((r) => setTimeout(r, stepTime));
63+
}
64+
65+
const { cell, dist } = queue.shift();
66+
if (dist > maxDist) {
67+
maxDist = dist;
68+
furthestCell = cell;
69+
}
70+
for (const neighbor of cell.edges) {
71+
if (!visited.has(neighbor)) {
72+
visited.add(neighbor);
73+
queue.push({ cell: neighbor, dist: dist + 1 });
74+
}
75+
}
76+
}
77+
78+
if (endCellName) {
79+
renderCellName(maze, furthestCell, endCellName, ctx);
80+
}
81+
82+
return { furthestCell, distance: maxDist };
83+
}
84+
85+
export async function findFurthestCells(
86+
maze: Maze,
87+
ctx: CanvasRenderingContext2D,
88+
stepTime: number,
89+
withDraw: boolean,
90+
): Promise<{ cell1: Cell; cell2: Cell; distance: number }> {
91+
const startCell = randomMember(maze.cells.flat());
92+
if (!startCell) throw new Error("Invalid starting cell");
93+
94+
const { furthestCell: cell1 } = await bfsFurthestCell(
95+
maze,
96+
startCell,
97+
ctx,
98+
stepTime,
99+
withDraw,
100+
withDraw && "A",
101+
withDraw && "B",
102+
);
103+
const { furthestCell: cell2, distance } = await bfsFurthestCell(
104+
maze,
105+
cell1,
106+
ctx,
107+
stepTime,
108+
withDraw,
109+
withDraw && "B",
110+
withDraw && "C",
111+
);
112+
113+
return { cell1, cell2, distance };
114+
}

0 commit comments

Comments
 (0)