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

552 lines
No EOL
45 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 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 2</a></h1><p class='date'>Posted on July 8th, 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-02-input-output.html">post two 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-02</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-state">State</a></li><li><a href="index.html#s3-the-user-interface">The User Interface</a></li><li><a href="index.html#s4-user-input">User Input</a></li><li><a href="index.html#s5-implementation">Implementation</a></li><li><a href="index.html#s6-testing">Testing</a></li></ol>
<h2 id="s1-summary"><a href="index.html#s1-summary">Summary</a></h2>
<p>In Trystan's second post he introduces the concept of the game loop, as well as
what he calls &quot;screens&quot;: objects that handle drawing the interface and
processing user input.</p>
<p>I could try to port his design directly over to Clojure, but instead I wanted to
step back and see if I could find a way to make things more functional.</p>
<p>I think I've figured out a way to make it work, so I'm going to implement that.</p>
<h2 id="s2-state"><a href="index.html#s2-state">State</a></h2>
<p>When I first started thinking about how to model the game's state and the main
game loop I had lots of crazy ideas bouncing around in my head. Most of them
involved an immutable world (immutable! good!) and agents representing Trystan's
&quot;screens&quot;.</p>
<p>The more I thought about it, though, the more it looked like the agents would
wind up being a tangled mess. I put down the keyboard, took a shower, had
dinner with a friend, and let the problem roll around in my head for a bit.</p>
<p>At some point the following train of thought happened somewhere in my brain:</p>
<ul>
<li>The immutable &quot;state&quot; that I keep should contain <em>everything</em> needed to render
the game on the user's screen.</li>
<li>I originally thought I'd need to track the &quot;world&quot; as the state, but the world
isn't enough!</li>
<li>In addition to the world, the user interface (menus, stats, etc) is also
rendered.</li>
</ul>
<p>So instead of keeping a &quot;world&quot; as my state, I'm going to keep a &quot;game&quot;.</p>
<h2 id="s3-the-user-interface"><a href="index.html#s3-the-user-interface">The User Interface</a></h2>
<p>If I'm going to keep track of the user interface in the &quot;game&quot; state, I need
a way to represent it.</p>
<p>There are two halves to the user interface: &quot;input&quot; and &quot;output&quot;. First let's
consider output.</p>
<p>Trystan's screens are objects that handle their own drawing. At any given time
there's one &quot;active&quot; screen object, which gets asked to draw itself. If you
peek ahead in his tutorial you'll see that he ends up introducing a &quot;subscreen&quot;
concept to get screens layered on top of each other.</p>
<p>Instead of having a single active screen with subscreens, I decided to keep
a flat vector of screens. The last screen in the vector is the &quot;active&quot; one,
and is effectively a subscreen of the one that comes before it.</p>
<p>At this point I'm going to switch terms. Unfortunately Lanterna uses the word
&quot;screen&quot; to mean something and I didn't want to try to keep two separate
concepts under the same word, so in the code I called screens &quot;UIs&quot;, and from
now on I'll be using that word.</p>
<p>So what <em>is</em> a UI in the code? Well, it's basically just a map with a <code>:kind</code>
mapping specifying what kind of UI it is! It also might have some extra keys
and values to represent its state too.</p>
<p>For example, at some point I expect to have a UI stack that looks something like
this:</p>
<pre><code><span class="code">[<span class="paren1">{<span class="code"><span class="keyword">:kind</span> <span class="keyword">:play}</span>
<span class="paren2">{<span class="code"><span class="keyword">:kind</span> <span class="keyword">:throw}</span>
<span class="paren3">{<span class="code"><span class="keyword">:kind</span> <span class="keyword">:inventory-select}</span></span>]</span></span></span></span></span></span></code></pre>
<p>This would be the UI stack when you were throwing something in your inventory at
a monster and were choosing what to throw. Once you pick an item you'd need to
target a monster, so the stack would become:</p>
<pre><code><span class="code">[<span class="paren1">{<span class="code"><span class="keyword">:kind</span> <span class="keyword">:play}</span>
<span class="paren2">{<span class="code"><span class="keyword">:kind</span> <span class="keyword">:throw</span> <span class="keyword">:item</span> foo}
<span class="paren3">{<span class="code"><span class="keyword">:kind</span> <span class="keyword">:target}</span></span>]</span></span></span></span></span></span></code></pre>
<p>Now the last (i.e.: &quot;active&quot;) UI is the targetting UI, but also notice that the
throwing UI has a bit of state attached to it now (the item the user picked).
We'll talk about how that got there a bit later.</p>
<p>As I said before, the &quot;state&quot; for our game is going to be a &quot;game&quot;, which
consists of the world and the user interface. So our &quot;game&quot; object is going to
be a map that looks something like this:</p>
<pre><code><span class="code">{:world <span class="paren1">{<span class="code"></span>}</span>
:uis <span class="paren1">[<span class="code">,,,</span>]</span>}</span></code></pre>
<p>For now the <code>:world</code> is empty. In the future it'll contain stuff like the tiles
of the map, the monsters, the player, and lots of other stuff. The <code>:uis</code> is
the UI stack.</p>
<p>Between the two I have enough information to draw the game fully to the user's
terminal by doing something like: <code>(map #(draw-ui ui game) (:uis game))</code>. We'll
see the real code shortly, but that's actually pretty close.</p>
<h2 id="s4-user-input"><a href="index.html#s4-user-input">User Input</a></h2>
<p>In an imperative programming style our game loop would look something like this:</p>
<ol>
<li>Draw the screen.</li>
<li>Get some input from the user.</li>
<li>Process that input, modifying the world.</li>
<li>GOTO 1.</li>
</ol>
<p>In this functional loop, I want it to look more like this:</p>
<ol>
<li>Draw the screen.</li>
<li>Get some user input.</li>
<li>Process the input and the game to get a new game.</li>
<li>Recur with this new game.</li>
</ol>
<p>How do I handle user input? Well it depends on the current UI — pressing <code>d</code>
at the main screen will do something different than pressing it in an inventory
selection screen, for example.</p>
<p>So the UIs need to know how to handle input. There are a number of different
ways I can do that. One option might be to have <code>:handle-input (fn ...)</code> as
part of the UI. I chose a different route which you'll see below, but that's
not important for now.</p>
<p>The important part is that one I glossed over in the last section. How do I go
from this:</p>
<pre><code><span class="code">[<span class="paren1">{<span class="code"><span class="keyword">:kind</span> <span class="keyword">:play}</span>
<span class="paren2">{<span class="code"><span class="keyword">:kind</span> <span class="keyword">:throw}</span>
<span class="paren3">{<span class="code"><span class="keyword">:kind</span> <span class="keyword">:inventory-select}</span></span>]</span></span></span></span></span></span></code></pre>
<p>to this:</p>
<pre><code><span class="code">[<span class="paren1">{<span class="code"><span class="keyword">:kind</span> <span class="keyword">:play}</span>
<span class="paren2">{<span class="code"><span class="keyword">:kind</span> <span class="keyword">:throw</span> <span class="keyword">:item</span> foo}
<span class="paren3">{<span class="code"><span class="keyword">:kind</span> <span class="keyword">:target}</span></span>]</span></span></span></span></span></span></code></pre>
<p>Let's follow the proposed game loop and see what happens.</p>
<ol>
<li>Draw the play UI, then the throw UI, then the inventory UI.</li>
<li>Get a keypress from the user.</li>
<li>Give that keypress and the game itself to the UI input handling function to
get a new game.</li>
<li>Recur with this new game.</li>
</ol>
<p>Step three is the tricky part. What does the inventory handler need to do to
give back a new game?</p>
<p>It would need to pop itself off the UI stack (which is okay), put the selected
item in the previous UI (a bit scary, but probably not a problem in practice),
and create the targeting UI.</p>
<p>This last part is a deal breaker. The inventory selection UI shouldn't know
anything about the targeting UI, because then I won't be able to reuse it for
other functions (like equipping items, eating food, etc)!</p>
<p>The throw UI is the one that should know about the inventory and targeting
UIs. Ideally it would set them up, get their &quot;return values&quot; and process those.
How can I send back the values?</p>
<p>There's actually a really elegant way I came up with for this. At least
I <em>think</em> it's elegant. I may end up immuting myself into a corner and
ragequitting this blog series. We'll see.</p>
<p>Anyway, here's the solution:</p>
<ul>
<li>Make &quot;input&quot; part of the game state.</li>
<li>Update the game input when you want to return a value from a UI.</li>
</ul>
<p>And I can change the game loop to look like this:</p>
<ol>
<li>Draw the screen.</li>
<li>If the game's input is empty, get some from the user to fill it.</li>
<li>Process the game to get a new game. The input is now part of the game and
gets processed along with it.</li>
<li>Recur with this new game.</li>
</ol>
<p>From what little I've used it so far, this method seems very promising.</p>
<p>Enough design talk. Let's look at the code.</p>
<h2 id="s5-implementation"><a href="index.html#s5-implementation">Implementation</a></h2>
<p>In Trystan's tutorial he had three &quot;screens&quot;: start, win, and lose. The user
presses keys to transition between them. Not a very fun game, but it lets you
get the game loop up and running before diving into gameplay.</p>
<p>I did the same thing. Right now everything is in one file because I tend to
code like that until I feel like something needs to be pulled out into its own
namespace. The file is still under a hundred lines of code, so that's not too
bad.</p>
<p>Let's walk through the code piece by piece. First the namespace:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code">ns caves.core
<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 define some basic data structures:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defrecord</span></i> UI <span class="paren2">[<span class="code">kind</span>]</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defrecord</span></i> World <span class="paren2">[<span class="code"></span>]</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defrecord</span></i> Game <span class="paren2">[<span class="code">world uis input</span>]</span></span>)</span></span></code></pre>
<p>I used Clojure's records here because I feel like they add a bit of helpful
structure to the code. They're also a bit faster, but that probably won't be
noticeable. You could skip these and just use plain maps if you wanted to, it's
really a personal preference.</p>
<p>Next is a helper function:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> clear-screen <span class="paren2">[<span class="code">screen</span>]</span>
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">[<span class="code">blank <span class="paren4">(<span class="code">apply str <span class="paren5">(<span class="code">repeat 80 \space</span>)</span></span>)</span></span>]</span>
<span class="paren3">(<span class="code">doseq <span class="paren4">[<span class="code">row <span class="paren5">(<span class="code">range 24</span>)</span></span>]</span>
<span class="paren4">(<span class="code">s/put-string screen 0 row blank</span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
<p>Unfortunately Lanterna doesn't provide a method for clearing the screen, so
I wrote my own little hacky one that just overwrites everything with spaces. It
assumes the terminal is 80 by 24 characters for now.</p>
<p>I'll be adding a feature request in the Lanterna issue tracker for this, so
hopefully I'll be able to delete this function in a later post.</p>
<p>Now to the meaty bits:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defmulti</span></i> draw-ui
<span class="paren2">(<span class="code">fn <span class="paren3">[<span class="code">ui game screen</span>]</span>
<span class="paren3">(<span class="code"><span class="keyword">:kind</span> ui</span>)</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defmethod</span></i> draw-ui <span class="keyword">:start</span> <span class="paren2">[<span class="code">ui game screen</span>]</span>
<span class="paren2">(<span class="code">s/put-string screen 0 0 <span class="string">&quot;Welcome to the Caves of Clojure!&quot;</span></span>)</span>
<span class="paren2">(<span class="code">s/put-string screen 0 1 <span class="string">&quot;Press enter to win, anything else to lose.&quot;</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defmethod</span></i> draw-ui <span class="keyword">:win</span> <span class="paren2">[<span class="code">ui game screen</span>]</span>
<span class="paren2">(<span class="code">s/put-string screen 0 0 <span class="string">&quot;Congratulations, you win!&quot;</span></span>)</span>
<span class="paren2">(<span class="code">s/put-string screen 0 1 <span class="string">&quot;Press escape to exit, anything else to restart.&quot;</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defmethod</span></i> draw-ui <span class="keyword">:lose</span> <span class="paren2">[<span class="code">ui game screen</span>]</span>
<span class="paren2">(<span class="code">s/put-string screen 0 0 <span class="string">&quot;Sorry, better luck next time.&quot;</span></span>)</span>
<span class="paren2">(<span class="code">s/put-string screen 0 1 <span class="string">&quot;Press escape to exit, anything else to go.&quot;</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> draw-game <span class="paren2">[<span class="code">game screen</span>]</span>
<span class="paren2">(<span class="code">clear-screen screen</span>)</span>
<span class="paren2">(<span class="code">doseq <span class="paren3">[<span class="code">ui <span class="paren4">(<span class="code"><span class="keyword">:uis</span> game</span>)</span></span>]</span>
<span class="paren3">(<span class="code">draw-ui ui game screen</span>)</span></span>)</span>
<span class="paren2">(<span class="code">s/redraw screen</span>)</span></span>)</span></span></code></pre>
<p>Here we have the drawing code.</p>
<p>The UIs are very simple for now. They each just output a couple of lines of
text. None of them actually look at the game state at all, but in the future
some of them will need to do that (e.g.: when showing the list of items in the
player's inventory).</p>
<p>I made the <code>draw-ui</code> function a multimethod to make it easy to define the logic
for each UI separately. Each definition could even live in its own file if
I wanted it to. There are other ways to do this, but I like the concision and
simplicity of this one.</p>
<p>The <code>draw-game</code> function takes the immutable game object and draws some text to
the user's terminal. It's fairly simple. The <code>redraw</code> call is needed because
Lanterna <a href="https://en.wikipedia.org/wiki/Multiple_buffering#Double_buffering_in_computer_graphics">double buffers</a> the output. Check out the <a href="https://sjl.bitbucket.io/clojure-lanterna/">clojure-lanterna
documentation</a> for more information if you're curious.</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defmulti</span></i> process-input
<span class="paren2">(<span class="code">fn <span class="paren3">[<span class="code">game input</span>]</span>
<span class="paren3">(<span class="code"><span class="keyword">:kind</span> <span class="paren4">(<span class="code">last <span class="paren5">(<span class="code"><span class="keyword">:uis</span> game</span>)</span></span>)</span></span>)</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defmethod</span></i> process-input <span class="keyword">:start</span> <span class="paren2">[<span class="code">game input</span>]</span>
<span class="paren2">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren3">(<span class="code">= input <span class="keyword">:enter</span></span>)</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="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></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defmethod</span></i> process-input <span class="keyword">:win</span> <span class="paren2">[<span class="code">game input</span>]</span>
<span class="paren2">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren3">(<span class="code">= input <span class="keyword">:escape</span></span>)</span>
<span class="paren3">(<span class="code">assoc game <span class="keyword">:uis</span> <span class="paren4">[<span class="code"></span>]</span></span>)</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">:start</span></span>)</span></span>]</span></span>)</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defmethod</span></i> process-input <span class="keyword">:lose</span> <span class="paren2">[<span class="code">game input</span>]</span>
<span class="paren2">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren3">(<span class="code">= input <span class="keyword">:escape</span></span>)</span>
<span class="paren3">(<span class="code">assoc game <span class="keyword">:uis</span> <span class="paren4">[<span class="code"></span>]</span></span>)</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">:start</span></span>)</span></span>]</span></span>)</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> get-input <span class="paren2">[<span class="code">game screen</span>]</span>
<span class="paren2">(<span class="code">assoc game <span class="keyword">:input</span> <span class="paren3">(<span class="code">s/get-key-blocking screen</span>)</span></span>)</span></span>)</span></span></code></pre>
<p>UIs need to know how to process their input. I used a multimethod for this too.</p>
<p>The method takes the game and the input as parameters and returns a modified
copy of the game object that represents the new state. Currently none of them
use the &quot;returning as input&quot; trick, but we'll see that in one of the next few
posts.</p>
<p>Notice how the UIs all simply replace the UI stack in the game they return?
This is fine for now, but in the future they'll be more likely to just pop off
the last one (themselves) rather than replace the entire stack.</p>
<p>An empty UI stack means &quot;quit the game&quot;, as we'll see in a moment.</p>
<p>You'll also see why the input is separate from the game soon.</p>
<p>The <code>get-input</code> function gets a keypress from the user and sticks it into the
game object. Nothing crazy there.</p>
<p>And now, the game loop:</p>
<pre><code><span class="code"><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 game 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>Here we go. The <code>run-game</code> function <code>loop</code>s on a game object each time.</p>
<p>First: if there are no UIs, we're done and can drop out. Cool.</p>
<p>If there are UIs, draw the game to the user's terminal.</p>
<p>Then it checks if it needs to get a keypress from the user. If so, do that,
update the game object, and start again.</p>
<p>I could make this a bit more efficient by continuing on to process the input
without another round through the loop, but performance probably isn't a concern
at the moment. I'll revisit this in the future if it becomes an issue, but for
now I like this structure.</p>
<p>Anyway, if we <em>do</em> have input (i.e.: either we grabbed a keypress or a UI
returned something the last time through the loop), process it. Remember that
the <code>process-input</code> function is a multimethod that dispatches on the <code>:kind</code> of
the last UI in the stack.</p>
<p>Here your can see why <code>process-input</code> takes the game and input separately. I
<em>could</em> just pass the game and pull out the <code>:input</code> value, but then I'd also
need to <code>dissoc</code> the input from the modified game object in every UI that didn't
return a value.</p>
<p>If I didn't <code>dissoc</code> the input, the input would always be present and would
cause an infinite loop. You can play around with this by replacing <code>(dissoc
game :input)</code> with <code>game</code> and watching what happens.</p>
<p>Next is a simple helper function:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> new-game <span class="paren2">[<span class="code"></span>]</span>
<span class="paren2">(<span class="code">new Game
<span class="paren3">(<span class="code">new World</span>)</span>
<span class="paren3">[<span class="code"><span class="paren4">(<span class="code">new UI <span class="keyword">:start</span></span>)</span></span>]</span>
nil</span>)</span></span>)</span></span></code></pre>
<p>Nothing fancy. You could just inline that body into the next function if you
wanted, but I'm thinking ahead to when I'm going to want to generate a random
world.</p>
<p>Finally, the bootstrapping:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> main
<span class="paren2">(<span class="code"><span class="paren3">[<span class="code">screen-type</span>]</span> <span class="paren3">(<span class="code">main screen-type false</span>)</span></span>)</span>
<span class="paren2">(<span class="code"><span class="paren3">[<span class="code">screen-type block?</span>]</span>
<span class="paren3">(<span class="code">letfn <span class="paren4">[<span class="code"><span class="paren5">(<span class="code"><i><span class="symbol">go</span></i> <span class="paren6">[<span class="code"></span>]</span>
<span class="paren6">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren1">[<span class="code">screen <span class="paren2">(<span class="code">s/get-screen screen-type</span>)</span></span>]</span>
<span class="paren1">(<span class="code">s/in-screen screen
<span class="paren2">(<span class="code">run-game <span class="paren3">(<span class="code">new-game</span>)</span> screen</span>)</span></span>)</span></span>)</span></span>)</span></span>]</span>
<span class="paren4">(<span class="code"><i><span class="symbol">if</span></i> block?
<span class="paren5">(<span class="code"><i><span class="symbol">go</span></i></span>)</span>
<span class="paren5">(<span class="code">future <span class="paren6">(<span class="code"><i><span class="symbol">go</span></i></span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> -main <span class="paren2">[<span class="code">&amp; args</span>]</span>
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">[<span class="code">args <span class="paren4">(<span class="code">set args</span>)</span>
screen-type <span class="paren4">(<span class="code"><i><span class="symbol">cond</span></i>
<span class="paren5">(<span class="code">args <span class="string">&quot;:swing&quot;</span></span>)</span> <span class="keyword">:swing</span>
<span class="paren5">(<span class="code">args <span class="string">&quot;:text&quot;</span></span>)</span> <span class="keyword">:text</span>
<span class="keyword">:else</span> <span class="keyword">:auto</span></span>)</span></span>]</span>
<span class="paren3">(<span class="code">main screen-type true</span>)</span></span>)</span></span>)</span></span></code></pre>
<p><code>-main</code> looks almost the same as before, but <code>main</code> has changed quite a bit.
What happened?</p>
<p>The short answer is that most of the change is to work around some Clojure/JVM
silliness. The important bit is that I now create a fresh game object and fire
up the game loop with <code>(run-game (new-game) screen)</code>.</p>
<p>If you're curious about the rest, read <a href="http://dev.clojure.org/jira/browse/CLJ-959">this Clojure bug report</a>. I wanted
to be able to run the game from the command line as normal, but from a REPL
without blocking the REPL itself, so I could play around with things.</p>
<p>That's it! It clocks in at 98 lines of code. Here's the whole file at once:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code">ns caves.core
<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 class="comment">; Data Structures -------------------------------------------------------------
</span><span class="paren1">(<span class="code"><i><span class="symbol">defrecord</span></i> UI <span class="paren2">[<span class="code">kind</span>]</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defrecord</span></i> World <span class="paren2">[<span class="code"></span>]</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defrecord</span></i> Game <span class="paren2">[<span class="code">world uis input</span>]</span></span>)</span>
<span class="comment">; Utility Functions -----------------------------------------------------------
</span><span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> clear-screen <span class="paren2">[<span class="code">screen</span>]</span>
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">[<span class="code">blank <span class="paren4">(<span class="code">apply str <span class="paren5">(<span class="code">repeat 80 \space</span>)</span></span>)</span></span>]</span>
<span class="paren3">(<span class="code">doseq <span class="paren4">[<span class="code">row <span class="paren5">(<span class="code">range 24</span>)</span></span>]</span>
<span class="paren4">(<span class="code">s/put-string screen 0 row blank</span>)</span></span>)</span></span>)</span></span>)</span>
<span class="comment">; Drawing ---------------------------------------------------------------------
</span><span class="paren1">(<span class="code"><i><span class="symbol">defmulti</span></i> draw-ui
<span class="paren2">(<span class="code">fn <span class="paren3">[<span class="code">ui game screen</span>]</span>
<span class="paren3">(<span class="code"><span class="keyword">:kind</span> ui</span>)</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defmethod</span></i> draw-ui <span class="keyword">:start</span> <span class="paren2">[<span class="code">ui game screen</span>]</span>
<span class="paren2">(<span class="code">s/put-string screen 0 0 <span class="string">&quot;Welcome to the Caves of Clojure!&quot;</span></span>)</span>
<span class="paren2">(<span class="code">s/put-string screen 0 1 <span class="string">&quot;Press enter to win, anything else to lose.&quot;</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defmethod</span></i> draw-ui <span class="keyword">:win</span> <span class="paren2">[<span class="code">ui game screen</span>]</span>
<span class="paren2">(<span class="code">s/put-string screen 0 0 <span class="string">&quot;Congratulations, you win!&quot;</span></span>)</span>
<span class="paren2">(<span class="code">s/put-string screen 0 1 <span class="string">&quot;Press escape to exit, anything else to restart.&quot;</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defmethod</span></i> draw-ui <span class="keyword">:lose</span> <span class="paren2">[<span class="code">ui game screen</span>]</span>
<span class="paren2">(<span class="code">s/put-string screen 0 0 <span class="string">&quot;Sorry, better luck next time.&quot;</span></span>)</span>
<span class="paren2">(<span class="code">s/put-string screen 0 1 <span class="string">&quot;Press escape to exit, anything else to go.&quot;</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> draw-game <span class="paren2">[<span class="code">game screen</span>]</span>
<span class="paren2">(<span class="code">clear-screen screen</span>)</span>
<span class="paren2">(<span class="code">doseq <span class="paren3">[<span class="code">ui <span class="paren4">(<span class="code"><span class="keyword">:uis</span> game</span>)</span></span>]</span>
<span class="paren3">(<span class="code">draw-ui ui game screen</span>)</span></span>)</span>
<span class="paren2">(<span class="code">s/redraw screen</span>)</span></span>)</span>
<span class="comment">; Input -----------------------------------------------------------------------
</span><span class="paren1">(<span class="code"><i><span class="symbol">defmulti</span></i> process-input
<span class="paren2">(<span class="code">fn <span class="paren3">[<span class="code">game input</span>]</span>
<span class="paren3">(<span class="code"><span class="keyword">:kind</span> <span class="paren4">(<span class="code">last <span class="paren5">(<span class="code"><span class="keyword">:uis</span> game</span>)</span></span>)</span></span>)</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defmethod</span></i> process-input <span class="keyword">:start</span> <span class="paren2">[<span class="code">game input</span>]</span>
<span class="paren2">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren3">(<span class="code">= input <span class="keyword">:enter</span></span>)</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="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></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defmethod</span></i> process-input <span class="keyword">:win</span> <span class="paren2">[<span class="code">game input</span>]</span>
<span class="paren2">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren3">(<span class="code">= input <span class="keyword">:escape</span></span>)</span>
<span class="paren3">(<span class="code">assoc game <span class="keyword">:uis</span> <span class="paren4">[<span class="code"></span>]</span></span>)</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">:start</span></span>)</span></span>]</span></span>)</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defmethod</span></i> process-input <span class="keyword">:lose</span> <span class="paren2">[<span class="code">game input</span>]</span>
<span class="paren2">(<span class="code"><i><span class="symbol">if</span></i> <span class="paren3">(<span class="code">= input <span class="keyword">:escape</span></span>)</span>
<span class="paren3">(<span class="code">assoc game <span class="keyword">:uis</span> <span class="paren4">[<span class="code"></span>]</span></span>)</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">:start</span></span>)</span></span>]</span></span>)</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> get-input <span class="paren2">[<span class="code">game screen</span>]</span>
<span class="paren2">(<span class="code">assoc game <span class="keyword">:input</span> <span class="paren3">(<span class="code">s/get-key-blocking screen</span>)</span></span>)</span></span>)</span>
<span class="comment">; Main ------------------------------------------------------------------------
</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 game 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 class="paren2">(<span class="code"><i><span class="symbol">defn</span></i> new-game <span class="paren3">[<span class="code"></span>]</span>
<span class="paren3">(<span class="code">new Game
<span class="paren4">(<span class="code">new World</span>)</span>
<span class="paren4">[<span class="code"><span class="paren5">(<span class="code">new UI <span class="keyword">:start</span></span>)</span></span>]</span>
nil</span>)</span></span>)</span>
<span class="paren2">(<span class="code"><i><span class="symbol">defn</span></i> main
<span class="paren3">(<span class="code"><span class="paren4">[<span class="code">screen-type</span>]</span> <span class="paren4">(<span class="code">main screen-type false</span>)</span></span>)</span>
<span class="paren3">(<span class="code"><span class="paren4">[<span class="code">screen-type block?</span>]</span>
<span class="paren4">(<span class="code">letfn <span class="paren5">[<span class="code"><span class="paren6">(<span class="code"><i><span class="symbol">go</span></i> <span class="paren1">[<span class="code"></span>]</span>
<span class="paren1">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren2">[<span class="code">screen <span class="paren3">(<span class="code">s/get-screen screen-type</span>)</span></span>]</span>
<span class="paren2">(<span class="code">s/in-screen screen
<span class="paren3">(<span class="code">run-game <span class="paren4">(<span class="code">new-game</span>)</span> screen</span>)</span></span>)</span></span>)</span></span>)</span></span>]</span>
<span class="paren5">(<span class="code"><i><span class="symbol">if</span></i> block?
<span class="paren6">(<span class="code"><i><span class="symbol">go</span></i></span>)</span>
<span class="paren6">(<span class="code">future <span class="paren1">(<span class="code"><i><span class="symbol">go</span></i></span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span>)</span>
<span class="paren2">(<span class="code"><i><span class="symbol">defn</span></i> -main <span class="paren3">[<span class="code">&amp; args</span>]</span>
<span class="paren3">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren4">[<span class="code">args <span class="paren5">(<span class="code">set args</span>)</span>
screen-type <span class="paren5">(<span class="code"><i><span class="symbol">cond</span></i>
<span class="paren6">(<span class="code">args <span class="string">&quot;:swing&quot;</span></span>)</span> <span class="keyword">:swing</span>
<span class="paren6">(<span class="code">args <span class="string">&quot;:text&quot;</span></span>)</span> <span class="keyword">:text</span>
<span class="keyword">:else</span> <span class="keyword">:auto</span></span>)</span></span>]</span>
<span class="paren4">(<span class="code">main screen-type true</span>)</span></span>)</span></span>)</span></span></span></span></code></pre>
<p>And here are some screenshots:</p>
<p><img src="../../../../static/images/blog/2012/07/caves-02-01.png" alt="Screenshot"></p>
<p><img src="../../../../static/images/blog/2012/07/caves-02-02.png" alt="Screenshot"></p>
<p><img src="../../../../static/images/blog/2012/07/caves-02-03.png" alt="Screenshot"></p>
<p>It's not a very exciting game yet, but it all works, and I've managed to use an
immutable data structure of basic maps and records to represent everything
I need.</p>
<p>The drawing functions aren't &quot;pure&quot; in the &quot;no I/O&quot; sense, but they're kind of
pure in another way — they take an immutable data structure and draw something
to the screen based solely on that. I think this is going to make things easy
to work with down the line.</p>
<h2 id="s6-testing"><a href="index.html#s6-testing">Testing</a></h2>
<p>I'll leave you with one final tidbit to read through if you want more.</p>
<p>Encapsulating the game state as an immutable objects means I can test actions
and their effects on the world individually, without a game loop:</p>
<pre><code><span class="code"><span class="paren1">(<span class="code">ns caves.core-test
<span class="paren2">(<span class="code"><span class="keyword">:import</span> <span class="paren3">[<span class="code">caves.core UI World Game</span>]</span></span>)</span>
<span class="paren2">(<span class="code"><span class="keyword">:use</span> clojure.test
caves.core</span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">defn</span></i> current-ui <span class="paren2">[<span class="code">game</span>]</span>
<span class="paren2">(<span class="code"><span class="keyword">:kind</span> <span class="paren3">(<span class="code">last <span class="paren4">(<span class="code"><span class="keyword">:uis</span> game</span>)</span></span>)</span></span>)</span></span>)</span>
<span class="paren1">(<span class="code"><i><span class="symbol">deftest</span></i> test-start
<span class="paren2">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren3">[<span class="code">game <span class="paren4">(<span class="code">new Game nil <span class="paren5">[<span class="code"><span class="paren6">(<span class="code">new UI <span class="keyword">:start</span></span>)</span></span>]</span> nil</span>)</span></span>]</span>
<span class="paren3">(<span class="code">testing <span class="string">&quot;Enter wins at the starting screen.&quot;</span>
<span class="paren4">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren5">[<span class="code">result <span class="paren6">(<span class="code">process-input game <span class="keyword">:enter</span></span>)</span></span>]</span>
<span class="paren5">(<span class="code">is <span class="paren6">(<span class="code">= <span class="paren1">(<span class="code">current-ui result</span>)</span> <span class="keyword">:win</span></span>)</span></span>)</span></span>)</span></span>)</span>
<span class="paren3">(<span class="code">testing <span class="string">&quot;Other keys lose at the starting screen.&quot;</span>
<span class="paren4">(<span class="code"><i><span class="symbol">let</span></i> <span class="paren5">[<span class="code">results <span class="paren6">(<span class="code">map <span class="paren1">(<span class="code">partial process-input game</span>)</span>
<span class="paren1">[<span class="code">\space \a \A <span class="keyword">:escape</span> <span class="keyword">:up</span> <span class="keyword">:backspace</span></span>]</span></span>)</span></span>]</span>
<span class="paren5">(<span class="code">doseq <span class="paren6">[<span class="code">result results</span>]</span>
<span class="paren6">(<span class="code">is <span class="paren1">(<span class="code">= <span class="paren2">(<span class="code">current-ui result</span>)</span> <span class="keyword">:lose</span></span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span>)</span></span></code></pre>
<p>That's pretty cool!</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>