437 lines
37 KiB
HTML
437 lines
37 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 5 / 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 5</a></h1><p class='date'>Posted on July 13th, 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/09/roguelike-tutorial-05-stationary.html">post five 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-05</code> tag to see the code as it stands
|
||
|
after this post.</p>
|
||
|
|
||
|
<p>Also, I live streamed myself writing the code that this entry is based on. You
|
||
|
can view the recordings <a href="http://www.twitch.tv/stevelosh/">on twitch.tv</a>, though
|
||
|
as I write this the video links are stuck in an infinite HTTP redirect loop.
|
||
|
Perhaps they will be fixed eventually.</p>
|
||
|
|
||
|
<p>Finally, I've started hanging out in <code>##cavesofclojure</code> on Freenode if you have
|
||
|
questions. I may or may not be around at any given point.</p>
|
||
|
|
||
|
<ol class="table-of-contents"><li><a href="index.html#s1-summary">Summary</a></li><li><a href="index.html#s2-multiple-entities">Multiple Entities</a></li><li><a href="index.html#s3-lichens">Lichens</a></li><li><a href="index.html#s4-populating-the-world">Populating the World</a></li><li><a href="index.html#s5-drawing-the-entities">Drawing the Entities</a></li><li><a href="index.html#s6-movement">Movement</a></li><li><a href="index.html#s7-killing">Killing</a></li><li><a href="index.html#s8-growing-lichens">Growing Lichens</a></li><li><a href="index.html#s9-results">Results</a></li></ol>
|
||
|
|
||
|
<h2 id="s1-summary"><a href="index.html#s1-summary">Summary</a></h2>
|
||
|
|
||
|
<p>In Trystan's fifth post he adds three things:</p>
|
||
|
|
||
|
<ul>
|
||
|
<li>A stationary monster</li>
|
||
|
<li>Attacking</li>
|
||
|
<li>A growing mechanic for the monster</li>
|
||
|
</ul>
|
||
|
|
||
|
<p>I'm going to add all three of those too, though I'll be doing it in the
|
||
|
entities/aspects fashion of the previous post.</p>
|
||
|
|
||
|
<h2 id="s2-multiple-entities"><a href="index.html#s2-multiple-entities">Multiple Entities</a></h2>
|
||
|
|
||
|
<p>First thing's first: I need to change a bit of code around to account for having
|
||
|
multiple entities instead of just a single player.</p>
|
||
|
|
||
|
<p>At the end of the previous post the player was stored in the world directly,
|
||
|
meaning the <code>game</code> object looked like this:</p>
|
||
|
|
||
|
<pre><code><span class="code">{:uis <span class="paren1">[<span class="code">...</span>]</span>
|
||
|
:world <span class="paren1">{<span class="code"><span class="keyword">:player</span> <span class="paren2">{<span class="code">...}
|
||
|
<span class="keyword">:tiles</span> <span class="paren3">[<span class="code">...</span>]</span></span>}</span></span>}</span></span></code></pre>
|
||
|
|
||
|
<p>I decided to remove some (but not all) of the special casing for the player and
|
||
|
make them an entity like any other. The <code>game</code> object is now structured like
|
||
|
this:</p>
|
||
|
|
||
|
<pre><code><span class="code">{:uis <span class="paren1">[<span class="code">...</span>]</span>
|
||
|
:world <span class="paren1">{<span class="code"><span class="keyword">:entities</span> <span class="paren2">{<span class="code"><span class="keyword">:player</span> <span class="paren3">{<span class="code">...}}
|
||
|
<span class="keyword">:tiles</span> <span class="paren4">[<span class="code">...</span>]</span></span>}</span></span>}</span></span></span></span></code></pre>
|
||
|
|
||
|
<p>Notice that the player has been moved into an <code>:entities</code> map, keyed by its id
|
||
|
(which is still the special-cased <code>:player</code>). I updated anywhere that needed to
|
||
|
change to account for this, and made sure it still worked. This was pretty easy
|
||
|
to do by just searching for <code>:player</code>, as the codebase is still small.</p>
|
||
|
|
||
|
<h2 id="s3-lichens"><a href="index.html#s3-lichens">Lichens</a></h2>
|
||
|
|
||
|
<p>Now it's time to actually add another type of entity. I play a lot of Nethack,
|
||
|
so naturally I decided to make it a simple lichen. I added
|
||
|
<code>entities/lichen.clj</code>:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">ns caves.entities.lichen
|
||
|
<span class="paren2">(<span class="code"><span class="keyword">:use</span> <span class="paren3">[<span class="code">caves.entities.core <span class="keyword">:only</span> <span class="paren4">[<span class="code">Entity get-id</span>]</span></span>]</span></span>)</span></span>)</span>
|
||
|
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defrecord</span></i> Lichen <span class="paren2">[<span class="code">id glyph location</span>]</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> make-lichen <span class="paren2">[<span class="code">location</span>]</span>
|
||
|
<span class="paren2">(<span class="code">->Lichen <span class="paren3">(<span class="code">get-id</span>)</span> <span class="string">"F"</span> location</span>)</span></span>)</span>
|
||
|
|
||
|
|
||
|
<span class="paren1">(<span class="code">extend-type Lichen Entity
|
||
|
<span class="paren2">(<span class="code">tick <span class="paren3">[<span class="code">this world</span>]</span>
|
||
|
<span class="paren3">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren4">(<span class="code">should-grow</span>)</span>
|
||
|
world</span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Like the <code>Player</code>, <code>Lichen</code>s implement the <code>Entity</code> protocol. For now they
|
||
|
don't do anything special during a tick.</p>
|
||
|
|
||
|
<p>You may have noticed the new <code>get-id</code> function. Entities must have IDs so I can
|
||
|
get them in and out of the entity map. The player has a special ID of
|
||
|
<code>:player</code>, but I needed a way to get a unique ID for other entities.</p>
|
||
|
|
||
|
<p>The simplest way I could think of was to use a simple counter over in
|
||
|
<code>entities/core.clj</code>:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">ns caves.entities.core</span>)</span>
|
||
|
|
||
|
|
||
|
<span class="paren1">(<span class="code">def ids <span class="paren2">(<span class="code">ref 0</span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defprotocol</span></i> Entity
|
||
|
<span class="paren2">(<span class="code">tick <span class="paren3">[<span class="code">this world</span>]</span>
|
||
|
<span class="string">"Update the world to handle the passing of a tick for this entity."</span></span>)</span></span>)</span>
|
||
|
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> get-id <span class="paren2">[<span class="code"></span>]</span>
|
||
|
<span class="paren2">(<span class="code">dosync
|
||
|
<span class="paren3">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren4">[<span class="code">id @ids</span>]</span>
|
||
|
<span class="paren4">(<span class="code">alter ids inc</span>)</span>
|
||
|
id</span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Not the prettiest solution, but it works. I might switch to a UUID library or
|
||
|
something in the future, but this'll do for now.</p>
|
||
|
|
||
|
<h2 id="s4-populating-the-world"><a href="index.html#s4-populating-the-world">Populating the World</a></h2>
|
||
|
|
||
|
<p>Unlike the <code>make-player</code> function, <code>make-lichen</code> takes a location directly
|
||
|
instead of trying to find an empty space for itself in the world. I figured it
|
||
|
was better to not have entities deciding where they emerge in the world all the
|
||
|
time! I went ahead and refactored <code>make-player</code> to act like this as well.</p>
|
||
|
|
||
|
<p>During this coding session I wasn't actually running and playing the full game
|
||
|
through as much as I should have been. I think as I get more and more of the
|
||
|
basic structure of the game in place I'll be able to do this more. Up to now
|
||
|
I've been doing large, sweeping refactorings that touch many different pieces of
|
||
|
code and break everything until they're finished.</p>
|
||
|
|
||
|
<p>Anyway, back to the world. I need a way to spawn some lichens in the world, so
|
||
|
I edited the <code>reset-game</code> function in <code>input.clj</code>:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> add-lichen <span class="paren2">[<span class="code">world</span>]</span>
|
||
|
<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"><span class="keyword">:as</span> lichen <span class="keyword">:keys</span> <span class="paren5">[<span class="code">id</span>]</span></span>}</span> <span class="paren4">(<span class="code">make-lichen <span class="paren5">(<span class="code">find-empty-tile world</span>)</span></span>)</span></span>]</span>
|
||
|
<span class="paren3">(<span class="code">assoc-in world <span class="paren4">[<span class="code"><span class="keyword">:entities</span> id</span>]</span> lichen</span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> populate-world <span class="paren2">[<span class="code">world</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">assoc-in world <span class="paren5">[<span class="code"><span class="keyword">:entities</span> <span class="keyword">:player</span></span>]</span>
|
||
|
<span class="paren5">(<span class="code">make-player <span class="paren6">(<span class="code">find-empty-tile world</span>)</span></span>)</span></span>)</span>
|
||
|
world <span class="paren4">(<span class="code">nth <span class="paren5">(<span class="code">iterate add-lichen world</span>)</span> 30</span>)</span></span>]</span>
|
||
|
world</span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> reset-game <span class="paren2">[<span class="code">game</span>]</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">[<span class="code">fresh-world <span class="paren4">(<span class="code">random-world</span>)</span></span>]</span>
|
||
|
<span class="paren3">(<span class="code">-> game
|
||
|
<span class="paren4">(<span class="code">assoc <span class="keyword">:world</span> fresh-world</span>)</span>
|
||
|
<span class="paren4">(<span class="code">update-in <span class="paren5">[<span class="code"><span class="keyword">:world</span></span>]</span> populate-world</span>)</span>
|
||
|
<span class="paren4">(<span class="code">assoc <span class="keyword">:uis</span> <span class="paren5">[<span class="code"><span class="paren6">(<span class="code">->UI <span class="keyword">:play</span></span>)</span></span>]</span></span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>It should be pretty easy to read. <code>add-lichen</code> adds a new lichen to an empty
|
||
|
tile. <code>populate-world</code> takes a world and adds a player, then 30 lichens.</p>
|
||
|
|
||
|
<p>This is getting to be a bit much to keep in <code>input.clj</code>. I'll probably pull
|
||
|
this out into a separate file soon.</p>
|
||
|
|
||
|
<h2 id="s5-drawing-the-entities"><a href="index.html#s5-drawing-the-entities">Drawing the Entities</a></h2>
|
||
|
|
||
|
<p>So now the lichens are part of the world, but I still need to draw them on the
|
||
|
screen. I split the <code>draw-player</code> function in <code>drawing.clj</code> into two separate
|
||
|
functions:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> draw-entity <span class="paren2">[<span class="code">screen start-x start-y <span class="paren3">{<span class="code"><span class="keyword">:keys</span> <span class="paren4">[<span class="code">location glyph color</span>]</span></span>}</span></span>]</span>
|
||
|
<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">entity-x entity-y</span>]</span> location
|
||
|
x <span class="paren4">(<span class="code">- entity-x start-x</span>)</span>
|
||
|
y <span class="paren4">(<span class="code">- entity-y start-y</span>)</span></span>]</span>
|
||
|
<span class="paren3">(<span class="code">s/put-string screen x y glyph <span class="paren4">{<span class="code"><span class="keyword">:fg</span> color}</span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">defn</span></i> highlight-player <span class="paren3">[<span class="code">screen start-x start-y player</span>]</span>
|
||
|
<span class="paren3">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren4">[<span class="code"><span class="paren5">[<span class="code">player-x player-y</span>]</span> <span class="paren5">(<span class="code"><span class="keyword">:location</span> player</span>)</span>
|
||
|
x <span class="paren5">(<span class="code">- player-x start-x</span>)</span>
|
||
|
y <span class="paren5">(<span class="code">- player-y start-y</span>)</span></span>]</span>
|
||
|
<span class="paren4">(<span class="code">s/move-cursor screen x y</span>)</span></span>)</span></span>)</span></span></span></span></code></pre>
|
||
|
|
||
|
<p>And then I use those in the main <code>draw-ui</code> function for the <code>:play</code> UI:</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</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> screen-size
|
||
|
vcols cols
|
||
|
vrows <span class="paren4">(<span class="code">dec rows</span>)</span>
|
||
|
<span class="paren4">[<span class="code">start-x start-y end-x end-y</span>]</span> <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 start-x start-y end-x end-y tiles</span>)</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 start-x start-y entity</span>)</span></span>)</span>
|
||
|
<span class="paren3">(<span class="code">draw-hud screen game start-x start-y</span>)</span>
|
||
|
<span class="paren3">(<span class="code">highlight-player screen start-x start-y player</span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Long but straightforward. I'm going to be cleaning this part of the code up
|
||
|
very soon, as I've just added a bunch of really useful stuff to the
|
||
|
<a href="https://sjl.bitbucket.io/clojure-lanterna/">clojure-lanterna</a> library that will let me delete a bunch of fiddly code
|
||
|
here.</p>
|
||
|
|
||
|
<p>If you're particularly eagle-eyed you might have noticed this new <code>color</code>
|
||
|
attribute that seems to be a part of entities. I didn't realize I had forgotten
|
||
|
to specify colors until I actually wrote this bit of code. Once I did I went
|
||
|
back and added the field to the <code>Player</code> and <code>Lichen</code> records, as well as
|
||
|
<code>make-player</code> and <code>make-lichen</code>.</p>
|
||
|
|
||
|
<p>Now the lichens appear on the screen!</p>
|
||
|
|
||
|
<p><img src="../../../../static/images/blog/2012/07/caves-05-01.png" alt="Screenshot"></p>
|
||
|
|
||
|
<h2 id="s6-movement"><a href="index.html#s6-movement">Movement</a></h2>
|
||
|
|
||
|
<p>At this point the lichens are on the screen but the player can walk straight
|
||
|
through them. I took care of that by first creating a few helper functions in
|
||
|
<code>world.clj</code>:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> get-entity-at <span class="paren2">[<span class="code">world coord</span>]</span>
|
||
|
<span class="paren2">(<span class="code">first <span class="paren3">(<span class="code">filter #<span class="paren4">(<span class="code">= coord <span class="paren5">(<span class="code"><span class="keyword">:location</span> %</span>)</span></span>)</span>
|
||
|
<span class="paren4">(<span class="code">vals <span class="paren5">(<span class="code"><span class="keyword">:entities</span> world</span>)</span></span>)</span></span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> is-empty? <span class="paren2">[<span class="code">world coord</span>]</span>
|
||
|
<span class="paren2">(<span class="code">and <span class="paren3">(<span class="code">#<span class="paren4">{<span class="code"><span class="keyword">:floor}</span> <span class="paren5">(<span class="code">get-tile-kind world coord</span>)</span></span>)</span>
|
||
|
<span class="paren4">(<span class="code">not <span class="paren5">(<span class="code">get-entity-at world coord</span>)</span></span>)</span></span>)</span></span>)</span></span></span></span></code></pre>
|
||
|
|
||
|
<p>They'll handle the grunt world of traversing the <code>world</code> data structure. Then
|
||
|
I updated the player's <code>can-move?</code> function:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">extend-type Player Mobile
|
||
|
<span class="paren2">(<span class="code">move ...</span>)</span>
|
||
|
<span class="paren2">(<span class="code">can-move? <span class="paren3">[<span class="code">this world dest</span>]</span>
|
||
|
<span class="paren3">(<span class="code">is-empty? world dest</span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Previously <code>can-move</code> checked the world's tile itself — now it delegates to
|
||
|
a basic helper function instead. I have a feeling a lot of things are going to
|
||
|
need to use the idea of "empty tiles" so this function will probably get a lot
|
||
|
of mileage.</p>
|
||
|
|
||
|
<p>Now the player can't walk through fungus. Great.</p>
|
||
|
|
||
|
<h2 id="s7-killing"><a href="index.html#s7-killing">Killing</a></h2>
|
||
|
|
||
|
<p>It's time to give the player a way to cut the lichens into little licheny bits.
|
||
|
I implemented this with another pair of aspects: <code>Attacker</code> and <code>Destructible</code>.</p>
|
||
|
|
||
|
<p><code>Attacker</code> should be implemented by anything that can attack other things:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">ns caves.entities.aspects.attacker</span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defprotocol</span></i> Attacker
|
||
|
<span class="paren2">(<span class="code">attack <span class="paren3">[<span class="code">this world target</span>]</span>
|
||
|
<span class="string">"Attack the target."</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p><code>Destructible</code> should be implemented by anything that can "take damage and go
|
||
|
away once it takes enough":</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">ns caves.entities.aspects.destructible</span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defprotocol</span></i> Destructible
|
||
|
<span class="paren2">(<span class="code">take-damage <span class="paren3">[<span class="code">this world damage</span>]</span>
|
||
|
<span class="string">"Take the given amount of damage and update the world appropriately."</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Lichens will be <code>Destructible</code> (for now the player will remain invincible):</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">extend-type Lichen Destructible
|
||
|
<span class="paren2">(<span class="code">take-damage <span class="paren3">[<span class="code"><span class="paren4">{<span class="code"><span class="keyword">:keys</span> <span class="paren5">[<span class="code">id</span>]</span> <span class="keyword">:as</span> this} world damage</span>]</span>
|
||
|
<span class="paren4">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren5">[<span class="code">damaged-this <span class="paren6">(<span class="code">update-in this <span class="paren1">[<span class="code"><span class="keyword">:hp</span></span>]</span> - damage</span>)</span></span>]</span>
|
||
|
<span class="paren5">(<span class="code">if-not <span class="paren6">(<span class="code">pos? <span class="paren1">(<span class="code"><span class="keyword">:hp</span> damaged-this</span>)</span></span>)</span>
|
||
|
<span class="paren6">(<span class="code">update-in world <span class="paren1">[<span class="code"><span class="keyword">:entities</span></span>]</span> dissoc id</span>)</span>
|
||
|
<span class="paren6">(<span class="code">update-in world <span class="paren1">[<span class="code"><span class="keyword">:entities</span> id</span>]</span> assoc damaged-this</span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span></span></span></code></pre>
|
||
|
|
||
|
<p>The logic here is pretty basic. When a <code>Destructible</code> entity takes some damage,
|
||
|
first its hit points are updated. If they wind up to be zero or fewer, the
|
||
|
entity gracefully removes itself from the world.</p>
|
||
|
|
||
|
<p>I have a feeling there's a more elegant way to write the updatey bits of that
|
||
|
function. If you've got suggestions please let me know.</p>
|
||
|
|
||
|
<p>If I'm going to be basing damage on the entity's <code>:hp</code> attribute they'd better
|
||
|
have one! I added a simple <code>:hp</code> of <code>1</code> to lichens:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defrecord</span></i> Lichen <span class="paren2">[<span class="code">id glyph color location hp</span>]</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> make-lichen <span class="paren2">[<span class="code">location</span>]</span>
|
||
|
<span class="paren2">(<span class="code">->Lichen <span class="paren3">(<span class="code">get-id</span>)</span> <span class="string">"F"</span> <span class="keyword">:green</span> location 1</span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Next I added the corresponding implementation of <code>Attacker</code> to the <code>Player</code> (for
|
||
|
now lichens can't strike back):</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">extend-type Player Attacker
|
||
|
<span class="paren2">(<span class="code">attack <span class="paren3">[<span class="code">this world target</span>]</span>
|
||
|
<span class="paren3">{<span class="code"><span class="keyword">:pre</span> <span class="paren4">[<span class="code"><span class="paren5">(<span class="code">satisfies? Destructible target</span>)</span></span>]</span></span>}</span>
|
||
|
<span class="paren3">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren4">[<span class="code">damage 1</span>]</span>
|
||
|
<span class="paren4">(<span class="code">take-damage target world damage</span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Again, a very basic system for the moment: all attacks do one damage. Lichens
|
||
|
only have one hit point, so this will kill them instantly.</p>
|
||
|
|
||
|
<p>Notice the precondition here: an attacker can attack something if and only if
|
||
|
it's something that satisfies the <code>Destructible</code> protocol.</p>
|
||
|
|
||
|
<p>Instead of doing something like checking if the target has <code>:hp</code> I simply check
|
||
|
if it's <code>Destructible</code>. This opens the door for things that don't necessarily
|
||
|
use hit points, like a monster whose mana and hit points are a single number.</p>
|
||
|
|
||
|
<p>Finally, I need to hook up the attacking functionality in the <code>move-player</code>
|
||
|
helper function:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> move-player <span class="paren2">[<span class="code">world dir</span>]</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">[<span class="code">player <span class="paren4">(<span class="code">get-in world <span class="paren5">[<span class="code"><span class="keyword">:entities</span> <span class="keyword">:player</span></span>]</span></span>)</span>
|
||
|
target <span class="paren4">(<span class="code">destination-coords <span class="paren5">(<span class="code"><span class="keyword">:location</span> player</span>)</span> dir</span>)</span>
|
||
|
entity-at-target <span class="paren4">(<span class="code">get-entity-at world target</span>)</span></span>]</span>
|
||
|
<span class="paren3">(<span class="code"><i><span class="symbol">cond</span></i>
|
||
|
entity-at-target <span class="paren4">(<span class="code">attack player world entity-at-target</span>)</span>
|
||
|
<span class="paren4">(<span class="code">can-move? player world target</span>)</span> <span class="paren4">(<span class="code">move player world target</span>)</span>
|
||
|
<span class="paren4">(<span class="code">can-dig? player world target</span>)</span> <span class="paren4">(<span class="code">dig player world target</span>)</span>
|
||
|
<span class="keyword">:else</span> world</span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>This once again overloads the <code>hjkl</code> keys, so now the player will attack
|
||
|
a monster when they try to move into it. Otherwise the player will move or dig
|
||
|
as before.</p>
|
||
|
|
||
|
<h2 id="s8-growing-lichens"><a href="index.html#s8-growing-lichens">Growing Lichens</a></h2>
|
||
|
|
||
|
<p>Now for the last part of Trystan's post. Lichens should have a chance of
|
||
|
spreading slowly every turn. Unlike Trystan, I'm not going to limit the number
|
||
|
of times the lichen can spread, so the player will need to use their newfound
|
||
|
attacking ability if they want to stem the tide of invading fungus!</p>
|
||
|
|
||
|
<p>This turned out to be surprisingly painless:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> should-grow <span class="paren2">[<span class="code"></span>]</span>
|
||
|
<span class="paren2">(<span class="code">< <span class="paren3">(<span class="code">rand</span>)</span> 0.01</span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> grow <span class="paren2">[<span class="code">lichen world</span>]</span>
|
||
|
<span class="paren2">(<span class="code">if-let <span class="paren3">[<span class="code">target <span class="paren4">(<span class="code">find-empty-neighbor world <span class="paren5">(<span class="code"><span class="keyword">:location</span> lichen</span>)</span></span>)</span></span>]</span>
|
||
|
<span class="paren3">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren4">[<span class="code">new-lichen <span class="paren5">(<span class="code">make-lichen target</span>)</span></span>]</span>
|
||
|
<span class="paren4">(<span class="code">assoc-in world <span class="paren5">[<span class="code"><span class="keyword">:entities</span> <span class="paren6">(<span class="code"><span class="keyword">:id</span> new-lichen</span>)</span></span>]</span> new-lichen</span>)</span></span>)</span>
|
||
|
world</span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code">extend-type Lichen Entity
|
||
|
<span class="paren2">(<span class="code">tick <span class="paren3">[<span class="code">this world</span>]</span>
|
||
|
<span class="paren3">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren4">(<span class="code">should-grow</span>)</span>
|
||
|
<span class="paren4">(<span class="code">grow this world</span>)</span>
|
||
|
world</span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Every tick, the lichen has a one percent chance to spread to an empty
|
||
|
neighboring tile. If there are no empty neighboring tiles, it can't spread.</p>
|
||
|
|
||
|
<p>The <code>find-empty-neighbor</code> function is new, and located in <code>world.clj</code>:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> find-empty-neighbor <span class="paren2">[<span class="code">world coord</span>]</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">[<span class="code">candidates <span class="paren4">(<span class="code">filter #<span class="paren5">(<span class="code">is-empty? world %</span>)</span> <span class="paren5">(<span class="code">neighbors coord</span>)</span></span>)</span></span>]</span>
|
||
|
<span class="paren3">(<span class="code">when <span class="paren4">(<span class="code">seq candidates</span>)</span>
|
||
|
<span class="paren4">(<span class="code">rand-nth candidates</span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>It uses <code>neighbors</code>, which is another function I created after a quick refactor
|
||
|
of <code>coords.clj</code>:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">ns caves.coords</span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code">def directions
|
||
|
<span class="paren2">{<span class="code"><span class="keyword">:w</span> <span class="paren3">[<span class="code">-1 0</span>]</span>
|
||
|
<span class="keyword">:e</span> <span class="paren3">[<span class="code">1 0</span>]</span>
|
||
|
<span class="keyword">:n</span> <span class="paren3">[<span class="code">0 -1</span>]</span>
|
||
|
<span class="keyword">:s</span> <span class="paren3">[<span class="code">0 1</span>]</span>
|
||
|
<span class="keyword">:nw</span> <span class="paren3">[<span class="code">-1 -1</span>]</span>
|
||
|
<span class="keyword">:ne</span> <span class="paren3">[<span class="code">1 -1</span>]</span>
|
||
|
<span class="keyword">:sw</span> <span class="paren3">[<span class="code">-1 1</span>]</span>
|
||
|
<span class="keyword">:se</span> <span class="paren3">[<span class="code">1 1</span>]</span></span>}</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> offset-coords
|
||
|
<span class="string">"Offset the starting coordinate by the given amount, returning the result coordinate."</span>
|
||
|
<span class="paren2">[<span class="code"><span class="paren3">[<span class="code">x y</span>]</span> <span class="paren3">[<span class="code">dx dy</span>]</span></span>]</span>
|
||
|
<span class="paren2">[<span class="code"><span class="paren3">(<span class="code">+ x dx</span>)</span> <span class="paren3">(<span class="code">+ y dy</span>)</span></span>]</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> dir-to-offset
|
||
|
<span class="string">"Convert a direction to the offset for moving 1 in that direction."</span>
|
||
|
<span class="paren2">[<span class="code">dir</span>]</span>
|
||
|
<span class="paren2">(<span class="code">directions dir</span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> destination-coords
|
||
|
<span class="string">"Take an origin's coords and a direction and return the destination's coords."</span>
|
||
|
<span class="paren2">[<span class="code">origin dir</span>]</span>
|
||
|
<span class="paren2">(<span class="code">offset-coords origin <span class="paren3">(<span class="code">dir-to-offset dir</span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> neighbors
|
||
|
<span class="string">"Return the coordinates of all neighboring squares of the given coord."</span>
|
||
|
<span class="paren2">[<span class="code">origin</span>]</span>
|
||
|
<span class="paren2">(<span class="code">map offset-coords <span class="paren3">(<span class="code">vals directions</span>)</span> <span class="paren3">(<span class="code">repeat origin</span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Nothing too crazy here. The small, composable functions build on top of each
|
||
|
other to create more interesting ones.</p>
|
||
|
|
||
|
<p>But there's one thing left to do, which is actually <code>tick</code> entities in the main
|
||
|
game loop in <code>core.clj</code>:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> tick-entity <span class="paren2">[<span class="code">world entity</span>]</span>
|
||
|
<span class="paren2">(<span class="code">tick entity world</span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> tick-all <span class="paren2">[<span class="code">world</span>]</span>
|
||
|
<span class="paren2">(<span class="code">reduce tick-entity world <span class="paren3">(<span class="code">vals <span class="paren4">(<span class="code"><span class="keyword">:entities</span> world</span>)</span></span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> run-game <span class="paren2">[<span class="code">game screen</span>]</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">loop</span></i> <span class="paren3">[<span class="code"><span class="paren4">{<span class="code"><span class="keyword">:keys</span> <span class="paren5">[<span class="code">input uis</span>]</span> <span class="keyword">:as</span> game} game</span>]</span>
|
||
|
<span class="paren4">(<span class="code">when-not <span class="paren5">(<span class="code">empty? uis</span>)</span>
|
||
|
<span class="paren5">(<span class="code">draw-game game screen</span>)</span>
|
||
|
<span class="paren5">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren6">(<span class="code">nil? input</span>)</span>
|
||
|
<span class="paren6">(<span class="code">recur <span class="paren1">(<span class="code">get-input <span class="paren2">(<span class="code">update-in game <span class="paren3">[<span class="code"><span class="keyword">:world</span></span>]</span> tick-all</span>)</span> screen</span>)</span></span>)</span>
|
||
|
<span class="paren6">(<span class="code">recur <span class="paren1">(<span class="code">process-input <span class="paren2">(<span class="code">dissoc game <span class="keyword">:input</span></span>)</span> input</span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span></span></span></code></pre>
|
||
|
|
||
|
<p>Notice how the <code>tick-all</code> function reduces over the values in the entities map.
|
||
|
Maps aren't deterministically ordered (or at least they're not <em>guaranteed</em> to
|
||
|
be), so this means that our entities may process their ticks in a different
|
||
|
order each turn.</p>
|
||
|
|
||
|
<p>I think I'm okay with that. Yes, it means that ticking the world isn't going to
|
||
|
be a pure function, but it won't be pure no matter what since we're going to
|
||
|
have random numbers involved in attacking and damage soon enough.</p>
|
||
|
|
||
|
<h2 id="s9-results"><a href="index.html#s9-results">Results</a></h2>
|
||
|
|
||
|
<p>All-in-all it took roughly an hour and a half to code the stuff in this entry.
|
||
|
This might sound like a lot, but remember what was added:</p>
|
||
|
|
||
|
<ul>
|
||
|
<li>The entire concept of "multiple entities in a map".</li>
|
||
|
<li>Support for drawing arbitrary entities on the map.</li>
|
||
|
<li>A new creature.</li>
|
||
|
<li>Entities blocking movement of others.</li>
|
||
|
<li>A rudimentary attacking gameplay mechanic.</li>
|
||
|
<li>A rudimentary killing mechanic.</li>
|
||
|
<li>Ticking entities.</li>
|
||
|
<li>Growing/spreading of creatures.</li>
|
||
|
<li>Lots of refactoring and helper functions.</li>
|
||
|
</ul>
|
||
|
|
||
|
<p>Not too bad!</p>
|
||
|
|
||
|
<p>You can view the code <a href="https://github.com/sjl/caves/tree/entry-05/src/caves">on GitHub</a> if you want to see the end
|
||
|
result.</p>
|
||
|
|
||
|
<p>And now some screenshots of our hero cutting a swath through some fungus!</p>
|
||
|
|
||
|
<p><img src="../../../../static/images/blog/2012/07/caves-05-02.png" alt="Screenshot"></p>
|
||
|
|
||
|
<p><img src="../../../../static/images/blog/2012/07/caves-05-03.png" alt="Screenshot"></p>
|
||
|
|
||
|
<p>I'll be moving on to Trystan's sixth post soon, but before that I'm going to
|
||
|
have another interlude where I explain some quick refactoring and then work
|
||
|
a bit of the blackest magic in Clojure: a non-trivial macro.</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>
|