249 lines
20 KiB
HTML
249 lines
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 "white noise" 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 "the row of tiles" any more but now talk about stuff like
|
||
|
"the sequence of row triples".</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">"Returns a lazy sequence of 3-item windows centered
|
||
|
around each item of coll, padded as necessary with
|
||
|
pad or nil."</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">"Creates a sequences of 3x3 windows from a triple of 3 sequences."</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 "block" 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">>= 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>
|