DEV Community

Chakrit Likitkhajorn
Chakrit Likitkhajorn

Posted on

Design a program in Functional #2 Poker Kata

From last article, I demonstrated how to think about program design in functional way.

In this article, I will demonstrate this again using Poker kata. The question is very simple, given two poker hand, determine who is winning.

Start modeling

So at the end, what we need is a function

compare(hand1, hand2) -> Win, Lose, Draw
Enter fullscreen mode Exit fullscreen mode

And we can model a data shape of hand, card as followed:

object Suit extends Enumeration {
  type Suit = Value

  val Heart = Value("Heart")
  val Spade = Value("Spade")
  val Club = Value("Club")
  val Diamond = Value("Diamond")
}
case class Card(value: Int, suit: Suit.Suit)
type Hand = (Card, Card, Card, Card, Card)
Enter fullscreen mode Exit fullscreen mode

Basically, we just told the compiler that a card is data consists of value as an integer, and a suit which can be either Heart, Spade, Club or Diamond. Then, a hand is a set of 5 cards.

I will represent Jack, Queen, King and Ace with 11, 12, 13 and 14 respectively.

First design

At the end, we want to implement

def comparePokerHand(hand1: Hand, hand2: Hand): CompareResult.CompareResult
Enter fullscreen mode Exit fullscreen mode

Ok. I will be the first to admit that implement comparePokerHand from the get go will be hard and complicated.

So I need to break it down.

Here comes the magic question: What is a data A in such that?

  1. It is simpler to calculate a hand comparison result from data A.
  2. It is simpler to calculate data A from each hand.

Creative thinking time.....
...
...
...

Ok, if I have the rank of each hand (Straight, Flush, Two Pairs, etc.) and highs of each hand, then I can simply compare those. I will compare hand power first, and if hand power is the same, then I will compare each highs until the end.

Here is the first breakdown pseudo code.

case class HandPower(rank: HandRank, highs: List[Int])

def comparePokerHand(hand1: Hand, hand2: Hand): CompareResult = {
  val handPower1 = handPower(hand1)
  val handPower2 = handPower(hand2)
  compareHandPower(handPower1, handPower2)
}
Enter fullscreen mode Exit fullscreen mode

Please note that in order to make above statement to be true, the highs must be ordered from the most significant card value to least significant card value.

For example:

Flush(1 3 10 5 4) -> Highs(10 5 4 3 1) // Order from highest value to lowest value
TwoPairs(3 3 5 5 10) -> Highs(5 3 10) // The pairs come first, sorted by higher value, then the non-pair
FullHouse(5 5 5 10 10) -> Highs(5 10) // Three of kind first, then pair
Enter fullscreen mode Exit fullscreen mode

If we have this, then we can easily compare highs by looking at each element one-by-one.

For example:

FullHouse(5 5 5 10 10) lose FullHouse(9 9 9 2 2)
Highs (5 10) lose to Highs(9 2) 
5 < 9

Two Pairs(10 10 14 14 4) win TwoPairs(9 9 14 14 5)
Highs(14 10 4) win Highs(14 9 5)
14 == 14
10 > 9
Enter fullscreen mode Exit fullscreen mode

This is how having a data highs with above properties make it easier to determine a winner.

Having this all said, next I need to implement two functions: HandPower and compareHandPower.

I will go through the easier one first: compareHandPower. This function will be implemented according to what I thought

I will compare hand power first, and if hand power is the same, then I will compare each highs until the end.

def compareRank(
    handRank1: HandRank.HandRank,
    handRank2: HandRank.HandRank
): CompareResult.CompareResult = {
  if (handRank1.id < handRank2.id) return CompareResult.Win
  if (handRank1.id > handRank2.id) return CompareResult.Lost
  CompareResult.Draw
}


def compareHighs(
    high1: List[Int],
    high2: List[Int]
): CompareResult.CompareResult = {
  high1
    .zip(high2)
    .foldLeft(CompareResult.Draw)((result, pair) => {
      if (result != CompareResult.Draw) {
        return result
      } else if (pair._1 > pair._2) {
        return CompareResult.Win
      } else if (pair._1 < pair._2) {
        return CompareResult.Lost
      } else {
        return CompareResult.Draw
      }
    })
}

def compareHandPower(
    handPower1: HandPower,
    handPower2: HandPower
): CompareResult.CompareResult = {
  val rankComparison = compareRank(handPower1.rank, handPower2.rank)
  if (rankComparison == CompareResult.Draw) {
    return compareHighs(
      handPower1.highs,
      handPower2.highs
    )
  } 
  return rankComparison
}
Enter fullscreen mode Exit fullscreen mode

Easy peasy!! (Hope you find this simple as well). First function done 😄

Then next, the handPower function.


The hand power function must consist of two ability

  • Ability to generate rank from hand
  • Ability to generate highs from hand

I have to admit that at first, I cannot figure out how to break it down.

So I start with a very simple and naive question: How can I determine if this hand is a four card hand?

The answer is simple: I need to check if any card value occurs four times within a hand. That means I need to determine the frequencies of each card value.

So I start implemented a freqMap like this:

val freqMap =
  hand.foldLeft(Map[Int, Int]())((acc, card: Card) => {
    acc.get(card.value) match {
      case None              => acc + (card.value -> 1)
      case Some(currentFreq) => acc + (card.value -> (currentFreq + 1))
    }
  })
Enter fullscreen mode Exit fullscreen mode

Basically, freqMap is a map between card value and card frequency.

With this, I can write the logic for four cards to be

if (freqMap.exists(keyValuePair => {
  val cardFrequency = keyValuePair._2
  cardFrequency == 4
})) {
  return HandPower(HandRank.FourCard, highs)
}
Enter fullscreen mode Exit fullscreen mode

Ok. Now I can check four cards. Let's check for the full house.

if (
  freqMap.exists(keyValuePair => {
    val cardFrequency = keyValuePair._2
    cardFrequency == 3
  }) && freqMap.exists(keyValuePair => {
    val cardFrequency = keyValuePair._2
    cardFrequency == 2
  })
) {
  return HandPower(HandRank.Fullhouse, highs)
}
Enter fullscreen mode Exit fullscreen mode

Now I realize that things start to get messy and duplicates.

Back to the core question: Is there any data A that will make our life easier?

Answer: Yes. If we have data A that is a list of frequencies of each card value sorted, we can easily determine many card ranks.

How? I think it is easier to show you by this code:

cardFrequenciesPattern match {
  case 1 :: 4 :: Nil      => return HandPower(HandRank.FourCard, highs)
  case 2 :: 3 :: Nil      => return HandPower(HandRank.Fullhouse, highs)
  case 1 :: 1 :: 3 :: Nil => return HandPower(HandRank.ThreeOfKind, highs)
  case 1 :: 2 :: 2 :: Nil => return HandPower(HandRank.TwoPairs, highs)
  case 1 :: 1 :: 1 :: 2 :: Nil =>
    return HandPower(HandRank.OnePair, highs)
  case _ => {
    // Something
  }
}
Enter fullscreen mode Exit fullscreen mode

You can see that if a cardFrequenciesPattern exists, and it is a frequencies of each card value sorted, we can easily match it to a corresponding pattern.

So the data A for this case is cardFrequenciesPatter.

I wrote a transformation from current freqMap to easier cardFrequenciesPattern as followed:

val cardFrequenciesPattern = freqMap.values.toList.sorted
Enter fullscreen mode Exit fullscreen mode

What's left for me is to write some if-else for straight, flush and nothing in the last case.


Since we were able to determine a rank, let us determine highs.

Definition: highs is a list of card values in hand, ordered from the most significant card value to least significant card value.

I notice that the most significant card values in any hand are also the most frequent card in hand. For example: In full house hand, three kinds of cards are considered to be more significant than a pair of cards.

So the logic could be order card values based on frequency first, then order by the values itself.

And since we already have a map of card frequency and card value, then here it is:

val highs = freqMap.toSeq
  .sortWith((pair1, pair2) => {
    val cardVal1 = pair1._1
    val cardVal2 = pair2._1
    val cardFreq1 = pair1._2
    val cardFreq2 = pair2._2
    if (cardFreq1 == cardFreq2) {
      cardVal1 > cardVal2
    } else {
      cardFreq1 > cardFreq2
    }
  })
  .map(_._1)
  .toList
Enter fullscreen mode Exit fullscreen mode

Surprisingly, the intermediate freqMap data make it very easy to determine both highs and rank.

Now we got everything: We have a function to create a hand power from a hand. We have a function to compare two hand power. That's mean we solved Poker Kata.

Done deal! 🎉🎉

Full implementation in Scala: https://github.com/chrisza4/scala-poker-kata

Full implementation in Clojure: https://github.com/chrisza4/clojure-poker-kata

Recap and Lesson

In this article, I demonstrate the thought process and program design that repeatedly ask a simple question: Is there data A?

And by asking ourselves this question, we found that:

  • Intermediate data HandPower make it way easier to compare poker hand easier.
  • Intermediate data freqMap make it way easier to both create a rank and highs.
  • Intermediate data cardFrequenciesPattern make it way easier to determine a card rank

This is the functional thinking. That is how we breakdown a problem in a functional paradigm. This is what it means to "write more functions".

Again, as I wrote in the previous article, this thinking and thought process line is useful even if you write a program in an imperative or object-oriented paradigm.

So I hope this article is useful for every programmer.

Thanks for reading!!

Discussion (0)