Personal project
- Typescript
- React
- Redux
- Headless UI
- DSA
Labyrinth is an interactive algorithm visualizer for graph search. You can build a grid, place obstacles and weights, choose an algorithm, and watch it work step by step. It currently supports Depth-First Search, Breadth-First Search, Dijkstra’s algorithm, and A*.
Why I built it
I started Labyrinth while learning data structures and algorithms. Graph search was one of the topics I found especially interesting, but I did not want to stop at being able to solve a few problems on paper. I wanted to understand the algorithms well enough to build them, visualize them, and explain why they worked.
That became the rule behind the project: if I cannot visualize an algorithm and teach it clearly, I probably do not understand it deeply enough yet.
So Labyrinth became part learning tool, part playground, and part test of my own understanding.
The first version became spaghetti
The first prototype worked, but I built it quickly while learning, so rendering logic, algorithm logic, animation state, and UI behavior slowly became tangled together. Adding something new meant touching code in several places. A new algorithm could require changes to the visualizer, while a new grid feature could break assumptions inside an algorithm.
The project was getting harder to change even though it was still relatively small. Instead of continuing to patch it, I restarted the architecture using everything I had learned from the first attempt.
Separating the algorithms from the visualizer
The biggest change was simple: the algorithms should not care how they are displayed.
The new architecture completely separates graph-search logic from rendering. Each algorithm follows the same interface and produces data that the visualizer knows how to display. The renderer does not need special knowledge about BFS, DFS, Dijkstra, or A*. It only needs to understand things like which nodes were visited, the order they were explored in, and the final path that was found.
That means adding another algorithm is mostly about implementing that algorithm correctly and matching the existing interface. The rendering system does not need to change.
This separation also made weighted nodes much easier to add. BFS and DFS do not really care about weights, while Dijkstra and A* do. Because the algorithm layer and visual layer were already separate, weighted algorithms could use the extra information without forcing the rest of the application to be redesigned around them.
That was the point where the rewrite really proved itself.
Keeping state out of the UI
Labyrinth is also a very state-heavy application. At any moment, the app may need to track the maze itself, walls, weights, start and end points, the selected algorithm, animation progress, visualization settings, maze generation options, and parts of the interface.
Trying to keep all of that inside React components would have pushed the project straight back toward the same tangled architecture I was trying to get away from.
So I used Redux Toolkit as the central state layer.
What I especially liked was its action model. Instead of UI components directly changing application state in lots of different ways, they can simply describe what happened: a cell was blocked, a weight changed, an algorithm started, the animation advanced, or the maze was reset.
The state logic decides what that action actually means.
That keeps the UI focused on displaying the application and responding to user input, rather than becoming the place where all of the application’s rules live.
It also fits naturally with the rest of the architecture. The algorithms can stay independent, the renderer can stay focused on visualization, and Redux sits between them as the shared source of truth.
For a project with this much constantly changing state, that separation made the code much easier to reason about.
Generating mazes instead of drawing everything by hand
My first idea was to make the grid fully manual. Click a cell to turn it into a wall, right-click to remove it, and use the scroll wheel to increase or decrease its weight. That worked well when I wanted to build a specific test case, but it quickly exposed another problem: sometimes I did not want to design a maze. I just wanted to press a button and watch an algorithm run.
Forcing someone to draw a complicated graph before they can use the visualizer gets boring fast, so I added maze generation.
The simplest option is Random generation. You choose the width and height of the maze along with a block chance. The block chance controls how likely each cell is to become a wall. At 0%, the grid is completely empty. At 100%, everything becomes blocked. Everything in between gives you a quick way to create different environments and immediately run an algorithm through them.
The more interesting option is Recursive Division. Instead of treating every cell independently, it repeatedly splits the maze into smaller rooms by adding walls. The size of those rooms can be controlled, and watching the maze build itself is almost as interesting as watching the search algorithm solve it.
Recursive Division also ended up inspiring the name of the project. Once the generated walls started forming smaller and smaller passages, the whole thing began to look like a labyrinth.
Both generators can also assign random weights to cells. That matters for algorithms like Dijkstra and A*, because you can immediately create a weighted graph without manually tweaking dozens of cells one at a time.
The manual tools are still there when I want precise control, but maze generation makes experimentation much faster. You can build exactly what you want, or generate something interesting and start playing immediately.
Correctness mattered more than the animation
A visualizer can look completely convincing while still teaching the wrong thing. If I was going to use Labyrinth to explain algorithms, I needed to be confident that the implementations themselves were correct.
So I added a proper test suite that every algorithm has to pass before I merge it. That meant testing much more than the happy path: unreachable destinations, obstacles, weighted paths, unusual graph shapes, and cases where different algorithms should behave differently.
Writing those tests taught me a lot about unit testing itself. I had to learn how to isolate the algorithm from the rest of the application, build small predictable test cases, and think about edge cases instead of only checking examples I already knew would work.
The rule became: if the visualization is going to teach the algorithm, the algorithm has to earn the right to be visualized first.
Keeping the interface out of the way
I wanted the controls to stay out of the way so the grid could remain the focus. Most of the information lives in the sidebar, ready to open when you need it.
With the sidebar closed, only the essentials, play and reset, stay visible. The remaining controls sit in a popover that opens when you click the bulb, keeping them within reach without filling the screen.
Weight visualization
Weighted cells also have multiple visualization modes. They can be shown as raw numbers when you care about their exact values, or as spheres whose size represents the weight. The second view makes it much easier to glance at a maze and understand which areas are expensive to travel through without reading every individual number.
These are small features, but they support the main idea behind the interface: give people more control when they want it without making that control get in their way.
I wanted someone to open Labyrinth and think “I want to play with this”, not “I need to learn how this interface works first.”
Turning learning into teaching
The visualization is useful on its own, but I wanted the project to explain what the user was seeing as well. So every algorithm has a learning section.
Rather than creating a separate documentation area, I added a simple Learn button in the bottom-right corner. Clicking it opens a sidebar for the algorithm you are currently using, so you can experiment with BFS, for example, and immediately read about what it is doing without leaving the visualizer or losing your current setup.
The learning material is written in Markdown and kept separate from the application code. That means I can improve an explanation, add examples, or expand a lesson without turning UI components into walls of text.
It also follows the same design principle as the rest of the project: algorithms are algorithms, content is content, and rendering is rendering. Each part should be able to change without dragging the others with it.
Where it stands
Labyrinth is one of those projects where I am as happy with the code behind it as I am with what appears on screen. The architecture is simple enough that I can come back after some time and still understand it, adding another algorithm no longer feels dangerous, and the tests give me confidence that I am not accidentally teaching something incorrectly.
There is still a lot I want to explore. Full mobile support is one obvious next step, but the bigger idea I want to experiment with is live coding: letting someone write their own search algorithm and then watching Labyrinth visualize it as it runs.
That would take the original idea behind the project even further. Instead of only asking “How does this algorithm work?”, Labyrinth could eventually let you ask “What happens if I write it myself?”