Hacker News new | past | comments | ask | show | jobs | submit login
Show HN: Solve and generate mazes with JavaScript on HTML Canvas (github.com/dmaydan)
96 points by davidim on Jan 27, 2019 | hide | past | favorite | 30 comments



https://bost.ocks.org/mike/algorithms/#maze-generation has some visualizations for various different maze generation algorithms.


Great link. Here's another good one.

https://www.jamisbuck.org/mazes/


And here's another one. It introduces maze building ideas in a simple way.

http://www.jamisbuck.org/presentations/rubyconf2011/index.ht...


I used that book to design mazes that were rendered as thick-walled SVGs so I could make them out of plywood on my laser cutter!

https://photos.app.goo.gl/UwSmp9mjWw4asBcZA


It would be cool to see the cells checked as well. To someone unfamiliar with BFS, the demo makes it seem like the process of finding the solution doesn't involve exploring any other tiles than the solution itself.


A pretty neat example of this is mbostock's A* search: https://beta.observablehq.com/@mbostock/best-first-search


I explored the test site and tried different maze sizes with values in the 50s, 70s, 110s, 150 (including odd numbers). Curiously, I found the maze could be visually divided into four Cartesian-like quadrants. Consistently the middle divider had only one gap, and other major quadrants also consistently have only one gap. So, when I wondered if BFS solution might be different if I ran it multiple times, I couldn't tell. Because of the curious constraint I just mentioned, all paths had to traverse these major divisions through the same gap. In other words, if there was variance, it would be constrained to a very small movement in a corner.

I imagine if I dived into the code I would realize if my hypothesis that multiple runs of the BFS would always result in the same solution, was plausible or not. And it might also be the case if I understood BFS more, I would also know if it would always result in the same solution.

But how much fun if I could see it visually with a few clicks in the GUI.

Thanks and Good luck.


Thanks so much for the compliment. This is probably a bi-product of the maze generation algorithm I used. Essentially, at every step, the algorithm bisects the maze horizontally or vertically - then, it chooses a random cell along this bisection to leave open (that way the 2 resulting regions are still connected). Then, the same algorithm is performed on the 2 new regions. Recursion continues until it no longer makes sense to continue bisecting.

Wikipedia has an article on maze generation that explains the algorithm - https://en.m.wikipedia.org/wiki/Maze_generation_algorithm


You’ll have fun if you implement all the generation algorithms in that article. If you want to get rid of the visible bisect structure, you can use your solver with only slight modifications to generate your maze.


I can confirm that that's exactly what happened, different generation algorithms have a different "texture" to them in precisely this way and those textures do sometimes have a byproduct of making the maze easier or harder to solve.

The algorithm with the least helpful "texture" is probably Wilson's algorithm, which basically says: choose a point at random out in the unsolved territory and begin a random walk: when you reach the solved territory (the maze as it is already known), add those points back to the maze; when you self-intersect you should delete the random walk back to where you self-intersected. (For the very first walk, furthermore, there is only one "solved" point in the maze so you may wish to speed up maze generation by doing a different random walk that just "busts down walls" into the as-yet-unknown area; this is called Aldous-Broder and it has the opposite problem where it tries to randomly walk into unsolved space so it takes forever when the unsolved space becomes rare.) So one would start from,

    function* randomWalkPoints({ x, y }, height, width) {
      const incr = [{dx: -1, dy: 0}, {dx: 1, dy: 0}, {dx: 0, dy: -1}, {dx: 0, dy: 1}];
      while (true) {
        const { dx, dy } = incr[Math.floor(4 * Math.random())];
        const new_x = x + dx, new_y = y + dy;
        if (new_x >= 0 && new_x < width && new_y >= 0 && new_y < width) {
          x = new_x;
          y = new_y;
          yield { x, y };
        }
      }
    }
Now, the algorithm might get a bit weird because it looks like you are using block mazes rather than conventional thin-walled mazes; every n-by-m thin-walled maze has an embedding as a block maze of size (2n+1)-by-(2m+1) simply by making (0, y) and (x, 0) all walls, every (odd, odd) point is empty space representing one of the squares of the original maze, every (even, even) point is a block, and (odd, even) and (even, odd) points are blocks if and only if there is a vertical/horizontal wall between the corresponding squares in the original maze. So all of the resources you will find are talking about mazes that look like,

    ╔═══════════╦═══════╗
    ║           ║       ║
    ║   ╔═══         ═══╣
    ║   ║               ║
    ║   ║    ═══╗    ═══╣
    ║   ║       ║       ║
    ║   ╚═══════╣   ║   ║
    ║           ║   ║   ║
    ╠═══════    ╚═══╣   ║
    ║               ║   ║
    ╚═══════════════╩═══╝
and then in your context this becomes:

    X X X X X X X X X X X
    X . . . . . X . . . X
    X . X X X . X . X X X
    X . X . . . . . . . X
    X . X . X X X . X X X
    X . X . . . X . . . X
    X . X X X X X . X . X
    X . . . . . X . X . X
    X X X X X . X X X . X
    X . . . . . . . X . X
    X X X X X X X X X X X
So I'm not 100% sure whether you would want to just restrict yourself to block mazes of this type, or whether you would want to just try something like "I modify Wilson's to keep track of walls I have already placed, too, and then when I intersect with myself on a path that already hit a block wall, I discard back to the open-space which placed that block" to get a slightly more general vision of the problem... I am not sure how you'd want to handle that.


+1 for the ascii art alone. HN never ceases to amaze me


It's a very noticeable pattern and detrimental to the beauty of the maze but the solver works well.


Yeah, currently it seems large mazes are generated in 7x7 or similar blocks which are then combined to fill the area. A good example of this visually is a 29x29 or a 49x49 maze.


Nice. I created a maze solver in js and canvas as well, solving using the A* and Tremaux algorithms.

You can even create your own mazes by passing json in the url.

Simple Maze Solver in HTML5 and Js http://www.primaryobjects.com/maze/


This will be an interesting book to read... also showcasing multiple maze generation algorithms: https://pragprog.com/book/jbmaze/mazes-for-programmers


I love this book. It's on my retirement pile! (Hopefully mazes will still be relevant by then.)


Bug Report: http://prntscr.com/mcyc1b

When the website loads, if I put in a large number such as 500 and hit the generate button, it does not clear the previous puzzle and overlays on top. Second click still shows it bit now its a but transparent. Third click removes it altogether. I tried to skim through the code but not sure what the cause is given that you create a new Maze every single time. It seems to be some sort of canvas rendering bug as if I clear the canvas [1] it seems to work.

1: https://stackoverflow.com/a/2142549


Thanks a lot for the bug report! You're right - I was forgetting to clear the canvas on render. So I added ctx.clearRect(0, 0, WIDTH, HEIGHT); to the start of the render method.


I'm surprised it doesn't allow you to "draw" on it to solve the maze. That seems like an essential (and easy to add) feature.


What sort of drawing are you thinking? Dragging the mouse around on the screen or navigating with the arrows?


Well I would pick drawing with a mouse, but arrow keys would work in a pinch.


Very cool! I started making a very unpolished game based on a similar sort of map design a few months ago for ludum dare -- caution that there are sounds on this link and no volume control: https://wcarss.ca/maze_of_sacrifice/


It's cool, but I expected some explanation about the steps, challenges. Luckily the HN crowd filled that in :)


The solutions for mazes of size 50-200 look quite like a river on a map; which is pleasant to me.


Nice work!

Seems to freeze if you put a number too high though. Perhaps you should add a limit?


Thanks for checking it out! I am wondering how big is the number you are putting in? If the number is really high, it's probably causing a stack overflow. I would limit the size, but each browser has its own recursion limits so that would be quite a pain. In retrospect, I probably should not have used a recursive algorithm for this task :).


I put in a value of 500, and while it didn't freeze completely, it really struggled to respond.


Recursive division may be elegant, but it seems that may also have its disadvantages.


One trick you could use is to add a setTimeout or asnyc/await somewhere during the recursion. By forcing a step to be asynchronous, the stack count will be reset.


Solving mazes of the kind of size one can see tends to be an easy problem in that once you know about eg breadth first search it isn’t too hard to write a program that will solve this sort of maze. Solving them is still necessary to show there is a solution but it isn’t super interesting because it is sort of obvious that breadth-first search gives the best solution and if you know about it then it is quite easy to think of using breadth-first search.

A much harder problem is generating mazes.

The algorithm used here seems to be:

1. Know how to generate a tiny maze 2. To generate a big maze, divide it into smaller mazes, generate those, then cut some things out (the tiny maze algorithm) to connect the sub mazes.

You can see the bias in the algorithm from the grid pattern inside the generated mazes.

One might wonder how to generate mazes such that each possible valid maze has the same probability of being generated.

First one should define what a valid maze is. Here it seems the definition should be something like:

A maze is an array of cells which are either white or black such that:

1. There is no 2x2 square of white squares

2. There is no 2x2 square of black squares (maybe this could be changed?)

3. Any two white squares are connected by a unique* path of (horizontally/vertically) adjacent white squares with no loops

* one may want to drop the uniqueness requirement.

I do not know how one would uniformly generate all such mazes.

Another definition of maze that I do know how to generate uniformly is one made of thick maze cells and thin separating lines:

A maze is a spanning tree of the graph where the nodes are cells and the edges are possible passages (so for a squarish maze the graph would basically be a grid: nodes at integer coordinates and edges between any horizontally/vertically adjacent nodes).

The problem of generating such a maze uniformly is therefore the same as that of picking a spanning tree such that each spanning tree is chosen with the same probability. An algorithm for this is Wilson’s algorithm and works as follows:

1. Pick (not necessarily randomly) some node and add it to your spanning tree (so your tree has one node and no edges)

2. Pick (not necessarily randomly) some node not in your tree. If no such node exists then you are done. Say that the current path is this node.

3. Randomly (uniformly) choose an edge touching the last node of the current path (it is ok to not choose the edge which is the last edge of the current path). Add that edge and the node on the end to the current path.

4 (a). If the current path contains a loop then delete all nodes (and edges) in the loop. Go to step 3.

4 (b). If the last node of the current path is in the spanning tree, add the current path to the spanning tree. Go to step 2.

4 (c). Otherwise go to step (3).

This algorithm isn’t too hard to implement (although drawing this sort of maze is a bit annoying). A compatible definition of maze with this one that fits into a “grid of black and white cells” pattern is the following:

A maze is a rectangular array of cells which are either black or white such that:

1. All cells with odd x and y coordinates are white

2. All cells with even x and y coordinates are black

3. For any two given white cells, there is exactly one path (ie with no loops) of adjacent white cells between them.

If one redraws the spanning tree and current path occasionally as this algorithm runs, the animation of maze generating is quite interesting to watch.

I think implementing these sorts of maze drawing generating and solving algorithms is good fun and quite a varied exercise. It’s definitely one I like doing.

Here is a visualisation of Wilson’s algorithm: https://bl.ocks.org/mbostock/11363008 Observe how different the shape of the tree is between runs.

If you want to skip implementing it yourself first and just see the result then here it is for a maze: https://bl.ocks.org/mbostock/11357811




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: