293 lines
No EOL
23 KiB
HTML
293 lines
No EOL
23 KiB
HTML
<!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 "regions" 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 "region map", 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">"Filter the given coordinates to include only walkable ones."</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">"Return the neighboring coordinates walkable from the given coord."</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 "flood
|
|
fill" 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">"Return all coordinates walkable from the given coord (including itself)."</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">->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: "debug flags".</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->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">->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">->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">->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">"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"</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> |