0% found this document useful (0 votes)
110 views10 pages

Maze Puzzle Medium

The document describes how to create a maze puzzle application using Angular and TypeScript. It discusses modeling mazes and cells, generating mazes using the hunt-and-kill algorithm with randomized scanning to produce more varied mazes, and solving mazes using depth-first search. Code examples are provided for the Cell and Maze classes to define the maze structure and generate mazes, as well as solving mazes with depth-first search.

Uploaded by

Slavi Rezashki
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
110 views10 pages

Maze Puzzle Medium

The document describes how to create a maze puzzle application using Angular and TypeScript. It discusses modeling mazes and cells, generating mazes using the hunt-and-kill algorithm with randomized scanning to produce more varied mazes, and solving mazes using depth-first search. Code examples are provided for the Cell and Maze classes to define the maze structure and generate mazes, as well as solving mazes with depth-first search.

Uploaded by

Slavi Rezashki
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 10

Upgrade Open in app

Published in The Startup · Follow

You have 1 free member-only story left this month. Upgrade for unlimited access.

Changhui Xu · Follow
Apr 23, 2020 · 6 min read · Listen

How to Create a Maze Puzzle

Today, we will create a maze puzzle application. The application can generate rectangle mazes
with arbitrary rows and columns, and find their paths from entrances to exits. To add some fun,
the final application allows us to traverse mazes using keyboard navigation or mouse clicks.

We will use the hunt-and-kill algorithm to generate mazes and use the depth-first search
algorithm to solve them. We use HTML canvas APIs to draw the generated maze on the web
page, and we can draw our own paths on the HTML canvas too.

The application is written in Angular/TypeScript. You can find the source code in my GitHub
repository, and play maze puzzles on the demo site. The following screen-recording illustrates
how the final application works.
Upgrade Open in app

Maze and Models


A maze can be considered as a grid of cells. In a two-dimensional rectangle maze, each cell has
up to four edges representing walls that could block a path from reaching another cell. If a cell is
connected to another cell, then each cell has one edge dropped. The following sketch illustrates
the models of cells and a maze.

The mazes we are going to create belong to the category of “perfect mazes”. A perfect maze is a
maze with no loop areas and no unreachable areas. In a perfect maze, every cell is connected to
another cell, and there is always one unique path between any two cells. In this article, the
unique path for a perfect maze is called the solution.
In this application, a maze is modelled as a Maze class which includes a grid of cells. The Maze

Upgrade
class needs the values for the number of rows and columns to construct a two-dimensional array
Open in app

of cells. The array indices of a cell indicate its row number and column number in the grid. A cell
is modelled as a Cell class, which has four Boolean properties standing for its four edges,
respectively. If an edge is dropped, then the cell will set that edge property to false.

The following code snippets show the model definitions of the Cell class and Maze class.

1 export class Cell {


2 northEdge: boolean = true;
3 eastEdge: boolean = true;
4 westEdge: boolean = true;
5 southEdge: boolean = true;
6 // ...
7
8 constructor(
9 public readonly row: number = 0,
10 public readonly col: number = 0
11 ) {}
12 // ...
13 }

cell.ts
hosted with ❤ by GitHub view raw

1 import { Cell } from './cell';


2
3 export class Maze {
4 public readonly cells: Cell[][] = [];
5
6 constructor(public nRow: number, public nCol: number) {
7 // initialize cells
8 for (let i = 0; i < nRow; i++) {
9 this.cells[i] = [];
10 for (let j = 0; j < nCol; j++) {
11 this.cells[i][j] = new Cell(i, j);
12 }
13 }
14
15 // generate maze
16 // ...
17 }
18 // ...
19 }

maze.ts
hosted with ❤ by GitHub view raw
In the code above, the constructor for the Maze Upgrade
class first initializes the cells with theirOpen
rowinIDs
app

and column IDs, then produces the maze structure.

Maze Generator
Now we are ready to generate the maze with its grid of cells predetermined by the row and
column number parameters. Among the many maze generation algorithms (listed here), I find
that the hunt-and-kill algorithm is very easy to understand and implement. The psuedo-
algorithm is as follows.

1. Choose a random starting cell.

2. Enter the “kill” mode.

Randomly select an unvisited neighbour from the current cell. Remove the edges between
the current cell and the selected neighbour, and the selected neighbour becomes the current
cell. Repeat until the current cell has no unvisited neighbours.

3. Enter the “hunt” mode.

Scan the grid to look for an unvisited cell that is adjacent to a visited cell. If found, then
connect the two cells by removing their adjacent edges, and let the formerly unvisited cell be
the new starting cell.

4. Repeat steps 2 and 3 until the “hunt” mode scans the entire grid and finds no unvisited cells.

In his blog “Maze Generation: Hunt-and-Kill algorithm”, Jamis Buck uses a JavaScript widget to
demonstrate the hunt-and-kill algorithm. I have recorded the widget running process so that we
can visualize the maze generation process in the following animated image.

Visualization of the Hunt-and-Kill Algorithm. This animation is a screen recording of a JavaScript widget from the
Buckblog by Jamis Buck (link)
The hunt-and-kill algorithm is pretty straightforward to implement. However, if the hunt mode
always scans the grid from top-left to bottom-right, then there is a good chance that the
Upgrade Open in app
generated mazes will end up with solutions favouring top rows, like the following, because top
rows are connected at an earlier time in the hunt-and-kill recursions.

Two mazes that have solutions (in blue) favour top rows.

These types of mazes are not so challenging because they lack randomness. To reduce the
possibility of top-rows solutions, we can randomize the scanning directions during the hunt
mode. The final implementation of the hunt-and-kill algorithm is shown below.

1 export class Maze {


2 public readonly cells: Cell[][] = [];
3 private readonly randomRowNumbers: number[];
4 private readonly randomColNumbers: number[];
5
6 constructor(public nRow: number, public nCol: number) {
7 // initialize cells and map their neighbors
8 // ...
9
10 // generate maze
11 this.randomRowNumbers = Utils.shuffleArray([...Array(this.nRow).keys()]);
12 this.randomColNumbers = Utils.shuffleArray([...Array(this.nCol).keys()]);
13 this.huntAndKill();
14 }
15
16 get randomCell(): Cell {
17 return this.cells[Utils.random(this.nRow)][Utils.random(this.nCol)];
18 }
19
20 private huntAndKill() {
21 let current = this.randomCell; // hunt-and-kill starts from a random Cell
22 while (current) {
23 this.kill(current);
24 current = this.hunt();
25 }
26 }
27 private kill(current: Cell) {
28 const next = current.neighbors.find(c => !c.visited); Upgrade Open in app
29 if (next) {
30 current.connectTo(next);
31 this.kill(next);
32 }
33 }
34 private hunt(): Cell {
35 for (let huntRow of this.randomRowNumbers) {
36 for (let huntColumn of this.randomColNumbers) {
37 const cell = this.cells[huntRow][huntColumn];
38 if (cell.visited) {
39 continue;
40 }
41 const next = cell.neighbors.find(c => c.visited);
42 if (next) {
43 cell.connectTo(next);
44 return cell;
45 }
46 }
47 }
48 }
49 // other functions...
50 }

maze.ts
hosted with ❤ by GitHub view raw

Maze generation using the hunt-and-kill algorithm (gist link)

In the code above, we have an array of shuffled row IDs and an array of shuffled column IDs for
the huntAndKill() method to scan the maze. In this way, the Maze class is able to generate a
strongly randomized maze given the numbers of rows and columns.

There could be some optimization to the huntAndKill() method, but the current implementation
is sufficient for generating mazes to be rendered in a browser window.

Maze Solver
If we have a perfect maze with unknown structure, then we can use the wall follower algorithm
(link), also known as either the left-hand rule or the right-hand rule, to solve the maze. On the
other hand, if we know the details of the maze structure, then we can use the depth-first search
algorithm (link) to traverse the maze between any two cells.

Here, our application already knows the whole structure of the generated maze, so we will adopt
the depth-first search algorithm, which simply loops through the cells that have not been
traversed until the goal is reached. The following code snippet shows an example
implementation. Upgrade Open in app

1 export class Maze {


2 // ...
3 get firstCell(): Cell {
4 return this.cells[0][0];
5 }
6
7 get lastCell(): Cell {
8 return this.cells[this.nRow - 1][this.nCol - 1];
9 }
10
11 findPath(): Cell[] {
12 this.cells.forEach(x => x.forEach(c => (c.traversed = false)));
13 const path: Cell[] = [this.firstCell];
14
15 while (1) {
16 let current = path[0];
17 current.traversed = true;
18
19 if (current.equals(this.lastCell)) {
20 break;
21 }
22
23 const traversableNeighbors = current.neighbors
24 .filter(c => c.isConnectedTo(current))
25 .filter(c => !c.traversed);
26 if (traversableNeighbors.length) {
27 path.unshift(traversableNeighbors[0]);
28 } else {
29 path.splice(0, 1);
30 }
31 }
32
33 return path.reverse();
34 }
35 // ...
36 }

maze.ts
hosted with ❤ by GitHub view raw

Maze solver using depth-first search (gist link)

The findPath() method implements the search starting from the first cell and ending with the
last cell, and returns an array of cells representing the solution to the maze, i.e., the path from
the first cell to the last cell.
Upgrade Open in app

Draw the Maze on Canvas


With all the models being established, we are ready to draw a maze on an HTML canvas. We first
add a canvas element in the HTML template, like the following.

1 <canvas id="maze"></canvas>

maze.component.html
hosted with ❤ by GitHub view raw

Then we set the canvas width and height based on the size of the desired maze, and draw all the
cells on the canvas. The following code snippet is an example implementation in an Angular
component. Since the HTML canvas drawing APIs are framework-agnostic, you should be able to
transfer the code to applications using other JavaScript frameworks.

1 export class MazeComponent implements OnInit, AfterViewInit {


2 // ...
3 ngAfterViewInit() {
4 this.canvas = <HTMLCanvasElement>document.getElementById('maze');
5 this.ctx = this.canvas.getContext('2d');
6 this.drawMaze();
7 }
8
9 drawMaze() {
10 this.maze = new Maze(this.row, this.col);
11 this.canvas.width = this.col * this.cellSize;
12 this.canvas.height = this.row * this.cellSize;
13
14 // draw the cells
15 this.ctx.lineWidth = this.cellEdgeThickness;
16 this.ctx.fillStyle = this.cellBackground;
17 this.maze.cells.forEach(x => x.forEach(c => this.draw(c)));
18 }
19
20 private draw(cell: Cell) {
21 this.ctx.fillRect(
22 cell.col * this.cellSize, cell.row * this.cellSize,
23 (cell.col + 1) * this.cellSize, (cell.row + 1) * this.cellSize
24 );
25 if (cell.northEdge) {
26 this.ctx.beginPath();
27 this.ctx.moveTo(cell.col * this.cellSize, cell.row * this.cellSize);
28 this.ctx.lineTo((cell.col + 1) * this.cellSize, cell.row * this.cellSize);
29 this.ctx.stroke();
30 }
31 if (cell.eastEdge) {
32 this.ctx.beginPath();
33 this.ctx.moveTo((cell.col + 1) * this.cellSize, cell.row * this.cellSize);
34 this.ctx.lineTo((cell.col + 1) * this.cellSize, (cell.row + 1) * this.cellSize);
Upgrade Open in app
35 this.ctx.stroke();
36 }
37 if (cell.southEdge) {
38 this.ctx.beginPath();
39 this.ctx.moveTo((cell.col + 1) * this.cellSize, (cell.row + 1) * this.cellSize);
40 this.ctx.lineTo(cell.col * this.cellSize, (cell.row + 1) * this.cellSize);
41 this.ctx.stroke();
42 }
43 if (cell.westEdge) {
44 this.ctx.beginPath();
45 this.ctx.moveTo(cell.col * this.cellSize, (cell.row + 1) * this.cellSize);
46 this.ctx.lineTo(cell.col * this.cellSize, cell.row * this.cellSize);
47 this.ctx.stroke();
48 }
49 }
50 // ...
51 }

maze.component.ts
hosted with ❤ by GitHub view raw

Maze component which draws the maze on canvas (gist link)

The method for drawing the maze path should be very similar to the code above, so I won’t
expand the drawings here.

To add some entertainment value, the demo application responds to keyboard events, so that
users can navigate around the maze using arrow keys. You can try it out on this demo website.

The maze puzzle is a fun game and an easy side project to learn something about canvas. To
summarize our journey in this article, we have covered topics about building maze models,
generating mazes using the hunt-and-kill algorithm, solving mazes using the depth-first search
algorithm, and drawing the mazes on HTML canvas.

That’s all for today. Again, you can check out the source code from my GitHub repository. Thanks
for reading.
Sign up for Top 10 Stories Upgrade Open in app
By The Startup

Get smarter at building your thing. Subscribe to receive The Startup's top 10 most read stories — delivered
straight into your inbox, twice a month. Take a look.

Emails will be sent to slavishkolo@gmail.com.


Get this newsletter Not you?

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy