One of the core features of the Obsidian Tower is procedural generation. Every time a player enters a new floor, the layout is different. Rooms aren't hand-designed — they're generated algorithmically at runtime. Getting this system right required careful thought about what makes a room feel both functional and interesting.
The Requirements
I needed the random room layouts to meet three criteria:
- Every door must be reachable. If a room has two, three, or four doors, the player needs to be able to walk from any door to any other door. A room where a door leads to a dead end with no way to reach the exit is a broken room.
- Visually interesting layouts. Rooms shouldn't just be big empty rectangles. They need walls, corridors, and varying shapes that make exploration feel rewarding and give combat encounters some tactical depth.
- Inclusion of enemies and chests. The generated rooms need to contain enemies to fight and chests to find, placed in locations that make sense within the layout.
The Algorithm: Depth-First Search Tunneling
The core of the room generation algorithm is Depth-First Search (DFS), starting from each door in the room. Think of having a completely solid dirt room and releasing a mole at each of the doors to begin digging. Each mole burrows in a random direction, carving out tunnels as it goes. It keeps going until it hits a dead end, then backtracks and tries another direction.
Each DFS instance starts at a door and begins replacing wall tiles with walkable path tiles. To keep track of which paths belong to which starting door, each DFS assigns a unique ID to the tiles it carves. So the path originating from door 1 has one ID, the path from door 2 has another, and so on.
Connecting the Paths
The key insight is what happens when paths from different doors intersect. As a DFS tunnel is being carved, if it encounters a tile that already belongs to a different DFS instance — that is, a tile with a different door's ID — the two paths merge. The system recognizes that these two doors are now connected through the merged tunnel network.
This process continues until all doors are linked. If a DFS runs out of moves before connecting to the others, the algorithm can detect the disconnection and re-run generation. In practice, because the DFS explores aggressively and the rooms aren't enormous, connections happen naturally in almost every case.
Preventing Large Open Areas
One challenge with this approach is preventing the tunnels from creating large open areas. If the DFS carves too many adjacent tiles, you end up with big empty spaces that feel like generic rectangles rather than interesting cave-like layouts. The system includes constraints that limit how wide a carved area can become, encouraging long winding corridors and small chambers over open fields.
Placing Enemies and Chests
Once the tunnel network is complete, the algorithm places enemies and chests. Enemy spawning is based on tunnel length — longer paths get more enemies, ensuring that larger rooms have more combat encounters while small connector rooms stay relatively quiet. Enemies are placed along walkable paths with enough spacing to avoid clustering.
Chests are placed by scanning for wall tiles that are adjacent to walkable paths. A random subset of these wall tiles is replaced with chest tiles, making chests feel like they're tucked into alcoves or hidden along corridor walls rather than sitting out in the open.
The result is a room that feels hand-crafted despite being entirely procedurally generated — corridors that wind and connect, enemies that populate the space naturally, and treasures that reward exploration.