<p>See also the <aclass="reference internal"href="../Contribs/Contrib-Mapbuilder.html"><spanclass="doc std std-doc">Mapbuilder</span></a> and <aclass="reference internal"href="../Contribs/Contrib-XYZGrid.html"><spanclass="doc std std-doc">XYZGrid</span></a> contribs, which offer alternative ways of both creating and displaying room maps.</p>
<h2>The Grid of Rooms<aclass="headerlink"href="#the-grid-of-rooms"title="Permalink to this headline">¶</a></h2>
<p>There are at least two requirements needed for this tutorial to work.</p>
<olclass="simple">
<li><p>The structure of your mud has to follow a logical layout. Evennia supports the layout of your world to be ‘logically’ impossible with rooms looping to themselves or exits leading to the other side of the map. Exits can also be named anything, from “jumping out the window” to “into the fifth dimension”. This tutorial assumes you can only move in the cardinal directions (N, E, S and W).</p></li>
<li><p>Rooms must be connected and linked together for the map to be generated correctly. Vanilla Evennia comes with a admin command <aclass="reference internal"href="../api/evennia.commands.default.building.html#evennia.commands.default.building.CmdTunnel"title="evennia.commands.default.building.CmdTunnel"><spanclass="xref myst py py-class">tunnel</span></a> that allows a user to create rooms in the cardinal directions, but additional work is needed to assure that rooms are connected. For example, if you <codeclass="docutils literal notranslate"><spanclass="pre">tunnel</span><spanclass="pre">east</span></code> and then immediately do <codeclass="docutils literal notranslate"><spanclass="pre">tunnel</span><spanclass="pre">west</span></code> you’ll find that you have created two completely stand-alone rooms. So care is needed if you want to create a “logical” layout. In this tutorial we assume you have such a grid of rooms that we can generate the map from.</p></li>
</ol>
</section>
<sectionid="concept">
<h2>Concept<aclass="headerlink"href="#concept"title="Permalink to this headline">¶</a></h2>
<p>Before getting into the code, it is beneficial to understand and conceptualize how this is going to work. The idea is analogous to a worm that starts at your current position. It chooses a direction and ‘walks’ outward from it, mapping its route as it goes. Once it has traveled a pre-set distance it stops and starts over in another direction. An important note is that we want a system which is easily callable and not too complicated. Therefore we will wrap this entire code into a custom Python class (not a typeclass as this doesn’t use any core objects from evennia itself). We are going to create something that displays like this when you type ‘look’:</p>
<p>Your current location is defined by <codeclass="docutils literal notranslate"><spanclass="pre">[@]</span></code> while the <codeclass="docutils literal notranslate"><spanclass="pre">[.]</span></code>s are other rooms that the “worm” has seen
since departing from your location.</p>
</section>
<sectionid="setting-up-the-map-display">
<h2>Setting up the Map Display<aclass="headerlink"href="#setting-up-the-map-display"title="Permalink to this headline">¶</a></h2>
<p>First we must define the components for displaying the map. For the “worm” to know what symbol to draw on the map we will have it check an Attribute on the room it visits called <codeclass="docutils literal notranslate"><spanclass="pre">sector_type</span></code>. For this tutorial we understand two symbols - a normal room and the room with us in it. We also define a fallback symbol for rooms without said Attribute - that way the map will still work even if we didn’t prepare the room correctly. Assuming your game folder is named <codeclass="docutils literal notranslate"><spanclass="pre">mygame</span></code>, we create this code in <codeclass="docutils literal notranslate"><spanclass="pre">mygame/world/map.py.</span></code></p>
<divclass="highlight-python notranslate"><divclass="highlight"><pre><span></span><spanclass="c1"># in mygame/world/map.py</span>
<spanclass="c1"># the symbol is identified with a key "sector_type" on the</span>
<spanclass="c1"># Room. Keys None and "you" must always exist.</span>
<spanclass="n">SYMBOLS</span><spanclass="o">=</span><spanclass="p">{</span><spanclass="kc">None</span><spanclass="p">:</span><spanclass="s1">' . '</span><spanclass="p">,</span><spanclass="c1"># for rooms without sector_type Attribute</span>
<p>Since trying to access an unset Attribute returns <codeclass="docutils literal notranslate"><spanclass="pre">None</span></code>, this means rooms without the <codeclass="docutils literal notranslate"><spanclass="pre">sector_type</span></code>
Atttribute will show as <codeclass="docutils literal notranslate"><spanclass="pre">.</span></code>. Next we start building the custom class <codeclass="docutils literal notranslate"><spanclass="pre">Map</span></code>. It will hold all
methods we need.</p>
<divclass="highlight-python notranslate"><divclass="highlight"><pre><span></span><spanclass="c1"># in mygame/world/map.py</span>
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">self.caller</span></code> is normally your Character object, the one using the map.</p></li>
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">self.max_width/length</span></code> determine the max width and length of the map that will be generated. Note that it’s important that these variables are set to <em>odd</em> numbers to make sure the display area has a center point.</p></li>
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">self.worm_has_mapped</span></code> is building off the worm analogy above. This dictionary will store all rooms the “worm” has mapped as well as its relative position within the grid. This is the most important variable as it acts as a ‘checker’ and ‘address book’ that is able to tell us where the worm has been and what it has mapped so far.</p></li>
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">self.curX/Y</span></code> are coordinates representing the worm’s current location on the grid.</p></li>
</ul>
<p>Before any sort of mapping can actually be done we need to create an empty display area and do some sanity checks on it by using the following methods.</p>
<divclass="highlight-python notranslate"><divclass="highlight"><pre><span></span><spanclass="c1"># in mygame/world/map.py</span>
<p>Before we can set our worm on its way, we need to know some of the computer science behind all this called ‘Graph Traversing’. In Pseudo code what we are trying to accomplish is this:</p>
<p>The beauty of Python is that our actual code of doing this doesn’t differ much if at all from this
Pseudo code example.</p>
<ulclass="simple">
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">max_distance</span></code> is a variable indicating to our Worm how many rooms AWAY from your current location will it map. Obviously the larger the number the more time it will take if your current location has many many rooms around you.</p></li>
</ul>
<p>The first hurdle here is what value to use for ‘max_distance’. There is no reason for the worm to travel further than what is actually displayed to you. For example, if your current location is placed in the center of a display area of size <codeclass="docutils literal notranslate"><spanclass="pre">max_length</span><spanclass="pre">=</span><spanclass="pre">max_width</span><spanclass="pre">=</span><spanclass="pre">9</span></code>, then the worm need only
go <codeclass="docutils literal notranslate"><spanclass="pre">4</span></code> spaces in either direction:</p>
<p>The <codeclass="docutils literal notranslate"><spanclass="pre">max_distance</span></code> can be set dynamically based on the size of the display area. As your width/length changes it becomes a simple algebraic linear relationship which is simply <codeclass="docutils literal notranslate"><spanclass="pre">max_distance</span><spanclass="pre">=</span><spanclass="pre">(min(max_width,</span><spanclass="pre">max_length)</span><spanclass="pre">-1)</span><spanclass="pre">/</span><spanclass="pre">2</span></code>.</p>
</section>
<sectionid="building-the-mapper">
<h2>Building the Mapper<aclass="headerlink"href="#building-the-mapper"title="Permalink to this headline">¶</a></h2>
<p>Now we can start to fill our Map object with some methods. We are still missing a few methods that are very important:</p>
<ulclass="simple">
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">self.draw(self,</span><spanclass="pre">room)</span></code> - responsible for actually drawing room to grid.</p></li>
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">self.has_drawn(self,</span><spanclass="pre">room)</span></code> - checks to see if the room has been mapped and worm has already been here.</p></li>
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">self.median(self,</span><spanclass="pre">number)</span></code> - a simple utility method that finds the median (middle point) from <codeclass="docutils literal notranslate"><spanclass="pre">0,</span><spanclass="pre">n</span></code></p></li>
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">self.update_pos(self,</span><spanclass="pre">room,</span><spanclass="pre">exit_name)</span></code> - updates the worm’s physical position by reassigning <codeclass="docutils literal notranslate"><spanclass="pre">self.curX/Y</span></code> accordingly.</p></li>
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">self.start_loc_on_grid(self)</span></code> - the very first initial draw on the grid representing your location in the middle of the grid.</p></li>
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">self.show_map</span></code> - after everything is done convert the map into a readable string</p></li>
<li><p><codeclass="docutils literal notranslate"><spanclass="pre">self.draw_room_on_map(self,</span><spanclass="pre">room,</span><spanclass="pre">max_distance)</span></code> - the main method that ties it all together.</p></li>
</ul>
<p>Now that we know which methods we need, let’s refine our initial <codeclass="docutils literal notranslate"><spanclass="pre">__init__(self)</span></code> to pass some
conditional statements and set it up to start building the display.</p>
<p>Here we check to see if the parameters for the grid are okay, then we create an empty canvas and map our initial location as the first room!</p>
<p>As mentioned above, the code for the <codeclass="docutils literal notranslate"><spanclass="pre">self.draw_room_on_map()</span></code> is not much different than the Pseudo code. The method is shown below:</p>
<divclass="highlight-python notranslate"><divclass="highlight"><pre><span></span><spanclass="c1"># in mygame/world/map.py, in the Map class</span>
<p>The first thing the “worm” does is to draw your current location in <codeclass="docutils literal notranslate"><spanclass="pre">self.draw</span></code>. Lets define that…</p>
<divclass="highlight-python notranslate"><divclass="highlight"><pre><span></span><spanclass="c1">#in mygame/word/map.py, in the Map class</span>
<spanclass="bp">self</span><spanclass="o">.</span><spanclass="n">curX</span><spanclass="p">,</span><spanclass="bp">self</span><spanclass="o">.</span><spanclass="n">curY</span><spanclass="o">=</span><spanclass="n">x</span><spanclass="p">,</span><spanclass="n">y</span><spanclass="c1"># updating worms current location</span>
</pre></div>
</div>
<p>After the system has drawn the current map it checks to see if the <codeclass="docutils literal notranslate"><spanclass="pre">max_distance</span></code> is <codeclass="docutils literal notranslate"><spanclass="pre">0</span></code> (since this
is the inital start phase it is not). Now we handle the iteration once we have each individual exit
in the room. The first thing it does is check if the room the Worm is in has been mapped already…
<p>If <codeclass="docutils literal notranslate"><spanclass="pre">has_drawn</span></code> returns <codeclass="docutils literal notranslate"><spanclass="pre">False</span></code> that means the worm has found a room that hasn’t been mapped yet. It
will then ‘move’ there. The self.curX/Y sort of lags behind, so we have to make sure to track the
position of the worm; we do this in <codeclass="docutils literal notranslate"><spanclass="pre">self.update_pos()</span></code> below.</p>
<h2>Using the Map<aclass="headerlink"href="#using-the-map"title="Permalink to this headline">¶</a></h2>
<p>In order for the map to get triggered we store it on the Room typeclass. If we put it in
<codeclass="docutils literal notranslate"><spanclass="pre">return_appearance</span></code> we will get the map back every time we look at the room.</p>
<blockquote>
<div><p><codeclass="docutils literal notranslate"><spanclass="pre">return_appearance</span></code> is a default Evennia hook available on all objects; it is called e.g. by the
<codeclass="docutils literal notranslate"><spanclass="pre">look</span></code> command to get the description of something (the room in this case).</p>
</div></blockquote>
<divclass="highlight-python notranslate"><divclass="highlight"><pre><span></span><spanclass="c1"># in mygame/typeclasses/rooms.py</span>
<p>Obviously this method of generating maps doesn’t take into account of any doors or exits that are hidden… etc… but hopefully it serves as a good base to start with. Like previously mentioned, it is very important to have a solid foundation on rooms before implementing this. You can try this on vanilla evennia by using @tunnel and essentially you can just create a long straight/edgy non- looping rooms that will show on your in-game map.</p>
<p>The above example will display the map above the room description. You could also use an <aclass="reference external"href="https://github.com/evennia/evennia/blob/main/evennia.utils.evtable">EvTable</a> to place description and map next to each other. Some other things you can do is to have a <aclass="reference internal"href="../Components/Commands.html"><spanclass="doc std std-doc">Command</span></a> that displays with a larger radius, maybe with a legend and other features.</p>
<p>Below is the whole <codeclass="docutils literal notranslate"><spanclass="pre">map.py</span></code> for your reference. You need to update your <codeclass="docutils literal notranslate"><spanclass="pre">Room</span></code> typeclass (see above) to actually call it. Remember that to see different symbols for a location you also need to set the <codeclass="docutils literal notranslate"><spanclass="pre">sector_type</span></code> Attribute on the room to one of the keys in the <codeclass="docutils literal notranslate"><spanclass="pre">SYMBOLS</span></code> dictionary. So in this example, to make a room be mapped as <codeclass="docutils literal notranslate"><spanclass="pre">[.]</span></code> you would set the room’s <codeclass="docutils literal notranslate"><spanclass="pre">sector_type</span></code> to <codeclass="docutils literal notranslate"><spanclass="pre">"SECT_INSIDE"</span></code>. Try it out with <codeclass="docutils literal notranslate"><spanclass="pre">@set</span><spanclass="pre">here/sector_type</span><spanclass="pre">=</span><spanclass="pre">"SECT_INSIDE"</span></code>. If you wanted all new rooms to have a given sector symbol, you could change the default in the <codeclass="docutils literal notranslate"><spanclass="pre">SYMBOLS</span></code> dictionary below, or you could add the Attribute in the Room’s <codeclass="docutils literal notranslate"><spanclass="pre">at_object_creation</span></code> method.</p>
<spanclass="c1"># These are keys set with the Attribute sector_type on the room.</span>
<spanclass="c1"># The keys None and "you" must always exist.</span>
<spanclass="n">SYMBOLS</span><spanclass="o">=</span><spanclass="p">{</span><spanclass="kc">None</span><spanclass="p">:</span><spanclass="s1">' . '</span><spanclass="p">,</span><spanclass="c1"># for rooms without a sector_type attr</span>
<spanclass="bp">self</span><spanclass="o">.</span><spanclass="n">curX</span><spanclass="p">,</span><spanclass="bp">self</span><spanclass="o">.</span><spanclass="n">curY</span><spanclass="o">=</span><spanclass="n">x</span><spanclass="p">,</span><spanclass="n">y</span><spanclass="c1"># updating worms current location</span>