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)