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
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)
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
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?
- It is simpler to calculate a hand comparison result from data A.
- 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) }
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
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
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 }
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)) } })
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) }
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) }
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 } }
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
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
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!!
Top comments (0)