Ranking poker hands

This article uses Dyalog APL to rank poker hands using the rules of Texas hold 'em and other popular poker variants. The function  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  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. The  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): These functions demonstrate the usefulness of Over, but the  function doesn't depend on any new features from Dyalog 18.0.

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

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  below displays a single card by picking the appropriate character for suit and rank. We use the character  for ten. Because  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).

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

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  and suits  : 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. 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. 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. 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. 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. 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.

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.