Skip to main content
Coding Quests
The Scroll Library
Visual Explainers

How to Test Whether a Generated Level Is Actually Playable

October 31, 20267 min read
Part 8 of 8: Procedural dungeons, explained

How a dungeon generator places rooms, joins them, builds walls, picks the boss room and proves the result is playable. Every part uses the same seeded 22 by 22 dungeon.

  1. 1.How dungeon generation works
  2. 2.Stopping rooms overlapping
  3. 3.Making every room reachable
  4. 4.Joining premade rooms at their doors
  5. 5.Walls around the floor
  6. 6.Where the boss room goes
  7. 7.Seeds: the same dungeon on purpose
  8. 8.Testing that a level is playable

You played twenty generated levels and every one was fine. Then a player hits the twenty-first, and the exit is behind a crate. A generator can make more levels in a minute than you will ever play, so playing them is not how you find the broken ones.

The answer is to make the generator check its own work. This is the last part of a series that builds one dungeon generator, pass by pass, and it turns everything the earlier parts learned into checks that run on every level.

Playable means something you can check

"Playable" sounds like a feeling. For a generator it has to be a list. Here is the one this series uses:

  1. Enough rooms. At least 6. A crowded map can leave the room pass short.
  2. A clear spawn. The cell the player starts on is floor, with nothing on it.
  3. Every room reachable. A flood fill from the spawn reaches every room, with props treated as walls.
  4. A real journey to the boss. The boss room is at least 12 steps from the spawn on foot.

Your game will need its own items. Keys before locked doors, a path wide enough for a large enemy, spawn points far from the player, a shop on every floor. The method stays the same. Each item becomes a function that looks at the level and either passes or returns a reason.

Check the level the player gets

The single most useful rule: validate after decoration, not before.

A layout can pass every check, then a later pass can quietly break it. The demo's careless decoration drops up to two crates in every room. A crate is one cell, and most corridors are one cell wide. When a crate lands in the doorway where a corridor meets a room, everything behind that doorway is cut off.

Pick Props anywhere and look through the report. Every failure here is a layout that passed until the crates went in.

A seed test report

Interactive, engine-accurate

5 of 60 seeds fail. 14 × "room # unreachable", 1 × "boss only # steps away"

0123456S

Seed 2: room 2 unreachable, room 3 unreachable, room 4 unreachable, room 5 unreachable, room 6 unreachable, boss only 7 steps away. Crates are props, counted as solid. Hatched floor is floor the player cannot reach once the props are in. The check runs on the finished level, after decoration, because that is the level the player gets.

This runs the GDScript from this article, ported to the browser. The same seed gives the same result in Godot 4.7.2.

With crates placed anywhere, 5 of seeds 1 to 60 fail. Seed 2 loses five rooms to crates in doorways. Switch to Keep doorways clear, which refuses to place a crate on any room cell that a corridor opens onto, and all 60 pass.

The same holds at scale. On seeds 1 to 500, careless crates break 70 levels, about 1 in 7. Keeping doorways clear leaves 11 failures, and all 11 are a different problem: the room pass only fit 5 rooms. A check found that problem too, without anyone looking for it.

When a level fails: reject, repair, or prevent

There are three ways to respond, and good generators use all three:

  • Prevent it. Fix the pass that causes it. Keeping doorways clear prevents the crate problem entirely. Prevention is best when you know the cause.
  • Repair it. Patch the level. Part 3 repairs unreachable rooms by adding corridors. Repair is good when the fix is local and cannot break anything else.
  • Reject it. Throw the level away and generate the next seed. It is the simplest and always works, as long as failures are rare. If 1 seed in 50 fails, the player never notices. If 1 in 2 fails, rejection hides a broken generator, and you should prevent instead.

Always log the seeds you reject. A seed that failed is a free test case.

Test hundreds of seeds at once

The demo checks 60 seeds in your browser. You can do the same in Godot, from the command line, with no window. This script loops over 500 seeds, runs the checks and prints every failure:

GDScript
func _initialize() -> void:
var keep_doorways_clear := not OS.get_cmdline_user_args().has("--props-anywhere")
var failed := 0
for seed_value in range(1, 501):
var layout = DungeonLayout.new()
layout.generate(seed_value)
var props: Dictionary = layout.scatter_props(keep_doorways_clear)
var problems: Array[String] = layout.validate(props)
if not problems.is_empty():
failed += 1
print("seed %d: %s" % [seed_value, ", ".join(problems)])
print("%d of 500 seeds failed" % failed)
quit(1 if failed > 0 else 0)

Save it as seed_report.gd. Above the function it needs two lines: extends SceneTree, and const DungeonLayout := preload("res://dungeon_layout.gd"). Run it with godot --headless --script res://seed_report.gd from your project folder. --headless runs Godot with no window. A script that extends SceneTree runs as the whole program, and _initialize() is its entry point. quit(1) makes the run fail when any seed fails, so a build server can stop a broken generator before players see it.

On the generator in this series, it prints "11 of 500 seeds failed". With -- --props-anywhere on the end, it prints "70 of 500 seeds failed".

In Godot: the checks themselves

validate() returns a list of problems in plain words. An empty list means the level passed:

GDScript
func validate(props: Dictionary) -> Array[String]:
var problems: Array[String] = []
if rooms.size() < min_rooms:
problems.append("only %d rooms" % rooms.size())
var start := rooms[0].get_center()
if props.has(start):
problems.append("spawn cell blocked")
var dist := walk_distances(start, props)
for i in unreachable_rooms(dist):
problems.append("room %d unreachable" % i)
var boss := farthest_room_on_foot(dist)
var steps := dist[index(rooms[boss].get_center())]
if steps < min_boss_steps:
problems.append("boss only %d steps away" % steps)
return problems

Props are a Dictionary used as a set: each key is a cell with a prop on it. Passing them to walk_distances(), the flood fill from part 3, makes the fill treat those cells as solid. That one argument is what makes the check see the decorated level instead of the bare layout.

The rule that prevents the crate problem asks whether a cell is a doorway, meaning a room cell with floor just outside the room next to it:

GDScript
func is_doorway(cell: Vector2i, rect: Rect2i) -> bool:
for dir in NEIGHBORS_4:
var outside := cell + dir
if not rect.has_point(outside) and is_floor(outside):
return true
return false

Rect2i.has_point() reports whether a cell is inside the room. A neighbour that is outside the room, yet still floor, must be a corridor, so the cell next to it is a doorway.

The complete generator, with the decoration pass, is dungeon_layout.gd.

Build it next

Procedural Dungeon & Run

A quest on CodingQuests: 12 interactive lessons that build one part of a game in Godot, in your browser, checked as you go.

No CodingQuests quest teaches level validation yet. The closest practice is Procedural Dungeon & Run, which builds the generator these checks would test. Lesson 2 builds the rooms and corridors and is free. Lesson 8 adds themed decoration, the pass most likely to break a level, and is a member lesson.

The course itself does not check reachability or reject seeds. Its chain corridors cannot strand a room. Add validate() from this article once you add anything that can block a path.

  • Lesson 2: Rooms & CorridorsFree
  • Lesson 8: Themes & DecorationMembers

Lessons 1 to 3 are free to open, no account needed. Guild Hall, the membership, opens the other 9.

FAQ

How do I test that a procedurally generated level is playable?

Write each requirement as a check the code can run, such as every room reachable from the spawn, and run all of them on the finished, decorated level. Reject levels that fail, and run the checks on hundreds of seeds whenever the generator changes.

How do I find the parts of a generated map the player cannot reach?

Flood fill from the player's start, treating walls and solid props as blocked. Any floor cell the fill never reaches is unreachable. If a whole room is unreachable, the level fails.

Should I fix a broken generated level or make a new one?

If failures are rare, make a new one with the next seed. It is simple and always works. If failures are common, find the pass that causes them and prevent it, because rejecting most levels hides a broken generator.

Sources and testing

How this was tested. Every GDScript block on this page was run in Godot 4.7.2 (headless, on Windows) on 31 October 2026. The seed report printed "11 of 500 seeds failed", and "70 of 500 seeds failed" with --props-anywhere. An automated check confirms the browser demo fails the same seeds, for the same reasons, as that Godot run.

procedural generationdungeontestingroguelikegodot

Stop reading. Start building.

Build a roguelike in Godot from the first lesson. The first quest, the player controller, is free from start to finish. Later quests in the same game add combat, enemies, rooms, loot and upgrades.

Free, and no card needed. Built by a real person, with new quests every month.

Get the next Godot build in your inbox

New quests, project breakdowns, and game-dev tips.

You'll get emails from CodingQuests with Godot tips, new tutorials, course updates, and occasional offers. Unsubscribe anytime. Privacy Policy

Written by Omar

We teach Godot 4 by making you build complete systems: inventories, save systems, action roguelike controllers, enemy AI. The scrolls are free. The quests are where it sticks.