Ranking poker hands

From APL Wiki
Jump to navigation Jump to search

This article uses Dyalog APL to rank poker hands using the rules of Texas hold 'em and other popular poker variants. The function handrank below transforms a representation of a poker hand into a "rank" array so that the ordering of ranks under total array ordering is the same as the ordering of the initial poker hands. Poker hands can then by sorted or compared by calling handrank on each one, then ordering with normal APL functions.

Dyalog '19 version

Marshall Lochbaum showed the following code in a presentation (D04: "Tacit Techniques with Dyalog version 18.0 Operators"; video here) at the Dyalog '19 user meeting.

handrank{{((8|@12-/).=¨1,⊃){(/),(2=≢),,⊂[;]},⍨∘≢⌸}/↓⍉[;]}
cmp  {⍺⍵-(⊃⍋)⍵⍺}
cmphand  cmp  handrank

The cmphand function compares two hands to yield ¯1 if the left hand is better, 1 if the right is better, and 0 if they are tied. In the talk Marshall calls it by inverting a display function (a task which Dyalog's Inverse cannot yet handle):

      I  0 99
      disp  I¨  ((2⎕D),'TJQKA')('♣♢♡♠')  1
      (5 2'6♡6♣6♢K♡K♠') cmphand(disp¯1) (5 2'A♠2♠3♣4♠5♢')
¯1

These functions demonstrate the usefulness of Over, but the handrank function doesn't depend on any new features from Dyalog 18.0.

The implementation of handrank above has an error: it ranks flushes above straights. This can be fixed by reversing in two places:

handrank{{((8|@12-/).=¨1,⊃){(/),(2=≢),,⊂[;]⊃⌽},⍨∘≢⌸}/↓⍉[;]}

Displaying and parsing hands

We will describe each card with an index for its rank (two to ace) and another index for its suit (clubs to spades). The function disp below displays a single card by picking the appropriate character for suit and rank. We use the character 'T' for ten.

      disp  ¨  ((2⎕D),'TJQKA')('♣♢♡♠')
      disp 1 1
2
      disp¯1 '2♣'
1 1

Because disp is invertible, we can also use it to read a single card, as shown in the last line above. We can parse a hand of five cards, written as a single string, by reshaping it to shape 5 2, and undisplaying each row (rank 1).

      (disp¯11) 5 2'A♠2♠3♣4♠5♢'
13 4
 1 4
 2 1
 3 4
 4 2
      hand  {(disp¯11) 5 2}

Hand rank

In poker ranking, the highest card in the hand is the most important. We start our ranking by placing the highest card first, that is, sorting in descending order:

      sort  {(⊂⍒)}
      sort hand 'A♠2♠3♣4♠5♢'
13 4
 4 2
 3 4
 2 1
 1 4

However, hands are sorted not just by the highest card but by how many of each rank there are. For example, two twos in a hand are more important than a single king. We take this into account by grouping equal ranks together with the Key operator. In the operand to Key, we place the number of cards of each rank (()) before the number of the rank (). Then we sort in descending order again, to put the ranks that appear the most times first. For this part of the ranking algorithm the cards didn't need to be sorted initially, but we'll use the initial sorting above later.

      / sort hand '6♡6♣6♢K♡K♠'
12 12 5 5 5
      {(),} / sort hand '6♡6♣6♢K♡K♠'
2 12
3  5

Straights and flushes

The small amount of code above is enough to rank any hands if we ignore the rules for straights (five ranks in a row) and flushes (all five cards of the same suit). However, poker has some complicated rules for these types of hands which we must take into account. We begin by splitting the hand into ranks r and suits s:

      r s  ↓⍉sort hand 'A♠2♠3♣4♠5♢'

First we compute whether the hand is a flush, a decision that depends only on the suits. If every suit is equal to the first one, then the hand is a flush.

      s
4 2 4 1 4
      s=⊃s
1 0 1 0 1
      /s=⊃s
0

Now we determine whether the hand is a straight. We can find the difference from each card's rank to the next highest (recall that the ranks are sorted descending) using a pair-wise difference, a kind of windowed reduction. Ordinarily, a hand is a straight if each difference is exactly one. However, there is an additional complication in that an ace (the highest rank; 13 here) may come before a two in a straight—it can be treated as the lowest rank if this improves the hand. To account for this we also allow the difference between the highest and next-highest card to be 9, the difference between ace and five. If this difference is 9 and each other difference is 1, then the ranks must be ace, five, four, three, and two—a straight. To allow both 1 and 9 for the first value, we use the residue on division by 8. Residue is applied to only the first difference using the At operator.

      r
13 4 3 2 1
      2-/r
9 1 1 1
      8|@12-/r
1 1 1 1
      1 .= 8|@12-/r
1

The rank and suit combination can be combined to save characters (an important consideration when presenting with limited screen width!). Each compares two arrays using scalar Equals and combines the results with And. Rather than writing this computation twice we can merge the two sides of each computation together and use Each.

      (8|@12-/r)s
┌───────┬─────────┐
1 1 1 14 2 4 1 4
└───────┴─────────┘
      (8|@12-/r)s = 1,⊃s
┌───────┬─────────┐
1 1 1 11 0 1 0 1
└───────┴─────────┘
      (8|@12-/r)s=1,⊃s
1 0
      (8|@12-/r)s.=¨1,⊃s
1 0

The choice of the inner product .= over a reduction is entirely stylistic.

Final ranking

Taking a sample hand, let's compute the ranks and a boolean indicator for whether it's a straight and/or flush.

      r s  ↓⍉sort hand 'A♠2♠3♣4♠5♢'
      ranks  sort {(),} r
1 13
1  4
1  3
1  2
1  1
      sf  (8|@12-/r)s.=¨1,⊃s
1 0

The final rank is now a combination of four arrays. At the end we will compare hands based on the number of each rank involved, but first we will modify this ranking based on some features that make certain hands more valuable than others. A straight flush is the highest-ranked hand, followed by a four of a kind and a full house (three of one rank and two of another). We can quickly test whether a hand has one of the latter combinations by testing whether there are exactly two different ranks present—if so, they must be split 4-1 or 3-2. After these, flushes and then straights are ranked higher than other hands.

      /sf                  ⍝ Is it a straight flush?
0
      2=≢ranks              ⍝ Is it a four of a kind or full house?
0
      sf                   ⍝ Flushes before straights
0 1
      (sf)ranks           ⍝ Treat high card as low for straights
1  4
1  3
1  2
1  1
1 13

Within each category based on the first three special values, hands are ranked by card ranks. Suits are never used for comparison: they only affect whether the hand is a straight.

To order straights correctly, we must take into account ace-low straights. In such straights five is considered the high card. We fix the ordering by rotating all straights to compare starting at the second-highest card.

Complete function

handrank  {
    sort  {(⊂⍒)}                      ⍝ Sort descending idiom
    r s↓⍉sort                           ⍝ Rank and suit after sorting
    ranks  sort {(),} r              ⍝ Number of each rank
    sf  (8|@12-/r)s.=¨1,⊃s             ⍝ Is straight, is flush
    (/sf),(2=≢ranks),(sf),⊂(sf)ranks  ⍝ Final rank
}

Examples

To test our function, we can try it on the sample hands given in Wikipedia's page on Texas hold 'em here. The ranks for each of these hands are shown, as well as a grade up to demonstrate that they are in ascending order.

      hands  ' '()'A♣4♡7♢K♣2♠ K♣K♡7♢2♣5♠ K♣K♡7♢7♣5♠ K♣K♡K♢7♣5♠ 3♣4♡5♢6♣7♠ K♣Q♣9♣8♣2♣ K♣K♡K♢7♣7♠ K♣K♡K♢K♠5♠ 3♣4♣5♣6♣7♣ T♡J♡Q♡K♡A♡'
      hands
A47K2
KK725
KK775
KKK75
34567
KQ982
KKK77
KKKK5
34567
TJQKA
      {,↑handrankhand¨} hands
┌──────────┬─┬─┬─┬─┬────┐
A47K2♠│00001 13
              1 12
              1  6
              1  3
              1  1
├──────────┼─┼─┼─┼─┼────┤
KK725♠│00002 12
              1  6
              1  4
              1  1
├──────────┼─┼─┼─┼─┼────┤
KK775♠│00002 12
              2  6
              1  4
├──────────┼─┼─┼─┼─┼────┤
KK775♠│00002 12
              2  6
              1  4
├──────────┼─┼─┼─┼─┼────┤
KKK75♠│00003 12
              1  6
              1  4
├──────────┼─┼─┼─┼─┼────┤
34567♠│00011 5 
              1 4 
              1 3 
              1 2 
              1 6 
├──────────┼─┼─┼─┼─┼────┤
KQ982♣│00101 12
              1 11
              1  8
              1  7
              1  1
├──────────┼─┼─┼─┼─┼────┤
KKK77♠│01003 12
              2  6
├──────────┼─┼─┼─┼─┼────┤
KKKK5♠│01004 12   
              1  4
├──────────┼─┼─┼─┼─┼────┤                                                              
34567♣│10111 5        
              1 4 
              1 3 
              1 2 
              1 6 
├──────────┼─┼─┼─┼─┼────┤
TJQKA♡│10111 12
              1 11
              1 10
              1  9
              1 13
└──────────┴─┴─┴─┴─┴────┘
       handrankhand¨ hands
1 2 3 4 5 6 7 8 9 10