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
