ClojureScript Core.Async Dots Game

12 Aug 2013

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.

(defn xy-message [ch msg-name xy-obj]
  (put! ch [msg-name {:x (.-pageX xy-obj) :y (.-pageY xy-obj)}]))

(defn touch-xy-message [ch msg-name xy-obj]
  (xy-message ch msg-name
              (aget (.-touches (.-originalEvent xy-obj)) 0)))

(defn mousemove-handler [in-chan jqevent]
  (if (pos? (.-which jqevent))
    (xy-message in-chan :draw jqevent)
    (put! in-chan [:drawend])))

(defn draw-event-capture [in-chan selector]
  (let [end-handler (fn [_] (put! in-chan [:drawend]))]
    (bind ($ selector) "mousemove" #(mousemove-handler in-chan %))
    (bind ($ selector) "mousedown" #(xy-message in-chan :draw %))
    (bind ($ selector) "mouseup"   end-handler)
    (bind ($ selector) "touchmove" #(touch-xy-message in-chan :draw %))
    (bind ($ selector) "touchend"  end-handler)))

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.

(defn get-drawing [input-chan out-chan]
  (go (loop [msg (<! input-chan)]
        (put! out-chan msg)
        (when (= (first msg) :draw)
          (recur (<! input-chan))))))

(defn draw-chan [selector]
  (let [input-chan (chan)
        out-chan   (chan)]
    (draw-event-capture input-chan selector)
    (go (loop [[msg-name _ :as msg] (<! input-chan)]
          (when (= msg-name :draw)
            (put! out-chan msg)
            (<! (get-drawing input-chan out-chan)))
          (recur (<! input-chan))))
    out-chan))

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:

full source for example

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.

(def grid-unit 45)
(def board-size 6)
(def dot-colors [:blue :green :yellow :purple :red])

(let [number-colors (count dot-colors)]
  (defn rand-color []
    (get dot-colors (rand-int number-colors))))

(defn get-rand-colors [number]
  (map (fn [x] (rand-color)) (range number)))

(defn dot-pos-to-corner-position [dot-pos]
  [(+ 23 (* grid-unit (- (dec board-size) dot-pos))) 23])

(defn dot-templ [i color]
  (let [[top left] (dot-pos-to-corner-position i)
        class (str "dot " (name color))
        style (str "top:" top "px; left: " left "px;")]
    [:div {:class class :style style}]))

(defn create-dot [i color]
  {:color color :pos i :elem (crate/html (dot-templ i color))})

(defn render-state [selector board]
  (mapv #(append ($ selector) (:elem %)) board))

(defn example-2 [selector]
  (render-state selector
                (map-indexed create-dot (get-rand-colors board-size))))

And the resulting board is here:

full source for example

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.

(def reverse-board-position (partial - (dec board-size)))

(defn coord->dot-pos [offset {:keys [x y]}]
  (let [[x y] (map - [x y] offset [13 13])]
    (when (and (< 12 x (+ 12 grid-unit))
               (< 12 y (* board-size grid-unit)))
      (reverse-board-position (int (/ y grid-unit))))))

(defn collect-dots [draw-input out-chan board-offset init-msg]
  (go
   (loop [last-pos nil
          msg init-msg]
     (when (= :draw (first msg))
       (let [cur-pos (coord->dot-pos board-offset (last msg))]
         (if (and (not (nil? cur-pos)) (not= cur-pos last-pos))
           (put! out-chan [:dot-pos cur-pos]))
         (recur (or cur-pos last-pos) (<! draw-input)))))))

(defn dot-chan [selector]
  (let [draw-input (draw-chan selector)
        board-offset ((juxt :left :top) (offset ($ selector)))
        out-chan (chan)
        dot-collector (partial collect-dots 
                               draw-input out-chan board-offset)]
    (go
     (loop [msg (<! draw-input)]
       (when (= (first msg) :draw)
         (<! (dot-collector msg))
         (put! out-chan [:end-dots]))
       (recur (<! draw-input))))
    out-chan))

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.

full source for example

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.

(def create-dots #(map-indexed create-dot (get-rand-colors %)))

(defn add-dots-to-board [selector dots]
  (mapv #(append ($ selector) (:elem %)) dots))

(def render-updates identity) ;; this is just a place holder

(defn get-dot-chain [state dot-ch first-dot-msg]
  (go
   (loop [dot-chain []
          msg first-dot-msg]
     (if (not= :dot-pos (first msg))
       dot-chain
       (recur (conj dot-chain (last msg)) (<! dot-ch))))))

(defn dot-chain-getter [state dot-ch]
  (go
   (loop [dot-msg (<! dot-ch)]
     (if (= :dot-pos (first dot-msg))
       (<! (get-dot-chain state dot-ch dot-msg))
       (recur (<! dot-ch))))))

(defn game-loop [selector init-state]
  (let [dot-ch (dot-chan selector)]
    (add-dots-to-board selector (init-state :board))
    (go
     (loop [state init-state]
       (let [state (assoc state :dot-chain 
                          (<! (dot-chain-getter state dot-ch)))]
         (recur (render-updates state)))))))

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.

(defn add-dots-to-board [selector dots]
  (mapv #(append ($ selector) (:elem %)) dots))

(defn move-dot-to-pos [dot i]
  (let [[top left] (dot-pos-to-corner-position i)]
    (css ($ (dot :elem)) {:top top :left left})))

(defn move-dots-to-new-positions [board]
  (go
   (loop [i 0 [dot & xdots] board]
     (when (not (nil? dot))
       (when (not= (dot :pos) i)
         (move-dot-to-pos dot i)
         (<! (timeout 100)))
       (recur (inc i) xdots)))))

(defn update-positions [board]
  (vec (map-indexed #(assoc %2 :pos %1) board)))

(defn remove-dots-from-dom [dots-to-remove]
  (doseq [dot dots-to-remove]
    (go
     (let [$elem ($ (dot :elem))]
       (.addClass $elem "scale-out")
       (<! (timeout 150))
       (.remove $elem)))))

(defn remove-dots [{:keys [dot-chain] :as state}]
  (let [pos-set        (set dot-chain)
        dots-to-remove (keep-indexed #(if (pos-set %1) %2) (state :board))
        next-board     (keep-indexed #(if (not (pos-set %1)) %2) 
                                          (state :board))]
    (remove-dots-from-dom dots-to-remove)
    (move-dots-to-new-positions next-board)
    (assoc state :board (update-positions next-board) :dot-chain [])))

(defn render-updates [state]
  (if (pos? (count (state :dot-chain)))
    (remove-dots state)
    state))

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 ;-).

full source for example

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.

(defn add-dots [state]
  (let [number-to-add (- board-size (count (state :board)))
        new-dots (map create-dot (repeat 8) (get-rand-colors number-to-add))
        next-board (concat (state :board) new-dots)]
    (add-dots-to-board (state :selector) new-dots)
    (go
     (<! (timeout 500))
     (move-dots-to-new-positions next-board))
    (assoc state :board (update-positions next-board))))

(defn render-updates [state]
  (if (pos? (count (state :dot-chain)))
    (add-dots (remove-dots state))
     state))

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.

full source for example

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.

(defn dot-follows? [{:keys [board]} prev-dot cur-dot]
  (let [prev-color (-> board (get prev-dot) :color)
        cur-color (-> board (get cur-dot) :color)]
    (or (nil? prev-dot)
        (and (= prev-color cur-color)
             (or (= cur-dot (inc prev-dot))
                 (= cur-dot (dec prev-dot)))))))

(defn get-dot-chain [state dot-ch first-dot-msg]
  (go
   (loop [dot-chain []
          msg first-dot-msg]
     (if (not= :dot-pos (first msg))
       dot-chain
       (recur (if (dot-follows? state (last dot-chain) (last msg))
                (conj dot-chain (last msg))
                dot-chain)
              (<! dot-ch))))))

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:

full source for example

Drawing feedback

We are going to finally give some feedback to our players so they know which dots they are selecting.

(defn dot-pos-to-center-position [dot-pos]
  (vec (map (partial + 10) (dot-pos-to-corner-position dot-pos))))

(defn render-chain-element [last-pos pos color]
  (let [[top1 left] (dot-pos-to-center-position last-pos)
        [top2 _] (dot-pos-to-center-position pos)
        style (str "width: 4px; height: 24px; top:"
                   (+ (min top1 top2) 11) "px; left: " ( - left 2) "px;")]
    [:div {:style style :class (str "line " (name (or color :blue)))}]))

(defn dot-highlight-templ [pos color]
  (let [[top left] (dot-pos-to-corner-position pos)
        style (str "top:" top "px; left: " left "px;")]
    [:div {:style style :class (str "dot-highlight " 
                                    (name (or color :blue)))}]))

(defn render-dot-chain [state dot-chain]
  (let [color (-> state :board
                  (get (first dot-chain))
                  :color)
        rends (map render-chain-element
                   (butlast dot-chain)
                   (rest dot-chain)
                   (repeat color))]
    (when (pos? (count dot-chain))
      (inner ($ (str (state :selector) " .dot-chain-holder"))
             (crate/html (concat [:div] rends)))
      (append ($ (str (state :selector) " .dot-highlights"))
              (crate/html (dot-highlight-templ (last dot-chain) color))))
    dot-chain))

(defn erase-dot-chain [state]
  (inner ($ (str (state :selector) " .dot-chain-holder")) "")
  (inner ($ (str (state :selector) " .dot-highlights")) ""))

(defn get-dot-chain [state dot-ch first-dot-msg]
  (go
   (loop [dot-chain []
          msg first-dot-msg]
     (if (not= :dot-pos (first msg))
       (do (erase-dot-chain state) dot-chain)
       (recur (if (dot-follows? state (last dot-chain) (last msg))
                (render-dot-chain state (conj dot-chain (last msg)))
                dot-chain)
              (<! dot-ch))))))

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.

full source for example

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: