Foognostic blogs Seeking knowledge of foo

13Nov/09Off

More poker with Clojure: what’s in a hand?

Standard disclaimer: Clojure is a new hobby.

In a previous post I wrote some code to create and shuffle a deck of cards. This post demonstrates one way to figure out what a hand is worth according to basic 5 card draw rules.

The implementation is basically one switch statement from a C/Java/Ruby/Groovy/Python perspective. Ruby and Groovy coders will recognize that the return value of the function is the value of the last expression evaluated. So an if statement returns either the value of the true expression or the value of the false expression.

(if (< 1 2 3)
    "good, 1 < 2 < 3"
    "what? someone file a bug report!")
;; "good, 1 < 2 < 3"

When conditional logic returns values like this, there is no need to use a stack variable to store a return value.

A quick introduction to some of the functions used in the code:

  • (if): Aside from returning its true/false expression result, no major difference than Java.
  • (cond): Basically a switch statement, returning a value just like (if)
  • (let): Defines and initializes "variables" which are immutable.
  • #(): This defines an anonymous function. Believe this is a macro for (fn).
  • #{}: This defines a set data structure; unordered and no duplicates. Also see the (set). function
  • {}: This defines a map data structure, populated with unordered key/value pairs. Bonus: (map key) AND (key map) both return the value for key in a map.

With some minimal descriptions listed, let's dig into the code:

When judging a hand it's critical to know how many of each suit are present AND how many of each value are present. One way to model this is to build histograms for suit and value.

(build-property-histogram
 (take 5 (shuffle deck))
 :suit
 suits)

;; {:clubs 2, :diamonds 2, :spades 0, :hearts 1}

(build-property-histogram
 (take 5 (shuffle deck))
 :name
 names)

;; {:queen 2, :king 1, :jack 0, :seven 1, :eight 0,
;;  :six 0, :nine 0, :five 0, :ace 0, :ten 0, :three 1,
;;  :two 0, :four 0}

The implementation behind that is not an easy read (probably because it's not great Clojure). Basically this code iterates over the pool of attributes (suits or values) and uses (conj) to aggregate maps into one big map. In the middle a filter function finds all cards matching the current property and then the total number is counted up.

(defn build-property-histogram [cards property-name property-pool]
  (reduce
   (fn [sieve value]
     (conj sieve
           { value
            (count (filter
                    (fn [card] (= value (card property-name)))
                    cards)) }))
   {}
   property-pool))

Only one more helper method to go. This method determines whether the hand is a straight, whether all of its cards have consecutive values. The approach taken was a little odd. An array of all possible straight values is built and then segmented into overlapping hands. For example, the first two arrays would be [ace, two, three, four, five] and [two, three, four, five, six]. Each of these possibilities is matched against the hand, using Clojure's facility to compare sets easily.

(defn straight? [hand]
  (some
   (fn [straight] (= (set (map :name hand)) (set straight)))
   (partition 5 1 [:ace, :two, :three, :four, :five, :six, :seven, :eight,
                   :nine, :ten, :jack, :queen, :king, :ace])))

(some) is a function which returns true when its filter function returns true for any of the collection items it examines.

After all that, here is the hand judging function. It's basically a few local "variables" and a switch statement. Some additional conditional logic appears inside the switch statement as well. This is required to determine whether a hand with three of a kind is a full house (are the remaining cards a pair?).

(defn rank-cards [cards]
  (let [suit-histo (build-property-histogram cards :suit suits)
        value-histo (build-property-histogram cards :name names)
        max-per-suit (apply max (vals suit-histo))
        max-per-value (apply max (vals value-histo))]
    (cond
      (= 4 max-per-value)
          :quads
     
      (= 3 max-per-value)
          (if (some #(= 2 %) (vals value-histo))
              :full-house
              :trips)

      (= 2 max-per-value)
          (if (< 1 (count (filter #(= 2 %) (vals value-histo))))
              :two-pair
              :pair)

      (and (straight? cards) (> 5 max-per-suit))
          :straight

      (and (not (straight? cards)) (= 5 max-per-suit))
          :flush

      (= 5 max-per-suit)
          (if (= #{:ace, :king, :queen, :jack, :ten} (set (map :name cards)))
              :royal-flush
              :straight-flush)

      true
          :high-card)))

In the near future all this code will be cleaned up and moved out to BitBucket. Ideally some basic chip tracking and logic would be added so you could actually play poker from a REPL. That's probably much more difficult than it seems, but hopefully this has been useful so far. Please don't hesitate to contact me (goof at foognostic dot net) with any questions!

Filed under: clojure, code 1 Comment
   

Foognostic blogs is Digg proof thanks to caching by WP Super Cache