Choose the last card in a poker hand

Pyth, 73 bytes

eo_S+*-5l@\AN}SPMJ+NZSM.:+\AT5+-4l{eMJlM.gPkJ-sM*=T+`M}2Tc4"JQKA""hscd"=Zc

This is pretty terrible. Parsing cards, sorting the values, ... Everything takes so many chars. But the approach is interesting.

Try it online: Demonstration or Test Suite

Explanation:

I generate all 52 cards, remove the four cards of the input, generate a score for each card (score of the hand), and print the card with the maximal score.

The score is a little bit odd. If I compare the score of two completely different hands, it may pick the wrong winner. E.g. a straight would beat 4 aces. But it works, if the first 4 cards are the same in both hands. And my computed score is actually not a value, but a list of values:

  • G: First I group the 5 cards by rank and take the lengths: 5h 5d 6c 5s Jd -> [3, 1, 1]
  • F: Then I append 4 minus the number of different suites to this list. Flush -> 3 gets appended, not flush -> 2/1/0 gets appended.
  • S: Add another number. 0 if it is not a straight, 4 if it is the straight A2345, or 5 if it is a higher straight.

These lists of 4-7 numbers get sorted in decreasing order and the list with maximal value is picked.

Why does this work? Here you see the possible configurations for all types. The letter beside the numbers, tell you with which rule this number got generated.

  • Straight flush: [5S, 3F, 1G, 1G, 1G, 1G, 1G] or [4S, 3F, 1G, 1G, 1G, 1G, 1G]
  • Four of a kind: [4G, 1G, 0F, 0S]
  • Full house: [3G, 2G, 1F, 0S] or [3G, 2G, 0F, 0S]
  • Flush: [3F, 1G, 1G, 1G, 1G, 1G, 0S]
  • Straight: [5S, 2F, 1G, 1G, 1G, 1G, 1G], [5S, 1F, 1G, 1G, 1G, 1G, 1G], [5S, 1G, 1G, 1G, 1G, 1G, 0F], [4S, 2F, 1G, 1G, 1G, 1G, 1G], [4S, 1F, 1G, 1G, 1G, 1G, 1G], [4S, 1G, 1G, 1G, 1G, 1G, 0F]
  • Three of a kind: [3G, 1G, 1G, 1F, 0S], [3G, 1G, 1G, 0F, 0S]
  • Two pair: [2G, 2G, 2F, 1G, 0S], [2G, 2G, 1F, 1G, 0S], [2G, 2G, 1G, 0F, 0S]
  • One pair: [2G, 2F, 1G, 1G, 1G, 0S], [2G, 1G, 1G, 1G, 1F, 0S], [2G, 1G, 1G, 1G, 0F, 0S]
  • High card: [2F, 1G, 1G, 1G, 1G, 1G, 0S], [1F, 1G, 1G, 1G, 1G, 1G, 0S], [1G, 1G, 1G, 1G, 1G, 0S, 0F]

Pyth compares lists element-wise. So it's obvious that a Straight flush will always beat Four of a kind. Most of the typical poker rules are obvious with these lists. Some seem conflicting.

  • A Straight will win against Four of a kind or a Full house: Not a problem. If you have a chance to get Four of a kind / Full house with the river card, than you can't reach a straight at the same time (since you already have 2 or 3 different suites in your hand).
  • A Straight will win against a flush. If you can reach a flush and a straight with the river card, then you also can reach a straight flush. And the straight flush has a better score than both the straight and the flush.
  • One pair [2G, 2F, 1G, 1G, 1G, 0S] will win against some two pair hands. Also no problem. If you get a two pair with the river card, than you had at least one pair before the river. But this means, that you can improve to three of a kind, which is better. So a two pair will actually never be the answer.
  • High card [2F, 1G, 1G, 1G, 1G, 1G, 0S] will win against some one pair hands. If this is the best score you can reach, before the river you will have 3 cards of one suite and one card of a different suite. But then you can choose the card with one of these two suites and with a value that already appears, and you'll end up with the score [2F, 2G, ...], which is also better.

So this chooses the correct type of solution. But how do I get the best one-pair (out of 4 possibilities), how do I choose the best straight, ...? Because two different one-pair solutions can have the same score.

That's easy. Pyth guaranties stable sorting (when taking the maximum). So I simple generate the cards in the order 2h 2s 2c 2d 3h 3s ... Ad. So the card with the highest value will automatically be the maximum.

Implementation details

=Zc splits the input string and stores the list of cards in Z. =T+`M}2Tc4"JQKA" generates the list of ranks ['2', ..., '10', 'J', 'Q', 'K', 'A'] and stores them in T. -sM*T..."hscd"Z generates each combination of rank with the suites, and removes the cards of Z.

o... orders these remaining cards by: lM.gPkJ the lenght of the groups of the ranks, +-4l{eMJlM appends 4 - length(suites), +*-5l@\AN}SPMJ+NZSM.:+\AT5 appends 0/4/5 depending on suite (generate each substring of length 5 of "A"+T, check if the hand one of them (requires sorting the hand and sorting all subsets), multiply by 5 - number of "A"s in the card), _S sorts the list decreasing.

e pick the maximum and print.


JavaScript (ES6), 329 324 317 312 309 bytes

H=>[..."cdhs"].map(Y=>[...L="AKQJT98765432"].map(X=>~H.indexOf(X+=Y)||([...H,X].map(([R,S])=>a|=eval(S+'|=1<<L.search(R)',F|=S!=H[0][1]),F=a=c=d=h=s=0),x=c|d,y=h|s,c&=d,h&=s,p=c|x&y|h,t=c&y|h&x,(S=a-7681?((j=a/31)&-j)-j?F?c&h?2e4+a:t?t^p?3e4+t:7e4:p?8e4+p:M:4e4+a:F?5e4+a:a:F?6e4:1e4)<M&&(R=X,M=S))),M=1/0)&&R

How it works

For each remaining card in the deck, we compute a hand score S. The lower the score, the better the hand.

Variables used to compute the score

  • F: falsy if the hand is a flush
  • c: bitmask of Clubs
  • d: bitmask of Diamonds
  • h: bitmask of Hearts
  • s: bitmask of Spades
  • x = c | d: bitmask of Clubs OR Diamonds
  • y = h | s: bitmask of Hearts OR Spades
  • a: bitmask of all combined suits
  • p = c & d | x & y | h & s: pair bitmask (1)
  • t = c & d & y | h & s & x: three of a kind bitmask (1)

(1) I wrote these formulas some years ago and used them in several poker engines. They do work. :-)

Other formulas

  • c & d & h & s: four of a kind bitmask
  • a == 7681: test for the special straight "A, 2, 3, 4, 5" (0b1111000000001)
  • ((j = a / 31) & -j) == j: test for all other straights

Score chart

Value    | Hand
---------+--------------------------------------------
0   + a  | Standard Straight Flush
1e4      | Special Straight Flush "A, 2, 3, 4, 5"
2e4 + a  | Four of a Kind
3e4 + t  | Full House
4e4 + a  | Flush
5e4 + a  | Standard Straight
6e4      | Special Straight "A, 2, 3, 4, 5"
7e4      | Three of a Kind
8e4 + p  | Pair
Max.     | Everything else

NB: We don't have to care about Two-Pair which can't possibly be our best option. (If we already have one pair, we can turn it into a Three of a Kind. And if we already have two pairs, we can turn them into a Full House.)

Test cases

let f =

H=>[..."cdhs"].map(Y=>[...L="AKQJT98765432"].map(X=>~H.indexOf(X+=Y)||([...H,X].map(([R,S])=>a|=eval(S+'|=1<<L.search(R)',F|=S!=H[0][1]),F=a=c=d=h=s=0),x=c|d,y=h|s,c&=d,h&=s,p=c|x&y|h,t=c&y|h&x,(S=a-7681?((j=a/31)&-j)-j?F?c&h?2e4+a:t?t^p?3e4+t:7e4:p?8e4+p:M:4e4+a:F?5e4+a:a:F?6e4:1e4)<M&&(R=X,M=S))),M=1/0)&&R

console.log(f(["Ah", "Kh", "Jh", "Th"]));   // Qh
console.log(f(["7d", "8h", "Tc", "Jd"]));   // 9d (or 9h, 9c, 9s)
console.log(f(["Js", "6c", "Ts", "8h"]));   // Jc (or Jh, Jd)
console.log(f(["Ac", "4c", "5d", "3d"]));   // 2h (or 2d, 2c, 2s)
console.log(f(["5s", "9s", "Js", "As"]));   // Ks
console.log(f(["2h", "3h", "4h", "5h"]));   // 6h
console.log(f(["Js", "Jc", "Ac", "Ah"]));   // As (or Ad)
console.log(f(["Td", "9d", "5h", "9c"]));   // 9h (or 9s)
console.log(f(["Ah", "Ac", "Ad", "As"]));   // Ks (or Kd, Kh, Kc)
console.log(f(["4d", "5h", "8c", "Jd"]));   // Jc (or Js, Jh)


JavaScript (ES6), 307 349

This is quite bulky and I'm not sure it's the best approach. Still a little golfable perhaps.

h=>(r='_23456789TJQKAT',R=x=>r.search(x[0]),M=i=>[...'hcds'].some(s=>h.indexOf(j=h[i][0]+s)<0)&&j,[u,v,w,y]=h.sort((a,b)=>R(a)-R(b)).map(x=>R(x)),[,,d,e,f,g,k]=[...new Set(h+h)].sort(),q=10-u-v-w,s=(z=y>12)&q>0?q:y-u<5&&u*5+q-y,d>r?z?'Kh':'Ah':f>r?M((e>r?v<w:u<v)+1):s?r[s]+g:k?M(3):r[13-z-(w>11)-(v>10)]+g)

Less golfed

h=>(
  // card rank, 1 to 13, 0 unused
  // fake rank 14 is T, to complete a straight JQKA?
  // as I always try to complete a straight going up
  r = '_23456789TJQKAT', 

  // R: rank a card
  R = x => r.search(x[0]),  

  // M: find a missing card (to complete a same-rank set like a poker)
  // look for a card with the same rank of the card at position i
  // but with a suit not present in the hand
  M = i => [...'hcds'].some(s => h.indexOf(j=h[i][0]+s) < 0) && j,
  h.sort((a, b) => R(a)-R(b) ), // sort hand by rank
  [u,v,w,y] = h.map(x=>R(x)),   // rank of cards 0..3 in u,v,w,y

  // Purpose: look for duplicate rank and/or duplicate suits
  // Put values and suits in d,e,f,g,k, with no duplicates and sorted
  // suits are lowercase and will be at right end
  [,,d,e,f,g,k] = [...new Set(h+h)].sort(),

  // Only if all ranks are different: find the missing value to get a straight
  // or 0 if a straight cannot be obtained
  // The first part manages the A before a 2
  q = 10-u-v-w, s = y>12&q>0 ? q : y - u < 5 && u * 5 + q - y,

  d > r // d is lowercase -> all cards have the same rank
    ? u < 13 ? 'Ah' : 'Kh' // add a K to a poker of A, else add an A
    : e > r // e is lowercase -> 2 distinct ranks
      ? M(v<w ? 2 : 1) // go for a poker or a full house
      : f > r // f is lowercase -> 3 distinct ranks, we have a pair
        ? M(u<v ? 2 : 1) // find the pair and go for 3 of a kind
        : s // all different ranks, could it become a straight?
          ? r[s] + g // if there is only a suit, it will be a flush straight too
          : k // if there are 2 or more different suits
            ? M(3) // go for a pair with the max rank
            : r[13-(y>12)-(w>11)-(v>10)]+g // flush, find the max missing card
)

Test

F=
h=>(r='_23456789TJQKAT',R=x=>r.search(x[0]),M=i=>[...'hcds'].some(s=>h.indexOf(j=h[i][0]+s)<0)&&j,[u,v,w,y]=h.sort((a,b)=>R(a)-R(b)).map(x=>R(x)),[,,d,e,f,g,k]=[...new Set(h+h)].sort(),q=10-u-v-w,s=(z=y>12)&q>0?q:y-u<5&&u*5+q-y,d>r?z?'Kh':'Ah':f>r?M((e>r?v<w:u<v)+1):s?r[s]+g:k?M(3):r[13-z-(w>11)-(v>10)]+g)

output=x=>O.textContent+=x+'\n'

;`Ah Kh Jh Th -> Qh
7d 8h Tc Jd -> 9d 9h 9c 9s
Js 6c Ts 8h -> Jc Jh Jd
Ac 4c 5d 3d -> 2h 2d 2c 2s
5s 9s Js As -> Ks
2h 3h 4h 5h -> 6h
Js Jc Ac Ah -> As Ad
Td 9d 5h 9c -> 9h 9s
Ah Ac Ad As -> Ks Kd Kh Kc
4d 5h 8c Jd -> Jc Js Jh`
.split('\n')
.forEach(s=>{
  var o = s.match(/\w+/g) // input and output
  var h = o.splice(0,4) // input in h, output in o
  var hs = h+''
  var r = F(h)
  var ok = o.some(x => x==r)
  
  output((ok?'OK ':'KO ')+ hs + ' -> ' + r)
})
<pre id=O></pre>