emacs.d/clones/lisp/stevelosh.com/blog/2012/10/caves-of-clojure-07-1/index.html

293 lines
23 KiB
HTML
Raw Normal View History

2022-10-07 15:47:14 +02:00
<!DOCTYPE html>
<html lang='en'><head><meta charset='utf-8' /><meta name='pinterest' content='nopin' /><link href='../../../../static/css/style.css' rel='stylesheet' type='text/css' /><link href='../../../../static/css/print.css' rel='stylesheet' type='text/css' media='print' /><title>The Caves of Clojure: Part 7.1 / Steve Losh</title></head><body><header><a id='logo' href='https://stevelosh.com/'>Steve Losh</a><nav><a href='../../../index.html'>Blog</a> - <a href='https://stevelosh.com/projects/'>Projects</a> - <a href='https://stevelosh.com/photography/'>Photography</a> - <a href='https://stevelosh.com/links/'>Links</a> - <a href='https://stevelosh.com/rss.xml'>Feed</a></nav></header><hr class='main-separator' /><main id='page-blog-entry'><article><h1><a href='index.html'>The Caves of Clojure: Part 7.1</a></h1><p class='date'>Posted on October 15th, 2012.</p><p>This post is part of an ongoing series. If you haven't already done so, you
should probably start at <a href="../../07/caves-of-clojure-01/index.html">the beginning</a>.</p>
<p>This entry corresponds to the beginning of <a href="http://trystans.blogspot.com/2011/09/roguelike-tutorial-07-z-levels-and.html">post seven in Trystan's
tutorial</a>.</p>
<p>If you want to follow along, the code for the series is <a href="http://bitbucket.org/sjl/caves/">on Bitbucket</a> and
<a href="http://github.com/sjl/caves/">on GitHub</a>. Update to the <code>entry-07-1</code> tag to see the code as it stands
after this post.</p>
<p>It's been a while since the last post, but I've been taking care of things and
hopefully should be able to write a bit more now.</p>
<p>This post is going to be short. It'll cover a relatively self-contained but
interesting bit of Trystan's seventh post. The rest of it will be covered in
the next entry.</p>
<ol class="table-of-contents"><li><a href="index.html#s1-summary">Summary</a></li><li><a href="index.html#s2-region-mapping">Region Mapping</a></li><li><a href="index.html#s3-visualization">Visualization</a></li><li><a href="index.html#s4-results">Results</a></li></ol>
<h2 id="s1-summary"><a href="index.html#s1-summary">Summary</a></h2>
<p>In Trystan's seventh post he adds vertical levels and stairs connecting them.
I'm going to cover the first part of that now: mapping out regions.</p>
<p>There's been a bit of refactoring since my last post which I'm not going to
cover. If you want to see what changed, diff the tags in your VCS of choice.</p>
<h2 id="s2-region-mapping"><a href="index.html#s2-region-mapping">Region Mapping</a></h2>
<p>In order to decide where to place stairs, Trystan maps out &quot;regions&quot; of
contiguous, walkable tiles after he generates and smooths the world. I'm going
to do the same thing.</p>
<p>My goal is to create a &quot;region map&quot;, which is a map of coordinates to region
numbers. For example, consider the following tiny world map:</p>
<pre><code>..##..
..#...
..#.##
..#.#.</code></pre>
<p>There are three distinct regions in this map:</p>
<pre><code>11##22
11#222
11#2##
11#2#3</code></pre>
<p>So the region map would look like:</p>
<pre><code><span class="code"><span class="comment">; x y region
</span>{<span class="paren1">[<span class="code">0 0</span>]</span> 1
<span class="paren1">[<span class="code">1 0</span>]</span> 1
<span class="paren1">[<span class="code">4 0</span>]</span> 2
<span class="paren1">[<span class="code">5 0</span>]</span> 2
...
<span class="paren1">[<span class="code">5 3</span>]</span> 3}</span></code></pre>
<p>This makes it easy to tell which region a particular tile is in (if any).</p>
<p>As usual, I'll start with a few helper functions. These two functions are just
for convenience and readability:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code">def all-coords
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">[<span class="code"><span class="paren4">[<span class="code">cols rows</span>]</span> world-size</span>]</span>
<span class="paren3">(<span class="code">for <span class="paren4">[<span class="code">x <span class="paren5">(<span class="code">range cols</span>)</span>
y <span class="paren5">(<span class="code">range rows</span>)</span></span>]</span>
<span class="paren4">[<span class="code">x y</span>]</span></span>)</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> get-tile-from-level <span class="paren2">[<span class="code">level <span class="paren3">[<span class="code">x y</span>]</span></span>]</span>
<span class="paren2">(<span class="code">get-in level <span class="paren3">[<span class="code">y x</span>]</span> <span class="paren3">(<span class="code"><span class="keyword">:bound</span> tiles</span>)</span></span>)</span></span>)</span></span></code></pre>
<p>The <code>all-coords</code> function simply returns a lazy sequence of <code>[x y]</code> coordinates
representing every coordinate in a level.</p>
<p><code>get-tile-from-level</code> encapsulates the act of pulling out a tile from a level
given an <code>[x y]</code> coordinate. This is helpful because of the way I'm storing
tiles (note the ugly <code>[y x]</code>).</p>
<p>Next up is a function that filters a set of coordinates to only contain those
that are actually walkable in the given level (i.e.: those that don't contain
a wall tile):</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> filter-walkable
<span class="string">&quot;Filter the given coordinates to include only walkable ones.&quot;</span>
<span class="paren2">[<span class="code">level coords</span>]</span>
<span class="paren2">(<span class="code">set <span class="paren3">(<span class="code">filter #<span class="paren4">(<span class="code">tile-walkable? <span class="paren5">(<span class="code">get-tile-from-level level %</span>)</span></span>)</span>
coords</span>)</span></span>)</span></span>)</span></span></code></pre>
<p>This uses the <code>get-tile-from-level</code> function as well as <code>tile-walkable?</code> from
<code>world.core</code>.</p>
<p>Next is a function to take a coordinate and return which of its neighboring
coordinates are walkable:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> walkable-neighbors
<span class="string">&quot;Return the neighboring coordinates walkable from the given coord.&quot;</span>
<span class="paren2">[<span class="code">level coord</span>]</span>
<span class="paren2">(<span class="code">filter-walkable level <span class="paren3">(<span class="code">neighbors coord</span>)</span></span>)</span></span>)</span></span></code></pre>
<p>This one is almost trivial, but I like building up functions in small steps like
this because it's easier for me to read.</p>
<p>Now we come to a function with a bit more meat. This is the core of the &quot;flood
fill&quot; algorithm I'm going to use to fill in the region map.</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> walkable-from
<span class="string">&quot;Return all coordinates walkable from the given coord (including itself).&quot;</span>
<span class="paren2">[<span class="code">level coord</span>]</span>
<span class="paren2">(<span class="code"><i><span class="symbol">loop</span></i> <span class="paren3">[<span class="code">walked #<span class="paren4">{<span class="code"></span>}</span>
to-walk #<span class="paren4">{<span class="code">coord}</span>]</span>
<span class="paren4">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren5">(<span class="code">empty? to-walk</span>)</span>
walked
<span class="paren5">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren6">[<span class="code">current <span class="paren1">(<span class="code">first to-walk</span>)</span>
walked <span class="paren1">(<span class="code">conj walked current</span>)</span>
to-walk <span class="paren1">(<span class="code">disj to-walk current</span>)</span>
candidates <span class="paren1">(<span class="code">walkable-neighbors level current</span>)</span>
to-walk <span class="paren1">(<span class="code">union to-walk <span class="paren2">(<span class="code">difference candidates walked</span>)</span></span>)</span></span>]</span>
<span class="paren6">(<span class="code">recur walked to-walk</span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span></span></span></code></pre>
<p>In a nutshell, this function loops over two sets: <code>walked</code> and <code>to-walk</code>.</p>
<p>Each iteration it grabs a coordinate from <code>to-walk</code> and sticks it into the
<code>walked</code> set. It then finds all the coordinates it can walk to from that
coordinate using a helper function. It uses <code>clojure.set/difference</code> to
determine which of those are new (i.e.: still need to be walked) and sticks them
into the <code>to-walk</code> set. Then it recurs.</p>
<p>The code for this is surprisingly simple and easy to read. It's mostly just
shuffling things between sets. Eventually the <code>to-walk</code> set will be empty and
<code>walked</code> will contain all the coordinates that we want.</p>
<p>Finally comes the function to create the region map for an entire level:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> get-region-map
<span class="paren2">[<span class="code">level</span>]</span>
<span class="paren2">(<span class="code"><i><span class="symbol">loop</span></i> <span class="paren3">[<span class="code">remaining <span class="paren4">(<span class="code">filter-walkable level all-coords</span>)</span>
region-map <span class="paren4">{<span class="code"></span>}</span>
n 0</span>]</span>
<span class="paren3">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren4">(<span class="code">empty? remaining</span>)</span>
region-map
<span class="paren4">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren5">[<span class="code">next-coord <span class="paren6">(<span class="code">first remaining</span>)</span>
next-region-coords <span class="paren6">(<span class="code">walkable-from level next-coord</span>)</span></span>]</span>
<span class="paren5">(<span class="code">recur <span class="paren6">(<span class="code">difference remaining next-region-coords</span>)</span>
<span class="paren6">(<span class="code">into region-map <span class="paren1">(<span class="code">map vector
next-region-coords
<span class="paren2">(<span class="code">repeat n</span>)</span></span>)</span></span>)</span>
<span class="paren6">(<span class="code">inc n</span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
<p>This function also uses Clojure sets to its advantage. Once again, I loop
over a couple of variables.</p>
<p><code>remaining</code> is a set containing all the coordinates whose regions has not yet
been determined.</p>
<p>Each iteration it pulls off one of the remaining coordinates. Note that I'm
using <code>first</code> to do this. <code>remaining</code> is a set, which is unordered, so <code>first</code>
effectively could return any element in the set. For this loop that doesn't
matter, but it's important to be aware of if you're going to use the same
strategy.</p>
<p>After pulling off a coordinate, it finds all coordinates walkable from that
coordinate with the <code>walkable-from</code> flood-fill function. It removes all of
those from the <code>remaining</code> set, shoves them into the region map, and increments
the region number before recurring.</p>
<h2 id="s3-visualization"><a href="index.html#s3-visualization">Visualization</a></h2>
<p>I'm going to save the rest of Trystan's seventh post for another entry, but
since this one ended up pretty short I'm also going to go over visualizing the
region map I've just created.</p>
<p>First I need to generate the region map when I create the world, and attach it
to the world itself so we can access it from other places:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> random-world <span class="paren2">[<span class="code"></span>]</span>
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">[<span class="code">world <span class="paren4">(<span class="code">-&gt;World <span class="paren5">(<span class="code">random-tiles</span>)</span> <span class="paren5">{<span class="code"></span>}</span></span>)</span>
world <span class="paren4">(<span class="code">nth <span class="paren5">(<span class="code">iterate smooth-world world</span>)</span> 3</span>)</span>
world <span class="paren4">(<span class="code">populate-world world</span>)</span>
world <span class="paren4">(<span class="code">assoc world <span class="keyword">:regions</span> <span class="paren5">(<span class="code">get-region-map <span class="paren6">(<span class="code"><span class="keyword">:tiles</span> world</span>)</span></span>)</span></span>)</span></span>]</span>
world</span>)</span></span>)</span></span></code></pre>
<p>The last line in the <code>let</code> is where it gets generated. It's pretty
straightforward.</p>
<p>I'd like to be able to toggle the visualization of regions off and on, so I'm
going to introduce a new concept to the game: &quot;debug flags&quot;.</p>
<p>I updated the <code>Game</code> record to include a slot for these flags:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defrecord</span></i> Game <span class="paren2">[<span class="code">world uis input debug-flags</span>]</span></span>)</span></span></code></pre>
<p>I then updated the <code>new-game</code> function to initialize them (currently there's
only one) to default values:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> new-game <span class="paren2">[<span class="code"></span>]</span>
<span class="paren2">(<span class="code">map-&gt;Game <span class="paren3">{<span class="code"><span class="keyword">:world</span> nil
<span class="keyword">:uis</span> <span class="paren4">[<span class="code"><span class="paren5">(<span class="code">-&gt;UI <span class="keyword">:start</span></span>)</span></span>]</span>
<span class="keyword">:input</span> nil
<span class="keyword">:debug-flags</span> <span class="paren4">{<span class="code"><span class="keyword">:show-regions</span> false}}</span>)</span></span>)</span></span></span></span></span></span></code></pre>
<p>The user needs a way to toggle them. For now I'll just bind it to a key. In
the future I could make a debug UI with a nice menu.</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defmethod</span></i> process-input <span class="keyword">:play</span> <span class="paren2">[<span class="code">game input</span>]</span>
<span class="paren2">(<span class="code">case input
<span class="keyword">:enter</span> <span class="paren3">(<span class="code">assoc game <span class="keyword">:uis</span> <span class="paren4">[<span class="code"><span class="paren5">(<span class="code">-&gt;UI <span class="keyword">:win</span></span>)</span></span>]</span></span>)</span>
<span class="keyword">:backspace</span> <span class="paren3">(<span class="code">assoc game <span class="keyword">:uis</span> <span class="paren4">[<span class="code"><span class="paren5">(<span class="code">-&gt;UI <span class="keyword">:lose</span></span>)</span></span>]</span></span>)</span>
\q <span class="paren3">(<span class="code">assoc game <span class="keyword">:uis</span> <span class="paren4">[<span class="code"></span>]</span></span>)</span>
\h <span class="paren3">(<span class="code">update-in game <span class="paren4">[<span class="code"><span class="keyword">:world</span></span>]</span> move-player <span class="keyword">:w</span></span>)</span>
\j <span class="paren3">(<span class="code">update-in game <span class="paren4">[<span class="code"><span class="keyword">:world</span></span>]</span> move-player <span class="keyword">:s</span></span>)</span>
<span class="comment">; ...
</span>
\R <span class="paren3">(<span class="code">update-in game <span class="paren4">[<span class="code"><span class="keyword">:debug-flags</span> <span class="keyword">:show-regions</span></span>]</span> not</span>)</span>
game</span>)</span></span>)</span></span></code></pre>
<p>Now when the user presses <code>R</code> (Shift and R) it will toggle the state of the
<code>:show-regions</code> debug flag in the game.</p>
<p>All that's left is to actually <em>draw</em> the regions somehow. First, we only want
to do this if <code>:show-regions</code> is <code>true</code>. I edited the <code>:play</code> UI's drawing
function to do this:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defmethod</span></i> draw-ui <span class="keyword">:play</span> <span class="paren2">[<span class="code">ui game screen</span>]</span>
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">[<span class="code">world <span class="paren4">(<span class="code"><span class="keyword">:world</span> game</span>)</span>
<span class="paren4">{<span class="code"><span class="keyword">:keys</span> <span class="paren5">[<span class="code">tiles entities regions</span>]</span></span>}</span> world
player <span class="paren4">(<span class="code"><span class="keyword">:player</span> entities</span>)</span>
<span class="paren4">[<span class="code">cols rows</span>]</span> <span class="paren4">(<span class="code">s/get-size screen</span>)</span>
vcols cols
vrows <span class="paren4">(<span class="code">dec rows</span>)</span>
origin <span class="paren4">(<span class="code">get-viewport-coords game <span class="paren5">(<span class="code"><span class="keyword">:location</span> player</span>)</span> vcols vrows</span>)</span></span>]</span>
<span class="paren3">(<span class="code">draw-world screen vrows vcols origin tiles</span>)</span>
<span class="comment">; ******************
</span> <span class="paren3">(<span class="code">when <span class="paren4">(<span class="code">get-in game <span class="paren5">[<span class="code"><span class="keyword">:debug-flags</span> <span class="keyword">:show-regions</span></span>]</span></span>)</span>
<span class="paren4">(<span class="code">draw-regions screen regions vrows vcols origin</span>)</span></span>)</span>
<span class="comment">; ******************
</span> <span class="paren3">(<span class="code">doseq <span class="paren4">[<span class="code">entity <span class="paren5">(<span class="code">vals entities</span>)</span></span>]</span>
<span class="paren4">(<span class="code">draw-entity screen origin vrows vcols entity</span>)</span></span>)</span>
<span class="paren3">(<span class="code">draw-hud screen game</span>)</span>
<span class="paren3">(<span class="code">draw-messages screen <span class="paren4">(<span class="code"><span class="keyword">:messages</span> player</span>)</span></span>)</span>
<span class="paren3">(<span class="code">highlight-player screen origin player</span>)</span></span>)</span></span>)</span></span></code></pre>
<p>The marked lines are the only new ones. I'm going to draw regions after/above
the world tiles (so they'll show up at all) but before/below the entities (so we
can still see what's going on).</p>
<p>Drawing the regions is fairly simple, if a bit tedious:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> draw-regions <span class="paren2">[<span class="code">screen region-map vrows vcols <span class="paren3">[<span class="code">ox oy</span>]</span></span>]</span>
<span class="paren2">(<span class="code">letfn <span class="paren3">[<span class="code"><span class="paren4">(<span class="code">get-region-glyph <span class="paren5">[<span class="code">region-number</span>]</span>
<span class="paren5">(<span class="code">str
<span class="paren6">(<span class="code">nth
<span class="string">&quot;0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ&quot;</span>
region-number</span>)</span></span>)</span></span>)</span></span>]</span>
<span class="paren3">(<span class="code">doseq <span class="paren4">[<span class="code">x <span class="paren5">(<span class="code">range ox <span class="paren6">(<span class="code">+ ox vcols</span>)</span></span>)</span>
y <span class="paren5">(<span class="code">range oy <span class="paren6">(<span class="code">+ oy vrows</span>)</span></span>)</span></span>]</span>
<span class="paren4">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren5">[<span class="code">region-number <span class="paren6">(<span class="code">region-map <span class="paren1">[<span class="code">x y</span>]</span></span>)</span></span>]</span>
<span class="paren5">(<span class="code">when region-number
<span class="paren6">(<span class="code">s/put-string screen <span class="paren1">(<span class="code">- x ox</span>)</span> <span class="paren1">(<span class="code">- y oy</span>)</span>
<span class="paren1">(<span class="code">get-region-glyph region-number</span>)</span>
<span class="paren1">{<span class="code"><span class="keyword">:fg</span> <span class="keyword">:blue}</span></span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span></span></span></code></pre>
<p>For now, bad things will happen if we have more than 62 regions in a single
level. In practice I usually end up with about 20 to 30, so it's not a big
deal.</p>
<p>To sum up this function: it iterates through every coordinate in the level
that's displayed in the viewport, looks up its region number in the region map,
and draws the appropriate letter if it has a region number.</p>
<h2 id="s4-results"><a href="index.html#s4-results">Results</a></h2>
<p>Now that I've got a way to visualize regions it becomes much easier to check
whether they're getting set correctly. Here's an example of what it looks like
when you toggle <code>:show-regions</code> with <code>R</code>:</p>
<p><img src="../../../../static/images/blog/2012/10/caves-07-1-1.png" alt="Screenshot without Regions"></p>
<p><img src="../../../../static/images/blog/2012/10/caves-07-1-2.png" alt="Screenshot with Regions"></p>
<p>As you can see, the small, closed off areas have their own numbers, while the
larger regions sprawl across the map.</p>
<p>You can view the code <a href="https://github.com/sjl/caves/tree/entry-07-1/src/caves">on GitHub</a> if you want to see the end
result.</p>
<p>The next article will finish Trystan's seventh post by adding multiple z-levels
to the caves.</p>
</article></main><hr class='main-separator' /><footer><nav><a href='https://github.com/sjl/'>GitHub</a><a href='https://twitter.com/stevelosh/'>Twitter</a><a href='https://instagram.com/thirtytwobirds/'>Instagram</a><a href='https://hg.stevelosh.com/.plan/'>.plan</a></nav></footer></body></html>