Skip to content

Instantly share code, notes, and snippets.

@rm-hull
Last active August 29, 2015 13:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rm-hull/8723389 to your computer and use it in GitHub Desktop.
Save rm-hull/8723389 to your computer and use it in GitHub Desktop.
A Rock-Paper-Scissors game in ClojureScript, using core.async and big-bang.
(ns big-bang.examples.rock-paper-scissors
(:require
[cljs.core.async :refer [<! chan mult tap] :as async]
[clojure.string :as str]
[enchilada :refer [proxy-request]]
[dommy.core :refer [insert-after!]]
[dommy.template :as template]
[big-bang.core :refer [big-bang]]
[big-bang.package :refer [make-package]]
[dataview.loader :refer [fetch-text]]
[big-bang.examples.rock-paper-scissors.component.human :as human]
[big-bang.examples.rock-paper-scissors.component.opponent :as opponent]
[big-bang.examples.rock-paper-scissors.component.referee :as referee])
(:require-macros
[cljs.core.async.macros :refer [go]]
[dommy.macros :refer [sel1 sel node]]))
(def url-root "https://raw.github.com/rm-hull/big-bang/master/examples/rock-paper-scissors/")
(defn style [& styles ]
[:style (str/join \newline styles)])
(defn player-div [id css-class title-text svg]
[:div {:class (name css-class)}
[:div.title [:h3 {:id (name id)} title-text]]
[:div.graphic
(template/html->nodes svg)]])
(defn init-play-area [human-svg opponent-svg]
(->>
(sel1 :#canvas-area)
(insert-after!
(node
[:div#app
(style
"#app { font-family: monospace; }"
"div.pull-right { float: right; width: 150px; padding: 7px; }"
"div.leftpanel { float:left; margin: 20px; }"
"div.rightpanel { margin: 20px; }"
"#discourse-area { width: 850px; border: 1px grey solid; }"
"#discourse-area h2 { padding: 15px; }")
[:div#discourse-area
[:div.pull-right [:p#winner][:p#score]]
[:h2 "Let's play a game - select your weapon"]]
(player-div :human :leftpanel "Choose:" human-svg)
(player-div :opponent :rightpanel "Your opponent is waiting..." opponent-svg)
[div {:style "clear:both;"}]]))))
(defn start-game [seed]
(let [results-chan (chan)
notify-chan (chan)
notify-mult (mult notify-chan)
notifos (fn [] (let [c (chan)] (tap notify-mult c) c))]
(go
(init-play-area
(<! (fetch-text (proxy-request (str url-root "rps.svg"))))
(<! (fetch-text (proxy-request (str url-root "vc.svg")))))
; referee
(big-bang
:initial-state referee/initial-state
:to-draw referee/render
:on-receive referee/incoming
:receive-channel results-chan
:send-channel notify-chan)
; opponent
(big-bang
:initial-state (opponent/initial-state seed)
:to-draw opponent/render
:on-receive opponent/incoming
:receive-channel (notifos)
:send-channel results-chan)
; human player
(big-bang
:initial-state human/initial-state
:to-draw human/render
:on-click human/update
:event-target (sel :g.clickable)
:on-receive human/incoming
:receive-channel (notifos)
:send-channel results-chan))))
(start-game (rand)) ; start the game with a random seed

Rock Paper Scissors

A familiar game implemented using Big-Bang, a ClojureScript library inspired by Racket's big-bang. It abstracts the GUI event-loop into a component based system, allowing self-contained big-bang 'worlds' to communicate over core.async channels, in a reactive CSP style.

In this example, the main file builds some DOM elements and fetches various SVG assets for inclusion on the page. Then, three big-bang components are initialized:

  • The referee is responsible for kicking the opponent into making a choice, and for collating responses as well as keeping track of the score and who won each round.

  • The human component listens for events delivered from the SVG, and propagates the choice onto the referee. The choice is extracted from the event target payload (each clickable element has a "data-type='...' attribute).

  • An opponent just randomly picks one of Rock, Paper or Scissors when it is notified to choose by the referee. After which, it sends its choice back to the referee

They communicate with a notification channel outbound from the referee, mult(iplied) with taps to the human and opponent. Presently, only the opponent does anything with the notification events, and althought the human is wired in, it ignores them. A shared results channel between the human and opponent delivers choice messages back to referee.

State transitions inside each big-bang component are entirely free of side effects (even the apparent randomness inside the opponent). The render operations however will mutate the DOM.

(ns big-bang.examples.rock-paper-scissors.component.human
(:require
[clojure.string :as str]
[dommy.core :refer [attr set-html!]]
[big-bang.events.browser :refer [target]]
[big-bang.package :refer [make-package]])
(:require-macros
[dommy.macros :refer [sel1]]))
; An opponent just randomly picks one of Rock, Paper or Scissors from the
; list below when it is notified to choose by the referee. After which,
; it sends its results back to the referee
(def initial-state {:id :human})
(defn update [event world-state]
(let [chosen-weapon (-> event target .-parentNode (attr "data-type") keyword)]
(make-package
(assoc world-state :weapon chosen-weapon) ; new world-state
{:from :human :weapon chosen-weapon}))) ; message
(defn incoming [event world-state] ; does not act upon incoming messages presently
world-state)
(defn render [world-state]
(when-let [weapon (:weapon world-state)]
(->
(sel1 :#human)
(set-html! (str "You chose: " (-> weapon name str/upper-case))))))
(ns big-bang.examples.rock-paper-scissors.component.opponent
(:require
[clojure.string :as str]
[dommy.core :refer [attr set-html!]]
[big-bang.package :refer [make-package]])
(:require-macros
[dommy.macros :refer [sel1]]))
; An opponent just randomly picks one of Rock, Paper or Scissors from the
; list below when it is notified to choose by the referee. After which,
; it sends its results back to the referee
(def choices [:rock :paper :scissors])
; see:
; https://github.com/richhickey/clojure-contrib/blob/master/src/main/clojure/clojure/contrib/probabilities/random_numbers.clj
; http://en.wikipedia.org/wiki/Linear_congruential_generator
(defn make-rand-seq [seed]
(let [m (Math/pow 2 32) ; From "Numerical Recipies"
a 1664525
c 1013904223]
(letfn [(seq0 [seed]
(let [value (/ (float seed) (float m))
new-seed (rem (+ c (* a seed)) m)]
(lazy-seq (cons value (seq0 new-seed)))))]
(seq0 seed))))
(defn initial-state [seed]
{:id :opponent
:rnd-seq (->>
seed
make-rand-seq
(map (comp choices int (partial * 3))))})
(defn render [world-state]
(when-let [weapon (:weapon world-state)]
(->
(sel1 :#opponent)
(set-html! (str "Your opponent chose: " (-> weapon name str/upper-case))))))
(defn choose [world-state]
(let [rnd (:rnd-seq world-state)
chosen-weapon (first rnd)]
(make-package
(assoc world-state :weapon chosen-weapon :rnd-seq (rest rnd))
{:from :opponent :weapon chosen-weapon})))
(defn incoming [event world-state]
(condp = event
{:to :opponent :choose true}
(choose world-state)
; default
world-state))
(ns big-bang.examples.rock-paper-scissors.component.referee
(:require
[clojure.string :as str]
[dommy.core :refer [attr set-html!]]
[big-bang.package :refer [make-package]])
(:require-macros
[dommy.macros :refer [sel1]]))
; The referee is responsible for kicking the opponent into making a choice,
; and for collating responses as well as keeping track of the score and
; who won each round.
(def initial-state
{:id :referee
:score {:human 0 :opponent 0}})
(defn render [world-state]
(->
(sel1 :#score)
(set-html! (str
(get-in world-state [:score :human])
" - "
(get-in world-state [:score :opponent]))))
(->
(sel1 :#winner)
(set-html! (str "Winner: " (str/upper-case (name (get world-state :winner "???")))))))
(defn beats? [w1 w2]
(condp = [w1 w2]
[:scissors :paper] true
[:paper :rock] true
[:rock :scissors] true
false))
(defn calc-score [score w1 w2]
(if (beats? w1 w2)
(inc score)
score))
(defn winner [w1 w2]
(let [b1 (beats? w1 w2)
b2 (beats? w2 w1)]
(condp = [b1 b2]
[true false] :human
[false true] :opponent
:draw)))
(defn incoming [{:keys [from weapon] :as event} world-state]
(condp = from
:human
(make-package
(assoc-in world-state [:select from] weapon)
{:to :opponent :choose true})
:opponent
(let [human-weapon (get-in world-state [:select :human])
opponent-weapon weapon]
(->
world-state
(update-in [:score :human] calc-score human-weapon opponent-weapon)
(update-in [:score :opponent] calc-score opponent-weapon human-weapon)
(assoc :winner (winner human-weapon opponent-weapon))))
; default
world-state))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment