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." [name] (let [out (chan)] (go (while true (>! out [name (rand-nth MOVES)]))) out))
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]] (cond (= 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)] (go (while true (let [m1 (<! p1) m2 (<! p2)] (>! out [m1 m2 (winner m1 m2)])))) out))
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) (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." [out-chan] (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/game 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) results (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