<h1><spanclass="section-number">13. </span>Procedurally generated Dungeon<aclass="headerlink"href="#procedurally-generated-dungeon"title="Permalink to this headline">¶</a></h1>
<p>The rooms that we discussed in the <aclass="reference internal"href="Beginner-Tutorial-Rooms.html"><spanclass="doc std std-doc">lesson about Rooms</span></a> are all <em>manually</em> generated. That is, a human builder would have to sit down and spawn each room manually, either in-game or using code.</p>
<p>In this lesson we’ll explore <em>procedural</em> generation of the rooms making up our game’s underground dungeon. Procedural means that its rooms are spawned automatically and semi-randomly as players explore, creating a different dungeon layout every time.</p>
<sectionid="design-concept">
<h2><spanclass="section-number">13.1. </span>Design Concept<aclass="headerlink"href="#design-concept"title="Permalink to this headline">¶</a></h2>
<p>This describes how the procedural generation should work at a high level. It’s important to understand this before we start writing code.</p>
<p>We will assume our dungeon exists on a 2D plane (x,y, no z directions). We will only use N,E,S,W compass directions, but there is no reason this design couldn’t work with SE, NW etc, except that this could make it harder for the player to visualize. More possible directions also make it more likely to produce collisions and one-way exits (see below).</p>
<p>This design is pretty simple, but just by playing with some of its settings, it can produce very different-feeling dungeon systems.</p>
<sectionid="the-starting-room">
<h3><spanclass="section-number">13.1.1. </span>The starting room<aclass="headerlink"href="#the-starting-room"title="Permalink to this headline">¶</a></h3>
<p>The idea is that all players will descend down a well to get to the start of the dungeon. The bottom of the well is a statically created room that won’t change.</p>
<divclass="code-block-caption"><spanclass="caption-text">Starting room</span><aclass="headerlink"href="#id1"title="Permalink to this code">¶</a></div>
<divclass="highlight-default notranslate"><divclass="highlight"><pre><span></span> Branch N
▲
│
┌────────┼────────┐
│ │n │
│ ▼ │
│ │
│ e│
Branch W ◄─┼─► up▲ ◄─┼─► Branch E1
│w │
│ │
│ ▲ │
│ │s │
└────────┼────────┘
│
▼
Branch S
</pre></div>
</div>
</div>
<p>The magic happens when you choose one of the exits from this room (except the one leading you back to the surface). Let’s assume a PC descends down to the start room and moves <codeclass="docutils literal notranslate"><spanclass="pre">east</span></code>:</p>
<ulclass="simple">
<li><p>The first person to go east will spawn a new “Dungeon branch” (Branch E1 in the diagram). This is a separate “instance” of dungeon compared to what would spawn if moving through any of the other exits. Rooms spawned within one dungeon branch will never overlap with that of another dungeon branch.</p></li>
<li><p>A timer starts. While this timer is active, everyone going <codeclass="docutils literal notranslate"><spanclass="pre">east</span></code> will end up in Branch E1. This allows for players to team up and collaborate to take on a branch.</p></li>
<li><p>After the timer runs out, everyone going <codeclass="docutils literal notranslate"><spanclass="pre">east</span></code> will instead end up in a <em>new</em> Branch E2. This is a new branch that has no overlap with Branch E1.</p></li>
<li><p>PCs in Branches E1 and E2 can always retreat <codeclass="docutils literal notranslate"><spanclass="pre">west</span></code> back to the starting room, but after the timer runs out this is now a one-way exit - they won’t be able to return to their old branches if they do.</p></li>
</ul>
</section>
<sectionid="generating-new-branch-rooms">
<h3><spanclass="section-number">13.1.2. </span>Generating new branch rooms<aclass="headerlink"href="#generating-new-branch-rooms"title="Permalink to this headline">¶</a></h3>
<divclass="code-block-caption"><spanclass="caption-text">Creating the eastern branch and its first room</span><aclass="headerlink"href="#id2"title="Permalink to this code">¶</a></div>
<p>A dungeon room is only created when actually moving to it. In the above example, the PC moved <codeclass="docutils literal notranslate"><spanclass="pre">east</span></code> from the start room, which initiated a new dungeon branch. The branch also created a new room (room <codeclass="docutils literal notranslate"><spanclass="pre">A</span></code>) at coordinate <codeclass="docutils literal notranslate"><spanclass="pre">(1,0)</span></code>. In this case it (randomly) seeded this room with three exits <codeclass="docutils literal notranslate"><spanclass="pre">north</span></code>, <codeclass="docutils literal notranslate"><spanclass="pre">east</span></code> and <codeclass="docutils literal notranslate"><spanclass="pre">south</span></code>.
<li><p>It always creates an exit back to the room we came from.</p></li>
<li><p>It checks how many unexplored exits we have in the dungeon right now. That is, how many exits we haven’t yet traversed. This number must never be zero unless we want a dungeon that can be ‘finished’. The maximum number of unexplored exits open at any given time is a setting we can experiment with. A small max number leads to linear dungeon, a bigger number makes the dungeon sprawling and maze-like.</p></li>
<li><p>Outgoing exits (exits not leading back to where we came) are generated with the following rules:</p>
<ul>
<li><p>Randomly create between 0 and the number of outgoing exits allowed by the room and the branches’ current budget of allowed open unexplored exits.</p></li>
<li><p>Create 0 outgoing exits (a dead-end) only if this would leave at least one unexplored exit open somewhere in the dungeon branch.</p></li>
<li><p>Do <em>not</em> create an exit that would connect the exit to a previously generated room (so we prefer exits leading to new places rather than back to old ones)</p></li>
<li><p>If a previously created exit end up pointing to a newly created room, this <em>is</em> allowed, and is the only time a one-way exit will happen (example below). All other exits are always two-way exits. This also presents the only small chance of closing out a dungeon with no way to proceed but to return to the start.</p></li>
<li><p>Never create an exit back to the start room (e.g. from another direction). The only way to get back to the start room is by back tracking.</p></li>
</ul>
</li>
</ul>
<p>In the following examples, we assume the maximum number of unexplored exits allowed open at any time is set to 4.</p>
<divclass="code-block-caption"><spanclass="caption-text">After four steps in the eastern dungeon branch</span><aclass="headerlink"href="#id3"title="Permalink to this code">¶</a></div>
<li><p>PC moves <codeclass="docutils literal notranslate"><spanclass="pre">east</span></code> from the start room. A new room <codeclass="docutils literal notranslate"><spanclass="pre">A</span></code> (coordinate <codeclass="docutils literal notranslate"><spanclass="pre">(1,</span><spanclass="pre">0)</span></code> ) is created. After a while the exit back to the start room becomes a one-way exit. The branch can have at most 4 unexplored exits, and the dungeon branch randomly adds three additional exits out of room <codeclass="docutils literal notranslate"><spanclass="pre">A</span></code>.</p></li>
<li><p>PC moves <codeclass="docutils literal notranslate"><spanclass="pre">south</span></code>. A new room <codeclass="docutils literal notranslate"><spanclass="pre">B</span></code> (<codeclass="docutils literal notranslate"><spanclass="pre">(1,-1)</span></code>) is created, with two random exits, which is as many as the orchetrator is allowed to create at this time (4 are now open). It also always creates an exit back to the previous room (<codeclass="docutils literal notranslate"><spanclass="pre">A</span></code>)</p></li>
<li><p>PC moves <codeclass="docutils literal notranslate"><spanclass="pre">east</span></code> (coordinate (<codeclass="docutils literal notranslate"><spanclass="pre">(2,</span><spanclass="pre">-1)</span></code>). A new room <codeclass="docutils literal notranslate"><spanclass="pre">C</span></code> is created. The dungaon branch already has 3 exits unexplored, so it can only add one exit our of this room.</p></li>
<li><p>PC moves <codeclass="docutils literal notranslate"><spanclass="pre">east</span></code> (<codeclass="docutils literal notranslate"><spanclass="pre">(3,</span><spanclass="pre">-1)</span></code>). While the dungeon branch still has a budget of one exit, it knows there are other unexplored exits elsewhere, and is allowed to randomly create 0 exits. This is a dead end. The PC must go back and explore another direction.</p></li>
<divclass="code-block-caption"><spanclass="caption-text">Looping around</span><aclass="headerlink"href="#id4"title="Permalink to this code">¶</a></div>
<p>In this example the PC moved <codeclass="docutils literal notranslate"><spanclass="pre">east</span></code>, <codeclass="docutils literal notranslate"><spanclass="pre">south</span></code>, <codeclass="docutils literal notranslate"><spanclass="pre">east</span></code> but the exit out of room <codeclass="docutils literal notranslate"><spanclass="pre">C</span></code> is leading north, into a coordinate where <codeclass="docutils literal notranslate"><spanclass="pre">A</span></code> already has an exit pointing to. Going <codeclass="docutils literal notranslate"><spanclass="pre">north</span></code> here leads to the following:</p>
<divclass="code-block-caption"><spanclass="caption-text">Creation of a one-way exit</span><aclass="headerlink"href="#id5"title="Permalink to this code">¶</a></div>
<p>As the PC moves <codeclass="docutils literal notranslate"><spanclass="pre">north</span></code>, the room <codeclass="docutils literal notranslate"><spanclass="pre">D</span></code> is created at <codeclass="docutils literal notranslate"><spanclass="pre">(2,0)</span></code>.</p>
<p>While <codeclass="docutils literal notranslate"><spanclass="pre">C</span></code> to <codeclass="docutils literal notranslate"><spanclass="pre">D</span></code> get a two-way exit as normal, this creates a one-way exit from <codeclass="docutils literal notranslate"><spanclass="pre">A</span></code> to <codeclass="docutils literal notranslate"><spanclass="pre">D</span></code>.</p>
<p>Whichever exit leads to actually creating the room gets the two-way exit, so if the PC had walked back from <codeclass="docutils literal notranslate"><spanclass="pre">C</span></code> and created room <codeclass="docutils literal notranslate"><spanclass="pre">D</span></code> by going <codeclass="docutils literal notranslate"><spanclass="pre">east</span></code> from room <codeclass="docutils literal notranslate"><spanclass="pre">A</span></code>, then the one-way exit would be from room <codeclass="docutils literal notranslate"><spanclass="pre">C</span></code> instead.</p>
<div><p>If the maximum allowed number of open unexplored exits is small, this case is the only situation where it’s possible to ‘finish’ the dungeon (having no more unexplored exits to follow). We accept this as a case where the PCs just have to turn back and try another dungeon branch.</p>
<divclass="code-block-caption"><spanclass="caption-text">Never link back to start room</span><aclass="headerlink"href="#id6"title="Permalink to this code">¶</a></div>
<p>Here the PC moved <codeclass="docutils literal notranslate"><spanclass="pre">west</span></code> from room <codeclass="docutils literal notranslate"><spanclass="pre">B</span></code> creating room <codeclass="docutils literal notranslate"><spanclass="pre">E</span></code> at <codeclass="docutils literal notranslate"><spanclass="pre">(0,</span><spanclass="pre">-1)</span></code>.</p>
<p>The dungeon branch never creates a link back to the start room, but it <em>could</em> have created up to two new exits <codeclass="docutils literal notranslate"><spanclass="pre">west</span></code> and/or <codeclass="docutils literal notranslate"><spanclass="pre">south</span></code>. Since there’s still an unexplored exit <codeclass="docutils literal notranslate"><spanclass="pre">north</span></code> from room <codeclass="docutils literal notranslate"><spanclass="pre">A</span></code>, the branch is also allowed to randomly assign 0 exits, which is what it did here.</p>
<p>The PC needs to backtrack and go <codeclass="docutils literal notranslate"><spanclass="pre">north</span></code> from <codeclass="docutils literal notranslate"><spanclass="pre">A</span></code> to continue exploring this dungeon branch.</p>
</section>
<sectionid="making-the-dungeon-dangerous">
<h3><spanclass="section-number">13.1.3. </span>Making the dungeon dangerous<aclass="headerlink"href="#making-the-dungeon-dangerous"title="Permalink to this headline">¶</a></h3>
<p>A dungeon would not be interesting without peril! There needs to be monsters to slay, puzzles to solve and treasure to be had.</p>
<p>When PCs first enters a room, that room is marked as <codeclass="docutils literal notranslate"><spanclass="pre">not</span><spanclass="pre">clear</span></code>. While a room is not cleared, the PCs <em>cannot use any of the unexplored exits out of that room</em>. They <em>can</em> still retreat back the way they came unless they become locked in combat, in which case they have to flee from that first.</p>
<p>Once PCs have overcome the challenge of the room (and probably earned some reward), will it change to <codeclass="docutils literal notranslate"><spanclass="pre">clear</span></code> . A room can auto-clear if it is spawned empty or has no challenge meant to block the PCs (like a written hint for a puzzle elsewhere).</p>
<p>Note that clear/non-clear only relates to the challenge associated with that room. Roaming monsters (see the <aclass="reference internal"href="Beginner-Tutorial-AI.html"><spanclass="doc std std-doc">AI tutorial</span></a>) can lead to combat taking place in previously ‘cleared’ rooms.</p>
</section>
<sectionid="difficulty-scaling">
<h3><spanclass="section-number">13.1.4. </span>Difficulty scaling<aclass="headerlink"href="#difficulty-scaling"title="Permalink to this headline">¶</a></h3>
<asideclass="sidebar">
<pclass="sidebar-title">Risk and reward</p>
<p>The concept of dungeon depth/difficulty works well together with limited resources. If healing is limited to what can be carried, this leads to players having to decide if they want to risk push deeper or take their current spoils and retreat back to the surface to recover.</p>
</aside>
<p>The “difficulty” of the dungeon is measured by the “depth” PCs have delved to. This is given as the <em>radial distance</em> from the start room, rounded down, found by the good old <aclass="reference external"href="https://en.wikipedia.org/wiki/Pythagorean_theorem">Pythagorean theorem</a>:</p>
<p>So if you are in room <codeclass="docutils literal notranslate"><spanclass="pre">(1,</span><spanclass="pre">1)</span></code> you are at difficulty 1. Conversely at room coordinate <codeclass="docutils literal notranslate"><spanclass="pre">(4,-5)</span></code> the difficulty is 6. Increasing depth should lead to tougher challenges but greater rewards.</p>
<h2><spanclass="section-number">13.2. </span>Start Implementation<aclass="headerlink"href="#start-implementation"title="Permalink to this headline">¶</a></h2>
<p>Let’s implement the design now!</p>
<asideclass="sidebar">
<p>You can also find code examples of the dungeon generator at <codeclass="docutils literal notranslate"><spanclass="pre">evennia/contrib/tutorials</span></code>, in <aclass="reference internal"href="../../../api/evennia.contrib.tutorials.evadventure.dungeon.html#evennia-contrib-tutorials-evadventure-dungeon"><spanclass="std std-ref">evadventure/dungeon.py</span></a>.</p>
</aside>
<blockquote>
<div><p>Create a new module <codeclass="docutils literal notranslate"><spanclass="pre">evadventure/dungeon.py</span></code>.</p>
</div></blockquote>
</section>
<sectionid="basic-dungeon-rooms">
<h2><spanclass="section-number">13.3. </span>Basic Dungeon rooms<aclass="headerlink"href="#basic-dungeon-rooms"title="Permalink to this headline">¶</a></h2>
<p>This is the fundamental element of the design, so let’s start here.</p>
<p>Back in the <aclass="reference internal"href="Beginner-Tutorial-Rooms.html"><spanclass="doc std std-doc">lesson about rooms</span></a> we created a basic <codeclass="docutils literal notranslate"><spanclass="pre">EvAdventureRoom</span></code> typeclass.
<spanclass="k">return</span><spanclass="s2">"|rThe path forwards is blocked!|n"</span>
</pre></div></td></tr></table></div>
</div>
<asideclass="sidebar">
<pclass="sidebar-title">Storing the room typeclass</p>
<p>For this tutorial, we’re keeping all dungeon-related code in one module. But one <em>could</em> also argue that they belong in <codeclass="docutils literal notranslate"><spanclass="pre">evadventure/rooms.py</span></code> together with the other rooms. This is only a matter of how you want to organize things. Feel free to organize however you prefer for your own game.</p>
</aside>
<ulclass="simple">
<li><p><strong>Lines 14-15</strong>: Dungeon rooms are dangerous, so unlike base EvAdventure rooms, we allow combat and death to happen in them.</p></li>
<li><p><strong>Line 17</strong>: We store a reference to the dungeon branch so that we can access it during room creation if we want. This could be relevant if we want to know things about the dungeon branch as part of creating rooms.</p></li>
<li><p><strong>Line 18</strong>: The xy coords will be simply stored as a tuple <codeclass="docutils literal notranslate"><spanclass="pre">(x,y)</span></code> on the room.</p></li>
</ul>
<p>All other functionality is built to manage the “clear” state of the room.</p>
<ulclass="simple">
<li><p><strong>Line 29</strong>: When we create the room Evennia will always call its <codeclass="docutils literal notranslate"><spanclass="pre">at_object_creation</span></code> hook. We make sure to add a add a <aclass="reference internal"href="../../../Components/Tags.html"><spanclass="doc std std-doc">Tag</span></a><codeclass="docutils literal notranslate"><spanclass="pre">not_clear</span></code> to it (category “dungeon_room” to avoid collisions with other systems).</p></li>
<li><p><strong>Line 32</strong>: We will use the <codeclass="docutils literal notranslate"><spanclass="pre">.clear_room()</span></code> method to remove this Tag once the room’s challenge is overcome.</p></li>
<li><p><strong>Line 36</strong><codeclass="docutils literal notranslate"><spanclass="pre">.is_room_clear</span></code> is a convenient property for checking the tag. This hides the Tag so we don’t need to worry about we track the clear-room state.</p></li>
<li><p><strong>Line 38</strong> The <codeclass="docutils literal notranslate"><spanclass="pre">get_display_footer</span></code> is a standard Evennia hook for customizing the room’s footer display.</p></li>
</ul>
</section>
<sectionid="dungeon-exits">
<h2><spanclass="section-number">13.4. </span>Dungeon exits<aclass="headerlink"href="#dungeon-exits"title="Permalink to this headline">¶</a></h2>
<p>The dungeon exits are special in that we want the very act of traversing them to create the room on the other side.</p>
<divclass="highlight-python notranslate"><divclass="highlight"><pre><span></span><spanclass="c1"># in evadventure/dungeon.py </span>
<spanclass="sd"> Called when failing to traverse.</span>
<spanclass="sd">"""</span>
<spanclass="n">traversing_object</span><spanclass="o">.</span><spanclass="n">msg</span><spanclass="p">(</span><spanclass="s2">"You can't get through this way yet!"</span><spanclass="p">)</span>
</pre></div>
</div>
<p>For now, we have not actually created the code for creating a new room in the branch, so we leave the <codeclass="docutils literal notranslate"><spanclass="pre">at_traverse</span></code> method un-implemented for now. This hook is what is called by Evennia when traversing the exit.</p>
<p>In the <codeclass="docutils literal notranslate"><spanclass="pre">at_object_creation</span></code> method we make sure to add a <aclass="reference internal"href="../../../Components/Locks.html"><spanclass="doc std std-doc">Lock</span></a> of type “traverse”, which will limit who can pass through this exit. We lock it with the <aclass="reference internal"href="../../../api/evennia.locks.lockfuncs.html#evennia.locks.lockfuncs.objloctag"title="evennia.locks.lockfuncs.objloctag"><spanclass="xref myst py py-func">objlocktag</span></a> Lock function. This checks if the accessed object (this exit)’s location (the dungeon room) has a tag “not_clear” with category “dungeon_room” on it. If it does, then the traversal <em>fails</em>. In other words, while the room is not cleared, this type of exit will not let anyone through.</p>
<p>The <codeclass="docutils literal notranslate"><spanclass="pre">at_failed_traverse</span></code> hook lets us customize the error message if a PC tries to use the exit before the room is cleared.</p>
</section>
<sectionid="dungeon-branch-and-the-xy-grid">
<h2><spanclass="section-number">13.5. </span>Dungeon Branch and the xy grid<aclass="headerlink"href="#dungeon-branch-and-the-xy-grid"title="Permalink to this headline">¶</a></h2>
<p>The dungeon branch is responsible for the structure of one instance of the dungeon.</p>
<sectionid="grid-coordinates-and-exit-mappings">
<h3><spanclass="section-number">13.5.1. </span>Grid coordinates and exit mappings<aclass="headerlink"href="#grid-coordinates-and-exit-mappings"title="Permalink to this headline">¶</a></h3>
<p>Before we start, we need to establish some constants about our grid - the xy plane we will be placing our rooms on.</p>
<divclass="highlight-python notranslate"><divclass="highlight"><pre><span></span><spanclass="c1"># in evadventure/dungeon.py </span>
<p>In this tutorial we only allow NESW movement. You could easily add the NE, SE, SW, NW directions too if you wanted to. We make mappings for exit aliases (there is only one here, but there could be multiple per direction too). We also figure out the “reverse” directions so we’ll easily be able to create a ‘back exit’ later.</p>
<p>The <codeclass="docutils literal notranslate"><spanclass="pre">_EXIT_GRID_SHIFT</span></code> mapping indicates how the (x,y) coordinate shifts if you are moving in the specified direction. So if you stand in <codeclass="docutils literal notranslate"><spanclass="pre">(4,2)</span></code> and move <codeclass="docutils literal notranslate"><spanclass="pre">south</span></code>, you’ll end up in <codeclass="docutils literal notranslate"><spanclass="pre">(4,1)</span></code>.</p>
<h4><spanclass="section-number">13.5.1.1. </span>Base structure of the Dungeon branch script<aclass="headerlink"href="#base-structure-of-the-dungeon-branch-script"title="Permalink to this headline">¶</a></h4>
<p>We will base this component off an Evennia <aclass="reference internal"href="../../../Components/Scripts.html"><spanclass="doc std std-doc">Script</span></a> - these can be thought of game entities without a physical presence in the world. Scripts also have time-keeping properties.</p>
<spanclass="sd"> Create a new Dungeon room leading from the provided exit.</span>
<spanclass="sd"> Args:</span>
<spanclass="sd"> from_exit (Exit): The exit leading to this new room.</span>
<spanclass="sd">"""</span>
<spanclass="k">pass</span><spanclass="c1"># to be implemented</span>
</pre></div></td></tr></table></div>
</div>
<p>This sets up useful properties needed for the branch and sketches out some methods we will implement below.</p>
<p>The branch has several main responsibilities:</p>
<ulclass="simple">
<li><p>Track how many un-explored exits are available (making sure to not exceed the maximum allowed). As PCs traverse these exits we must update appropriately.</p></li>
<li><p>Create new rooms when an unexplored exit is traversed. This room can in turn have outgoing exits. We must also track these rooms and exits so we can delete them later when the branch is cleaned up.</p></li>
<li><p>The branch must also be able to delete itself, cleaning up all its resources and rooms.</p></li>
</ul>
<p>Since the <codeclass="docutils literal notranslate"><spanclass="pre">register_exit_traversed</span></code> and <codeclass="docutils literal notranslate"><spanclass="pre">create_out_exit</span></code> are straightforward, we implement them right away. The only extra thing about exit creation is that it must make sure to register the new exit as ‘un-visited’ so the branch can track it.</p>
</section>
</section>
<sectionid="a-note-about-the-room-generator">
<h3><spanclass="section-number">13.5.2. </span>A note about the room-generator<aclass="headerlink"href="#a-note-about-the-room-generator"title="Permalink to this headline">¶</a></h3>
<p>Of special note is the <codeclass="docutils literal notranslate"><spanclass="pre">room_generator</span></code> property of <codeclass="docutils literal notranslate"><spanclass="pre">EvAdventureDungeonBranch</span></code>. This will point to a function. We make this a plug-in since generating a room is something we will probably want to heavily customize as we create the game content - this is where we would generate our challenges, room descriptions etc.</p>
<p>It makes sense that the room generator must have a link to the dungeon branch, the current expected difficulty (depth in our case) and the xy coordinates to create the room at.</p>
<p>Here is an example of a very basic room generator that just maps depth to different room descriptions:</p>
<divclass="highlight-default notranslate"><divclass="highlight"><pre><span></span><spanclass="c1"># in evadventure/dungeon.py (could also be put with game content files)</span>
<spanclass="mi">1</span><spanclass="p">:</span><spanclass="p">(</span><spanclass="s2">"Water-logged passage"</span><spanclass="p">,</span><spanclass="s2">"This earth-walled passage is dripping of water."</span><spanclass="p">),</span>
<spanclass="mi">2</span><spanclass="p">:</span><spanclass="p">(</span><spanclass="s2">"Passage with roots"</span><spanclass="p">,</span><spanclass="s2">"Roots are pushing through the earth walls."</span><spanclass="p">),</span>
<spanclass="mi">3</span><spanclass="p">:</span><spanclass="p">(</span><spanclass="s2">"Hardened clay passage"</span><spanclass="p">,</span><spanclass="s2">"The walls of this passage is of hardened clay."</span><spanclass="p">),</span>
<spanclass="mi">4</span><spanclass="p">:</span><spanclass="p">(</span><spanclass="s2">"Clay with stones"</span><spanclass="p">,</span><spanclass="s2">"This passage has clay with pieces of stone embedded."</span><spanclass="p">),</span>
<spanclass="mi">5</span><spanclass="p">:</span><spanclass="p">(</span><spanclass="s2">"Stone passage"</span><spanclass="p">,</span><spanclass="s2">"Walls are crumbling stone, with roots passing through it."</span><spanclass="p">),</span>
<spanclass="mi">6</span><spanclass="p">:</span><spanclass="p">(</span><spanclass="s2">"Stone hallway"</span><spanclass="p">,</span><spanclass="s2">"Walls are cut from rough stone."</span><spanclass="p">),</span>
<spanclass="mi">7</span><spanclass="p">:</span><spanclass="p">(</span><spanclass="s2">"Stone rooms"</span><spanclass="p">,</span><spanclass="s2">"A stone room, built from crude and heavy blocks."</span><spanclass="p">),</span>
<spanclass="mi">8</span><spanclass="p">:</span><spanclass="p">(</span><spanclass="s2">"Granite hall"</span><spanclass="p">,</span><spanclass="s2">"The walls are of well-fitted granite blocks."</span><spanclass="p">),</span>
<spanclass="mi">9</span><spanclass="p">:</span><spanclass="p">(</span><spanclass="s2">"Marble passages"</span><spanclass="p">,</span><spanclass="s2">"The walls are blank and shiny marble."</span><spanclass="p">),</span>
<spanclass="mi">10</span><spanclass="p">:</span><spanclass="p">(</span><spanclass="s2">"Furnished rooms"</span><spanclass="p">,</span><spanclass="s2">"The marble walls have tapestries and furnishings."</span><spanclass="p">),</span>
<spanclass="p">}</span>
<spanclass="n">key</span><spanclass="p">,</span><spanclass="n">desc</span><spanclass="o">=</span><spanclass="n">name_depth_map</span><spanclass="o">.</span><spanclass="n">get</span><spanclass="p">(</span><spanclass="n">depth</span><spanclass="p">,</span><spanclass="p">(</span><spanclass="s2">"Dark rooms"</span><spanclass="p">,</span><spanclass="s2">"There is very dark here."</span><spanclass="p">))</span>
<p>There’s a <em>lot</em> of logic that can go into this function - depending on depth, coordinate or random chance we could generate all sorts of different rooms, and fill it with mobs, puzzles or what have you. Since we have access to the dungeon-branch object we could even change things in other rooms to make for really complex interactions (multi-room puzzles, anyone?).</p>
<p>This will come into play in <aclass="reference internal"href="../Part4/Beginner-Tutorial-Part4-Overview.html"><spanclass="doc std std-doc">Part 4 of this tutorial</span></a>, where we’ll make use of the tools we are creating here to actually build the game world.</p>
</section>
<sectionid="deleting-a-dungeon-branch">
<h3><spanclass="section-number">13.5.3. </span>Deleting a dungeon branch<aclass="headerlink"href="#deleting-a-dungeon-branch"title="Permalink to this headline">¶</a></h3>
<p>We will want to be able to clean up a branch. There are many reasons for this:</p>
<ulclass="simple">
<li><p>Once every PC has left the branch there is no way for them to return, so all that data is now just taking up space.</p></li>
<li><p>Branches are not meant to be permanent. So if players were to just stop exploring and sit around in the branch for a very long time, we should have a way to just force them back out.</p></li>
</ul>
<p>In order for properly cleaning out characters inside this dungeon, we make a few assumptions:</p>
<ulclass="simple">
<li><p>When we create the dungeon branch, we give its script a unique identifier (e.g. something involving the current time).</p></li>
<li><p>When we start the dungeon branch, we tag that character with the branch’s unique identifier.</p></li>
<li><p>Similarly, when we create rooms inside this branch, we tag them with the branch’s identifier.</p></li>
</ul>
<p>If have done that it will be easy to find all characters and rooms associated with the branch in order to do this cleanup operation.</p>
<divclass="highlight-python notranslate"><divclass="highlight"><pre><span></span><spanclass="c1"># in evadventure/dungeon.py </span>
<spanclass="s2">"|rAfter a long time of silence, the room suddenly rumbles and then collapses! "</span>
<spanclass="s2">"All turns dark ...|n</span><spanclass="se">\n\n</span><spanclass="s2">Then you realize you are back where you started."</span>
<p>The <codeclass="docutils literal notranslate"><spanclass="pre">evennia.search.search_object_by_tag</span></code> is an in-built Evennia utility for finding objects tagged with a specific tag+category combination.</p>
<olclass="simple">
<li><p>First we get the characters and move them safely to the start room, with a relevant message.</p></li>
<li><p>Then we get all the rooms in the branch and delete them (exits will be deleted automatically).</p></li>
<li><p>Finally we delete the branch itself.</p></li>
</ol>
</section>
<sectionid="creating-a-new-dungeon-room">
<h3><spanclass="section-number">13.5.4. </span>Creating a new dungeon room<aclass="headerlink"href="#creating-a-new-dungeon-room"title="Permalink to this headline">¶</a></h3>
<p>This is the meat of the Dungeon branch’s responsibilities. In this method we create the new room but also need to create exits leading back to where we came from as well as (randomly) generate exits to other parts of the dungeon.</p>
<li><p><strong>Line 17</strong>: We store the ‘last updated’ time as the current UTC timestamp. As we discussed in the deletion section just above we need to know if a branch has been ‘idle’ for a long time, and this helps track that.</p></li>
<li><p><strong>Line 20</strong>: The <codeclass="docutils literal notranslate"><spanclass="pre">from_exit</span></code> input is an Exit object (probably a <codeclass="docutils literal notranslate"><spanclass="pre">EvAdventureDungeonExit)</span></code> It is located in the ‘source’ location (where we start moving from). On the subsequent lines we figure out the coordinates of the source and where we’d end up by moving in the direction suggested</p></li>
<li><p><strong>Line 30</strong>: Here we call the <codeclass="docutils literal notranslate"><spanclass="pre">room_generator</span></code> plugin function we exemplified above to get the new room.</p></li>
<li><p><strong>Line 34</strong>: We always create a back-exit the way we came. This <em>overrides</em> the default dungeon exit lock with <codeclass="docutils literal notranslate"><spanclass="pre">"traverse:true()"</span></code>, meaning the PCs will always be able to go back the way they came.</p></li>
<li><p><strong>Line 44</strong>: We could leave the <codeclass="docutils literal notranslate"><spanclass="pre">destination</span></code> field empty, but Evennia assumes exits have a <codeclass="docutils literal notranslate"><spanclass="pre">destination</span></code> field set when it displays things in the room etc. So to avoid having to change how rooms display things, this value should be set to <em>something</em>. Since we don’t want to create the actual destination yet we instead instead point the <codeclass="docutils literal notranslate"><spanclass="pre">destination</span></code> back to the current room. That is - if you could pass through this exit you’d end up in the same place. We’ll use this below to identify non-explored exits.</p></li>
<li><p><strong>Line 55</strong>: We only create new exits our ‘budget’ of unexplored exits allows it.</p></li>
<li><p><strong>Line 64</strong>: On the line above we create a new list of all possible exits-directions the room can have (excluding the must-have back-exit). Here we shuffle this list in a random order.</p></li>
<li><p><strong>Line 69</strong>: In this loop we pop off the first element of the shuffled list (so this is a random direction). On the following lines we check so that this direction is not pointing to an already existing dungeon room, nor back to the start room. If all is good we call our exit-creation method on <strong>Line 74</strong>.</p></li>
</ul>
<p>In the end the outcome is a new room with at least one back-exit and 0 or more unexplored exits.</p>
</section>
</section>
<sectionid="back-to-the-dungeon-exit-class">
<h2><spanclass="section-number">13.6. </span>Back to the dungeon exit class<aclass="headerlink"href="#back-to-the-dungeon-exit-class"title="Permalink to this headline">¶</a></h2>
<p>Now that we have the tools, we can go back to the <codeclass="docutils literal notranslate"><spanclass="pre">EvAdventureDungeonExit</span></code> class to implement that <codeclass="docutils literal notranslate"><spanclass="pre">at_traverse</span></code> method we skipped before.</p>
<divclass="highlight-python notranslate"><divclass="highlight"><pre><span></span><spanclass="c1"># in evadventure/dungeon.py </span>
<p>We get the <codeclass="docutils literal notranslate"><spanclass="pre">EvAdventureDungeonBranch</span></code> instance and check out if this current exit is pointing back to the current room. If you read line 44 in the previous section, you’ll notice that this is the way to find if this exit is previously non-explored!</p>
<p>If so, we call the dungeon branche’s <codeclass="docutils literal notranslate"><spanclass="pre">new_room</span></code> to generate a new room and change this exit’s <codeclass="docutils literal notranslate"><spanclass="pre">destination</span></code> to it. We also make sure to call <codeclass="docutils literal notranslate"><spanclass="pre">.register_exit_traversed</span></code> to show that is exit is now ‘explored’.</p>
<p>We must also call the parent class’<codeclass="docutils literal notranslate"><spanclass="pre">at_traverse</span></code> using <codeclass="docutils literal notranslate"><spanclass="pre">super()</span></code> since that is what is actually moving the PC to the newly created location.</p>
</section>
<sectionid="starting-room-exits">
<h2><spanclass="section-number">13.7. </span>Starting room exits<aclass="headerlink"href="#starting-room-exits"title="Permalink to this headline">¶</a></h2>
<p>We now have all the pieces for actually running a procedural dungeon branch once it’s created. What’s missing is the start room from which all branches originate.</p>
<p>As described in the design, the room’s exits will spawn new branches, but there should also be a time period while PCs will all end up in the same branch. So we need a special type of exit for those exits leading out of the starting room.</p>
<p>This exit has everything it needs for creating a new dungeon branch.</p>
<ulclass="simple">
<li><p><strong>Line 12</strong>: Disconnects the exit from whatever it was connected to and links it back to the current room (a looping, worthless exit).</p></li>
<li><p><strong>Line 19</strong>: The <codeclass="docutils literal notranslate"><spanclass="pre">at_traverse</span></code> is called when someone moves through this exit. We detect that special condition above (destination equal to current location) to determine that this exit is currently leading nowhere and we should create a new branch.</p></li>
<li><p><strong>Line 22</strong>: We create a new <codeclass="docutils literal notranslate"><spanclass="pre">EvAdventureDungeonBranch</span></code>and make sure to give it a unique <codeclass="docutils literal notranslate"><spanclass="pre">key</span></code> based on the current time. We also make sure to set its starting Attributes.</p></li>
<li><p><strong>Line 32</strong>: When the player traverses this exit, the character gets tagged with the appropriate tag for this dungeon branch. This can be used by the deletion mechanism later.</p></li>
</ul>
</section>
<sectionid="utility-scripts">
<h2><spanclass="section-number">13.8. </span>Utility scripts<aclass="headerlink"href="#utility-scripts"title="Permalink to this headline">¶</a></h2>
<p>Before we can create the starting room, we need two last utilities:</p>
<ulclass="simple">
<li><p>A timer for regularly resetting exits out of the starting room (so they create new branches).</p></li>
<li><p>A repeating task for cleaning out old/idle dungeon branches.</p></li>
</ul>
<p>Both of these scripts are expected to be created ‘on’ the start room, so <codeclass="docutils literal notranslate"><spanclass="pre">self.obj</span></code> will be the start room.</p>
<divclass="highlight-python notranslate"><divclass="highlight"><pre><span></span><spanclass="c1"># in evadventure/dungeon.py</span>
<p>This script checks all branches and sees how long it was since they were last updated (that is, a new room created in them). If it’s been too long, the branch will be deleted (which will dump all players back in the start room).</p>
</section>
<sectionid="starting-room">
<h2><spanclass="section-number">13.9. </span>Starting room<aclass="headerlink"href="#starting-room"title="Permalink to this headline">¶</a></h2>
<p>Finally, we need a class for the starting room. This room will need to be manually created, after which the branches should create themselves automatically.</p>
<divclass="highlight-python notranslate"><divclass="highlight"><pre><span></span><spanclass="c1"># in evadventure/dungeon.py</span>
<spanclass="n">branch_check_time</span><spanclass="o">=</span><spanclass="mi">60</span><spanclass="o">*</span><spanclass="mi">60</span><spanclass="c1"># one hour</span>
<p>All that is left for this room to do is to set up the scripts we created and make sure to clear out the branch tags of any object returning from a branch into this room. All other work is handled by the exits and the dungeon-branches.</p>
</section>
<sectionid="testing">
<h2><spanclass="section-number">13.10. </span>Testing<aclass="headerlink"href="#testing"title="Permalink to this headline">¶</a></h2>
<asideclass="sidebar">
<p>Examples of unit testing files are found at <codeclass="docutils literal notranslate"><spanclass="pre">evennia/contrib/tutorials/</span></code> in <aclass="reference internal"href="../../../api/evennia.contrib.tutorials.evadventure.tests.test_dungeon.html#evennia-contrib-tutorials-evadventure-tests-test-dungeon"><spanclass="std std-ref">evadventure/tests/test_dungeon.py</span></a>.</p>
<p>You should now be able to head out one of the exits and start exploring the dungeon! This is particularly useful once everything works a</p>
<p>To unit test, you create a start room and exits in code, and then emulate a character moving through the exits, making sure the results are as expected. We leave this an exercise to the reader.</p>
</section>
<sectionid="conclusions">
<h2><spanclass="section-number">13.11. </span>Conclusions<aclass="headerlink"href="#conclusions"title="Permalink to this headline">¶</a></h2>
<p>This is only skimming the surface of the possibilities of procedural generation, but with relatively easy means one can create an infinitely growing dungeon for players to explore.</p>
<p>It’s also worth that this only touches on how to procedurally generate the dungeon structure. It doesn’t yet have much <em>content</em> to fill the dungeon with. We will get back to that in <aclass="reference internal"href="../Part4/Beginner-Tutorial-Part4-Overview.html"><spanclass="doc std std-doc">Part 4</span></a>, where we’ll make use of the code we’ve created to create game content.</p>