Skip to main content
Coding Quests
The Scroll Library
Visual Explainers

Seeds Explained: Regenerate the Same Dungeon on Purpose

October 31, 20267 min read
Part 7 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

A player sends you a screenshot: the run broke on a level where the boss spawned inside a wall. You would love to see that level. But it was random, and it is gone. Or you want a daily challenge where everyone plays the same dungeon. Or you just changed one line, and want to see whether the bug is fixed on the exact floor that showed it.

All three need the same thing: the ability to make the same "random" level again on purpose. That is what a seed is for. This is part 7 of a series that builds one dungeon generator, pass by pass.

Random numbers that are not random

The numbers a computer game calls random come from a formula. Each new number is worked out from a hidden internal state, and the state moves on one step. That formula is a pseudo-random number generator. Godot's documentation says its RandomNumberGenerator "currently uses PCG32", a generator designed by Melissa O'Neill.

The seed sets that internal state. Set the same seed, and the generator walks the same path through its numbers, every time, on every machine running the same code. In the docs' words: "A given seed will give a reproducible sequence of pseudo-random numbers."

For seed 214, the first three numbers from randi_range(3, 6) are 3, 4 and 4. They will be 3, 4 and 4 tomorrow, and on your phone, and in the demo on this page. The demo's generator reproduces Godot's exactly, so a seed you try here builds the same map in Godot 4.7.2.

The order is part of the seed

A seed fixes the sequence of numbers, not which part of your code gets each one. Your generator takes them in order: the first room's width, its height, its position, the next room, and so on. If anything takes an extra number in the middle, everything after it receives a different number than before.

Here is the most ordinary way that happens. You decide half the rooms should get a torch, and add one line to the room pass:

GDScript
d.rooms.append(rect)
d.carve_room(rect)
if torch_rng.randf() < 0.5: # NEW: one extra random call
torches.append(rect.get_center())

If torch_rng is the same generator that places rooms, that one roll per room shifts every room after the first. Try it below.

Same seed, one extra random call

Interactive, engine-accurate

One extra random call per room, and 148 of 484 cells changed. Every number the layout drew after the first torch roll came out different.

Before

01234567

After adding the torch roll

012345678ttt

Seed 214 makes the layout stream start with 3, 4, 4 for the first three room sizes, every time, in the browser and in Godot. The second map is the same seed after adding the line if rng.randf() < 0.5: to the room pass. Rooms marked t got a torch.

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

On seed 214, with one generator for everything, the torch roll changes 148 of the 484 cells and even the number of rooms, from 8 to 9. It changed the layout on every one of seeds 1 to 500. You added torches, and lost every map you had saved.

The fix: one stream per job

Give the layout and the decoration their own generators. Seed a third, short-lived generator with your seed, and use its first two numbers to seed the other two:

GDScript
func set_seed(seed_value: int) -> void:
var seeds := RandomNumberGenerator.new()
seeds.seed = seed_value
rng.seed = seeds.randi()
decor_rng.seed = seeds.randi()

Now the torch roll draws from decor_rng. The layout stream never sees it. Switch the demo to Layout and decoration apart: the same change now moves 0 cells. Seed 214 keeps its map, and gets torches in 4 rooms.

The same idea scales. Give enemy spawns, loot and decoration their own streams, and you can rebalance loot without moving a single wall.

Where seeds still break

Godot's global random functions. randi(), randf() and randi_range() called without a generator use a single global seed, which Godot randomizes when the project starts. Array.shuffle() and Array.pick_random() use that global seed too. The docs say so directly: they use "a common, global random seed". One shuffle() in your generator and your seed stops working. Use your own generator, and shuffle with it, by swapping elements with rng.randi_range().

Using the generator for gameplay too. If enemies roll their attacks on the same generator that builds the next floor, how you fight changes the next floor. Keep level generation on its own streams.

Similar seeds. The docs warn that the generator "does not have an avalanche effect": seeds 1 and 2 can start with similar numbers. If you build seeds from a date or a counter, mix them first. hash() on the number or on a string is an easy way.

Saving mid-run. A seed rebuilds the level from the start. To resume a generator part way through its sequence, save its state property and restore it later. The docs say not to set state to arbitrary values, only ones you saved.

Different engine versions. The numbers come from Godot's code. This page's numbers were checked against Godot 4.7.2 only. A future version could change how a function draws numbers, and old seeds would make new levels. If seeds matter to players, such as shared daily challenges, record the game version with the seed.

Seeds you can share

A seed is a short number that stands for a whole level, which makes it useful outside the generator:

  • Bug reports. Show the seed on the pause screen or in the log. "Seed 8812, floor 3" is a reproducible bug.
  • Daily challenges. Build the seed from today's date, so every player gets the same dungeon. Everyone needs the same game version, too.
  • Testing. Run your playability checks on seeds 1 to 1,000 every time you change the generator, as part 8 does. A seed that broke once can join the list for good.

In Godot: one generator object, seeded once

The smallest example of the whole idea, run in Godot 4.7.2:

GDScript
func same_seed_same_numbers(seed_value: int) -> Array:
var rng := RandomNumberGenerator.new()
rng.seed = seed_value
return [rng.randi_range(3, 6), rng.randi_range(3, 6), rng.randi_range(3, 6)]

Call it twice with 214 and you get [3, 4, 4] both times. In the full generator, generate() seeds both streams before any pass runs:

GDScript
func generate(seed_value: int) -> void:
set_seed(seed_value)
clear()
place_rooms()
connect_chain()

To let players type a seed, export it on your dungeon node with @export var dungeon_seed := 214 and pass it to generate(). For a fresh level each run, pick a seed once, then use it: dungeon_seed = randi(). Keep that number, and show it somewhere.

The complete generator, with both streams, 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.

Procedural Dungeon & Run does not teach seeds. What it does, from lesson 2, is create its own RandomNumberGenerator and roll every room size and position through it. That generator is exactly the piece you seed.

Add an exported dungeon_seed and set the generator's seed from it before the layout runs, and the course's dungeon becomes repeatable. If you also add decoration rolls, give them a second generator as shown above.

  • Lesson 2: Rooms & CorridorsFree

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

FAQ

What is a seed in procedural generation?

A seed is the number a random number generator starts from. The same seed produces the same sequence of numbers, so a generator that draws all its choices from that sequence builds the same level every time.

Why does my Godot seed not give the same result every time?

Something in your generator uses a random number that is not from your seeded generator. The usual culprits are the global randi() or randf(), and Array.shuffle() or pick_random(), which all use Godot's global seed. Another cause is an extra random call added earlier in the generator.

How do I make a daily challenge with the same level for everyone?

Turn the date into a seed, for example with hash() on a "2026-10-31" string, and seed the level generator with it. Every player who runs the same game version gets the same level that day.

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 demo's random number generator is a port of Godot's, checked number for number against Godot on 7 seeds, and its dungeons are checked against Godot's on 40 seeds. The 500-seed claim comes from the port. Nothing here was checked on another Godot version or on a non-Windows build.

procedural generationrandomnessseedsdungeongodot

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.