552 lines
No EOL
45 KiB
HTML
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 "screens": 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
|
|
"screens".</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 "state" 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 "world" 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 "world" as my state, I'm going to keep a "game".</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 "game" state, I need
|
|
a way to represent it.</p>
|
|
|
|
<p>There are two halves to the user interface: "input" and "output". First let's
|
|
consider output.</p>
|
|
|
|
<p>Trystan's screens are objects that handle their own drawing. At any given time
|
|
there's one "active" screen object, which gets asked to draw itself. If you
|
|
peek ahead in his tutorial you'll see that he ends up introducing a "subscreen"
|
|
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 "active" 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
|
|
"screen" 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 "UIs", 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.: "active") 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 "state" for our game is going to be a "game", which
|
|
consists of the world and the user interface. So our "game" 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 "return values" 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 "input" 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 "screens": 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">"Welcome to the Caves of Clojure!"</span></span>)</span>
|
|
<span class="paren2">(<span class="code">s/put-string screen 0 1 <span class="string">"Press enter to win, anything else to lose."</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">"Congratulations, you win!"</span></span>)</span>
|
|
<span class="paren2">(<span class="code">s/put-string screen 0 1 <span class="string">"Press escape to exit, anything else to restart."</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">"Sorry, better luck next time."</span></span>)</span>
|
|
<span class="paren2">(<span class="code">s/put-string screen 0 1 <span class="string">"Press escape to exit, anything else to go."</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 "returning as input" 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 "quit the game", 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">& 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">":swing"</span></span>)</span> <span class="keyword">:swing</span>
|
|
<span class="paren5">(<span class="code">args <span class="string">":text"</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">"Welcome to the Caves of Clojure!"</span></span>)</span>
|
|
<span class="paren2">(<span class="code">s/put-string screen 0 1 <span class="string">"Press enter to win, anything else to lose."</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">"Congratulations, you win!"</span></span>)</span>
|
|
<span class="paren2">(<span class="code">s/put-string screen 0 1 <span class="string">"Press escape to exit, anything else to restart."</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">"Sorry, better luck next time."</span></span>)</span>
|
|
<span class="paren2">(<span class="code">s/put-string screen 0 1 <span class="string">"Press escape to exit, anything else to go."</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">& 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">":swing"</span></span>)</span> <span class="keyword">:swing</span>
|
|
<span class="paren6">(<span class="code">args <span class="string">":text"</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 "pure" in the "no I/O" 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">"Enter wins at the starting screen."</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">"Other keys lose at the starting screen."</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> |