510 lines
39 KiB
HTML
510 lines
39 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: Interlude 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: Interlude 1</a></h1><p class='date'>Posted on July 14th, 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 is an interlude after <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>interlude-1</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-refactoring">Refactoring</a></li><li><a href="index.html#s3-the-problem">The Problem</a></li><li><a href="index.html#s4-the-not-quite-solutions">The Not-Quite Solutions</a></li><li><a href="index.html#s5-the-macro">The Macro</a></li><li><a href="index.html#s6-usage">Usage</a></li><li><a href="index.html#s7-results">Results</a></li></ol>
|
||
|
|
||
|
<h2 id="s1-summary"><a href="index.html#s1-summary">Summary</a></h2>
|
||
|
|
||
|
<p>At the end of the last post I said that this one would be about refactoring and
|
||
|
a macro, so that's what it's going to be.</p>
|
||
|
|
||
|
<h2 id="s2-refactoring"><a href="index.html#s2-refactoring">Refactoring</a></h2>
|
||
|
|
||
|
<p>I don't want to bore you with lots of long chunks of refactored code, so I'll
|
||
|
just outline the changes and if you want to see what happened you can look in
|
||
|
the repo on GitHub.</p>
|
||
|
|
||
|
<p>I ran <a href="https://github.com/jonase/kibit/">Kibit</a> over the code and fixed what it complained.</p>
|
||
|
|
||
|
<p><a href="https://github.com/jdmarble">jdmarble</a> on GitHub pointed out a place where I was using <code>update-in</code> and
|
||
|
could simplify it to <code>assoc-in</code>. This is the kind of thing Kibit should catch,
|
||
|
so I added it in and sent a pull request.</p>
|
||
|
|
||
|
<p><a href="https://github.com/jmgimeno">jmgimeno</a> on GitHub pointed out that I could use an <code>atom</code> for the entity ID
|
||
|
generation instead of a <code>ref</code>. That cleaned up a few lines nicely.</p>
|
||
|
|
||
|
<p>I updated the game loop and moved the call to <code>draw-game</code> so it doesn't get draw
|
||
|
more times than is necessary.</p>
|
||
|
|
||
|
<p>I added a bunch of comments and docstrings throughout the code.</p>
|
||
|
|
||
|
<p>I added a few functions to the latest release of <a href="https://sjl.bitbucket.io/clojure-lanterna/">clojure-lanterna</a> that
|
||
|
allowed me to clean up the UI drawing code. I was able to completely remove the
|
||
|
<code>clear-screen</code> function, and replaced the hardcoded screen size with a dymanic
|
||
|
lookup.</p>
|
||
|
|
||
|
<p>I also changed how the actual screen gets drawn. Look in the repo for the full
|
||
|
details — I think it's much nicer now (though I'm still not 100% happy).</p>
|
||
|
|
||
|
<p>I think that's about it. On to the meat of this post.</p>
|
||
|
|
||
|
<h2 id="s3-the-problem"><a href="index.html#s3-the-problem">The Problem</a></h2>
|
||
|
|
||
|
<p>Right now, the entity system in the Caves works like this:</p>
|
||
|
|
||
|
<ul>
|
||
|
<li>Aspects are Clojure protocols. They define what functions an entity must
|
||
|
implement to have that aspect.</li>
|
||
|
<li>Entity types use <code>extend-type</code> to add the appropriate implementations for the
|
||
|
aspects they want to be.</li>
|
||
|
</ul>
|
||
|
|
||
|
<p>This is all vanilla Clojure, and up until now it's been fine because there was
|
||
|
no crossover between the <code>Lichen</code> aspects and the <code>Player</code> aspects. But what's
|
||
|
going to happen when I create a creature that shares behavior with one or both
|
||
|
of them?</p>
|
||
|
|
||
|
<p>To see the problem, I'm going to create a <code>Bunny</code> entity that will hop around
|
||
|
the screen, and can also be destroyed (assuming the player is a terrible,
|
||
|
terrible person). So I'll create <code>entities/bunny.clj</code>:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">ns caves.entities.bunny</span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defrecord</span></i> Bunny <span class="paren2">[<span class="code">id glyph color location hp</span>]</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code">extend-type Bunny Entity
|
||
|
<span class="paren2">(<span class="code">tick <span class="paren3">[<span class="code">this world</span>]</span>
|
||
|
world</span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>We'll worry about <code>tick</code> soon, but so far, so good. Now I need to let bunnies
|
||
|
move around:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">extend-type Bunny Mobile
|
||
|
<span class="paren2">(<span class="code">move <span class="paren3">[<span class="code">this world dest</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">can-move? this world dest</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> <span class="paren5">(<span class="code"><span class="keyword">:id</span> this</span>)</span> <span class="keyword">:location</span></span>]</span> dest</span>)</span></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>Hmm, where have we seen this code before?</p>
|
||
|
|
||
|
<p>It's an almost exact copy of the <code>Player</code>'s implementation of the <code>Mobile</code>
|
||
|
protocol!</p>
|
||
|
|
||
|
<p>When you think about it, this makes sense. Most of the entities in the game
|
||
|
will have the same implementation for most aspects. The flexibility of Clojure
|
||
|
protocols means I have the power to customize behavior for every one of them,
|
||
|
but it also means that I have to redefine the same behavior over and over.</p>
|
||
|
|
||
|
<p>Or do I?</p>
|
||
|
|
||
|
<h2 id="s4-the-not-quite-solutions"><a href="index.html#s4-the-not-quite-solutions">The Not-Quite Solutions</a></h2>
|
||
|
|
||
|
<p>There are a number of ways I could try to get around this duplication.</p>
|
||
|
|
||
|
<p>First, I could define the "default" implementations as separate, normal
|
||
|
functions, and then the entity-specific implementations could just call those.</p>
|
||
|
|
||
|
<p>This would work, absolutely. It would isolate the generic functionality in one
|
||
|
place successfully. But it means I'd have to manually type out the calls to
|
||
|
those generic functions all the time. This is a Lisp — I can do better.</p>
|
||
|
|
||
|
<p>The next idea is to make <code>Object</code> implement every aspect (with the default
|
||
|
implementations). This isn't ideal for two reasons.</p>
|
||
|
|
||
|
<p>First, it means that the type of an entity is no longer useful. If <code>Object</code>
|
||
|
implements <code>Mobile</code> to provide the default functionality, it means <em>every</em>
|
||
|
entity will effectively be <code>Mobile</code> even if it shouldn't be!</p>
|
||
|
|
||
|
<p>Second, it doesn't even give me everything I want. Observe:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defprotocol</span></i> Foo
|
||
|
<span class="paren2">(<span class="code">hello <span class="paren3">[<span class="code">this</span>]</span></span>)</span>
|
||
|
<span class="paren2">(<span class="code">world <span class="paren3">[<span class="code">this</span>]</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defrecord</span></i> A <span class="paren2">[<span class="code"></span>]</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code">extend-type Object Foo
|
||
|
<span class="paren2">(<span class="code">hello <span class="paren3">[<span class="code">this</span>]</span> <span class="keyword">:hello-object</span></span>)</span>
|
||
|
<span class="paren2">(<span class="code">world <span class="paren3">[<span class="code">this</span>]</span> <span class="keyword">:world-object</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code">extend-type A Foo
|
||
|
<span class="paren2">(<span class="code">hello <span class="paren3">[<span class="code">this</span>]</span> <span class="keyword">:hello-a</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code">def a <span class="paren2">(<span class="code">->A</span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code">hello a</span>)</span> <span class="comment">; Works
|
||
|
</span><span class="paren1">(<span class="code">world a</span>)</span> <span class="comment">; Doesn't work</span></span></code></pre>
|
||
|
|
||
|
<p>In this example, the <code>Foo</code> object doesn't get the benefit of the default
|
||
|
implementation because it implements the protocol itself. So when figuring out
|
||
|
what <code>world</code> function to call Clojure asks "Hmm, does A implement Foo? Oh, it
|
||
|
does? Okay, I'll use A's implementations then".</p>
|
||
|
|
||
|
<p>So the entities would either have to implement all of the aspect functions
|
||
|
(resulting in the duplication of the ones they don't need to change) or none of
|
||
|
them (which <em>would</em> give them the defaults).</p>
|
||
|
|
||
|
<p>So this isn't ideal. I could also have used multimethods here, because they
|
||
|
<em>do</em> support default implementations. But multimethods don't give me a nice
|
||
|
way to group related functions together like protocols.</p>
|
||
|
|
||
|
<p>Protocols also interact with the type system to give me handy functionality
|
||
|
like:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> find-targets
|
||
|
<span class="string">"Find potential things to kill!"</span>
|
||
|
<span class="paren2">[<span class="code">world</span>]</span>
|
||
|
<span class="paren2">(<span class="code">filter #<span class="paren3">(<span class="code">satisfies? Destructible %</span>)</span>
|
||
|
<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></code></pre>
|
||
|
|
||
|
<p>The concept of "bunnies are destructible" is a useful one, and I'd lose it if
|
||
|
I used multimethods.</p>
|
||
|
|
||
|
<h2 id="s5-the-macro"><a href="index.html#s5-the-macro">The Macro</a></h2>
|
||
|
|
||
|
<p>Macros are not something you should reach for right away. They're tricky and
|
||
|
much harder to understand than a normal function. But when all else fails,
|
||
|
they're there as a last resort.</p>
|
||
|
|
||
|
<p>I couldn't figure out a way to do this without macros, so it's time to roll up
|
||
|
my sleeves and work some dark Lispy magic to get a nice syntax.</p>
|
||
|
|
||
|
<p>When I'm writing a macro, my first step is usually to start at the end by
|
||
|
writing out what I want its ultimate usage to be. For this functionality I'm
|
||
|
actually going to need a pair of macros.</p>
|
||
|
|
||
|
<p>First, <code>defaspect</code> will replace <code>defprotocol</code> and allow me to define a protocol
|
||
|
<em>and</em> provide the default implementations:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defaspect</span></i> Mobile
|
||
|
<span class="paren2">(<span class="code">move <span class="paren3">[<span class="code">this world dest</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">can-move? this world dest</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> <span class="paren5">(<span class="code"><span class="keyword">:id</span> this</span>)</span> <span class="keyword">:location</span></span>]</span> dest</span>)</span></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>Those implementations are generic enough (moving moves an entity from their
|
||
|
current space into an empty one) that many entities will probably be able to use
|
||
|
them unchanged.</p>
|
||
|
|
||
|
<p>I'll also need a macro to replace <code>extend-type</code>. I decided to call it
|
||
|
<code>add-aspect</code>:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">add-aspect EarthElemental Mobile
|
||
|
<span class="paren2">(<span class="code">can-move? <span class="paren3">[<span class="code">this world</span>]</span>
|
||
|
<span class="paren3">(<span class="code">entity-at? world dest</span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>In this example the <code>EarthElemental</code> entity is implementing <code>Mobile</code>. It will
|
||
|
use the default <code>move</code> implementation (which just changes its location), but it
|
||
|
overrides <code>can-move?</code>. Earth elementals can't walk through other entities, but
|
||
|
they <em>can</em> walk through the rock walls of the Caves.</p>
|
||
|
|
||
|
<p>So I've got my examples of usage, now it's time to implement the macros. I'll
|
||
|
start with <code>defaspect</code>.</p>
|
||
|
|
||
|
<p>My second step when writing a macro is writing out what the usage should be
|
||
|
expanded into. After a bit of thinking I came up with this:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defaspect</span></i> Mobile
|
||
|
<span class="paren2">(<span class="code">move <span class="paren3">[<span class="code">this world dest</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">can-move? this world dest</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> <span class="paren5">(<span class="code"><span class="keyword">:id</span> this</span>)</span> <span class="keyword">:location</span></span>]</span> dest</span>)</span></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 class="comment">; should expand into
|
||
|
</span>
|
||
|
<span class="paren1">(<span class="code">do
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">defprotocol</span></i> Mobile
|
||
|
<span class="paren3">(<span class="code">move <span class="paren4">[<span class="code">this world dest</span>]</span></span>)</span>
|
||
|
<span class="paren3">(<span class="code">can-move? <span class="paren4">[<span class="code">this world dest</span>]</span></span>)</span></span>)</span>
|
||
|
<span class="paren2">(<span class="code">def Mobile
|
||
|
<span class="paren3">(<span class="code"><i><span class="symbol">with-meta</span></i> Mobile
|
||
|
<span class="paren4">{<span class="code"><span class="keyword">:defaults</span>
|
||
|
<span class="paren5">{<span class="code"><span class="keyword">:move</span> <span class="paren6">(<span class="code">fn <span class="paren1">[<span class="code">this world dest</span>]</span>
|
||
|
<span class="paren1">{<span class="code"><span class="keyword">:pre</span> <span class="paren2">[<span class="code"><span class="paren3">(<span class="code">can-move? this world dest</span>)</span></span>]</span></span>}</span>
|
||
|
<span class="paren1">(<span class="code">assoc-in world <span class="paren2">[<span class="code"><span class="keyword">:entities</span> <span class="paren3">(<span class="code"><span class="keyword">:id</span> this</span>)</span> <span class="keyword">:location</span></span>]</span> dest</span>)</span></span>)</span>
|
||
|
<span class="keyword">:can-move?</span> <span class="paren6">(<span class="code">fn <span class="paren1">[<span class="code">this world dest</span>]</span>
|
||
|
<span class="paren1">(<span class="code">is-empty? world dest</span>)</span></span>)</span></span>}</span></span>}</span></span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>This looks a bit complicated because of the method implementations, which aren't
|
||
|
really important when writing the macro, so let's remove those:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defaspect</span></i> Mobile
|
||
|
<span class="paren2">(<span class="code">move <span class="paren3">[<span class="code">this world dest</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">can-move? this world dest</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> <span class="paren5">(<span class="code"><span class="keyword">:id</span> this</span>)</span> <span class="keyword">:location</span></span>]</span> dest</span>)</span></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 class="comment">; should expand into
|
||
|
</span>
|
||
|
<span class="paren1">(<span class="code">do
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">defprotocol</span></i> Mobile
|
||
|
<span class="paren3">(<span class="code">move <span class="paren4">[<span class="code">this world dest</span>]</span></span>)</span>
|
||
|
<span class="paren3">(<span class="code">can-move? <span class="paren4">[<span class="code">this world dest</span>]</span></span>)</span></span>)</span>
|
||
|
<span class="paren2">(<span class="code">def Mobile
|
||
|
<span class="paren3">(<span class="code"><i><span class="symbol">with-meta</span></i> Mobile
|
||
|
<span class="paren4">{<span class="code"><span class="keyword">:defaults</span>
|
||
|
<span class="paren5">{<span class="code"><span class="keyword">:move</span> <span class="paren6">(<span class="code">fn <span class="paren1">[<span class="code">this world dest</span>]</span> ...</span>)</span>
|
||
|
<span class="keyword">:can-move?</span> <span class="paren6">(<span class="code">fn <span class="paren1">[<span class="code">this world dest</span>]</span> ...</span>)</span></span>}</span></span>}</span></span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>That's a bit easier to read. The <code>defaspect</code> macro is going to take all the
|
||
|
forms I give it and expand into a <code>do</code> form with two actions: defining the
|
||
|
protocol as before, and attaching a map to the Protocol itself with Clojure's
|
||
|
metadata feature.</p>
|
||
|
|
||
|
<p>This map will contain the default implementations. For now just trust me that
|
||
|
I'm going to need them in a map later.</p>
|
||
|
|
||
|
<p>Now to write the actual macro! It'll go in <code>entities/core.clj</code> for the moment.
|
||
|
I'll start with a skeleton:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defmacro</span></i> <i><span class="symbol">defaspect</span></i> <span class="paren2">[<span class="code">label & fns</span>]</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">[<span class="code">fnmap <span class="paren4">(<span class="code">make-fnmap fns</span>)</span>
|
||
|
fnheads <span class="paren4">(<span class="code">make-fnheads fns</span>)</span></span>]</span>
|
||
|
`<span class="paren3">(<span class="code">do
|
||
|
<span class="paren4">(<span class="code"><i><span class="symbol">defprotocol</span></i> ~label
|
||
|
~@fnheads</span>)</span>
|
||
|
<span class="paren4">(<span class="code">def ~label
|
||
|
<span class="paren5">(<span class="code"><i><span class="symbol">with-meta</span></i> ~label <span class="paren6">{<span class="code"><span class="keyword">:defaults</span> ~fnmap}</span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span></span></span></code></pre>
|
||
|
|
||
|
<p>If you've used macros before, this should be pretty easy to read. I've pulled
|
||
|
as much functionality as possible into two helper functions. Let's look at
|
||
|
those:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> make-fnmap
|
||
|
<span class="string">"Make a function map out of the given sequence of fnspecs.
|
||
|
|
||
|
A function map is a map of functions that you'd pass to extend. For example,
|
||
|
this sequence of fnspecs:
|
||
|
|
||
|
((foo [a] (println a)
|
||
|
(bar [a b] (+ a b)))
|
||
|
|
||
|
Would be turned into this fnmap:
|
||
|
|
||
|
{:foo (fn [a] (println a))
|
||
|
:bar (fn [a b] (+ a b))}
|
||
|
|
||
|
"</span>
|
||
|
<span class="paren2">[<span class="code">fns</span>]</span>
|
||
|
<span class="paren2">(<span class="code">into <span class="paren3">{<span class="code"></span>}</span> <span class="paren3">(<span class="code">for <span class="paren4">[<span class="code"><span class="paren5">[<span class="code">label fntail</span>]</span> <span class="paren5">(<span class="code">map <span class="paren6">(<span class="code">juxt first rest</span>)</span> fns</span>)</span></span>]</span>
|
||
|
<span class="paren4">[<span class="code"><span class="paren5">(<span class="code">keyword label</span>)</span>
|
||
|
`<span class="paren5">(<span class="code">fn ~@fntail</span>)</span></span>]</span></span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> make-fnheads
|
||
|
<span class="string">"Make a sequence of fnheads of of the given sequence of fnspecs.
|
||
|
|
||
|
A fnhead is a sequence of (name args) like you'd pass to defprotocol. For
|
||
|
example, this sequence of fnspecs:
|
||
|
|
||
|
((foo [a] (println a))
|
||
|
(bar [a b] (+ a b)))
|
||
|
|
||
|
Would be turned into this sequence of fnheads:
|
||
|
|
||
|
((foo [a])
|
||
|
(bar [a b]))
|
||
|
|
||
|
"</span>
|
||
|
<span class="paren2">[<span class="code">fns</span>]</span>
|
||
|
<span class="paren2">(<span class="code">map #<span class="paren3">(<span class="code">take 2 %</span>)</span> fns</span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Hopefully the docstrings will make them pretty clear. If you have questions let
|
||
|
me know (or play around with them in a REPL to see how they behave).</p>
|
||
|
|
||
|
<p>And with that, <code>defaspect</code> is complete! I now have a way to define a protocol
|
||
|
and attach some default implementations to it in one easy, beautiful call.</p>
|
||
|
|
||
|
<p>The other macro, <code>add-aspect</code>, is a piece of cake now that I've got the helper
|
||
|
functions:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defmacro</span></i> add-aspect <span class="paren2">[<span class="code">entity aspect & fns</span>]</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">[<span class="code">fnmap <span class="paren4">(<span class="code">make-fnmap fns</span>)</span></span>]</span>
|
||
|
`<span class="paren3">(<span class="code">extend ~entity ~aspect <span class="paren4">(<span class="code">merge <span class="paren5">(<span class="code"><span class="keyword">:defaults</span> <span class="paren6">(<span class="code">meta ~aspect</span>)</span></span>)</span>
|
||
|
~fnmap</span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>The important thing in understanding this macro is <code>extend</code>. <code>extend</code> is what
|
||
|
<code>extend-type</code> and <code>extend-protocol</code> sugar over. It takes a type, a protocol,
|
||
|
and a map of the implementations of that protocol's functions.</p>
|
||
|
|
||
|
<p>The key word there is "map", which really does mean a plain old Clojure map. So
|
||
|
this macro will expand like so:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">add-aspect EarthElemental Mobile
|
||
|
<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">entity-at? world dest</span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="comment">; should expand into
|
||
|
</span>
|
||
|
<span class="paren1">(<span class="code">extend EarthElemental Mobile
|
||
|
<span class="paren2">(<span class="code">merge <span class="paren3">(<span class="code"><span class="keyword">:defaults</span> <span class="paren4">(<span class="code">meta Mobile</span>)</span></span>)</span>
|
||
|
<span class="paren3">{<span class="code"><span class="keyword">:can-move?</span> <span class="paren4">(<span class="code">fn <span class="paren5">[<span class="code">this world dest</span>]</span> ...</span>)</span></span>}</span></span>)</span></span></span></span></code></pre>
|
||
|
|
||
|
<p>The <code>(:defaults (meta Mobile))</code> simply retrieves the function mapping that
|
||
|
<code>defaspect</code> attached to the Protocol, so in effect I get something like:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">extend EarthElemental Mobile
|
||
|
<span class="paren2">(<span class="code">merge <span class="paren3">{<span class="code"><span class="keyword">:move</span> <span class="paren4">(<span class="code">fn <span class="paren5">[<span class="code">this world dest</span>]</span> ...</span>)</span>
|
||
|
<span class="keyword">:can-move?</span> <span class="paren4">(<span class="code">fn <span class="paren5">[<span class="code">this world dest</span>]</span> ...</span>)</span></span>}</span>
|
||
|
<span class="paren3">{<span class="code"><span class="keyword">:can-move?</span> <span class="paren4">(<span class="code">fn <span class="paren5">[<span class="code">this world dest</span>]</span> ...</span>)</span></span>}</span></span>)</span></span></span></span></code></pre>
|
||
|
|
||
|
<p><code>merge</code> is just the vanilla Clojure <code>merge</code> function, so the resulting map will
|
||
|
have the default implementations overridden by any custom ones given.</p>
|
||
|
|
||
|
<p>And that's it! Let's see it in action.</p>
|
||
|
|
||
|
<h2 id="s6-usage"><a href="index.html#s6-usage">Usage</a></h2>
|
||
|
|
||
|
<p>First I need to update the aspects to use <code>defaspect</code> and include their default
|
||
|
implementations. Here's <code>Destructible</code>:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">ns caves.entities.aspects.destructible
|
||
|
<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"><i><span class="symbol">defaspect</span></i></span>]</span></span>]</span></span>)</span></span>)</span>
|
||
|
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defaspect</span></i> 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">assoc-in world <span class="paren1">[<span class="code"><span class="keyword">:entities</span> id</span>]</span> damaged-this</span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span></span></span></code></pre>
|
||
|
|
||
|
<p>The code is just torn out of the <code>Lichen</code> code. Since this is how lichens will
|
||
|
act, I can update them to use <code>add-aspect</code>:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">add-aspect Lichen Destructible</span>)</span></span></code></pre>
|
||
|
|
||
|
<p>One line! Nice.</p>
|
||
|
|
||
|
<p>I'll make bunnies destructible too:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">add-aspect Bunny Destructible</span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Perfect. I then updated <code>Mobile</code> to use the <code>defaspect</code> macro. Look in the
|
||
|
repository if you want to see that. Now players and bunnies can both use the
|
||
|
same default implementations for movement:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">add-aspect Bunny Mobile</span>)</span>
|
||
|
<span class="paren1">(<span class="code">add-aspect Player Mobile</span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Beautiful. I then converted the remaining aspects and implementations to use
|
||
|
these macros.</p>
|
||
|
|
||
|
<p>Let's add some bunnies to the world. First I'll need a <code>make-bunny</code> function
|
||
|
similar to <code>make-lichen</code>:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> make-bunny <span class="paren2">[<span class="code">location</span>]</span>
|
||
|
<span class="paren2">(<span class="code">->Bunny <span class="paren3">(<span class="code">get-id</span>)</span> <span class="string">"v"</span> <span class="keyword">:yellow</span> location 1</span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>I don't know why I picked yellow. Are there yellow bunnies? There are in
|
||
|
<em>this</em> world. I used a <code>v</code> as the glyph because it kind of looks like bunny
|
||
|
ears.</p>
|
||
|
|
||
|
<p>Then I updated the world-populating code over 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-creature <span class="paren2">[<span class="code">world make-creature</span>]</span>
|
||
|
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">[<span class="code">creature <span class="paren4">(<span class="code">make-creature <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> <span class="paren5">(<span class="code"><span class="keyword">:id</span> creature</span>)</span></span>]</span> creature</span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> add-creatures <span class="paren2">[<span class="code">world make-creature n</span>]</span>
|
||
|
<span class="paren2">(<span class="code">nth <span class="paren3">(<span class="code">iterate #<span class="paren4">(<span class="code">add-creature % make-creature</span>)</span>
|
||
|
world</span>)</span>
|
||
|
n</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></span>]</span>
|
||
|
<span class="paren3">(<span class="code">-> world
|
||
|
<span class="paren4">(<span class="code">add-creatures make-lichen 30</span>)</span>
|
||
|
<span class="paren4">(<span class="code">add-creatures make-bunny 20</span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Bunnies will be a bit rarer than lichens for the moment (and maybe in the future
|
||
|
they could eat them).</p>
|
||
|
|
||
|
<p>Finally, let's run the game!</p>
|
||
|
|
||
|
<p><img src="../../../../static/images/blog/2012/07/caves-interlude-1-01.png" alt="Screenshot"></p>
|
||
|
|
||
|
<p>Bunnies! They're populated into the world and the player can kill them because
|
||
|
they're <code>Destructible</code>.</p>
|
||
|
|
||
|
<p>Right now they <em>can</em> move, but choose not to. I'll fix that by updating their
|
||
|
<code>tick</code> function:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">extend-type Bunny Entity
|
||
|
<span class="paren2">(<span class="code">tick <span class="paren3">[<span class="code">this world</span>]</span>
|
||
|
<span class="paren3">(<span class="code">if-let <span class="paren4">[<span class="code">target <span class="paren5">(<span class="code">find-empty-neighbor world <span class="paren6">(<span class="code"><span class="keyword">:location</span> this</span>)</span></span>)</span></span>]</span>
|
||
|
<span class="paren4">(<span class="code">move this world target</span>)</span>
|
||
|
world</span>)</span></span>)</span></span>)</span></span></code></pre>
|
||
|
|
||
|
<p>Now they'll move whenever they're not backed into a corner. Obviously move
|
||
|
complicated AI is possible, but this is fine for now.</p>
|
||
|
|
||
|
<p>So this is all good, but I haven't even used the "override one function of an
|
||
|
aspect but not others" part of my fancy macro. I'll add another creature to
|
||
|
show that off:</p>
|
||
|
|
||
|
<pre><code><span class="code"><span class="paren1">(<span class="code">ns caves.entities.silverfish
|
||
|
<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 add-aspect</span>]</span></span>]</span>
|
||
|
<span class="paren3">[<span class="code">caves.entities.aspects.destructible <span class="keyword">:only</span> <span class="paren4">[<span class="code">Destructible</span>]</span></span>]</span>
|
||
|
<span class="paren3">[<span class="code">caves.entities.aspects.mobile <span class="keyword">:only</span> <span class="paren4">[<span class="code">Mobile move can-move?</span>]</span></span>]</span>
|
||
|
<span class="paren3">[<span class="code">caves.world <span class="keyword">:only</span> <span class="paren4">[<span class="code">get-entity-at</span>]</span></span>]</span>
|
||
|
<span class="paren3">[<span class="code">caves.coords <span class="keyword">:only</span> <span class="paren4">[<span class="code">neighbors</span>]</span></span>]</span></span>)</span></span>)</span>
|
||
|
|
||
|
|
||
|
<span class="paren1">(<span class="code"><i><span class="symbol">defrecord</span></i> Silverfish <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-silverfish <span class="paren2">[<span class="code">location</span>]</span>
|
||
|
<span class="paren2">(<span class="code">->Silverfish <span class="paren3">(<span class="code">get-id</span>)</span> <span class="string">"~"</span> <span class="keyword">:white</span> location 1</span>)</span></span>)</span>
|
||
|
|
||
|
|
||
|
<span class="paren1">(<span class="code">extend-type Silverfish 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">let</span></i> <span class="paren4">[<span class="code">target <span class="paren5">(<span class="code">rand-nth <span class="paren6">(<span class="code">neighbors <span class="paren1">(<span class="code"><span class="keyword">:location</span> this</span>)</span></span>)</span></span>)</span></span>]</span>
|
||
|
<span class="paren4">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren5">(<span class="code">get-entity-at world target</span>)</span>
|
||
|
world
|
||
|
<span class="paren5">(<span class="code">move this world target</span>)</span></span>)</span></span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code">add-aspect Silverfish Mobile
|
||
|
<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">not <span class="paren4">(<span class="code">get-entity-at world dest</span>)</span></span>)</span></span>)</span></span>)</span>
|
||
|
|
||
|
<span class="paren1">(<span class="code">add-aspect Silverfish Destructible</span>)</span>
|
||
|
</span></code></pre>
|
||
|
|
||
|
<p>Oh no! The horrible silverfish from Minecraft! They wormy little guys are
|
||
|
<code>Mobile</code> and <code>Destructible</code>, but they can move through walls with their custom
|
||
|
<code>can-move?</code> function.</p>
|
||
|
|
||
|
<p>Notice how I didn't have to provide an implementation of <code>move</code>. Silverfish
|
||
|
move like any other mobile entity (just by updating their location), the only
|
||
|
thing special is where they can go.</p>
|
||
|
|
||
|
<p>After adding them to the world population, we can see a few wriggling their way
|
||
|
though the walls in the northeast corner:</p>
|
||
|
|
||
|
<p><img src="../../../../static/images/blog/2012/07/caves-interlude-1-02.png" alt="Screenshot"></p>
|
||
|
|
||
|
<h2 id="s7-results"><a href="index.html#s7-results">Results</a></h2>
|
||
|
|
||
|
<p>You can view the code <a href="https://github.com/sjl/caves/tree/interlude-1/src/caves">on GitHub</a> if you want to see the end
|
||
|
result.</p>
|
||
|
|
||
|
<p>These two macros allowed me to add a new mob, with custom movement, in about 13
|
||
|
lines of code (excluding imports). That's pretty nice! They certainly aren't
|
||
|
perfect though.</p>
|
||
|
|
||
|
<p>For one, every aspect <em>has</em> to define default implementations for its methods.
|
||
|
You can't force all entities to implement it. This isn't a big deal for now,
|
||
|
but I may want to add it in later.</p>
|
||
|
|
||
|
<p>Second, it doesn't handle docstrings properly. Again, not a huge problem at the
|
||
|
moment but something to put on the todo list for later.</p>
|
||
|
|
||
|
<p>Finally, defining the protocol and implementations in the same form may lead to
|
||
|
some tricky circular import issues. I can always split them into separate files
|
||
|
in the future though.</p>
|
||
|
|
||
|
<p>That about wraps it up. If you have any questions or comments let me know! In
|
||
|
the next entry I'll get back to Trystan's tutorial.</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>
|