Blackjack in ClojureScript

Many years ago—in 1998—I implemented a version of Blackjack using dynamic HTML. Hey, using JavaScript to control a web page was new and shiny back then.

I decided to do an update of the code to learn some ClojureScript. Also to update the graphics :)

Data Structures

The first question is how to represent a playing card. I could store it as a vector of two elements: like so: [3 "clubs"], but that’s not a good representation, as some of the card names would be strings rather than numbers, e.g., "Queen". Instead, I decided to go with a number from 0 to 51. Numbers 0-12 are the clubs, 13-25 diamonds, and similarly for hearts and spades. Within each set of thirteen, the first card is the Ace, then 2 through 10, Jack, Queen, and King.

This is a better representation, since it makes calculating a card’s base value a simple matter of division and remainders. When I need the text representation of a card for the alt attribute of a card image, it’s easy enough to write a function to generate that.

When a card goes into a player’s hand, I need to know if it is face up or face down. So, in a player’s hand, a card is represented by a two-element vector: [0 :up]—the Ace of Clubs, face up.

Once that fundamental issue is taken care of, what other data does the game need?

At this point, you may be wondering “So, where’s the ClojureScript? It will be here soon, but one of the things that functional programming encourages is figuring out what you need to do before you start coding. Now that this part seems reasonably figured out, here is the ClojureScript for the game state. It’s an atom that holds a map, with the deck initialized to a shuffle of the numbers 0 through 52.

(defonce game (atom {:deck (into [] (shuffle (range 0 52)))
                     :discard-pile []
                     :dealer-hand []
                     :player-hand []
                     :player-money 1000.0
                     :current-bet 0}))

Transformations

Now it’s time to figure out what transformations are needed. That’s what functions do—they transform input to output.

Dealing A Card

Here are the transformations that have to happen when you deal a card:

InputOutputCode
  • The deck
  • A hand
  • How to deal the card (:up or :down)
  • The deck without the first card
  • The hand with a new card, dealt :up or :down
(defn deal
  [deck hand position]
  (let [card (first deck)]
    (vec [(rest deck) (conj hand [card position])])))

Reshuffling

At some point the deck will be empty and you have to reshuffle the discard pile, which becomes the new deck, and the discard pile is emptied.

Stop sign

This is not a good approach. In fact, from a functional standpoint, it’s dreadful. Because I’ve split up the dealing process into two parts, then when you deal two cards to the player’s hand, the code is well on its way to something like this:

(let [[deck player-hand] (del player-hand deck :up)
      [deck discard-pile2] (reshuffle deck discard-pile)
      [deck player-hand] (deal player-hand deck :up)
      [deck discard-pile] (reshuffle deck discard-pile)]
  ...)

...and you’re back in imperative programming land again. After further thought, the deck, hand, and discard pile are all of a piece. Why not have deal take two arguments: a vector of [deck hand discard-pile] and a position (:up or :down). Then the return value of a deal can be fed directly to the first parameter of another deal:

(deal (deal [deck player-hand discard-pile] :up) :up)

Here is the better way:

InputOutputCode
  • The deck
  • A hand
  • The discard pile
  • How to deal the card (:up or :down)
  • The deck without the first card
  • The hand with a new card, dealt :up or :down
  • The discard pile (changed if the deck was empty)
(defn deal
  [[deck hand discard-pile] position]
  [[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]))

Evaluating a Hand

Now that you can deal someone a hand, you have to know what its total value is. The function to do that has a hand as its input and produces a vector in the form [total :ok|:blackjack|:bust]. The first entry in the vector is the total value of the hand; the second entry tells whether the total is :ok (less than or equal to 21), :blackjack (an Ace and any card worth 10), or :bust (greater than 21). Let’s break this into smaller pieces.

Finding the total for a hand involves adding up the values of each card. If you think this is a good place to use the reduce function, you are correct. But this code won’t work:

(reduce (fn [acc card] (+ acc (mod (first card) 13)) 1) hand)

It doesn’t work for two reasons:

The first problem is easily solved. For non-Aces, take the minimum of the mod value and 10; that caps the value at 10. The second problem is a bit trickier. The function used with reduce needs to keep track of two things in its accumulator: the current total and the current value of an Ace. Luckily for us, there’s no law that says the accumulator cannot be a vector. Here is the code that will accumulate the value of a hand.

1 (defn accumulate-value
2   [[acc ace-value] card]
3   (let [card-mod (inc (mod (first card) 13))
4         card-value (if (= card-mod 1)
5                      ace-value
6                      (min 10 card-mod))]
7     [(+ acc card-value)
8      (if (= card-mod 1) 1 ace-value)]))

Going through the relevant lines:

Line 2:
This line uses destructuring to take the first argument (a vector) and bind it to the symbols acc (the total value) and ace-value (the current value of an Ace)
Line 3:
Calculate the “base value” of the card by taking it mod 13 and adding 1.
Line 4:
If the base value is 1, this is an Ace, so...
Line 5:
The card’s final value is the current value of an Ace
Line 6:
Otherwise, its final value is capped at 10
Line 7:
The return value for the next stage of reduce must be a vector. The first item in the vector is the current total plus the card’s final value
Line 8:
And the second item is the new value for an Ace. If this card is an Ace, then this at least our first Ace, and the new value for any subsequent Aces is 1; otherwise the value of an Ace remains unchanged.

Now evaluating a hand is straightforward.

1 (defn evaluate-hand [hand]
2   (let [[pre-total ace-value] (reduce accumulate-value [0 11] hand)
3         total (if (and (> pre-total 21) (= ace-value 1)) (- pre-total 10) pre-total)]
4    (vec [total (cond
5                  (and (= total 21) (= (count hand) 2)) :blackjack
6                  (<= total 21) :ok
7                  :else :bust)])))
Line 2:
Notice the initial value [0 11] passed to reduce: the zero is the initial value of the total, and the value of an Ace (which will be the first one) is 11.
Line 3:
If the hand would have gone over 21 and there is at least one Ace, count the first Ace as only 1 instead of 11 by subtracting 10.
Line 5:
The cond gives the hand’s status. The test for a Blackjack works because if you have a total of 21 and only two cards, you must have an Ace and a card worth 10.

Miscellaneous Functions

At some point, you have to turn over the face-down cards that went to the dealer. The reveal function takes a hand and returns a new hand with all the cards face up:

(defn reveal
  "This function takes a player's hand and returns a new hand
  with all the cards in the :up position"
  [hand]
  (let [result (vec (map (fn [card] [(first card) :up]) hand))]
    result))

And you have to be able to take a hand and put it on the discard pile. The discard function takes a vector consisting of the hand and the current discard pile as it input. It returns a similar vector with an empty hand and the new value of the discard pile. (This will allow the calls to be chained if that is ever desired.) Note that just the card number goes into the discard pile, the :up or :down is irrelevant.

(defn discard
  [[hand pile]]
  [ [] (vec (reduce (fn [acc x] (conj acc (first x))) pile hand)]))

After the first two cards are dealt, we need to know if there is an immediate win or not:

Here is the code:

(defn immediate-win
  [dealer-hand player-hand]
  (let [[ptotal _] (evaluate-hand player-hand)
        [dtotal _] (evaluate-hand dealer-hand)]
    (or (= ptotal 21) (= dtotal 21))))

That takes care of the game logic.

User Interface

Two more items need to be added to the game atom: :player-feedback (initially the empty string) and :feedback-color (:red, :green, or :black for loss, win, and tie). I’m not showing the code here; it’s just two more entries into the key/value map. (Originally the design had two feedback areas; one above the dealer’s cards and one above the player’s. During development, I eliminated the feedback at the dealer area, as it was too confusing to look in two different places to figure out whether you had won or lost.)

The rest of the code displays the card playing area (the tableau) and handles events, all using the Reagent library. If you’re not familiar with Reagent, stop and read this excellent introduction. TL;DR: In Reagent, a user interface component is a function that describes the HTML to be displayed and the functions to which it should react. Here is the code that generates the tableau.

  1  (defn tableau
  2    "Display the playing area (the tableau)."
  3    []  
  4    (let [{:keys [dealer-hand player-hand playing player-feedback feedback-color
  5                  player-money current-bet]} @game]
  6    [:div
  7     [:h2 "Dealer’s Cards"]
  8     [show-cards dealer-hand]
  9     [:hr]
 10     [:h2 "Your Cards" [:span {:style {:color feedback-color}} player-feedback]]
 11     [show-cards player-hand]
 12     [:p 
 13      [:input {:type "button"
 14               :value "Start Game"
 15               :style {:display (if playing "none" "inline")}
 16               :on-click start-game}]
 17      [:input {:type "button"
 18               :value "Hit"
 19               :on-click hit-me
 20               :style {:display (if playing "inline" "none")}}]
 21      [:input {:type "button"
 22               :value "Stand"
 23               :on-click stand
 24               :style {:display (if playing "inline" "none")}}]
 25       " Your bet: $"
 26       [:input {:type "text"
 27                :size "5" 
 28                :on-change change-bet
 29                :value current-bet}]
 30       " Current money: $" player-money
 31      "\u00a0 \u00a0" ;; spacing
 32      [:input {:type "button"
 33               :value "Recharge money"
 34               :on-click recharge-money
 35               :style {:display (if (= player-money 0) "inline" "none")}}]]]))

Here are the Reagent concepts you need to know from these lines.

Lines 4-5
Get all the values from the game atom
Line 6
HTML is represented as a vector, with element names as keywords.
Line 9
You can nest elements (the [:span...]). Attributes are represented as a ClojureScript map. The content of the span is player-feedback, which is bound to the value from the game atom. This means that any change to that part of the atom will be reflected in the HTML.
Line 8
The :h2 element is followed by another component: show-cards player-hand will call the show-cards function with player-hand as its argument. Again, because player-hand is bound to part of the atom, any change to that value will re-display that component.
Line 15
CSS properties are also represented by a map. The display property will be set depending upon the state of playing, which, again, is bound to a value in the game atom.
Line 16
This is how you do event handling. When the button is clicked, the start-game function will be called with the event as its argument.

Displaying Cards

This code creates an HTML <div> element that contains the images of the cards in a person’s hand by mapping card-image across all the cards. The function given to map-indexed will receive the index number of the item in the sequence and the item as its arguments. The result goes into a vector and thus becomes a component.

(defn show-cards [hand]
  (into [] (concat [:div {:class "cards"}]
                   (map-indexed card-image hand))))

card-image is given an index number and a card as its arguments. It then creates an <img> element with an appropriate src attribute (lines 3 and 4 in the following code). It also sets the alt and title attribute using the english function (lines 5 and 6). The reason the index is required is so that the cards can be relatively positioned to overlap (line 8).

 1 (defn card-image
 2   [n [card pos]]
 3   (let [filename (if (= pos :up) card "blue_grid_back")]
 4     [:img {:src (str "./images/" filename ".svg")
 5            :alt (english card pos)
 6            :title (english card pos)
 7            :style {:position "relative"
 8                    :left (* -55 n)
 9                    :height "133px"
10                    :width "98px"}}]))

This is the code for conversion to English; just an integer division and remainder to figure out the suit and card:

(def cardnames ["Ace", "2", "3", "4", "5", "6", "7", "8", "9" "10" "Jack" "Queen" "King"])
(def suits ["clubs" "diamonds" "hearts" "spades"])

(defn english [cval pos]
  (if (= pos :up)
    (str (nth cardnames (rem cval 13)) " of "
         (nth suits (quot cval 13)))
    "face down card")
)

Event Handling

The events that the code has to handle are:

Starting the Game

 1 (defn start-game
 2   [event]
 3   (let [{:keys [deck discard-pile dealer-hand player-hand current-bet]} @game
 4         [player1 pile0] (discard [player-hand discard-pile])
 5         [dealer1 pile1] (discard [dealer-hand pile0])]
 6         [deck2 dealer2 pile2] (deal (deal [deck dealer1 pile1] :up) :down)
 7         [deck3 player2 pile3] (deal (deal [deck2 player1 pile2] :up) :up)]
 8     (swap! game assoc :playing true :discard-pile pile3 :player-hand player2
 9            :dealer-hand dealer2 :deck deck3 :dealer-feedback "" :player-feedback "")
10     (if (immediate-win dealer2 player2)
11       (do
12         (swap! game assoc :dealer-hand (reveal dealer2))
13         (end-game dealer2 player2)))))

At the beginning of a new game, the code has to:

You may be wondering why I am creating variables like deck3 and player2 instead of re-using the variable names:

  (let [{:keys [deck discard-pile dealer-hand player-hand current-bet]} @game
        [player pile] (discard [player-hand discard-pile])
        [dealer pile] (discard [dealer-hand pile])
        [deck dealer pile] (deal (deal [deck dealer pile] :up) :down)
        [deck player pile] (deal (deal [deck player pile] :up) :up)]

The reason is that I come from an Erlang background, where variables are not only immutable but are also single-assignment. Once you bind a value to a symbol, you cannot rebind another value to that symbol; you must create a new symbol.

Hit and Stand

If the player or dealer don’t have blackjack, the player can either hit to request another card or stand to say they are happy with their hand. The following code deals a card face up to the player, evaluates the hand, updates the game atom, and ends the game if the total is over 21 (the player has “busted”).

(defn hit-me [event]
  (let [{:keys [deck discard-pile dealer-hand player-hand]} @game
        [deck2 player2 discard2] (deal [deck player-hand discard-pile] :up)
        [total status] (evaluate-hand player2)]
    (swap! game assoc :player-hand player2 :deck deck2 :discard-pile discard2)
    (if (= status :bust) (end-game dealer-hand player2))))

Here is the code that is invoked when the player clicks the Stand button.

  1 (defn stand [event]
  2   (let [{:keys [deck dealer-hand player-hand discard-pile]} @game
  3         dhand (reveal dealer-hand)]
  4     (swap! game assoc :dealer-hand dhand)
  5     (loop [loop-deck deck
  6            loop-hand dhand
  7            loop-pile discard-pile]
  8       (let [[total status] (evaluate-hand loop-hand)]
  9         (if (or (= status :bust) (>= total 17))
 10           (do
 11             (swap! game assoc :dealer-hand loop-hand)
 12             (end-game loop-hand player-hand))
 13           (let [[new-deck new-hand new-discard] (deal [loop-deck loop-hand loop-pile] :up)]
 14             (swap! game assoc :dealer-hand new-hand)
 15             (recur new-deck new-hand new-discard)))))))

The code reveals the dealer’s face-down card (line 3) and updates the atom in order to force the card to redisplay (line 4). Lines 5-7 start a loop that evaluates the hand (line 8). If the dealer has to stop taking cards (either because they went bust or over 17), then update the display and end the game (lines 11-12). Otherwise, deal another card into the dealer hand (line 13), update (line 14), and get another card (line 15)—recur invokes the loop again.

Ending the Game

If the game is over, the code must look at the dealer’s and player’s hand, determine the winner, update the feedback area, and update the money. The end-game is pretty much a list of conditions.

The end-game code is pretty much a list of conditions:

(defn end-game [dealer player]
  (let [[ptotal pstatus] (evaluate-hand player)
        [dtotal dstatus] (evaluate-hand dealer)]
    (cond
      (> ptotal 21) (feedback "Sorry, you busted." :red)
      (> dtotal 21) (feedback "Dealer goes bust. You win!" :green)
      (= ptotal dtotal) (feedback "Tie game." :black)
      (= pstatus :blackjack) (feedback "You win with blackjack!" :green)
      (= dstatus :blackjack) (feedback "Dealer has blackjack." :red)
      (< ptotal dtotal) (feedback "Dealer wins." :red)
      (> ptotal dtotal) (feedback "You win!" :green)
      :else (feedback "Unknown result (Shouldn't happen.)" :gray))
    (update-money dtotal dstatus ptotal pstatus)
    (swap! game assoc :playing false)))

The feedback function changes the :player-feedback and :feedback-color values in the game, and Reagent will automagically do the update.

(defn feedback [message color]
  (swap! game assoc :player-feedback (str "\u00a0" message) :feedback-color (name color)))

Updating the money is, again, a list of conditions. However, I do feel rather guilty about accessing the player money and current bet directly from the game atom instead of passing them as parameters. It really feels imperative rather than functional. Sorry about that.

(defn update-money [dtotal dstatus ptotal pstatus]
  (let [{:keys [player-money current-bet]} @game
        new-money (cond
                    (= ptotal dtotal) player-money ;; least common, but avoids lots of ugliness later
                    (= pstatus :blackjack) (+ player-money (* 1.5 current-bet))
                    (= pstatus :bust) (- player-money current-bet)
                    (or (= dstatus :bust) (> ptotal dtotal)) (+ player-money current-bet)
                    (< ptotal dtotal)(- player-money current-bet)
                    :else player-money)]
    (swap! game assoc :player-money new-money :current-bet (max 0 (min current-bet new-money)))))

More Money Matters

Finally, the code for handling a change in the amount of bet and allowing people to recharge their money if they gamble their entire bankroll and lose:

(defn change-bet [event]
  (let [val (.parseFloat js/window (.-value (.-target event)))
        amount (if (js/isNaN val) 0 val)
        total (:player-money @game)]
    (swap! game assoc :current-bet (max 0 (min amount total)))))

(defn recharge-money [event]
  (swap! game assoc :current-bet 10 :player-money 1000))

The code for change-bet is the only event handler that uses the information from the event parameter to retrieve the value the player typed. It’s converted to a number (or zero if that isn’t possible), and then the game atom is updated so that the current bet cannot go negative or greater than the amount of money you have. It is possible to bet zero, and that wasn’t intentional, so I rationalized it: people who have moral restrictions against gambling can still play the game without having to bet.

Recharging the money updates the appropriate values in the game atom.

Wrap-up

Well, there you have it. Since this one of my first non-trivial ClojureScript programs, it probably has a large number of violations of coding conventions and isn’t as functionally pure as it could be. Feel free to fork the source on GitHub and improve it.