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

Mike Talbot ⭐ - Jun 16 '20 - - Dev Community

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.

Alt Text

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
}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

And then we make a lookup out of those counts by simply "counting the counts"!:

    const duplicates = Object.values(counts).reduce(count, {})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .