You, like me, may have a play at solving some of the daily challenges here on DEV. Usually if the answer is more than one or two lines of some fancy function I don't have the time, but yesterday the challenge was to rank poker hands and it struck me as one of those things that "should" be easy!
The end result works well, is concise and readable (it's a lot shorter than the other proposed solutions at least).
Sure enough we can press into service map
and reduce
to get us the information we need. But it's a really nice example of how to use these tools to solve a multistep problem in the real world.
The challenge
The challenge is to rank two poker hands and decide which one wins.
Poker hands are represented by strings of 2 characters separated by a space. So 2H is the 2 of Hearts and TC is the ten of clubs etc.
"2C 5C 3C 4C 6C" is a straight flush in clubs to the 6.
The hand rankings are as for Texas Hold'em.
There are hidden complexities though in ranking hands  using supplementary cards to resolve a draw and using the face value of pairs etc.
The solution
Ok so how to solve this problem. Firstly we need a way of comparing hands that solves for hand ranking and then when the rankings match, resolving a winner if possible by comparing supplementary cards.
As the challenge specifies that no suit is better than another we propose a simple object to represent hand rankings:
{
rank: 1, // A value from 1  9 to rank hands, lower is better
value: 'ABCDE' // A value that represents the faces to compare, lower is better
}
We can now write a simple function to compare two hands that are represented by this structure:
function compareHands(h1, h2) {
let d1 = getHandDetails(h1)
let d2 = getHandDetails(h2)
if (d1.rank === d2.rank) {
if (d1.value < d2.value) {
return "WIN"
} else if (d1.value > d2.value) {
return "LOSE"
} else {
return "DRAW"
}
}
return d1.rank < d2.rank ? "WIN" : "LOSE"
}
So now all we have to do is to create the result object from the hand  this is where the fun starts!
Getting the details of a poker hand
So in solving problems like this you need to work out the core data that you need to resolve the problem. Here our first problem is to rank hands.
Poker hands can be a straight, a flush or some combination of multiple cards with the same face value. Our job is first to assemble this information from our input string. The first step of that is to decide how we want to parse our input.
Parsing the input
const input = "AH KS TC 9D 3S" // Something like this
We need both suits and faces, but given that the only reason we care about suits is if they are all the same, then there is no need to keep the face and the suit related. This makes parsing pretty straight forward.
 Convert the string into cards

Extract the face and the suit
However, if we want to be able to sort our face cards we need them to be easily compared to each other. For instance A > K (Ace is better than King) but Q > J (Queen is better than Jack) so it's not alphabetical. So we add a third step:
Convert the face into something easily comparable
We have 5 cards in the hand, at the end of this we want a value to resolve draws that can be compared in a single operation  so it needs to be a string. Therefore we will rank our card faces as characters so we can put them back into a string later. Just now we want A to be Ace, B to be King, C to be Queen etc
const order = "23456789TJQKA"
const cards = hand.split(" ") // Split into cards
const faces = cards.map(a => String.fromCharCode([77  order.indexOf(a[0])])).sort()
const suits = cards.map(a => a[1]).sort()
So here we've extracted the cards and the faces, mapped the faces to A onwards by looking up their position in the order
string and taking this value away from 77, turning that back into a string. 65 is the code for A so this creates us a comparable string starting with A being best.
We also sorted the faces and the suits, that's so we can do the next step!
Creating comparable data
Ok we now need to generate some more data so we can write some code to rank the hand.
 Identify a flush
 Identify a straight
 Identify duplicate faces  which we will use for all of the other types of hand
Identify a flush
This is super easy now we've parsed the data and sorted the suits. If the last suit entry is the same as the first, we have a flush.
const flush = suits[0] === suits[4]
Identify a straight
A straight isn't much harder, if the cards are all in sequence we know it is a straight.
So we find the first card and use every
to check that the values are sequential, using the index passed to the callback like so:
const first = faces[0].charCodeAt(0)
const straight = faces.every((f, index) => f.charCodeAt(0)  first === index)
Identify duplicates
Ok so this step is a little harder, we need to count the number of each face in our hand, but then we need some way of identifying pairs, 3 of a kind etc to make it easy to rank the hand so what we want to do here is:
 Count the number of each face

Convert the count into something we can lookup
We want to be able to say "is there a four of a kind", how many pairs are there etc
So first we count the faces:
const counts = faces.reduce(count, {})
function count(c, a) {
c[a] = (c[a]  0) + 1
return c
}
And then we make a lookup out of those counts by simply "counting the counts"!:
const duplicates = Object.values(counts).reduce(count, {})
Rank the hand
We now have all the information we need to rank the hand, without the draw resolution.
let rank =
(flush && straight && 1) 
(duplicates[4] && 2) 
(duplicates[3] && duplicates[2] && 3) 
(flush && 4) 
(straight && 5) 
(duplicates[3] && 6) 
(duplicates[2] > 1 && 7) 
(duplicates[2] && 8) 
9
So a straight flush wins with rank 1 (we will let the draw resolution fix a royal straight flush), then four of a kind, full house etc
This uses fancy Javascript && to which resolve to the last value if the previous are truthy. So (flush && straight && 1)
returns 1 if flush and straight are true, otherwise false.
Value resolution
If two hands resolve the same rank we need to disambiguate them if possible. This does have some rules associated.

Pair versus pair, highest pair wins. If they are the same, highest next card wins. (Works for 2 pairs as well)
So we compare 2H 2D AH KC 3D with 4H 4C JC TC 3H and the 4's win even though the first hand has a higher next card  an ace.
Full house versus full house, it's the highest triple that wins.
So we need to sort by count and then by face value in our output string. Remember be want a five character string in order that can be used to resolve a rank match.
let value = faces.sort(byCountFirst).join("")
function byCountFirst(a, b) {
//Counts are in reverse order  bigger is better
const countDiff = counts[b]  counts[a]
if (countDiff) return countDiff // If counts don't match return
return b > a ? 1 : b === a ? 0 : 1
}
And that's it!
The Whole Shebang
const order = "23456789TJQKA"
function getHandDetails(hand) {
const cards = hand.split(" ")
const faces = cards.map(a => String.fromCharCode([77  order.indexOf(a[0])])).sort()
const suits = cards.map(a => a[1]).sort()
const counts = faces.reduce(count, {})
const duplicates = Object.values(counts).reduce(count, {})
const flush = suits[0] === suits[4]
const first = faces[0].charCodeAt(0)
const straight = faces.every((f, index) => f.charCodeAt(0)  first === index)
let rank =
(flush && straight && 1) 
(duplicates[4] && 2) 
(duplicates[3] && duplicates[2] && 3) 
(flush && 4) 
(straight && 5) 
(duplicates[3] && 6) 
(duplicates[2] > 1 && 7) 
(duplicates[2] && 8) 
9
return { rank, value: faces.sort(byCountFirst).join("") }
function byCountFirst(a, b) {
//Counts are in reverse order  bigger is better
const countDiff = counts[b]  counts[a]
if (countDiff) return countDiff // If counts don't match return
return b > a ? 1 : b === a ? 0 : 1
}
function count(c, a) {
c[a] = (c[a]  0) + 1
return c
}
}
function compareHands(h1, h2) {
let d1 = getHandDetails(h1)
let d2 = getHandDetails(h2)
if (d1.rank === d2.rank) {
if (d1.value < d2.value) {
return "WIN"
} else if (d1.value > d2.value) {
return "LOSE"
} else {
return "DRAW"
}
}
return d1.rank < d2.rank ? "WIN" : "LOSE"
}
Conclusion
As you can see, if we break down the problem we can easily apply map
and reduce
to prepare all of the information we need to solve this problem.
If you have heavy lifting to do in Javascript and don't want to glitch, check out my jscoroutines library which could well help you out.
Discussion (10)
The original challenge specified no low straights from the Ace, but it jarred with me and this routine is easily adjustable to support it. We just have to worry about sort orders during straights and to recognise the straight in the first place. The following updated version of getHandDetails() manages both of these:
We perform a simple string check for the low straight and then switch the order of 'A' to 'N' for aces in this circumstance.
Love this. Thanks! It's a really good problem to show off "modern" javascript.
Only piece of feedback... the nested ternary in
byCountFirst
is probably too terse.Thanks! Yeah that's fair on the ternary. I'll adjust it.
To take into account the "steel wheel" (straight 5432A) you need to add an imaginary face value of 1:
const order = "123456789TJQKA"
And if the input contains 2, 3, 4, 5 and A, then make the replacement of A by 1:
function SteelWheel(str) { if((str.indexOf('2') + 1) && (str.indexOf('3') + 1) && (str.indexOf('4') + 1) && str.indexOf('5') + 1) && (str.indexOf('A') + 1)) { return str.replace('A', '1') } else { return str } }
As mentioned in my comment above, the initial challenge had no straight from the ace. My version in the comment above supports the "steel wheel"
Wow nice post dev.to/miketalbot