Maze Generation & Solving
Mix.install([
{:yog_ex, path: "/home/mafinar/repos/elixir/yog_ex"},
{:kino_vizjs, "~> 0.5.0"}
])
Introduction
A perfect maze is a spanning tree on a grid. Every cell is reachable from every other cell by exactly one path. This means there are no loops and no isolated sections.
Yog provides a rich suite of maze generation algorithms, ranging from the extremely fast but biased (Binary Tree) to the perfectly uniform but slower (Wilson’s).
🎨 Comparing Textures
Different algorithms produce mazes with different “textures.” Some prefer long straight corridors, while others create twisty, winding passages.
rows = 15
cols = 15
# 1. Binary Tree - Very fast, strong diagonal bias
binary_tree = Yog.Generator.Maze.binary_tree(rows, cols)
# 2. Sidewinder - Efficient, strong vertical/horizontal corridors
sidewinder = Yog.Generator.Maze.sidewinder(rows, cols)
# 3. Recursive Backtracker - Most popular for games, very twisty
backtracker = Yog.Generator.Maze.recursive_backtracker(rows, cols)
IO.puts "--- Binary Tree ---"
IO.puts Yog.Render.ASCII.grid_to_string_unicode(binary_tree)
IO.puts "\n--- Sidewinder ---"
IO.puts Yog.Render.ASCII.grid_to_string_unicode(sidewinder)
IO.puts "\n--- Recursive Backtracker ---"
IO.puts Yog.Render.ASCII.grid_to_string_unicode(backtracker)
🧠 Solving the Maze
Because a maze is just a graph, we can use Yog‘s pathfinding algorithms to find the exit.
Let’s generate a complex maze and solve it from the top-left to the bottom-right.
maze = Yog.Generator.Maze.recursive_backtracker(20, 20)
graph = Yog.Builder.GridGraph.to_graph(maze)
start_node = Yog.Builder.GridGraph.coord_to_id(maze, 0, 0)
end_node = Yog.Builder.GridGraph.coord_to_id(maze, 19, 19)
# Solve using Dijkstra
case Yog.Pathfinding.Dijkstra.shortest_path(graph, from: start_node, to: end_node) do
{:ok, path} ->
# Create "occupants" to mark the path in ASCII
occupants =
path.nodes
|> Map.new(fn id -> {id, "•"} end)
|> Map.put(start_node, "S")
|> Map.put(end_node, "E")
IO.puts Yog.Render.ASCII.grid_to_string_unicode(maze, occupants)
IO.puts "\nPath length: #{path.total_weight} steps"
:error ->
IO.puts "No path found! (Wait, perfect mazes always have a path... check implementation)"
end
🎥 Visualizing with DOT
If you want a more “premium” look, you can render the maze using Graphviz.
# We'll use a smaller maze for visualization
small_maze = Yog.Generator.Maze.recursive_backtracker(10, 10)
dot = Yog.Render.DOT.to_dot(Yog.Builder.GridGraph.to_graph(small_maze))
Kino.VizJS.render(dot)
🎲 Randomness and Seeds
All generators accept a :seed option for reproducibility.
maze1 = Yog.Generator.Maze.recursive_backtracker(10, 10, seed: 123)
maze2 = Yog.Generator.Maze.recursive_backtracker(10, 10, seed: 123)
# These will be identical
Yog.Render.ASCII.grid_to_string(maze1) == Yog.Render.ASCII.grid_to_string(maze2) |> IO.inspect(label: "Identical?")
Summary
Yog makes it easy to:
- Generate mazes using 10+ different algorithms.
- Render them to ASCII for debugging or DOT for presentation.
- Solve them by treating them as standard graphs.
Try changing the algorithm in the “Solving” section to aldous_broder/3 or wilson/3 to see how the path changes!