User:Devon McCormick/Poker/Lowball

From J Wiki
Jump to navigation Jump to search

Low Poker

One basic variant of poker is low poker, sometimes called "lowball".

A low poker hand is a 5-card hand with no pairs and, in some variants like Omaha, no card higher than 8. In a low hand, the ace counts as a one. Also, under "California rules", a straight or a flush does not count against a low hand. For simplification, we will not assume anything specific to Omaha and we assume "California rules" so we do not have to deal with that complication.

This means that the best low hand is ace-2-3-4-5; this is called a "wheel". As a high hand, this is also the lowest possible straight. If all the suits were the same in this hand, it would also be a flush, hence, a straight flush. However, we ignore any high interpretations of a hand here.

For purposes of a low hand, a wheel may be called a 5-high, or a 5-4-3-2-1 if you have to spell out the whole hand; this might be necessary in order to decide which of two hands is lower. We typically name low hands from the highest card on down. This is because a low hand is only as good as its highest card. Therefore, under the rule of "no card higher than 8", the worst low hand is 8-7-6-5-4. This would lose to the 2nd worst hand, 8-7-6-5-3, on the last (i.e. lowest) card.

Basic Code

My existing poker code works with the deck

 i. 52

. This may be treated as a mixed-base number to break any integer from 0 to 51 into a base "4 13" number, giving us "suit, rank" in this form, like this:

   suitRank=: |:@((4 13)&#:)

   suitRank i.13              NB. 2 to Ace of suit 0 (clubs)
0 0 0 0 0 0 0 0 0 0  0  0  0
0 1 2 3 4 5 6 7 8 9 10 11 12
   suitRank 51-i.13           NB. Ace to 2 of suit 3 (spades)
 3  3  3 3 3 3 3 3 3 3 3 3 3
12 11 10 9 8 7 6 5 4 3 2 1 0

Note that numeric card rank 0 maps to display card rank 2 and 12 maps to (high) ace.

To nail down which suits 0 to 3 are, we take them alphabetically, as is also done in the game of bridge, using this global:

SUIT=: 'CDHS'

To name the ranks as well:

RANK=: ,&.>(<"0 '23456789'),'10';'J';'Q';'K';'A'

For various reasons, we want to switch between the two forms - scalar integer 0 to 51 and 2-element vector 0 0 to 3 12. So, I decided to make suitRank its own inverse:

NB. suitRank=: |:@((4 13)&#:)
NB.* suitRank: convert between int and s,r.
suitRank=: 3 : 0
NB.* suitRank: convert nums: 2 rows: suit, rank or 2 rows->nums 0-51.
   if. 2=#$y do.
       13#.|:y
   else.
       |:@((4 13)&#:)y
   end.
)

Thus,

   suitRank suitRank i.13
0 1 2 3 4 5 6 7 8 9 10 11 12
   suitRank^:2 ] 51-i.13
51 50 49 48 47 46 45 44 43 42 41 40 39

Similarly, the function showCards displays the numeric or character form of a card, using the globals SUIT and RANK. It is also (kind of) its own inverse.

showCards=: 3 : 0"1
NB.* showCards: show char version of cards (e.g. 'AD';'10S') or vice
NB.* versa if not numeric.
   if. isNum y do.                                    NB. Num->char
       if. 2~:#$y do. y=. suitRank y end.
       (RANK{~1{y),&.>SUIT{~0{y
   else. (SUIT i. ;{:&.>y),:(,&.>RANK) i. }:&.>y     NB. char->num
   end.
NB.EG showCards 3 7 20 34 24
NB.EG showCards '5C';'9C';'9D';'10H';'KD'    NB. 5 Clubs, 9 Clubs, etc.
)

So,

   showCards i.13
+--+--+--+--+--+--+--+--+---+--+--+--+--+
|2C|3C|4C|5C|6C|7C|8C|9C|10C|JC|QC|KC|AC|
+--+--+--+--+--+--+--+--+---+--+--+--+--+
   showCards^:2 ] i.13
0 0 0 0 0 0 0 0 0 0  0  0  0
0 1 2 3 4 5 6 7 8 9 10 11 12

So we see how this is not an exact inverse but the two-part form is generally more useful. The singleton form is more convenient for large-scale manipulation but the suit-rank form is necessary for ranking hands.

Determining the Best Low

The following code assumes we may start with more than five cards as it will be used in the context of a game like Omaha where we are looking at as many as nine cards and we need to figure out the best possible five-card low hand that can be assembled from these cards.

An example problem might be looking at a set of cards like this:

   showCards cards=. 9?52
+--+--+---+--+--+--+--+--+--+
|8C|KD|10C|AD|7D|5D|4S|AH|5C|
+--+--+---+--+--+--+--+--+--+

Display them in rank order to more easily figure out the hand:

   showCards suitRank (]/:1&{"1)&.|: suitRank cards
+--+--+--+--+--+---+--+--+--+
|4S|5D|5C|7D|8C|10C|KD|AD|AH|
+--+--+--+--+--+---+--+--+--+

By examination, we see that there is a low hand possible because there are five unique ranks of 8 or below. The best low hand here is 8-7-5-4-A.

If these nine cards immediately above reflected a game of Omaha where the first four are hole cards and the latter five are common cards, there would be no low hand since there are not three unpaired cards lower than eight in the common cards. However, we ignore that complication for this exercise. (More on how this would work in Omaha here.)

Initial Code

The low-hand functions can ignore suit except for accurately displaying the cards in a hand. This is why you see so many repetitions of expressions like "1{" and "1}" in this code: we are working solely with the card ranks.

lowerAce=: 3 : '((12=1{y)}(1{y),:_1) 1}y'   NB. card rank 12->_1 (ace->1)
raiseAce=: 3 : '((_1=1{y)}(1{y),:12) 1}y'   NB. card rank _1->12 (1->ace)

NB.* bestLow: return best low hand given some cards.
NB. California rules->straights and flushes do not count against low hand,
NB. so we ignore them here.
bestLow=: 3 : 0
   y=. (]/:1{"1]) &.|: lowerAce suitRank y
   y=. (]#"1~1,2~:/\1{]) y                    NB. Remove duplicate ranks
   y=. (5<.{:$y){."1 (]#"1~1,2~:/\1{]) y      NB. Pick lowest 5 if enough
   (([: (|.) 1 {  ]) ; suitRank@:raiseAce) y  NB. Ranks, high to low; full hand
)

So, for the cards defined above - 4S 5D 5C 7D 8C 10C KD AD AH - we get this result:

      bestLow cards
+----------+-------------+
|6 5 3 2 _1|25 41 16 18 6|
+----------+-------------+

The result gives us both the bare ranks and the particular cards we chose to represent the hand. The bare ranks (left box) are shown with any ace lowered to be one less than two, so it is represented by _1. Also, in this case, there are multiple possible cards which comprise the best hand because we have duplicate aces and fives. Since suits do not matter, we choose one of each arbitrarily: this result of this arbitrary choice is shown in the right box.

Some Help Delivered

The code so far could be improved. First of all, it looks like it may be overly complicated but that's just a gut feeling. Also, the result of bestLow is not very readable. We could use something to help us display the low hand in the usual order: descending by rank with ace considered low, like this:

   showCards 6 18 16 41 25
+--+--+--+--+--+
|8C|7D|5D|4S|AD|
+--+--+--+--+--+

We may also want to display all nine cards in this example in the low-hand order, like this:

+--+---+--+--+--+--+--+--+--+
|KD|10C|8C|7D|5C|5D|4S|AH|AD|
+--+---+--+--+--+--+--+--+--+

This display makes it clearer that we have a possible low.

Raul Miller came up with a good solution:

require'stats'             NB. Need "comb" for combinations of y things taken x at a time
bestLow=: {{
  cards=. y\:13|y
  kinds=. 14|2+13|cards
  hands=. 5 comb #y        NB. Try all 5-card combos
  ranks=. 13 #. (,.~ [: >./"1 #/.~"1) hands{kinds
  (hands{~(i.<./)ranks){cards
}}

I also wrote this to display low hands in their proper order:

lowOrdering=: ] \: 1 {  [: lowerAce suitRank NB. Show low hands in descending order of rank with ace low.

So let's create some test cases and look at what we get:

      ,/showCards&>,.tests=. 9?&.>5$52
+--+--+--+---+--+--+---+--+--+
|9S|6H|3H|JH |4D|5S|2D |9C|JS|
+--+--+--+---+--+--+---+--+--+
|QC|KH|2H|3H |JH|4C|4H |QH|KS|
+--+--+--+---+--+--+---+--+--+
|7S|4C|2C|2H |8C|2S|10C|3H|AC|
+--+--+--+---+--+--+---+--+--+
|2S|3S|8S|5S |6S|AC|4D |QC|JC|
+--+--+--+---+--+--+---+--+--+
|8C|AH|4C|10C|8H|7C|AC |QD|8S|
+--+--+--+---+--+--+---+--+--+
   >tests                        NB. Values to allow you to re-create these examples.
46 30 27 35 15 42 13  7 48
10 37 26 27 35  2 28 36 50
44  2  0 26  6 39  8 27 12
39 40 45 42 43 12 15 10  9
 6 38  2  8 32  5 12 23 45
<pre>
   (showCards@:lowOrdering)"1 bestLowRM&>tests
+---+--+--+--+--+
|6H |5S|4D|3H|2D|
+---+--+--+--+--+
|QC |JH|4C|3H|2H|
+---+--+--+--+--+
|7S |4C|3H|2C|AC|
+---+--+--+--+--+
|5S |4D|3S|2S|AC|
+---+--+--+--+--+
|10C|8C|7C|4C|AH|
+---+--+--+--+--+

We can also display these more nicely:

   'ExampleLowHandsInPreferredOrder.png' cardImages lowOrdering"1 bestLowRM&>tests

Which gives us a file with this display:
ExampleLowHandsInPreferredOrder.png

More Detailed Example Assuming Omaha

The preceding exposition is intended to cover cases more general than Omaha, but can also deal with its rules, which are that we may use exactly two hole cards and exactly three of the common cards.

For the set of cards dealt in the above section "Determining the Best Low", i.e.

   cards=. 6 24 8 25 18 16 41 38 3
   'initialHCeg0.png' cardImages 4{.cards

As dealt, taking the first four as the hole cards, we have these:
InitialHCeg0.png

The common cards are these:
InitialCommoneg0.png
So there is a possible low hand based on the common cards and in fact, using the ace and the 8 in the hole, we can construct our best possible one by choosing the lowest ranked cards but excluding pairs, giving us this:
BestLoweg0.png


We could have used the 5 of clubs instead of the 5 of diamonds: the choice does not matter to the value of the hand.

On the other hand, for a different set of cards, like these hole cards:
2dHCeg0.png
and these common cards:
2dCommoneg0.png
there is no possible low because the common cards do not allow it.