McRogueFace/roguelike_tutorial/rogueliketutorials.com/Part 3 - Generating a dunge...

1092 lines
69 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en" style="color-scheme: dark;"><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>
Part 3 - Generating a dungeon · Roguelike Tutorials
</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<meta name="description" content="Note: This part of the tutorial relies on TCOD version 11.14 or higher. You might need to upgrade the library (and your requirements.txt file, if youre using one).
Remember how we created a wall in the last part? We wont need that anymore. Additionally, our dungeon generator will start by filling the entire map with “wall” tiles and “carving” out rooms, so we can modify our GameMap class to fill in walls instead of floors.">
<meta name="keywords" content="">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Part 3 - Generating a dungeon">
<meta name="twitter:description" content="Note: This part of the tutorial relies on TCOD version 11.14 or higher. You might need to upgrade the library (and your requirements.txt file, if youre using one).
Remember how we created a wall in the last part? We wont need that anymore. Additionally, our dungeon generator will start by filling the entire map with “wall” tiles and “carving” out rooms, so we can modify our GameMap class to fill in walls instead of floors.">
<meta property="og:title" content="Part 3 - Generating a dungeon">
<meta property="og:description" content="Note: This part of the tutorial relies on TCOD version 11.14 or higher. You might need to upgrade the library (and your requirements.txt file, if youre using one).
Remember how we created a wall in the last part? We wont need that anymore. Additionally, our dungeon generator will start by filling the entire map with “wall” tiles and “carving” out rooms, so we can modify our GameMap class to fill in walls instead of floors.">
<meta property="og:type" content="article">
<meta property="og:url" content="https://rogueliketutorials.com/tutorials/tcod/v2/part-3/"><meta property="article:section" content="tutorials">
<meta property="article:published_time" content="2020-06-23T00:00:00+00:00">
<meta property="article:modified_time" content="2020-06-23T00:00:00+00:00">
<link rel="canonical" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-3/">
<link rel="preload" href="https://rogueliketutorials.com/fonts/forkawesome-webfont.woff2?v=1.2.0" as="font" type="font/woff2" crossorigin="">
<link rel="stylesheet" href="Part%203%20-%20Generating%20a%20dungeon%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.c4d7e93a158eda5a65b3df343745d2092a0a1e2170feeec909.css" integrity="sha256-xNfpOhWO2lpls980N0XSCSoKHiFw/u7JCbiolEOQPGo=" crossorigin="anonymous" media="screen">
<link rel="stylesheet" href="Part%203%20-%20Generating%20a%20dungeon%20%C2%B7%20Roguelike%20Tutorials_files/coder-dark.min.78b5fe3864945faf5207fb8fe3ab2320d49c3365def0e.css" integrity="sha256-eLX+OGSUX69SB/uP46sjINScM2Xe8OiKwd8N2txUoDw=" crossorigin="anonymous" media="screen">
<link rel="stylesheet" href="Part%203%20-%20Generating%20a%20dungeon%20%C2%B7%20Roguelike%20Tutorials_files/style.min.9d3eb202952dddb888856ff12c83bc88de866c596286bfb4c1.css" integrity="sha256-nT6yApUt3biIhW/xLIO8iN6GbFlihr+0wfjmvq2a42Y=" crossorigin="anonymous" media="screen">
<link rel="icon" type="image/png" href="https://rogueliketutorials.com/images/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="https://rogueliketutorials.com/images/favicon-16x16.png" sizes="16x16">
<link rel="apple-touch-icon" href="https://rogueliketutorials.com/images/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="180x180" href="https://rogueliketutorials.com/images/apple-touch-icon.png">
<link rel="manifest" href="https://rogueliketutorials.com/site.webmanifest">
<link rel="mask-icon" href="https://rogueliketutorials.com/images/safari-pinned-tab.svg" color="#5bbad5">
<meta name="generator" content="Hugo 0.110.0">
<style>:is([id*='google_ads_iframe'],[id*='taboola-'],.taboolaHeight,.taboola-placeholder,#top-ad,#credential_picker_container,#credentials-picker-container,#credential_picker_iframe,[id*='google-one-tap-iframe'],#google-one-tap-popup-container,.google-one-tap__module,.google-one-tap-modal-div,#amp_floatingAdDiv,#ez-content-blocker-container) {display:none!important;min-height:0!important;height:0!important;}</style></head>
<body class="colorscheme-dark vsc-initialized">
<div class="float-container">
<a id="dark-mode-toggle" class="colorscheme-toggle">
<i class="fa fa-adjust fa-fw" aria-hidden="true"></i>
</a>
</div>
<main class="wrapper">
<nav class="navigation">
<section class="container">
<a class="navigation-title" href="https://rogueliketutorials.com/">
Roguelike Tutorials
</a>
<input type="checkbox" id="menu-toggle">
<label class="menu-button float-right" for="menu-toggle">
<i class="fa fa-bars fa-fw" aria-hidden="true"></i>
</label>
<ul class="navigation-list">
<li class="navigation-item">
<a class="navigation-link" href="https://rogueliketutorials.com/">Home</a>
</li>
<li class="navigation-item">
<a class="navigation-link" href="https://rogueliketutorials.com/tutorials/tcod/v2/">TCOD Tutorial (2020)</a>
</li>
<li class="navigation-item">
<a class="navigation-link" href="https://rogueliketutorials.com/tutorials/tcod/2019/">TCOD Tutorial (2019)</a>
</li>
<li class="navigation-item">
<a class="navigation-link" href="https://rogueliketutorials.com/about/">About</a>
</li>
</ul>
</section>
</nav>
<div class="content">
<section class="container page">
<article>
<header>
<h1 class="title">
<a class="title-link" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-3/">
Part 3 - Generating a dungeon
</a>
</h1>
</header>
<p><em>Note: This part of the tutorial relies on TCOD version 11.14
or higher. You might need to upgrade the library (and your
requirements.txt file, if youre using one).</em></p>
<p>Remember how we created a wall in the last part? We wont need that
anymore. Additionally, our dungeon generator will start by filling the
entire map with “wall” tiles and “carving” out rooms, so we can modify
our <code>GameMap</code> class to fill in walls instead of floors.</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class GameMap:
</span></span><span style="display:flex;"><span> def __init__(self, width: int, height: int):
</span></span><span style="display:flex;"><span> self.width, self.height = width, height
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.tiles = np.full((width, height), fill_value=tile_types.floor, order="F")
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- self.tiles[30:33, 22] = tile_types.wall
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class GameMap:
def __init__(self, width: int, height: int):
self.width, self.height = width, height
<span class="crossed-out-text">self.tiles = np.full((width, height), fill_value=tile_types.floor, order="F")</span>
<span class="new-text">self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")</span>
<span class="crossed-out-text">self.tiles[30:33, 22] = tile_types.wall</span>
...</pre>
</div>
</div>
<p>Now, on to our dungeon generator.</p>
<p>The original version of this tutorial put all of the dungeon generation in the <code>GameMap</code>
class. In fact, this was my plan for this tutorial as well. But, as
HexDecimal (author of the TCOD library) pointed out in a pull request,
thats not very extensible. It puts a lot of code in <code>GameMap</code>
where it doesnt necessarily belong, and the class will grow to huge
proportions if you ever decide to add an alternate dungeon generator.</p>
<p>The better approach is to put our new code in a separate file, and utilize <code>GameMap</code> there. Lets create a new file, called <code>procgen.py</code>, which will house our procedural generator.</p>
<p>Lets start by creating a class which well use to create our rooms. We can call it <code>RectangularRoom</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Tuple
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">RectangularRoom</span>:
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> __init__(self, x: int, y: int, width: int, height: int):
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>x1 <span style="color:#f92672">=</span> x
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>y1 <span style="color:#f92672">=</span> y
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>x2 <span style="color:#f92672">=</span> x <span style="color:#f92672">+</span> width
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>y2 <span style="color:#f92672">=</span> y <span style="color:#f92672">+</span> height
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@property</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">center</span>(self) <span style="color:#f92672">-&gt;</span> Tuple[int, int]:
</span></span><span style="display:flex;"><span> center_x <span style="color:#f92672">=</span> int((self<span style="color:#f92672">.</span>x1 <span style="color:#f92672">+</span> self<span style="color:#f92672">.</span>x2) <span style="color:#f92672">/</span> <span style="color:#ae81ff">2</span>)
</span></span><span style="display:flex;"><span> center_y <span style="color:#f92672">=</span> int((self<span style="color:#f92672">.</span>y1 <span style="color:#f92672">+</span> self<span style="color:#f92672">.</span>y2) <span style="color:#f92672">/</span> <span style="color:#ae81ff">2</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> center_x, center_y
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@property</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">inner</span>(self) <span style="color:#f92672">-&gt;</span> Tuple[slice, slice]:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Return the inner area of this room as a 2D array index."""</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> slice(self<span style="color:#f92672">.</span>x1 <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>, self<span style="color:#f92672">.</span>x2), slice(self<span style="color:#f92672">.</span>y1 <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>, self<span style="color:#f92672">.</span>y2)
</span></span></code></pre></div><p>The <code>__init__</code> function
takes the x and y coordinates of the top left corner, and computes the
bottom right corner based on the w and h parameters (width and height).</p>
<p><code>center</code> is a “property”, which essentially acts like a read-only variable for our <code>RectangularRoom</code> class. It describes the “x” and “y” coordinates of the center of a room. It will be useful later on.</p>
<p>The <code>inner</code> property returns two “slices”, which represent
the inner portion of our room. This is the part that well be “digging
out” for our room in our dungeon generator. It gives us an easy way to
get the area to carve out, which well demonstrate soon.</p>
<p>Well be adding more to this class shortly, but to get us started, thats all we need.</p>
<p>Whats with the + 1 on <code>self.x1</code> and <code>self.y1</code>?
Think about what were saying when we tell our program that we want a
room at coordinates (1, 1) that goes to (6, 6). You might assume that
would carve out a room like this one (remember that lists are 0-indexed,
so (0, 0) is a wall in this case):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span> 0 1 2 3 4 5 6 7
</span></span><span style="display:flex;"><span>0 # # # # # # # #
</span></span><span style="display:flex;"><span>1 # . . . . . . #
</span></span><span style="display:flex;"><span>2 # . . . . . . #
</span></span><span style="display:flex;"><span>3 # . . . . . . #
</span></span><span style="display:flex;"><span>4 # . . . . . . #
</span></span><span style="display:flex;"><span>5 # . . . . . . #
</span></span><span style="display:flex;"><span>6 # . . . . . . #
</span></span><span style="display:flex;"><span>7 # # # # # # # #
</span></span></code></pre></div><p>Thats all fine and good, but what
happens if we put a room right next to it? Lets say this room starts at
(7, 1) and goes to (9, 6)</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span> 0 1 2 3 4 5 6 7 8 9 10
</span></span><span style="display:flex;"><span>0 # # # # # # # # # # #
</span></span><span style="display:flex;"><span>1 # . . . . . . . . . #
</span></span><span style="display:flex;"><span>2 # . . . . . . . . . #
</span></span><span style="display:flex;"><span>3 # . . . . . . . . . #
</span></span><span style="display:flex;"><span>4 # . . . . . . . . . #
</span></span><span style="display:flex;"><span>5 # . . . . . . . . . #
</span></span><span style="display:flex;"><span>6 # . . . . . . . . . #
</span></span><span style="display:flex;"><span>7 # # # # # # # # # # #
</span></span></code></pre></div><p>Theres no wall separating the two!
That means that if two rooms are one right next to the other, then there
wont be a wall between them! So long story short, our function needs
to take the walls into account when digging out a room. So if we have a
rectangle with coordinates x1 = 1, x2 = 6, y1 = 1, and y2 = 6, then the
room should actually look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span> 0 1 2 3 4 5 6 7
</span></span><span style="display:flex;"><span>0 # # # # # # # #
</span></span><span style="display:flex;"><span>1 # # # # # # # #
</span></span><span style="display:flex;"><span>2 # # . . . . # #
</span></span><span style="display:flex;"><span>3 # # . . . . # #
</span></span><span style="display:flex;"><span>4 # # . . . . # #
</span></span><span style="display:flex;"><span>5 # # . . . . # #
</span></span><span style="display:flex;"><span>6 # # # # # # # #
</span></span><span style="display:flex;"><span>7 # # # # # # # #
</span></span></code></pre></div><p>This ensures that well always have
at least a one tile wide wall between our rooms, unless we choose to
create overlapping rooms. In order to accomplish this, we add + 1 to x1
and y1.</p>
<p>Before we dive into a truly procedurally generated dungeon, lets
begin with a simple map that consists of two rooms, connected by a
tunnel. We can create a new function to create our dungeon, intuitively
named <code>generate_dungeon</code>, which will return a <code>GameMap</code>. As arguments, it will take the needed width and the height to create the <code>GameMap</code>, and it will utilize the <code>RectangularRoom</code> class to create the needed rooms. Heres what that looks like:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from typing import Tuple
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from game_map import GameMap
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import tile_types
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class RectangularRoom:
</span></span><span style="display:flex;"><span> def __init__(self, x: int, y: int, width: int, height: int):
</span></span><span style="display:flex;"><span> self.x1 = x
</span></span><span style="display:flex;"><span> self.y1 = y
</span></span><span style="display:flex;"><span> self.x2 = x + width
</span></span><span style="display:flex;"><span> self.y2 = y + height
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> @property
</span></span><span style="display:flex;"><span> def inner(self) -&gt; Tuple[slice, slice]:
</span></span><span style="display:flex;"><span> """Return the inner area of this room as a 2D array index."""
</span></span><span style="display:flex;"><span> return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+def generate_dungeon(map_width, map_height) -&gt; GameMap:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dungeon = GameMap(map_width, map_height)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ room_1 = RectangularRoom(x=20, y=15, width=10, height=15)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ room_2 = RectangularRoom(x=35, y=15, width=10, height=15)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dungeon.tiles[room_1.inner] = tile_types.floor
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dungeon.tiles[room_2.inner] = tile_types.floor
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return dungeon
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from typing import Tuple
<span class="new-text">from game_map import GameMap
import tile_types</span>
class RectangularRoom:
def __init__(self, x: int, y: int, width: int, height: int):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def inner(self) -&gt; Tuple[slice, slice]:
"""Return the inner area of this room as a 2D array index."""
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
<span class="new-text">def generate_dungeon(map_width, map_height) -&gt; GameMap:
dungeon = GameMap(map_width, map_height)
room_1 = RectangularRoom(x=20, y=15, width=10, height=15)
room_2 = RectangularRoom(x=35, y=15, width=10, height=15)
dungeon.tiles[room_1.inner] = tile_types.floor
dungeon.tiles[room_2.inner] = tile_types.floor
return dungeon</span></pre>
</div>
</div>
<p>Now we can modify <code>main.py</code> to utilize our now <code>generate_dungeon</code> function.</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>#!/usr/bin/env python3
</span></span><span style="display:flex;"><span>import tcod
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>from engine import Engine
</span></span><span style="display:flex;"><span>from entity import Entity
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from game_map import GameMap
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>from input_handlers import EventHandler
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from procgen import generate_dungeon
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>def main() -&gt; None:
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> entities = {npc, player}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- game_map = GameMap(map_width, map_height)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ game_map = generate_dungeon(map_width, map_height)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> engine = Engine(entities=entities, event_handler=event_handler, game_map=game_map, player=player)
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>#!/usr/bin/env python3
import tcod
from engine import Engine
from entity import Entity
<span class="crossed-out-text">from game_map import GameMap</span>
from input_handlers import EventHandler
<span class="new-text">from procgen import generate_dungeon</span>
def main() -&gt; None:
...
entities = {npc, player}
<span class="crossed-out-text">game_map = GameMap(map_width, map_height)</span>
<span class="new-text">game_map = generate_dungeon(map_width, map_height)</span>
engine = Engine(entities=entities, event_handler=event_handler, game_map=game_map, player=player)
...</pre>
</div>
</div>
<p>Now is a good time to run your code and make sure everything works as
expected. The changes weve made puts two sample rooms on the map, with
our player in one of them (our poor NPC is stuck in a wall though).</p>
<p><img src="Part%203%20-%20Generating%20a%20dungeon%20%C2%B7%20Roguelike%20Tutorials_files/part-3-two-rooms.png" alt="Part 3 - Two Rooms"></p>
<p>Im sure youve noticed already, but the rooms are not connected.
Whats the use of creating a dungeon if were stuck in one room? Not to
worry, lets write some code to generate tunnels from one room to
another. Add the following function to <code>procgen.py</code>:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#a6e22e">+import random
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span><span style="color:#f92672">-from typing import Tuple
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Iterator, Tuple
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+import tcod
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>from game_map import GameMap
</span></span><span style="display:flex;"><span>import tile_types
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span> return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+def tunnel_between(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ start: Tuple[int, int], end: Tuple[int, int]
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+) -&gt; Iterator[Tuple[int, int]]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Return an L-shaped tunnel between these two points."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x1, y1 = start
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x2, y2 = end
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if random.random() &lt; 0.5: # 50% chance.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Move horizontally, then vertically.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ corner_x, corner_y = x2, y1
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ else:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Move vertically, then horizontally.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ corner_x, corner_y = x1, y2
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Generate the coordinates for this tunnel.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for x, y in tcod.los.bresenham((x1, y1), (corner_x, corner_y)).tolist():
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ yield x, y
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for x, y in tcod.los.bresenham((corner_x, corner_y), (x2, y2)).tolist():
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ yield x, y
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>def generate_dungeon(map_width, map_height) -&gt; GameMap:
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre><span class="new-text">import random</span>
<span class="crossed-out-text">from typing import Tuple</span>
<span class="new-text">from typing import Iterator, Tuple</span>
<span class="new-text">import tcod</span>
from game_map import GameMap
import tile_types
...
...
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
<span class="new-text">def tunnel_between(
start: Tuple[int, int], end: Tuple[int, int]
) -&gt; Iterator[Tuple[int, int]]:
"""Return an L-shaped tunnel between these two points."""
x1, y1 = start
x2, y2 = end
if random.random() &lt; 0.5: # 50% chance.
# Move horizontally, then vertically.
corner_x, corner_y = x2, y1
else:
# Move vertically, then horizontally.
corner_x, corner_y = x1, y2
# Generate the coordinates for this tunnel.
for x, y in tcod.los.bresenham((x1, y1), (corner_x, corner_y)).tolist():
yield x, y
for x, y in tcod.los.bresenham((corner_x, corner_y), (x2, y2)).tolist():
yield x, y</span>
def generate_dungeon(map_width, map_height) -&gt; GameMap:
...</pre>
</div>
</div>
<p>Lets dive into this method.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">tunnel_between</span>(
</span></span><span style="display:flex;"><span> start: Tuple[int, int], end: Tuple[int, int]
</span></span><span style="display:flex;"><span>) <span style="color:#f92672">-&gt;</span> Iterator[Tuple[int, int]]:
</span></span></code></pre></div><p>The function takes two arguments,
both Tuples consisting of two integers. It should return an Iterator of a
Tuple of two ints. All the Tuples will be “x” and “y” coordinates on
the map.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#e6db74">"""Return an L-shaped tunnel between these two points."""</span>
</span></span><span style="display:flex;"><span> x1, y1 <span style="color:#f92672">=</span> start
</span></span><span style="display:flex;"><span> x2, y2 <span style="color:#f92672">=</span> end
</span></span></code></pre></div><p>We grab the coordinates out of the Tuples. Simple enough.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> random<span style="color:#f92672">.</span>random() <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">0.5</span>: <span style="color:#75715e"># 50% chance.</span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Move horizontally, then vertically.</span>
</span></span><span style="display:flex;"><span> corner_x, corner_y <span style="color:#f92672">=</span> x2, y1
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Move vertically, then horizontally.</span>
</span></span><span style="display:flex;"><span> corner_x, corner_y <span style="color:#f92672">=</span> x1, y2
</span></span></code></pre></div><p>Were randomly picking between two
options: Moving horizontally, then vertically, or the opposite. Based on
whats chosen, well set the <code>corner_x</code> and <code>corner_y</code> values to different points.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#75715e"># Generate the coordinates for this tunnel.</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> x, y <span style="color:#f92672">in</span> tcod<span style="color:#f92672">.</span>los<span style="color:#f92672">.</span>bresenham((x1, y1), (corner_x, corner_y))<span style="color:#f92672">.</span>tolist():
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">yield</span> x, y
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> x, y <span style="color:#f92672">in</span> tcod<span style="color:#f92672">.</span>los<span style="color:#f92672">.</span>bresenham((corner_x, corner_y), (x2, y2))<span style="color:#f92672">.</span>tolist():
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">yield</span> x, y
</span></span></code></pre></div><p>This part is where the “magic” happens.</p>
<p>tcod includes a function in its line-of-sight module to draw <a href="https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm">Bresenham lines</a>.
While were not working with line-of-sight in this case, the function
still proves useful to get a line from one point to another. In this
case, we get one line, then another, to create an “L” shaped tunnel. <code>.tolist()</code> converts the points in the line into, as you might have already guessed, a list.</p>
<p>Whats with the <code>yield</code> lines though? <a href="https://docs.python.org/3.5/reference/expressions.html#yield-expressions">Yield expressions</a>
are an interesting part of Python, which allows you to return a
“generator”. Essentially, rather than returning the values and exiting
the function altogether, we return the values, but keep the local state.
This allows the function to pick up where it left off when called
again, instead of starting from the beginning, as most functions do.</p>
<p>Why is this helpful? In the next section, were going to iterate the <code>x</code> and <code>y</code> values that we receive from the <code>tunnel_between</code> function to dig out our tunnel.</p>
<p>Lets put this code to use by drawing a tunnel between our two rooms.</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span> dungeon.tiles[room_2.inner] = tile_types.floor
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for x, y in tunnel_between(room_2.center, room_1.center):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dungeon.tiles[x, y] = tile_types.floor
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> return dungeon
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre> ...
dungeon.tiles[room_2.inner] = tile_types.floor
<span class="new-text">for x, y in tunnel_between(room_2.center, room_1.center):
dungeon.tiles[x, y] = tile_types.floor</span>
return dungeon</pre>
</div>
</div>
<p>Run the project, and youll see a horizontal tunnel that connects the two rooms. Its starting to come together!</p>
<p><img src="Part%203%20-%20Generating%20a%20dungeon%20%C2%B7%20Roguelike%20Tutorials_files/part-3-two-rooms-connected.png" alt="Part 3 - Two Rooms"></p>
<p>Now that weve demonstrated to ourselves that our room and tunnel
functions work as intended, its time to move on to an actual dungeon
generation algorithm. Ours will be fairly simple; well place rooms one
at a time, making sure they dont overlap, and connect them with
tunnels.</p>
<p>Well want a method that tells us if our room is intersecting with another room. Enter the following into the <code>RectangularRoom</code> class:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span><span style="color:#a6e22e">+from __future__ import annotations
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>import random
</span></span><span style="display:flex;"><span>from typing import Iterator, Tuple
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>import tcod
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>from game_map import GameMap
</span></span><span style="display:flex;"><span>import tile_types
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class RectangularRoom:
</span></span><span style="display:flex;"><span> def __init__(self, x: int, y: int, width: int, height: int):
</span></span><span style="display:flex;"><span> self.x1 = x
</span></span><span style="display:flex;"><span> self.y1 = y
</span></span><span style="display:flex;"><span> self.x2 = x + width
</span></span><span style="display:flex;"><span> self.y2 = y + height
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> @property
</span></span><span style="display:flex;"><span> def center(self) -&gt; Tuple[int, int]:
</span></span><span style="display:flex;"><span> center_x = int((self.x1 + self.x2) / 2)
</span></span><span style="display:flex;"><span> center_y = int((self.y1 + self.y2) / 2)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> return center_x, center_y
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> @property
</span></span><span style="display:flex;"><span> def inner(self) -&gt; Tuple[slice, slice]:
</span></span><span style="display:flex;"><span> """Return the inner area of this room as a 2D array index."""
</span></span><span style="display:flex;"><span> return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def intersects(self, other: RectangularRoom) -&gt; bool:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Return True if this room overlaps with another RectangularRoom."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return (
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.x1 &lt;= other.x2
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ and self.x2 &gt;= other.x1
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ and self.y1 &lt;= other.y2
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ and self.y2 &gt;= other.y1
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>def tunnel_between(
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre><span class="new-text">from __future__ import annotations</span>
import random
from typing import Iterator, Tuple
import tcod
from game_map import GameMap
import tile_types
class RectangularRoom:
def __init__(self, x: int, y: int, width: int, height: int):
self.x1 = x
self.y1 = y
self.x2 = x + width
self.y2 = y + height
@property
def center(self) -&gt; Tuple[int, int]:
center_x = int((self.x1 + self.x2) / 2)
center_y = int((self.y1 + self.y2) / 2)
return center_x, center_y
@property
def inner(self) -&gt; Tuple[slice, slice]:
"""Return the inner area of this room as a 2D array index."""
return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2)
<span class="new-text">def intersects(self, other: RectangularRoom) -&gt; bool:
"""Return True if this room overlaps with another RectangularRoom."""
return (
self.x1 &lt;= other.x2
and self.x2 &gt;= other.x1
and self.y1 &lt;= other.y2
and self.y2 &gt;= other.y1
)</span>
def tunnel_between(
...</pre>
</div>
</div>
<p><code>intersects</code> checks if the room and another room (<code>other</code> in the arguments) intersect or not. It returns <code>True</code> if the do, <code>False</code> if they dont. Well use this to determine if two rooms are overlapping or not.</p>
<p>Were going to need a few variables to set the maximum and minimum
size of the rooms, along with the maximum number of rooms one floor can
have. Add the following to <code>main.py</code>:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span> map_height = 45
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ room_max_size = 10
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ room_min_size = 6
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_rooms = 30
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> tileset = tcod.tileset.load_tilesheet(
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre> ...
map_height = 45
<span class="new-text">room_max_size = 10
room_min_size = 6
max_rooms = 30</span>
tileset = tcod.tileset.load_tilesheet(
...</pre>
</div>
</div>
<p>At long last, its time to modify <code>generate_dungeon</code> to, well, generate our dungeon! You can completely remove our old implementation and replace it with the following:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>from __future__ import annotations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>import random
</span></span><span style="display:flex;"><span><span style="color:#f92672">-from typing import Iterator, Tuple
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+from typing import Iterator, List, Tuple, TYPE_CHECKING
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>from game_map import GameMap
</span></span><span style="display:flex;"><span>import tile_types
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+if TYPE_CHECKING:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ from entity import Entity
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">-def generate_dungeon(map_width, map_height) -&gt; GameMap:
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- dungeon = GameMap(map_width, map_height)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- room_1 = RectangularRoom(x=20, y=15, width=10, height=15)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- room_2 = RectangularRoom(x=35, y=15, width=10, height=15)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- dungeon.tiles[room_1.inner] = tile_types.floor
</span></span></span><span style="display:flex;"><span><span style="color:#f92672">- dungeon.tiles[room_2.inner] = tile_types.floor
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- create_horizontal_tunnel(dungeon, 25, 40, 23)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- return dungeon
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+def generate_dungeon(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_rooms: int,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ room_min_size: int,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ room_max_size: int,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ map_width: int,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ map_height: int,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player: Entity,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+) -&gt; GameMap:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Generate a new dungeon map."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dungeon = GameMap(map_width, map_height)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ rooms: List[RectangularRoom] = []
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for r in range(max_rooms):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ room_width = random.randint(room_min_size, room_max_size)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ room_height = random.randint(room_min_size, room_max_size)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ x = random.randint(0, dungeon.width - room_width - 1)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ y = random.randint(0, dungeon.height - room_height - 1)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # "RectangularRoom" class makes rectangles easier to work with
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ new_room = RectangularRoom(x, y, room_width, room_height)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Run through the other rooms and see if they intersect with this one.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if any(new_room.intersects(other_room) for other_room in rooms):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ continue # This room intersects, so go to the next attempt.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # If there are no intersections then the room is valid.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Dig out this rooms inner area.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dungeon.tiles[new_room.inner] = tile_types.floor
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if len(rooms) == 0:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # The first room, where the player starts.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player.x, player.y = new_room.center
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ else: # All rooms after the first.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Dig out a tunnel between this room and the previous one.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ for x, y in tunnel_between(rooms[-1].center, new_room.center):
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dungeon.tiles[x, y] = tile_types.floor
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # Finally, append the new room to the list.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ rooms.append(new_room)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ return dungeon
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>from __future__ import annotations
import random
<span class="crossed-out-text">from typing import Iterator, Tuple</span>
<span class="new-text">from typing import Iterator, List, Tuple, TYPE_CHECKING</span>
from game_map import GameMap
import tile_types
<span class="new-text">if TYPE_CHECKING:
from entity import Entity</span>
...
<span class="crossed-out-text">def generate_dungeon(map_width, map_height) -&gt; GameMap:</span>
<span class="crossed-out-text">dungeon = GameMap(map_width, map_height)</span>
<span class="crossed-out-text">room_1 = RectangularRoom(x=20, y=15, width=10, height=15)</span>
<span class="crossed-out-text">room_2 = RectangularRoom(x=35, y=15, width=10, height=15)</span>
<span class="crossed-out-text">dungeon.tiles[room_1.inner] = tile_types.floor</span>
<span class="crossed-out-text">dungeon.tiles[room_2.inner] = tile_types.floor</span>
<span class="crossed-out-text">create_horizontal_tunnel(dungeon, 25, 40, 23)</span>
<span class="crossed-out-text">return dungeon</span>
<span class="new-text">def generate_dungeon(
max_rooms: int,
room_min_size: int,
room_max_size: int,
map_width: int,
map_height: int,
player: Entity,
) -&gt; GameMap:
"""Generate a new dungeon map."""
dungeon = GameMap(map_width, map_height)
rooms: List[RectangularRoom] = []
for r in range(max_rooms):
room_width = random.randint(room_min_size, room_max_size)
room_height = random.randint(room_min_size, room_max_size)
x = random.randint(0, dungeon.width - room_width - 1)
y = random.randint(0, dungeon.height - room_height - 1)
# "RectangularRoom" class makes rectangles easier to work with
new_room = RectangularRoom(x, y, room_width, room_height)
# Run through the other rooms and see if they intersect with this one.
if any(new_room.intersects(other_room) for other_room in rooms):
continue # This room intersects, so go to the next attempt.
# If there are no intersections then the room is valid.
# Dig out this rooms inner area.
dungeon.tiles[new_room.inner] = tile_types.floor
if len(rooms) == 0:
# The first room, where the player starts.
player.x, player.y = new_room.center
else: # All rooms after the first.
# Dig out a tunnel between this room and the previous one.
for x, y in tunnel_between(rooms[-1].center, new_room.center):
dungeon.tiles[x, y] = tile_types.floor
# Finally, append the new room to the list.
rooms.append(new_room)
return dungeon</span></pre>
</div>
</div>
<p>Thats quite a lengthy function! Lets break it down and figure out whats doing what.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">generate_dungeon</span>(
</span></span><span style="display:flex;"><span> max_rooms: int,
</span></span><span style="display:flex;"><span> room_min_size: int,
</span></span><span style="display:flex;"><span> room_max_size: int,
</span></span><span style="display:flex;"><span> map_width: int,
</span></span><span style="display:flex;"><span> map_height: int,
</span></span><span style="display:flex;"><span> player: Entity,
</span></span><span style="display:flex;"><span>) <span style="color:#f92672">-&gt;</span> GameMap:
</span></span></code></pre></div><p>This is the function definition itself. We pass several arguments to it.</p>
<ul>
<li><code>max_rooms</code>: The maximum number of rooms allowed in the dungeon. Well use this to control our iteration.</li>
<li><code>room_min_size</code>: The minimum size of one room.</li>
<li><code>room_max_size</code>: The maximum size of one room. Well pick a random size between this and <code>room_min_size</code> for both the width and the height of one room to carve out.</li>
<li><code>map_width</code> and <code>map_height</code>: The width and height of the <code>GameMap</code> to create. This is no different than it was before.</li>
<li><code>player</code>: The player Entity. We need this to know where to place the player.</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#e6db74">"""Generate a new dungeon map."""</span>
</span></span><span style="display:flex;"><span> dungeon <span style="color:#f92672">=</span> GameMap(map_width, map_height)
</span></span></code></pre></div><p>This isnt anything new, were just creating the initial <code>GameMap</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> rooms: List[RectangularRoom] <span style="color:#f92672">=</span> []
</span></span></code></pre></div><p>Well keep a running list of all the rooms.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> r <span style="color:#f92672">in</span> range(max_rooms):
</span></span></code></pre></div><p>We iterate from 0 to <code>max_rooms</code>
- 1. Our algorithm may or may not place a room depending on if it
intersects with another, so we wont know how many rooms were going to
end up with. But at least well know that number cant exceed a certain
amount.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> room_width <span style="color:#f92672">=</span> random<span style="color:#f92672">.</span>randint(room_min_size, room_max_size)
</span></span><span style="display:flex;"><span> room_height <span style="color:#f92672">=</span> random<span style="color:#f92672">.</span>randint(room_min_size, room_max_size)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> x <span style="color:#f92672">=</span> random<span style="color:#f92672">.</span>randint(<span style="color:#ae81ff">0</span>, dungeon<span style="color:#f92672">.</span>width <span style="color:#f92672">-</span> room_width <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span> y <span style="color:#f92672">=</span> random<span style="color:#f92672">.</span>randint(<span style="color:#ae81ff">0</span>, dungeon<span style="color:#f92672">.</span>height <span style="color:#f92672">-</span> room_height <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># "RectangularRoom" class makes rectangles easier to work with</span>
</span></span><span style="display:flex;"><span> new_room <span style="color:#f92672">=</span> RectangularRoom(x, y, room_width, room_height)
</span></span></code></pre></div><p>Here, we use the given minimum and maximum room sizes to set the rooms width and height. We then get a random pair of <code>x</code> and <code>y</code> coordinates to try and place the room down. The coordinates must be between 0 and the maps width and heights.</p>
<p>We use these variables to then create an instance of our <code>RectangularRoom</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#75715e"># Run through the other rooms and see if they intersect with this one.</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> any(new_room<span style="color:#f92672">.</span>intersects(other_room) <span style="color:#66d9ef">for</span> other_room <span style="color:#f92672">in</span> rooms):
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">continue</span> <span style="color:#75715e"># This room intersects, so go to the next attempt.</span>
</span></span></code></pre></div><p>So what happens if a room <em>does</em> intersect with another? In that case, we can just toss it out, by using <code>continue</code>
to skip the rest of the loop. Obviously there are more elegant ways of
dealing with a collision, but for our simplistic algorithm, well just
pretend like it didnt happen and try the next one.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#75715e"># If there are no intersections then the room is valid.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Dig out this rooms inner area.</span>
</span></span><span style="display:flex;"><span> dungeon<span style="color:#f92672">.</span>tiles[new_room<span style="color:#f92672">.</span>inner] <span style="color:#f92672">=</span> tile_types<span style="color:#f92672">.</span>floor
</span></span></code></pre></div><p>Here, we “dig” the room out. This is similar to what we were doing before to dig out the two connected rooms.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> len(rooms) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># The first room, where the player starts.</span>
</span></span><span style="display:flex;"><span> player<span style="color:#f92672">.</span>x, player<span style="color:#f92672">.</span>y <span style="color:#f92672">=</span> new_room<span style="color:#f92672">.</span>center
</span></span></code></pre></div><p>We put our player down in the center of the first room we created. If this room isnt the first, we move on to the <code>else</code> statement:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">else</span>: <span style="color:#75715e"># All rooms after the first.</span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># Dig out a tunnel between this room and the previous one.</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> x, y <span style="color:#f92672">in</span> tunnel_between(rooms[<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>]<span style="color:#f92672">.</span>center, new_room<span style="color:#f92672">.</span>center):
</span></span><span style="display:flex;"><span> dungeon<span style="color:#f92672">.</span>tiles[x, y] <span style="color:#f92672">=</span> tile_types<span style="color:#f92672">.</span>floor
</span></span></code></pre></div><p>This is similar to how we dug the tunnel before, except this time, were using a negative index with <code>rooms</code> to grab the previous room, and connecting the new room to it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#75715e"># Finally, append the new room to the list.</span>
</span></span><span style="display:flex;"><span> rooms<span style="color:#f92672">.</span>append(new_room)
</span></span></code></pre></div><p>Regardless if its the first room or not, we want to append it to the list, so the next iteration can use it.</p>
<p>So thats our <code>generate_dungeon</code> function, but were not quite finished yet. We need to modify the call we make to this function in <code>main.py</code>:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span> entities = {npc, player}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">- game_map = generate_dungeon(map_width, map_height)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ game_map = generate_dungeon(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ max_rooms=max_rooms,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ room_min_size=room_min_size,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ room_max_size=room_max_size,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ map_width=map_width,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ map_height=map_height,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ player=player
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> engine = Engine(entities=entities, event_handler=event_handler, game_map=game_map, player=player)
</span></span><span style="display:flex;"><span> ...
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre> ...
entities = {npc, player}
<span class="crossed-out-text">game_map = generate_dungeon(map_width, map_height)</span>
<span class="new-text">game_map = generate_dungeon(
max_rooms=max_rooms,
room_min_size=room_min_size,
room_max_size=room_max_size,
map_width=map_width,
map_height=map_height,
player=player
)</span>
engine = Engine(entities=entities, event_handler=event_handler, game_map=game_map, player=player)
...</pre>
</div>
</div>
<p>And thats it! Theres our functioning, albeit basic, dungeon
generation algorithm. Run the project now and you should be placed in a
procedurally generated dungeon! Note that our NPC isnt being placed
intelligently here, so it may or may not be stuck in a wall.</p>
<p><img src="Part%203%20-%20Generating%20a%20dungeon%20%C2%B7%20Roguelike%20Tutorials_files/part-3-dungeon.png" alt="Part 3 - Generated Dungeon"></p>
<p><em>Note: Your dungeon will look different from this one, so dont worry if it doesnt match the screenshot.</em></p>
<p>If you want to see the code so far in its entirety, <a href="https://github.com/TStand90/tcod_tutorial_v2/tree/2020/part-3">click
here</a>.</p>
<p><a href="https://rogueliketutorials.com/tutorials/tcod/v2/part-4">Click here to move on to the next part of this
tutorial.</a></p>
</article>
</section>
</div>
<footer class="footer">
<section class="container">
©
2023
·
Powered by <a href="https://gohugo.io/">Hugo</a> &amp; <a href="https://github.com/luizdepra/hugo-coder/">Coder</a>.
</section>
</footer>
</main>
<script src="Part%203%20-%20Generating%20a%20dungeon%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.236049395dc3682fb2719640872958e12f1f24067bb09c327b2.js" integrity="sha256-I2BJOV3DaC+ycZZAhylY4S8fJAZ7sJwyeyM+YpDH7aw="></script>
<script src="Part%203%20-%20Generating%20a%20dungeon%20%C2%B7%20Roguelike%20Tutorials_files/codetabs.min.cc52451e7f25e50f64c1c893826f606d58410d742c214dce.js" integrity="sha256-zFJFHn8l5Q9kwciTgm9gbVhBDXQsIU3OI/tEfJlh8rA="></script>
</body></html>