## DEV Community

Mike Talbot ⭐

Posted on • Updated on

# Real world Javascript map/reduce, solving the Poker Hand problem

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 multi-step 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.

1. Convert the string into cards
2. 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:

3. 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.

1. Identify a flush
2. Identify a straight
3. 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 js-coroutines library which could well help you out.

Mike Talbot ⭐ • Edited

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:

``````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)
//Also handle low straight
const lowStraight = faces.join("") === "AJKLM"
faces[0] = lowStraight ? "N" : faces[0]
const straight = lowStraight || 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
}
}
``````

We perform a simple string check for the low straight and then switch the order of 'A' to 'N' for aces in this circumstance.

Jeremy Gollehon

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.

Mike Talbot ⭐

Thanks! Yeah that's fair on the ternary. I'll adjust it.

Mike Talbot ⭐

Yes, that's in the article list of hands picture and in the solution.

Макаров Валерий

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 } }

Mike Talbot ⭐ • Edited

JulianPugliese • Edited

The topic of gambling is very interesting and exciting for me, so I often study similar JavaScript map/reduce. Thanks for your post, I really liked it, as I'm getting ready to launch my project. Now I play a lot in online casinos in my free time, I study various content of sites, and I recommend you a useful source aussiebestcasinos.com/paysafecard-... where you can study a useful review at the best Australian online casino that accept PaySafe 2021, which will help you make a choice and to you.

wdrfree

Thanks!! I learn about min-max algo!!

shopalino

Wow nice post dev.to/miketalbot

Hey there! I totally get where you're coming from with the daily challenges on DEV - sometimes they can be a bit too time-consuming. By the way, I'm new to this forum, but I wanted to chime in and say that I've been having a blast trying out different coding challenges and learning new things. And speaking of new things, have you tried out any Gratis Casino Spielautomaten ohne Anmeldung in Österreich? I've been getting into online slots lately and it's been a fun way to unwind after a long day of coding. Anyway, thanks for sharing your experience with solving the poker hand problem - it's always cool to see how different people approach the same challenge. And thanks for the tip about js-coroutines - I'll have to check it out!