First go ahead and play the current game below. You play by connecting dots of the same color. When you make a cycle of dots, all the dots of that color are erased from the board.
Play the game in a window by itself.
Here is the full source of the game.
This game was developed in Chrome on OSX and on an IPhone 4Gs. If your browser doesn’t support hardware acceleration for CSS 3d transforms, the game isn’t going to perform very well.
The game is derived from the iPhone game Dots. The game isn’t a complete copy of the original but it is enough to play well. This version is written in ClojureScript using the core.async library and weighs in around 390 lines of code.
The game is drawn with DOM elements and uses CSS 3d transforms for animation.
Building the Game
Using ClojureScript and core.async I was able to build the game in a straight forward manner addressing each part of the game sequentially as it came up. I made no real effort to be clever. There are very few pure functions.
Writing this game is another exercise to help me learn more about Clojure and core.async, so take my Clojure idioms with a grain of salt.
That being said, I think this is seems like a reasonable way to write the game and as a bonus it works.
This post is intended follow my last post on core.async. The code in this post uses the same channel manipulation pattern introduced in that last post. The last post also provides helpful links for learning Clojure/ClojureScript.
Composing events
Gestures are a composition of events. Drawing on a screen normally commences after an initiating event like a click or a touch. A mousemove event means something different depending if the mouse button is down or not. A gesture is over once the mouse button is released or your finger leaves the touch screen.
What we would like to do, is bottle all of these raw input events up and emit a stream of messages that capture drawing actions at a higher level. Thus keeping the lower level details out of our main application loop.
Here are some functions to help us gather the events we need into a channel.
If you are new to Clojure this cheatsheet may help.
The code above works great for Webkit browsers. See the full example source for a more complete example.
The draw-event-capture
method directs the different touch
and mouse events into the supplied input channel. We are capturing
both mouse events and touch events so the resulting draw channel will
work on both platforms.
Let’s take these helpers and compose a stream of messages that capture the act of drawing.
We are using the core.async library here to build a channel which
will behave as a blocking message queue. We can create a channel with
the chan
function and we can block on input from the
channel inside of a go block with the <!
function. You can use the put!
function to asynchronously
put messages into a channel.
Given a CSS selector the draw-chan
function will compose
and return a channel that emits drawing events relevant to the
selected DOM elements. It emits [:draw {:x - :y -}]
messages while a draw action is occurring and ends a complete drawing
action with one [:drawend]
message.
When the loop in draw-chan
receives a :draw message
it passes the composed input-chan
to the
get-drawing
loop. get-drawing
will only emit
:draw messages until it receives a message that isn’t a :draw
message and then control flow returns to the context of the
draw-chan
loop and waits for the next drawing action to
start.
An interesting thing to notice is that I’m not setting a flag to indicate when we are in “drawing mode”. We flow into and out of a drawing context.
This cleans up the act of drawing from a set of separate events into a single act.
Go ahead and draw in the window below:
As you can see, each act of drawing is distinct and has its own color.
A single column
The animations and actions are more easily explored using one column of the game. To start we will work on rendering a list of dots. Below we have a set of functions that will help us render a board of random colored dots.
And the resulting board is here:
Gestures to dots
The main action of the game is to remove dots from the board by connecting them. Let’s make it easier on our main game loop by turning draw gestures into dot positions.
This code follows the same pattern used above to create the draw channel. It maps the mouse position coordinates to dot positions and prevents duplicate messages for individual dots. We will probably receive several messages for each dot as we swipe over them. It is better to eliminate these extra messages and provide as nice clean stream of draw actions to the channels consumers.
You can see this code in action if you use your mouse to swipe over the dots below.
What’s happening here is we are emitting a series of messages that represent the dots that have been touched in a single draw gesture and ending the gesture with an :end-dots message.
Using queues and messages
It’s critical that the currency of event coordination be at higher level than concrete events sources like key presses and mouse movement - this will allow our system to be responsive.
It’s important to call out this pattern of decoupling event sources and main application actors. The common practice in JavaScript land is to put actions directly into event callbacks. The approach that we have taken here is to have callbacks insert messages into a message queue.
A message queue effectively decouples the events from the actions being taken. With a message queue in place, we can easily create new event sources without rewriting our application code. For instance, we can easily create a separate input channel for testing purposes or even an automated player.
Having a message queue also allows us to filter, repeat and otherwise morph the queue as we are above
Putting together a game loop
Now that we’ve turned low level events into a high level game information stream, it is time to consume that stream.
The game-loop
creates the dot channel and passes it to
the dot-chain-getter
. The code blocks and waits for the
dot-chain-getter
to return a chain of dots selected by
the player. The returned dot-chain gets added to the state and
then rendered.
The dot-chain-getter
and get-dot-chains
functions follow the established pattern for filtering a message
queue. They collect a vector of dot messages until we get to the
:end-dots message and then return a vector of dot positions.
Removing dots and using timeout
Now let’s look at rendering the removal of the dots.
The render-upates
function checks to see if we have a
dot-chain in our state and if so calls remove-dots
.
remove-dots
takes the dot-chain and uses it to get
the dots that are to be kept and removed.
The remove-dots-from-dom
function deserves some attention
because of its sequential use of the blocking call (<!
(timeout 150))
. The timeout
function produces a
channel that sends a message at the end of the timeout. This allows
us to do a scale out animation on an dot and then remove the dot from
the DOM after the animation has run.
The remove-dots-from-dom
function also uses a go
block for the removal of each individual block. It’s helpful to
consider a go as a separate asynchronous process that is getting
launched. So each “dot removal” is running in “parallel”. It’s not
really running in parallel but conceptually it is. The effect is that
all the selected dots shrink and disappear at the same time.
Here the use of a blocking timeout is a small win over doing a callback based timeout. It’s simply less typing. The blocking timeout becomes a much bigger win when the things that occur after the timeout become more complex. Sequences such as timeout -> action -> timeout -> action become much more easy to understand and adjust as sequential instructions. When a timeout is a blocking call it’s easy to change its position in a chain of actions.
You can see a sequential use of timeout in the
move-dots-to-new-positions
function. This function
alters the absolute positions of the dots to bring them in line with
their actual position in the board. The loop that iterates over the
dots in the board is inside the go block. This means that the
blocking 100ms timeouts will happen sequentially. The result is that
each dot falls down to position one after the other. This is a bigger
win for a blocking timeout and IMHO is a very straight forward
expression of the desired action.
Go ahead and swipe over the dots below. Swipe over the dots on the bottom of the column to see the animation of the dots falling downward one at a time. To reset it reload the page ;-).
Adding new dots
Now that we have removed the dots, we need to add some back. This is relatively easy given the functions that we have created already.
The add-dots
function simply creates some new dots and adds them to
the board. In order to support animating these dots into the scene
correctly we will add them at an off screen position and then use the
move-dots-to-new-positions
function to move the new dots down to their
proper positions.
Furthermore we use a go block and a 500ms timeout to delay the rendering of the new dots to give the existing displaced dots a chance to fall into place before moving the new ones into view.
Here is the code working below.
Selecting dots of the same color
For the game to work we to restrict the selection of dots to a single color that matches the first selected dot.
Here we only append a dot to the dot-chain if the next dot is of the same color and is right next to the previous dot. I left it so that dot-chains of length one will still get removed to make single column interaction easier for the time being. It’s easy enough to require it be at least two dots long later.
Give it a try:
Drawing feedback
We are going to finally give some feedback to our players so they know which dots they are selecting.
Here we simply insert render-dot-chain
and
erase-dot-chain
calls into our previously defined
get-dot-chain
function.
The code is pretty straight forward and if you have followed along up until now you should be able to parse it.
Again try out the highlighting below.
Conclusion
Well, that was a walk through of the major parts of the Dots game. Go ahead and browse the actual code for the game here especially this file.
You can write a pretty responsive game using ClojureScript, Core.Async and very basic DOM manipulation. This was a surprise to me. JavaScript engines are freaking fast.
ClojureScript core.async code is concise and expresses intent with very little noise that is normally introduced by the constant necessity for callbacks. As you can see, there is not a whole bunch of code on this page.
While the code is side effect ridden it seems to be the result of necessity (i.e. the phrasing of the animations) and is always directed towards the DOM. I treat the application state purely and never mutate it.
Features to explore:
- DONE:: –make the game more accessible for color blind people–
- add a real time multi player element with cooperative scoring
- solve performance problems on other platforms
- decouple rendering and communicate with it over a channel
- create a canvas renderer
Resources: