emacs.d/clones/lisp/stevelosh.com/blog/2012/07/caves-of-clojure-03-2/index.html
2022-10-07 15:47:14 +02:00

249 lines
No EOL
20 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 3.2 / 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 3.2</a></h1><p class='date'>Posted on July 10th, 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="../caves-of-clojure-01/index.html">the beginning</a>.</p>
<p>This entry corresponds to <a href="http://trystans.blogspot.com/2011/08/roguelike-tutorial-03-scrolling-through.html">post three 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-03-2</code> tag to see the code as it stands
after this post.</p>
<ol class="table-of-contents"><li><a href="index.html#s1-summary">Summary</a></li><li><a href="index.html#s2-debugging">Debugging</a></li><li><a href="index.html#s3-smoothing">Smoothing</a></li><li><a href="index.html#s4-interactive-development">Interactive Development</a></li><li><a href="index.html#s5-results">Results</a></li></ol>
<h2 id="s1-summary"><a href="index.html#s1-summary">Summary</a></h2>
<p>When the last post left off I had a random world generated, but it wasn't very
pretty. Every tile had an equal chance of being a wall or a floor, which
results in an uninteresting &quot;white noise&quot; of rock. Not a very nice setting for
a roguelike.</p>
<p>This post is going to show how I added Trystan's world smoothing to make
nicer-looking caves. He uses a <a href="http://roguebasin.roguelikedevelopment.org/index.php?title=Cellular_Automata_Method_for_Generating_Random_Cave-Like_Levels">cellular automata-based world-smoothing
algorithm</a> that I think is really cool, so I'm going to do pretty much
the same thing.</p>
<h2 id="s2-debugging"><a href="index.html#s2-debugging">Debugging</a></h2>
<p>Let's jump right in. The world smoothing code is going to go in <code>world.clj</code>.</p>
<p>Before I start writing the actual smoothing code, I added two helper functions
to print worlds to the console so I could see what I was doing:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> print-row <span class="paren2">[<span class="code">row</span>]</span>
<span class="paren2">(<span class="code">println <span class="paren3">(<span class="code">apply str <span class="paren4">(<span class="code">map <span class="keyword">:glyph</span> row</span>)</span></span>)</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> print-world <span class="paren2">[<span class="code">world</span>]</span>
<span class="paren2">(<span class="code">dorun <span class="paren3">(<span class="code">map print-row <span class="paren4">(<span class="code"><span class="keyword">:tiles</span> world</span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
<p>Simple, but very helpful because <code>(:tiles world)</code> contains <code>Tile</code> records
instead of just the raw glyphs, so printing it without these helpers makes it
impossible to read.</p>
<h2 id="s3-smoothing"><a href="index.html#s3-smoothing">Smoothing</a></h2>
<p>Okay, on to the real code. I'll go through it from the bottom up this time
because I think it's easier to understand that way.</p>
<p>First I added a <code>smooth-world</code> function that will be what I eventually need to
call repeatedly to smooth out the terrain:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> smooth-world <span class="paren2">[<span class="code"><span class="paren3">{<span class="code"><span class="keyword">:keys</span> <span class="paren4">[<span class="code">tiles</span>]</span> <span class="keyword">:as</span> world}</span>]</span>
<span class="paren3">(<span class="code">assoc world <span class="keyword">:tiles</span> <span class="paren4">(<span class="code">get-smoothed-tiles tiles</span>)</span></span>)</span></span>)</span></span></span></span></code></pre>
<p>It's pretty much a helper function that handles getting the tile map in and out
of the world object. The smoothing process only cares about the tile map, not
anything else the world might later contain.</p>
<p>Next up:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> get-smoothed-tiles <span class="paren2">[<span class="code">tiles</span>]</span>
<span class="paren2">(<span class="code">mapv <span class="paren3">(<span class="code">fn <span class="paren4">[<span class="code">y</span>]</span>
<span class="paren4">(<span class="code">get-smoothed-row tiles y</span>)</span></span>)</span>
<span class="paren3">(<span class="code">range <span class="paren4">(<span class="code">count tiles</span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
<p>I use Clojure 1.4's new <code>mapv</code> function, which is basically a version of <code>map</code>
that creates a vector as the end product instead of a lazy sequence. Our
<code>:tiles</code> object is a vector of vectors going in, so it should be the same coming
out.</p>
<p>I loop map over the row indices. For each row number I get the result of
<code>get-smoothed-row</code>, and the <code>mapv</code> concatenates all of those into a vector for
me, so I end up with <code>[[smoothed row], [smoothed row], ...]</code>.</p>
<p>You might notice that I'm using an index-based approach here. Isn't that a bad
idea in Clojure? Shouldn't I be using sequenced-based things instead?</p>
<p>I spent about twenty minutes trying to get the sequence-based approach in the
Programming Clojure book to work and eventually gave up. It sounds like
a beautiful idea but I couldn't deal with it for a number of reasons:</p>
<ul>
<li>Harder to debug, with infinite padding sequences making some intermediate
steps unprintable.</li>
<li>Very general, which sounds good, but makes it harder to understand because we
can't talk about &quot;the row of tiles&quot; any more but now talk about stuff like
&quot;the sequence of row triples&quot;.</li>
<li>In general just very alien and hard to use for what should be a
straightforward, 10 minute task.</li>
</ul>
<p>Here's an example of a few of the functions from the book I would have been
using if I had gone that route:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> window
<span class="string">&quot;Returns a lazy sequence of 3-item windows centered
around each item of coll, padded as necessary with
pad or nil.&quot;</span>
<span class="paren2">(<span class="code"><span class="paren3">[<span class="code">coll</span>]</span> <span class="paren3">(<span class="code">window nil coll</span>)</span></span>)</span>
<span class="paren2">(<span class="code"><span class="paren3">[<span class="code">pad coll</span>]</span>
<span class="paren3">(<span class="code">partition 3 1 <span class="paren4">(<span class="code">concat <span class="paren5">[<span class="code">pad</span>]</span> coll <span class="paren5">[<span class="code">pad</span>]</span></span>)</span></span>)</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> cell-block
<span class="string">&quot;Creates a sequences of 3x3 windows from a triple of 3 sequences.&quot;</span>
<span class="paren2">[<span class="code"><span class="paren3">[<span class="code">left mid right</span>]</span></span>]</span>
<span class="paren2">(<span class="code">window <span class="paren3">(<span class="code">map vector left mid right</span>)</span></span>)</span></span>)</span></span></code></pre>
<p>I personally find it easier to read things like <code>(get-smoothed-row tiles y)</code>
than <code>(map vector left right mid)</code>. You might feel differently, but this was
what I ended up with because I didn't want to spend a ton of time on the
smoothing process.</p>
<p>Anyway, back to the code. Now I need a way to smooth a single row:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> get-smoothed-row <span class="paren2">[<span class="code">tiles y</span>]</span>
<span class="paren2">(<span class="code">mapv <span class="paren3">(<span class="code">fn <span class="paren4">[<span class="code">x</span>]</span>
<span class="paren4">(<span class="code">get-smoothed-tile <span class="paren5">(<span class="code">get-block tiles x y</span>)</span></span>)</span></span>)</span>
<span class="paren3">(<span class="code">range <span class="paren4">(<span class="code">count <span class="paren5">(<span class="code">first tiles</span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
<p>Once again I use <code>mapv</code> because a row needs to be a vector. This time I'm
mapping over the column indices, but for the most part it's very similar to the
previous function.</p>
<p>I need a function to smooth a tile, but first I need a way to get a &quot;block&quot; of
tiles.</p>
<p>The basic rule I'm using for the smoothing comes from the <a href="http://roguebasin.roguelikedevelopment.org/index.php?title=Cellular_Automata_Method_for_Generating_Random_Cave-Like_Levels">page about cellular
automata smoothing on RogueBasin</a>:</p>
<p>A tile will become a floor tile if and only if the 3 by 3 square of tiles
centered on it contains 5 or more floor tiles.</p>
<p>This means I need a way to get the 3 by 3 block of tiles centered on any given
tile:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> block-coords <span class="paren2">[<span class="code">x y</span>]</span>
<span class="paren2">(<span class="code">for <span class="paren3">[<span class="code">dx <span class="paren4">[<span class="code">-1 0 1</span>]</span>
dy <span class="paren4">[<span class="code">-1 0 1</span>]</span></span>]</span>
<span class="paren3">[<span class="code"><span class="paren4">(<span class="code">+ x dx</span>)</span> <span class="paren4">(<span class="code">+ y dy</span>)</span></span>]</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> get-block <span class="paren2">[<span class="code">tiles x y</span>]</span>
<span class="paren2">(<span class="code">map <span class="paren3">(<span class="code">fn <span class="paren4">[<span class="code"><span class="paren5">[<span class="code">x y</span>]</span></span>]</span>
<span class="paren4">(<span class="code">get-tile tiles x y</span>)</span></span>)</span>
<span class="paren3">(<span class="code">block-coords x y</span>)</span></span>)</span></span>)</span></span></code></pre>
<p>First we have a helper function that returns the coordinates of all the tiles
we're going to look at. For example, if you pass it <code>[22 30]</code> it will return:</p>
<pre><code><span class="code">[<span class="paren1">[<span class="code">21 29</span>]</span> <span class="paren1">[<span class="code">22 29</span>]</span> <span class="paren1">[<span class="code">23 29</span>]</span>
<span class="paren1">[<span class="code">21 30</span>]</span> <span class="paren1">[<span class="code">22 30</span>]</span> <span class="paren1">[<span class="code">23 30</span>]</span>
<span class="paren1">[<span class="code">21 31</span>]</span> <span class="paren1">[<span class="code">22 31</span>]</span> <span class="paren1">[<span class="code">23 31</span>]</span>]</span></code></pre>
<p>Note that <code>get-block</code> doesn't do any bounds checking, so passing it <code>[0 0]</code> will
happily return coordinates like <code>[-1 -1]</code>, which are off the edge of the map.</p>
<p>This isn't a problem because our <code>get-tile</code> method will return <code>:bound</code> tiles
for those coordinates, which are not <code>:floor</code> tiles and so are effectively walls
for the purposes of this algorithm.</p>
<p><code>get-block</code> itself is just a glue function that gets coordinates from
<code>block-coords</code> and maps <code>get-tile</code> over them.</p>
<p>So now I have a way to get a sequence of all the tiles in a block centered
around a target. The last step is actually figuring out what the resulting
block for that target should be:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> get-smoothed-tile <span class="paren2">[<span class="code"><i><span class="symbol">block</span></i></span>]</span>
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">[<span class="code">tile-counts <span class="paren4">(<span class="code">frequencies <span class="paren5">(<span class="code">map <span class="keyword">:kind</span> <i><span class="symbol">block</span></i></span>)</span></span>)</span>
floor-threshold 5
floor-count <span class="paren4">(<span class="code">get tile-counts <span class="keyword">:floor</span> 0</span>)</span>
result <span class="paren4">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren5">(<span class="code">&gt;= floor-count floor-threshold</span>)</span>
<span class="keyword">:floor</span>
<span class="keyword">:wall</span></span>)</span></span>]</span>
<span class="paren3">(<span class="code">tiles result</span>)</span></span>)</span></span>)</span></span></code></pre>
<p>This looks long, but that's mostly because I like using named intermediate
variables to make it more readable. It should be pretty easy to understand,
just go ahead and read through it.</p>
<p>So now the <code>smooth-world</code> function has all the machinery it needs to smooth
a world. The last step is to actually <em>use</em> it. I changed the <code>random-world</code>
function to look like this:</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">new World <span class="paren5">(<span class="code">random-tiles</span>)</span></span>)</span>
world <span class="paren4">(<span class="code">nth <span class="paren5">(<span class="code">iterate smooth-world world</span>)</span> 0</span>)</span></span>]</span>
world</span>)</span></span>)</span></span></code></pre>
<p>At the moment it takes the zeroth iteration, which actually means the unsmoothed
world. What gives?</p>
<h2 id="s4-interactive-development"><a href="index.html#s4-interactive-development">Interactive Development</a></h2>
<p>I wasn't sure right away how much smoothing would look good, so I wanted to try
out a bunch of levels and see how they behaved. I could have done it by
printing to the console, but it's a pain to compare the multiple hunks of text.</p>
<p>I decided to just add it to the game itself for now to make it easy to see how
the smoothing behaves. Back in <code>core.clj</code> I pulled in the <code>smooth-world</code>
function:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code">ns caves.core
<span class="paren2">(<span class="code"><span class="keyword">:use</span> <span class="paren3">[<span class="code">caves.world <span class="keyword">:only</span> <span class="paren4">[<span class="code">random-world smooth-world</span>]</span></span>]</span></span>)</span>
<span class="paren2">(<span class="code"><span class="keyword">:require</span> <span class="paren3">[<span class="code">lanterna.screen <span class="keyword">:as</span> s</span>]</span></span>)</span></span>)</span></span></code></pre>
<p>Next I added another command to the <code>:play</code> UI: pressing <code>s</code> will smooth the
current world by one more level:</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">new 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">new UI <span class="keyword">:lose</span></span>)</span></span>]</span></span>)</span>
\s <span class="paren3">(<span class="code">assoc game <span class="keyword">:world</span> <span class="paren4">(<span class="code">smooth-world <span class="paren5">(<span class="code"><span class="keyword">:world</span> game</span>)</span></span>)</span></span>)</span>
game</span>)</span></span>)</span></span></code></pre>
<p>Yes, it only took one line to add that. I simply replace the world with the
smooth(er) world and return the resulting game. I don't need to touch the UI
stack because I want to remain at the play UI for subsequent commands.</p>
<p>I'm really liking this immutable game structure so far!</p>
<h2 id="s5-results"><a href="index.html#s5-results">Results</a></h2>
<p>Once you fire up the game and press a key to begin, you're presented with the
white-noise map from the last entry:</p>
<p><img src="../../../../static/images/blog/2012/07/caves-03-2-01.png" alt="Screenshot"></p>
<p>But now you can press <code>s</code> and the caves will smooth out a bit:</p>
<p><img src="../../../../static/images/blog/2012/07/caves-03-2-02.png" alt="Screenshot"></p>
<p>Another press of <code>s</code> smooths them further:</p>
<p><img src="../../../../static/images/blog/2012/07/caves-03-2-03.png" alt="Screenshot"></p>
<p>You can use enter or backspace to win or lose, then any key to go back to the
start screen and get a new world to play with.</p>
<p>Screenshots really don't do this justice, because seeing the world change before
your eyes is <em>really</em> cool. I made a 30-second <a href="http://www.screenr.com/FSk8">screencast</a> that demonstrates
the effect if you don't want to actually run it locally.</p>
<p>I still haven't decided exactly how smooth I want to make the caves, so I'll
leave that <code>0</code> in the <code>nth</code> call for now and figure it out later.</p>
<p>You can view the code <a href="https://github.com/sjl/caves/tree/entry-03-2/src/caves">on GitHub</a> if you want to see it all at
once.</p>
<p>Next post: scrolling!</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>