dependencies
| (this space intentionally left almost blank) | |||||||||||||||||||||||||||||||||
This namespace contains the game specific, non-tableau related
functions required. It's designed for two players (at the moment)
plus a dealer. The namespace has no external dependency: it can
run on server or client side as required. The namespaced methods
are invoked from the Majority of these functions are fork from: https://github.com/jdeisenberg/cljs-blackjack | (ns data15-blackjack.blackjack (:require [data15-blackjack.utils :refer [other-player keywordize]])) | |||||||||||||||||||||||||||||||||
| (defonce game (atom {:deck (into [] (shuffle (range 0 52))) :dealer-hand [] :dealer-status nil :player1-hand [] :player1-name nil :player2-hand [] :player2-name nil :player1-status [0 :na] :player2-status [0 :na] :player1-feedback :player2-feeback })) | |||||||||||||||||||||||||||||||||
Set player name and ensure that one player has only one user id | (defn set-player-name! [player name] (when (= (get @game (keywordize (other-player player) :name)) name) ;; Log out other player if the user-id is the same (swap! game assoc (keywordize (other-player player) :name) nil)) (swap! game assoc (keywordize player :name) name)) | |||||||||||||||||||||||||||||||||
Game Logic | ||||||||||||||||||||||||||||||||||
Deal one card from the deck to a hand in the given position (face up or face down), possibly using the discard pile. Return a vector of the remaining deck, new hand, and discard pile | (defn deal [[deck hand discard-pile] position] (let [new-deck (if (empty? deck) (shuffle discard-pile) deck) new-discard (if (empty? deck) [] discard-pile) card [(first new-deck) position]] [(rest new-deck) (conj hand card) new-discard])) | |||||||||||||||||||||||||||||||||
Discard contents of a hand onto a pile; just the card number, not the up/down position. Return an empty hand and the new discard pile. | (defn discard [[hand pile]] [[] (vec (reduce (fn [acc x] (conj acc (first x))) pile hand))]) | |||||||||||||||||||||||||||||||||
Helper function to total a hand. The accumulator is a vector giving the current total of the hand and the current value of an ace (1 or 11) | (defn accumulate-value [[acc ace-value] card] (let [card-mod (inc (mod (first card) 13)) card-value (if (= card-mod 1) ace-value (min 10 card-mod))] [(+ acc card-value) (if (= card-mod 1) 1 ace-value)])) | |||||||||||||||||||||||||||||||||
Get total value of hand. Return a vector with total and
the status | (defn evaluate-hand [hand] (let [[pre-total ace-value] (reduce accumulate-value [0 11] hand) total (if (and (> pre-total 21) (= ace-value 1)) (- pre-total 10) pre-total)] (vec [total (cond (and (= total 21) (= (count hand) 2)) :blackjack (<= total 21) :ok :else :bust)]))) | |||||||||||||||||||||||||||||||||
Given players hand and dealer hand, return true if dealer has blackjack, players have blackjack, false otherwise. | (defn immediate-win [dealer-hand player1-hand player2-hand] (let [[p1total _] (evaluate-hand player1-hand) [p2total _] (evaluate-hand player2-hand) [dtotal _] (evaluate-hand dealer-hand)] (or (and (= p1total 21) (= p2total 21)) (= dtotal 21)))) | |||||||||||||||||||||||||||||||||
This function takes a player's hand and returns a new hand with all the cards in the :up position | (defn reveal [hand] (vec (map (fn [card] [(first card) :up]) hand))) | |||||||||||||||||||||||||||||||||
Store personalized feedback message, practically the
result from | (defn feedback [player message] (swap! game assoc (keywordize player :feedback) message)) | |||||||||||||||||||||||||||||||||
Evaluate the dealer's and player's hands when the game has ended. | (defn end-game [dealer-hand player-hand player] (let [[ptotal pstatus] (evaluate-hand player-hand) [dtotal dstatus] (evaluate-hand dealer-hand)] (cond (> ptotal 21) (feedback player "Sorry, you are busted.") (> dtotal 21) (feedback player "Dealer goes bust. You win!") (= ptotal dtotal) (feedback player "Tie game.") (= pstatus :blackjack) (feedback player "You win with blackjack!") (= dstatus :blackjack) (feedback player "Dealer has blackjack.") (< ptotal dtotal) (feedback player "Dealer wins.") (> ptotal dtotal) (feedback player "You win!") :else (feedback player "Unknown result (Shouldn't happen.)")))) | |||||||||||||||||||||||||||||||||
Deal two cards to the player (both face up), and two to the dealer (one face down and one face up). Update the game atom, and check for an immediate win. | (defn start-game [] (let [{:keys [deck discard-pile dealer-hand player1-hand player2-hand]} @game [player1-1 pile0] (discard [player1-hand discard-pile]) [player2-1 pile1] (discard [player2-hand pile0]) [dealer1 pile2] (discard [dealer-hand pile1]) [deck2 dealer2 pile3] (deal (deal [deck dealer1 pile2] :up) :down) [deck3 player1-2 pile4] (deal (deal [deck2 player1-1 pile3] :up) :up) [deck4 player2-2 pile-after-deal] (deal (deal [deck3 player2-1 pile4] :up) :up) ] (swap! game assoc :playing true :discard-pile pile-after-deal :player1-hand player1-2 :player2-hand player2-2 :dealer-hand dealer2 :deck deck4 :player1-feedback "" :player2-feedback "" :player1-status (evaluate-hand player1-2) :player2-status (evaluate-hand player2-2) :dealer-status nil) (if (immediate-win dealer2 player1-2 player2-2) (do (swap! game assoc :dealer-hand (reveal dealer2)) (end-game dealer2 player1-2 :player1) (end-game dealer2 player2-2 :player2))))) | |||||||||||||||||||||||||||||||||
Deal a card face up to the player, and evaluate the hand. If the player went bust, end the game. | (defn hit-me [player] (let [{:keys [deck discard-pile dealer-hand]} @game player-hand (@game (keywordize player :hand)) [deck2 player2 discard2] (deal [deck player-hand discard-pile] :up) [total status] (evaluate-hand player2)] (swap! game assoc (keywordize player :hand) player2 (keywordize player :status) [total status] :deck deck2 :discard-pile discard2) (if (= status :bust) (end-game dealer-hand player2 player)))) | |||||||||||||||||||||||||||||||||
Player is satisfied with hand. Reveal the dealer's hand, then deal cards one at a time until the dealer has to stand or goes bust. | (defn stand [role num-players] (swap! game assoc-in [(keywordize role :status) 2] :stand) (if (and (= num-players 2) (not= (get-in @game [(keywordize (other-player role) :status) 2]) :stand)) ; Other player is still playing (swap! game assoc (keywordize role :feedback) "Waiting for other player to stand.") ; We are done (let [{:keys [deck dealer-hand player1-hand player2-hand discard-pile]} @game dhand (reveal dealer-hand)] (swap! game assoc :dealer-hand dhand) (loop [loop-deck deck loop-hand dhand loop-pile discard-pile] (let [[total status] (evaluate-hand loop-hand)] (if (or (= status :bust) (>= total 17)) (do (swap! game assoc :dealer-hand loop-hand :dealer-status [total status]) (end-game loop-hand player1-hand :player1) (end-game loop-hand player2-hand :player2)) (let [[new-deck new-hand new-discard] (deal [loop-deck loop-hand loop-pile] :up)] (swap! game assoc :dealer-hand new-hand) (recur new-deck new-hand new-discard)))))))) | |||||||||||||||||||||||||||||||||
Web Server (Ring) functionsThis namespace manage the web server functionalities including request dispatch, session management, websocket/ajax calls, logon in addition to blackjack related functionalities. The namespace uses | (ns data15-blackjack.server (:require [ring.middleware.resource :as resources] [clojure.string :as str] [ring.middleware.defaults] [compojure.core :as comp :refer (defroutes GET POST)] [compojure.route :as route] [hiccup.core :as hiccup] [hiccup.page :refer (include-js include-css)] [clojure.core.async :as async :refer (<! <!! >! >!! put! chan go go-loop)] [taoensso.encore :as encore :refer ()] [taoensso.timbre :as timbre :refer (tracef debugf infof warnf errorf)] [taoensso.sente :as sente] [org.httpkit.server :as http-kit] [taoensso.sente.server-adapters.http-kit :refer (sente-web-server-adapter)] [data15-blackjack.blackjack :as blackjack]) (:gen-class)) | |||||||||||||||||||||||||||||||||
Logging config. The more the better. | (sente/set-logging-level! :trace) | |||||||||||||||||||||||||||||||||
Start our fancy | (defn start-web-server!* [ring-handler port] (println "Starting http-kit...") (let [http-kit-stop-fn (http-kit/run-server ring-handler {:port port})] {:server nil ; http-kit doesn't expose this :port (:local-port (meta http-kit-stop-fn)) :stop-fn (fn [] (http-kit-stop-fn :timeout 100))})) | |||||||||||||||||||||||||||||||||
Server-side setup | ||||||||||||||||||||||||||||||||||
Register async channels used for the communication. The directly used defs are:
| (let [{:keys [ch-recv send-fn ajax-post-fn ajax-get-or-ws-handshake-fn connected-uids]} (sente/make-channel-socket! sente-web-server-adapter {:packer :edn})] (def ring-ajax-post ajax-post-fn) (def ring-ajax-get-or-ws-handshake ajax-get-or-ws-handshake-fn) (def ch-chsk ch-recv) ; ChannelSocket's receive channel (def chsk-send! send-fn) ; ChannelSocket's send API fn (def connected-uids connected-uids)) ; Watchable, read-only atom | |||||||||||||||||||||||||||||||||
Number of identified users connected | (defn number-of-connected-users [] (count (filter #(not= :taoensso.sente/nil-uid %) (:any @connected-uids)))) | |||||||||||||||||||||||||||||||||
Ring Handlers | ||||||||||||||||||||||||||||||||||
Login methods sets the user name. No real login, just associate the username with the websocket. Later on this user-id will be used to send messages to one client only | (defn login! [ring-request] (let [{:keys [session params]} ring-request {:keys [role user-id]} params] (debugf "Login request: %s" params) (blackjack/set-player-name! role user-id) {:status 200 :session (assoc session :role role :uid user-id)})) | |||||||||||||||||||||||||||||||||
Langing page containing the tableau JS API vizardry. This is the main HTML
page serving requests hitting the | (defn landing-pg-handler [_] (hiccup/html [:head (include-js "http://public.tableau.com/javascripts/api/tableau-2.0.1.min.js") (include-css "css/page.css")] [:div#debug] [:div#tableau-viz] [:div#div-login [:h2 "Set user user-id"] [:p [:input#input-login {:type :text :placeholder "Enter your name:"}] [:button#btn-player1 {:class "login-button" :type "button"} "I'm Player 1"] [:button#btn-player2 {:class "login-button" :type "button"} "I'm Player 2"]]] [:p#game-buttons [:button#btn-hit {:class "game-button" :type "button"} "hit"] [:button#btn-stand {:class "game-button" :type "button"} "stand"] [:button#btn-reset {:class "game-button" :type "button"} "new"]] [:script {:src "js/client.js"}])) ; Include our cljs target | |||||||||||||||||||||||||||||||||
Basic endpoints:
| (defroutes my-routes (GET "/" req (landing-pg-handler req)) ;; (GET "/chsk" req (ring-ajax-get-or-ws-handshake req)) (POST "/chsk" req (ring-ajax-post req)) (POST "/login" req (login! req)) ;; (route/resources "/") ; Static files, notably public/js/client.js (our cljs target) (route/not-found (hiccup/html [:h1 "Invalid URL"]))) | |||||||||||||||||||||||||||||||||
The ring handler is reponsible to start ring web server, setup routing and session management default | (def my-ring-handler (let [ring-defaults-config (assoc-in ring.middleware.defaults/site-defaults [:security :anti-forgery] {:read-token (fn [req] (-> req :params :csrf-token))})] (ring.middleware.defaults/wrap-defaults my-routes ring-defaults-config))) | |||||||||||||||||||||||||||||||||
Routing handlers | ||||||||||||||||||||||||||||||||||
Send message to all connected, authenticated users | (defn server->all-users! [message] (doseq [uid (:any @connected-uids)] (when-not (= uid :sente/nil-uid) (chsk-send! uid message)))) | |||||||||||||||||||||||||||||||||
Broadcast state: send blackjack game's state to connected users | (defn broadcast-state! [] (server->all-users! [:data15-blackjack/broadcast-state @blackjack/game])) | |||||||||||||||||||||||||||||||||
Define multifunction to dispach messages based on | (defmulti event-msg-handler :id) | |||||||||||||||||||||||||||||||||
Log every incoming message and dispatch them. | (defn event-msg-handler* [{:as ev-msg :keys [id ?data event]}] (debugf "Event: %s" event) (event-msg-handler ev-msg)) | |||||||||||||||||||||||||||||||||
default event handler: log (and optionally reply) for each unidentified event message | (defmethod event-msg-handler :default [{:as ev-msg :keys [event id ?data ring-req ?reply-fn send-fn]}] (let [session (:session ring-req) uid (:uid session)] (debugf "Unhandled event: %s" event) (when ?reply-fn (?reply-fn {:umatched-event-as-echoed-from-from-server event})))) | |||||||||||||||||||||||||||||||||
This is out main event, the Finally we call | (defmethod event-msg-handler :data15-blackjack/click [{:as ev-msg :keys [event id ?data ring-req ?reply-fn send-fn]}] (let [{:keys [uid role]} (:session ring-req)] (debugf "Initalize request: %s uid: %s data: %s" event uid ?data) (condp = ?data "load" nil "reset" (blackjack/start-game) "stand" (blackjack/stand role (number-of-connected-users)) "hit" (blackjack/hit-me role)) (broadcast-state!))) | |||||||||||||||||||||||||||||||||
Init web server | ||||||||||||||||||||||||||||||||||
Web server atom to store server information in {:server _ :port _ :stop-fn (fn [])} format | (defonce web-server_ (atom nil)) ; | |||||||||||||||||||||||||||||||||
Invoke server's stop function. | (defn stop-web-server! [] (when-let [m @web-server_] ((:stop-fn m)))) | |||||||||||||||||||||||||||||||||
Start web server on | (defn start-web-server! [& [port]] (stop-web-server!) (let [{:keys [stop-fn port] :as server-map} (start-web-server!* (var my-ring-handler) (or port 3000)) uri (format "http://localhost:%s/" port)] (debugf "Web server is running at `%s`" uri) (reset! web-server_ server-map))) | |||||||||||||||||||||||||||||||||
Atom to store message router state. | (defonce router_ (atom nil)) | |||||||||||||||||||||||||||||||||
Stop router if we aware of any router stopper callback function. | (defn stop-router! [] (when-let [stop-f @router_] (stop-f))) | |||||||||||||||||||||||||||||||||
Stop and start router while storing the router stop-function in | (defn start-router! [] (stop-router!) (reset! router_ (sente/start-chsk-router! ch-chsk event-msg-handler*))) | |||||||||||||||||||||||||||||||||
Start message web socket async message router and web server | (defn start! [] (start-router!) (start-web-server!)) | |||||||||||||||||||||||||||||||||
start things from REPL immediately | (start-router!) | |||||||||||||||||||||||||||||||||
In case you start the server from command line here is a nice | (defn -main [& args] (start!)) | |||||||||||||||||||||||||||||||||
Client-side browser functionsThis namespace responsible for the client side code running in the browser side. This includes the websocket/ajax communication channel management, sending, receiving and dispatching messages. | (ns data15_blackjack.client (:require [data15-blackjack.tableau :as tableau] [clojure.string :as str] [cljs.core.async :as async :refer (<! >! put! chan)] [dommy.core :as dommy :refer-macros [sel sel1]] [taoensso.encore :as encore :refer ()] [taoensso.timbre :as timbre :refer-macros (tracef debugf infof warnf errorf)] [taoensso.sente :as sente :refer (cb-success?)]) (:require-macros [cljs.core.async.macros :as asyncm :refer (go go-loop)])) | |||||||||||||||||||||||||||||||||
Logging config. We do love logs, so level is | (sente/set-logging-level! :trace) | |||||||||||||||||||||||||||||||||
Client-side setup | ||||||||||||||||||||||||||||||||||
Create a new | (debugf "ClojureScript appears to have loaded correctly.") (let [{:keys [chsk ch-recv send-fn state]} (sente/make-channel-socket! "/chsk" ; Note the same URL as before {:type :auto :packer :edn})] (def chsk chsk) (def ch-chsk ch-recv) ; ChannelSocket's receive channel (def chsk-send! send-fn) ; ChannelSocket's send API fn (def chsk-state state) ; Watchable, read-only atom) | |||||||||||||||||||||||||||||||||
Convert a ClickEvent object to string while removing the btn- prefix Client-side logic and helpers | (defn- unbuttonize [click-event] (-> (.-toElement click-event) (.-id) (clojure.string/replace "btn-" ""))) | |||||||||||||||||||||||||||||||||
Setup username and player role. Called from login buttons, thus,
| (defn login-event-handler [event] (debugf "setting username to user-id") (let [user-id (dommy/value (sel1 :#input-login))] (if (str/blank? user-id) (js/alert "Please enter user name first") (do (debugf "Logging in with user-id %s" user-id) (sente/ajax-call "/login" {:method :post :params {:user-id (str user-id) :role (unbuttonize event) ; button name :csrf-token (:csrf-token @chsk-state)}} (fn [ajax-resp] (debugf "Ajax login response: %s" ajax-resp) (sente/chsk-reconnect! chsk))))))) | |||||||||||||||||||||||||||||||||
Client-side UI | ||||||||||||||||||||||||||||||||||
Call | (doseq [button (sel :.login-button)] (dommy/listen! button :click login-event-handler)) | |||||||||||||||||||||||||||||||||
Send new/hit/stand events to the server. The message payload is simply
the button's name without the | (doseq [button (sel :.game-button)] (dommy/listen! button :click (fn [e] (chsk-send! [:data15-blackjack/click (unbuttonize e)])))) | |||||||||||||||||||||||||||||||||
Toggle buttons in game-button class based on | (defn show-game-buttons! [show?] (doseq [button (sel :.game-button)] (dommy/toggle! button show?))) | |||||||||||||||||||||||||||||||||
Toggle tableau viz based on | (defn show-tableau-viz! [show?] (dommy/toggle! (sel1 :div#tableau-viz) show?)) | |||||||||||||||||||||||||||||||||
Toggle login box based on | (defn show-login! [show?] (dommy/toggle! (sel1 :div#div-login) show?)) | |||||||||||||||||||||||||||||||||
Message handlers | ||||||||||||||||||||||||||||||||||
Watch for | (add-watch tableau/viz :ui (fn [_ _ _ new-state] (when (= :viz-ready (new-state :status)) (chsk-send! [:data15-blackjack/click " load "])))) | |||||||||||||||||||||||||||||||||
Define new multi-function which will dispatch on event-id | (defmulti event-msg-handler :id) | |||||||||||||||||||||||||||||||||
Wrap for logging and dispatching. We will use a | (defn event-msg-handler* [{:as ev-msg :keys [id ?data event]}] (debugf " Event: % s " event) (event-msg-handler ev-msg)) | |||||||||||||||||||||||||||||||||
Just log unhandled events (like | (defmethod event-msg-handler :default [{:as ev-msg :keys [event]}] (debugf " Unhandled event: % s " event)) | |||||||||||||||||||||||||||||||||
We just received a new push notification from the server.
If we already authenticated, update the tableau visualization
with | (defmethod event-msg-handler :chsk/recv [{:as ev-msg :keys [?data]}] (when-not (= (get @chsk-state :uid) :sente/nil-uid) (tableau/update-tableau (get @chsk-state :uid) (second ?data))) (debugf " Push event from server: % s " ?data)) | |||||||||||||||||||||||||||||||||
The handshare is the first callback from server side after
establishing connection. Change visible div's according to
user-id: if the user-id is | (defmethod event-msg-handler :chsk/handshake [{:as ev-msg :keys [?data]}] (let [[?uid] ?data] (debugf " Handshake: % s " ?data) (let [logged-in? (not= ?uid :taoensso.sente/nil-uid)] (show-game-buttons! logged-in?) (show-tableau-viz! logged-in?) (show-login! (not logged-in?))))) | |||||||||||||||||||||||||||||||||
Atom to store stop function for stopping the router Router InitalizationStop the message router by calling the previously saved stop function | (def router_ (atom nil)) (defn stop-router! [] (when-let [stop-f @router_] (stop-f))) (defn start-router! [] " Stop router first, then start and save the result (which is a stop callback) in `router_ `. " (stop-router!) (reset! router_ (sente/start-chsk-router! ch-chsk event-msg-handler*))) | |||||||||||||||||||||||||||||||||
Start router by default, on-load | (start-router!) | |||||||||||||||||||||||||||||||||
Tableau related functionsThis namespace loads and changes the tableau visualisation. | (ns data15-blackjack.tableau (:require [data15-blackjack.utils :refer [other-player keywordize select-values]] [dommy.core :refer-macros [sel sel1]])) | |||||||||||||||||||||||||||||||||
Initializing Viz | ||||||||||||||||||||||||||||||||||
Map (atom) to store tableau viz load status & object reference | (def viz (atom {:status :not-loaded :vizobj nil})) | |||||||||||||||||||||||||||||||||
The public URL of the tableau viz. | (def viz-url "https://public.tableau.com/views/Blackjack/BlackjackTableau") | |||||||||||||||||||||||||||||||||
Tableau viz's div element. This is where the visualization will go | (def placeholder-div (sel1 :div#tableau-viz)) | |||||||||||||||||||||||||||||||||
Javascript object to interface with | (def viz-options (js-obj "hideTabs" true "hideToolbar" true "onFirstInteractive" #(swap! viz assoc :status :viz-ready))) | |||||||||||||||||||||||||||||||||
Initialize our visualization with | (swap! viz assoc :status :initalize :vizobj (js/tableau.Viz. placeholder-div viz-url viz-options)) | |||||||||||||||||||||||||||||||||
Interaction with Viz | ||||||||||||||||||||||||||||||||||
Get the | (defn workbook [] (.getWorkbook (get @viz :vizobj))) | |||||||||||||||||||||||||||||||||
Get the | (defn get-sheet-in-active-sheet [sheet] (-> (workbook) (.getActiveSheet) (.getWorksheets) (.get sheet))) | |||||||||||||||||||||||||||||||||
Filter supplied | (defn filter-update [sheet key values] (-> (get-sheet-in-active-sheet sheet) (.applyFilterAsync key (clj->js values) js/tableau.FilterUpdateType.REPLACE))) | |||||||||||||||||||||||||||||||||
Update | (defn parameter-update [workbook key value] (.changeParameterValueAsync workbook key (clj->js value))) | |||||||||||||||||||||||||||||||||
Card related helper functions | ||||||||||||||||||||||||||||||||||
We show always five cards for the players. If the player has less then
five cards then we are padding his hand with "empty" cards. Empty cards
are the ones with ID=52. Thus, | (defn pad-with-blank-cards [cards] (let [num-cards (count cards)] (concat cards (take (- 5 num-cards) [452 352 252 152 52])))) | |||||||||||||||||||||||||||||||||
Show hand in | (defn update-cards [sheet cards] (filter-update sheet "Location-ID" (pad-with-blank-cards cards))) | |||||||||||||||||||||||||||||||||
Calculate cards unique ID in tableau. Basic calculation is: | (defn get-cards [hand] (map-indexed (fn [idx [card pos]] (if (= pos :down) 99 ; face down card (+ (* idx 100) card))) ; 100 * pos in hand + card ID hand)) | |||||||||||||||||||||||||||||||||
Synchronize Server state with Tableau | ||||||||||||||||||||||||||||||||||
Synchronize Tableau report with the blackjack game state broadcasted by the server after every action:
| (defn update-tableau [uid state] (let [{:keys [player1-name dealer-hand]} state my-role (if (= uid player1-name) :player1 :player2) other-user (other-player my-role) [other-status your-status feedback other-name your-hand] (select-values state [(keywordize other-user "status") (keywordize my-role "status") (keywordize my-role "feedback") (keywordize other-user "name") (keywordize my-role "hand")])] (update-cards "Player" (get-cards your-hand)) (update-cards "Dealer" (get-cards dealer-hand)) (parameter-update (workbook) "other-score" (first other-status)) (parameter-update (workbook) "your-score" (first your-status)) (parameter-update (workbook) "feedback" feedback) (parameter-update (workbook) "other-name" (or other-name "Player2")))) | |||||||||||||||||||||||||||||||||