Pure Danger Tech


Rock Paper Scissors with core.async

10 Jul 2013

Just for fun I implemented Rock Paper Scissors with core.async. Each player is modeled as a go process that generates moves on a channel. A judge is modeled as a go process that takes moves from each player via their channel and reports the match results on its own channel.

To start, let’s pull in core.async:

(require 'clojure.core.async :refer :all)

and define some helpful definitions:

(def MOVES [:rock :paper :scissors])
(def BEATS {:rock :scissors, :paper :rock, :scissors :paper})

Let’s make a player that randomly throws moves on an output channel:

(defn rand-player
  "Create a named player and return a channel to report moves."
  (let [out (chan)]
    (go (while true (>! out [name (rand-nth MOVES)])))

Here, chan creates an unbuffered (0-length channel). We create a go process which you can think of as a lightweight thread that will loop forever creating random moves (represented as a vector of [name throw]) and placing them on the out channel.

However, because the channel is unbuffered, the put (>!) will not succeed until someone is ready to read it. Inside a go process, these puts will NOT block a thread; the go process is simply parked waiting.

To create our judge, we’ll need a helper method to decide the winner:

(defn winner
  "Based on two moves, return the name of the winner."
  [[name1 move1] [name2 move2]]
   (= move1 move2) "no one"
   (= move2 (BEATS move1)) name1
   :else name2))

And now we’re ready to create our judging process:

(defn judge
  "Given two channels on which players report moves, create and return an
   output channel to report the results of each match as [move1 move2 winner]."
  [p1 p2]
  (let [out (chan)]
     (while true
       (let [m1 (<! p1)
             m2 (<! p2)]
         (>! out [m1 m2 (winner m1 m2)]))))

Similar to the players, the judge is a go process that sits in a loop forever. Each time through the loop it takes a move from each player, computes the winner and reports the match results on the out channel (as [move1 move2 winner]).

We need a bit of wiring code to start up the players and the judge and get the match result channel:

(defn init
  "Create 2 players (by default Alice and Bob) and return an output channel of match results."
  ([] (init "Alice" "Bob"))
  ([n1 n2] (judge (rand-player n1) (rand-player n2))))

And then we can play the game by simply taking a match result from the output channel and reporting.

(defn report
  "Report results of a match to the console."
  [[name1 move1] [name2 move2] winner]
  (println name1 "throws" move1)
  (println name2 "throws" move2)
  (println winner "wins!"))

(defn play
  "Play by taking a match reporting channel and reporting the results of the latest match."
  (apply report (<!! out-chan)))

Here we use the actual blocking form of take (<!!) since we are outside a go block in the main thread.

We can then play like this:

user> (def game (init))
user> (play game)

Alice throws :scissors
Bob throws :paper
Alice wins!
user> (play game)

Alice throws :scissors
Bob throws :rock
Bob wins!

We might also want to play a whole bunch of games:

(defn play-many
  "Play n matches from out-chan and report a summary of the results."
  [out-chan n]
  (loop [remaining n
         results {}]
    (if (zero? remaining)
      (let [[m1 m2 winner] (<!! out-chan)]
        (recur (dec remaining)
               (merge-with + results {winner 1}))))))

Which you’ll find is pretty fast:

user> (time (play-many game 10000))
"Elapsed time: 145.405 msecs"
{"no one" 3319, "Bob" 3323, "Alice" 3358}

Hope that was fun!

See: Rich Hickey podcast on core.async

See: Full gist