I got some great feedback on my last post and decided to re-work my solution a bit. I think this turned out much nicer.
The main things I improved:
- My first solution involved “predicates” (technically they may not have been true predicates, since they returned a keyword or nil, instead of true or false) for each possible hand within one big
letbinding, and then at the end I removed all the nil values and returned the first possible hand in the list. This was fairly readable, but there were two problems:- The code could be more concise if I didn’t have to wrap each “predicate” in an
ifstatement to either return the keyword or nil. Usingwheninstead would help to some extent (since it returns either a value or nil), but a better solution altogether came from the other problem, which was that - Wrapping all of my “predicates” in a
letbinding forces them all to be evaluated, even if we may only need to evaluate a handful of them. Usingororcondinstead allows you to take advantage of short-circuiting and stop evaluating once you reach a predicate that is met, which is better from a performance standpoint. Since the first possibility we check for (i.e. the highest ranking possible hand) is a straight flush, I decided to include the predicatesflush?andstraight?in myletbinding and then refer to them in the main part of my code within acondstatement.
- The code could be more concise if I didn’t have to wrap each “predicate” in an
- It turns out that my previous definition of the
flush?predicate,(= 1 (count (set suits))), could be simplified to(apply = suits), thanks to=being a variable-arity function in Clojure. - My first solution repeated this piece of code an awful lot:
(some (fn [[r num]] (>= num n)) (frequencies ranks)). This was a classic opportunity to apply the DRY principle, so I added arank-freqssymbol to myletbinding. To make it easier to use, I defined it as(sort (vals (frequencies ranks))), so that the value ofrank-freqsis always[2 3]for a full house, for instance.
Without further ado, here is my revised solution for Clojure Problem #178:
(fn best-hand [card-strings]
(let [card-parser (fn [[s r]]
(let [suit ({\S :spade, \H :heart,
\D :diamond, \C :club} s)
rank (if (> (Character/digit r 10) -1)
(- (Character/digit r 10) 2)
({\T 8, \J 9,
\Q 10, \K 11, \A 12} r))]
{:suit suit, :rank rank}))
cards (map card-parser card-strings)
suits (map :suit cards)
ranks (map :rank cards)
rank-freqs (sort (vals (frequencies ranks)))
flush?
(apply = suits)
straight?
(let [aces-high (sort ranks)
aces-low (sort (replace {12 -1} ranks))]
(or
(= aces-high (take 5 (iterate inc (first aces-high))))
(= aces-low (take 5 (iterate inc (first aces-low))))))]
(cond
(and flush? straight?) :straight-flush
(= rank-freqs [1 4]) :four-of-a-kind
(= rank-freqs [2 3]) :full-house
flush? :flush
straight? :straight
(some #{3} rank-freqs) :three-of-a-kind
(= rank-freqs [1 2 2]) :two-pair
(some #{2} rank-freqs) :pair
:else :high-card)))