The simplest algorithm for poker hand evaluation

有些话、适合烂在心里 提交于 2019-12-17 15:23:57

问题


I am thinking about poker hand (5 cards) evaluation in Java. Now I am looking for simplicity and clarity rather than performance and efficiency. I probably can write a "naive" algorithm but it requires a lot of code.

I saw also a few poker evaluation libraries, which use hashing and bitwise operations, but they look rather complex.

What is the "cleanest and simplest" algorithm for poker hand evaluation ?


回答1:


Here is a very short but complete histogram based 5 card poker scoring function in Python (2.x). It will get considerably longer if converted to Java.

def poker(hands):
    scores = [(i, score(hand.split())) for i, hand in enumerate(hands)]
    winner = sorted(scores , key=lambda x:x[1])[-1][0]
    return hands[winner]

def score(hand):
    ranks = '23456789TJQKA'
    rcounts = {ranks.find(r): ''.join(hand).count(r) for r, _ in hand}.items()
    score, ranks = zip(*sorted((cnt, rank) for rank, cnt in rcounts)[::-1])
    if len(score) == 5:
        if ranks[0:2] == (12, 3): #adjust if 5 high straight
            ranks = (3, 2, 1, 0, -1)
        straight = ranks[0] - ranks[4] == 4
        flush = len({suit for _, suit in hand}) == 1
        '''no pair, straight, flush, or straight flush'''
        score = ([1, (3,1,1,1)], [(3,1,1,2), (5,)])[flush][straight]
    return score, ranks

 >>> poker(['8C TS KC 9H 4S', '7D 2S 5D 3S AC', '8C AD 8D AC 9C', '7C 5H 8D TD KS'])
 '8C AD 8D AC 9C'



回答2:


Lookup tables are the most straightforward and simplest solution to the problem, and also the fastest. The trick is managing the size of the table and keeping the mode of use simple enough to process very quickly (space–time tradeoff). Obviously, in theory you could just encode each hand that could be held and have an array of evaluations, then --poof-- one table lookup and you are done. Unfortunately, such a table would be huge and unmanageable for most machines, and would invariably have you thrashing disks anyway as memory gets swapped out lots.

The so-called two-plus-two solution sports a big 10M table, but literally involves one table lookup for each card in the hand. You are not likely to find a faster and simpler to understand algorithm.

Other solutions involve more compressed tables with more complex indexing, but they are readily comprehensible and pretty fast (although much slower than 2+2). This is where you see language concerning hashing and so forth -- tricks to reduce a table size to more manageable sizes.

In any case, lookup solutions are orders of magnitude faster than the histogram-sort-dance-on-your-head-compare-special-case-and-by-the-way-was-it-a-flush solutions, almost none of which are worthy of a second glance.




回答3:


You actually don't need any advanced functions, it can be all done bitwise: (source: http://www.codeproject.com/Articles/569271/A-Poker-hand-analyzer-in-JavaScript-using-bit-math)

(This one is actually written in JavaScript, but you can evaluate JavaScript from Java if needed, so it shouldn't be a problem. Also, this is as short as it gets, so if even for illustration of the approach...):

First you split your cards into two arrays: ranks (cs) and suits (ss) and to represent suits, you will use either 1,2,4 or 8 (that is 0b0001, 0b0010,...):

var J=11, Q=12, K=13, A=14, C=1, D=2, H=4, S=8;

Now here's the magic:

function evaluateHand(cs, ss) {
    var pokerHands = ["4 of a Kind", "Straight Flush","Straight","Flush","High Card","1 Pair","2 Pair","Royal Flush", "3 of a Kind","Full House"];

    var v,i,o,s = 1 << cs[0] | 1 << cs[1] | 1 << cs[2] | 1 << cs[3] | 1 << cs[4];
    for (i = -1, v = o = 0; i < 5; i++, o = Math.pow(2, cs[i] * 4)) {v += o * ((v / o & 15) + 1);}
    v = v % 15 - ((s / (s & -s) == 31) || (s == 0x403c) ? 3 : 1);
    v -= (ss[0] == (ss[1] | ss[2] | ss[3] | ss[4])) * ((s == 0x7c00) ? -5 : 1);
    return pokerHands[v];
}

Usage:

evaluateHand([A,10,J,K,Q],[C,C,C,C,C]); // Royal Flush

Now what it does (very briefly) is that it puts 1 into 3rd bit of s when there's a 2, into 4th when there's 3, etc., so for the above example s looks like this:

0b111110000000000

for [A,2,3,4,5] it would look like this

0b100 0000 0011 1100

etc.

v uses four bits to record multiple occurencies of same card, so it's 52bits long and if you have three Aces and two kings, its 8 MSB bits look like:

0111 0011 ...

The last line then checks for a flush or straight flush or royal flush (0x7c00).




回答4:


Here's a naive approach to five-card hand comparison that I'm using to help initially populate a lookup table:

  • https://gist.github.com/gdejohn/8293321#file-hand-java

Instead of being as terse as possible, I prioritized type safety and clear, self-documenting code. If you're not familiar with the Guava types I'm using, you can browse their documentation.

And I'll include the code here (minus static imports for the enum constants at the bottom), although it's really too long to comfortably view in an answer.

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Ordering.from;
import static com.google.common.collect.Ordering.natural;
import static java.util.Comparator.comparing;
import static java.util.Comparator.comparingInt;

import java.util.Comparator;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.function.Function;

import com.google.common.collect.EnumMultiset;
import com.google.common.collect.Multiset;
import com.google.common.collect.Multiset.Entry;
import com.google.common.collect.Ordering;

public class Hand implements Comparable<Hand> {
    public final Category category;

    private final LinkedList<Rank> distinctRanks = new LinkedList<>();

    public Hand(Set<Card> cards) {
        checkArgument(cards.size() == 5);
        Set<Suit> suits = EnumSet.noneOf(Suit.class);
        Multiset<Rank> ranks = EnumMultiset.create(Rank.class);
        for (Card card : cards) {
            suits.add(card.suit);
            ranks.add(card.rank);
        }
        Set<Entry<Rank>> entries = ranks.entrySet();
        for (Entry<Rank> entry : byCountThenRank.immutableSortedCopy(entries)) {
            distinctRanks.addFirst(entry.getElement());
        }
        Rank first = distinctRanks.getFirst();
        int distinctCount = distinctRanks.size();
        if (distinctCount == 5) {
            boolean flush = suits.size() == 1;
            if (first.ordinal() - distinctRanks.getLast().ordinal() == 4) {
                category = flush ? STRAIGHT_FLUSH : STRAIGHT;
            }
            else if (first == ACE && distinctRanks.get(1) == FIVE) {
                category = flush ? STRAIGHT_FLUSH : STRAIGHT;
                // ace plays low, move to end
                distinctRanks.addLast(distinctRanks.removeFirst());
            }
            else {
                category = flush ? FLUSH : HIGH_CARD;
            }
        }
        else if (distinctCount == 4) {
            category = ONE_PAIR;
        }
        else if (distinctCount == 3) {
            category = ranks.count(first) == 2 ? TWO_PAIR : THREE_OF_A_KIND;
        }
        else {
            category = ranks.count(first) == 3 ? FULL_HOUSE : FOUR_OF_A_KIND;
        }
    }

    @Override
    public final int compareTo(Hand that) {
        return byCategoryThenRanks.compare(this, that);
    }

    private static final Ordering<Entry<Rank>> byCountThenRank;

    private static final Comparator<Hand> byCategoryThenRanks;

    static {
        Comparator<Entry<Rank>> byCount = comparingInt(Entry::getCount);
        Comparator<Entry<Rank>> byRank = comparing(Entry::getElement);
        byCountThenRank = from(byCount.thenComparing(byRank));
        Comparator<Hand> byCategory = comparing((Hand hand) -> hand.category);
        Function<Hand, Iterable<Rank>> getRanks =
                (Hand hand) -> hand.distinctRanks;
        Comparator<Hand> byRanks =
                comparing(getRanks, natural().lexicographical());
        byCategoryThenRanks = byCategory.thenComparing(byRanks);
    }

    public enum Category {
        HIGH_CARD,
        ONE_PAIR,
        TWO_PAIR,
        THREE_OF_A_KIND,
        STRAIGHT,
        FLUSH,
        FULL_HOUSE,
        FOUR_OF_A_KIND,
        STRAIGHT_FLUSH;
    }

    public enum Rank {
        TWO,
        THREE,
        FOUR,
        FIVE,
        SIX,
        SEVEN,
        EIGHT,
        NINE,
        TEN,
        JACK,
        QUEEN,
        KING,
        ACE;
    }

    public enum Suit {
        DIAMONDS,
        CLUBS,
        HEARTS,
        SPADES;
    }

    public enum Card {
        TWO_DIAMONDS(TWO, DIAMONDS),
        THREE_DIAMONDS(THREE, DIAMONDS),
        FOUR_DIAMONDS(FOUR, DIAMONDS),
        FIVE_DIAMONDS(FIVE, DIAMONDS),
        SIX_DIAMONDS(SIX, DIAMONDS),
        SEVEN_DIAMONDS(SEVEN, DIAMONDS),
        EIGHT_DIAMONDS(EIGHT, DIAMONDS),
        NINE_DIAMONDS(NINE, DIAMONDS),
        TEN_DIAMONDS(TEN, DIAMONDS),
        JACK_DIAMONDS(JACK, DIAMONDS),
        QUEEN_DIAMONDS(QUEEN, DIAMONDS),
        KING_DIAMONDS(KING, DIAMONDS),
        ACE_DIAMONDS(ACE, DIAMONDS),

        TWO_CLUBS(TWO, CLUBS),
        THREE_CLUBS(THREE, CLUBS),
        FOUR_CLUBS(FOUR, CLUBS),
        FIVE_CLUBS(FIVE, CLUBS),
        SIX_CLUBS(SIX, CLUBS),
        SEVEN_CLUBS(SEVEN, CLUBS),
        EIGHT_CLUBS(EIGHT, CLUBS),
        NINE_CLUBS(NINE, CLUBS),
        TEN_CLUBS(TEN, CLUBS),
        JACK_CLUBS(JACK, CLUBS),
        QUEEN_CLUBS(QUEEN, CLUBS),
        KING_CLUBS(KING, CLUBS),
        ACE_CLUBS(ACE, CLUBS),

        TWO_HEARTS(TWO, HEARTS),
        THREE_HEARTS(THREE, HEARTS),
        FOUR_HEARTS(FOUR, HEARTS),
        FIVE_HEARTS(FIVE, HEARTS),
        SIX_HEARTS(SIX, HEARTS),
        SEVEN_HEARTS(SEVEN, HEARTS),
        EIGHT_HEARTS(EIGHT, HEARTS),
        NINE_HEARTS(NINE, HEARTS),
        TEN_HEARTS(TEN, HEARTS),
        JACK_HEARTS(JACK, HEARTS),
        QUEEN_HEARTS(QUEEN, HEARTS),
        KING_HEARTS(KING, HEARTS),
        ACE_HEARTS(ACE, HEARTS),

        TWO_SPADES(TWO, SPADES),
        THREE_SPADES(THREE, SPADES),
        FOUR_SPADES(FOUR, SPADES),
        FIVE_SPADES(FIVE, SPADES),
        SIX_SPADES(SIX, SPADES),
        SEVEN_SPADES(SEVEN, SPADES),
        EIGHT_SPADES(EIGHT, SPADES),
        NINE_SPADES(NINE, SPADES),
        TEN_SPADES(TEN, SPADES),
        JACK_SPADES(JACK, SPADES),
        QUEEN_SPADES(QUEEN, SPADES),
        KING_SPADES(KING, SPADES),
        ACE_SPADES(ACE, SPADES);

        public final Rank rank;

        public final Suit suit;

        Card(Rank rank, Suit suit) {
            this.rank = rank;
            this.suit = suit;
        }
    }
}



回答5:


Here's a modified version of dansalmo's program which works for holdem hands:

def holdem(board, hands):
    scores = [(evaluate((board + ' ' + hand).split()), i) for i, hand in enumerate(hands)]
    best = max(scores)[0]
    return [x[1] for x in filter(lambda(x): x[0] == best, scores)]

def evaluate(hand):
    ranks = '23456789TJQKA'
    if len(hand) > 5: return max([evaluate(hand[:i] + hand[i+1:]) for i in range(len(hand))])
    score, ranks = zip(*sorted((cnt, rank) for rank, cnt in {ranks.find(r): ''.join(hand).count(r) for r, _ in hand}.items())[::-1])
    if len(score) == 5: # if there are 5 different ranks it could be a straight or a flush (or both)
        if ranks[0:2] == (12, 3): ranks = (3, 2, 1, 0, -1) # adjust if 5 high straight
        score = ([1,(3,1,2)],[(3,1,3),(5,)])[len({suit for _, suit in hand}) == 1][ranks[0] - ranks[4] == 4] # high card, straight, flush, straight flush
    return score, ranks

def test():
    print holdem('9H TC JC QS KC', [
        'JS JD', # 0
        'AD 9C', # 1 A-straight
        'JD 2C', # 2
        'AC 8D', # 3 A-straight
        'QH KH', # 4
        'TS 9C', # 5
        'AH 3H', # 6 A-straight
        '3D 2C', # 7
      # '8C 2C', # 8 flush
    ])

test()

holdem() returns a list of indices of the winning hand(s). In the test() example that's [1, 3, 6], since the three hands with aces split the pot, or [8] if the flush hand is uncommented.




回答6:


If you are representing a hand as an array of, for example, Card objects, then I would have methods for looping through this array and determining if it has a 2-of-a-kind, flush etc - and if it does, what type it is; so you could have the 3ofaKind() method return 5 if a hand had three 5s. Then I would establish a hierarchy of possibilities (e.g. 3 of a kind is higher than 2 of a kind) and work from there. The methods themselves should be pretty straightforward to write.




回答7:


If you just want to understand how it works here is simple algorithm:

HandStrength(ourcards,boardcards)
{
    ahead = tied = behind = 0
    ourrank = Rank(ourcards,boardcards)
    /* Consider all two-card combinations
    of the remaining cards. */
    for each case(oppcards)
    {
        opprank = Rank(oppcards,boardcards)
        if(ourrank>opprank)
            ahead += 1
        else if(ourrank==opprank)
            tied += 1
        else /* < */
            behind += 1
    }
    handstrength = (ahead+tied/2) / (ahead+tied+behind)
    return(handstrength)
}

It is from "ALGORITHMS AND ASSESSMENT IN COMPUTER POKER" by Darse Billings.




回答8:


Here is the algorithm translated to R, tested with a 6 card deck, corresponding to 42.504 combinations given by the result of:

combinations of poker hands. Did not tested with 13 card deck due to processing limitations (it would correspond to 2.598.960 combinations).

The algorithm represents the value of a hand by a string, composed by 2 parts:

  • 5 character with ordered card count (ex. "31100" means three of a kind)
  • The card numbers are valued by letters from 'B' (Deuce) to 'N' (Ace) (ex. 'NILH' means Ace, Queen, Nine and Eight). It starts in letter 'B' because of the A2345 poker hand where the Ace comes before the '2' which (the Ace) will have the value 'A'.

So, for example, "32000NB" will be a Full House of three Aces and two Deuce.

The poker hand value string is convenient for comparative and ordering purposes.

library(tidyverse)
library(gtools)

hand_value <- function(playerhand) {

  numbers <- str_split("23456789TJQKA", "")[[1]]
  suits <- str_split("DCHS", "")[[1]]

  playerhand <- data.frame(card = playerhand) %>% separate(card, c("number", "suit"), sep = 1)

  number_values <- data.frame(number = numbers, value = LETTERS[2:14], stringsAsFactors = FALSE)

  playerhand_number <- playerhand %>% 
    group_by(number) %>% 
    count(number) %>%
    inner_join(number_values, by = "number") %>%
    arrange(desc(n), desc(value))

  playerhand_suit <- playerhand %>% 
    group_by(suit) %>% 
    count(suit) %>%
    arrange(desc(n))

  if (nrow(playerhand_number) == 5)
    {
      if (playerhand_number[1,1] == 'A' & playerhand_number[2,1] == '5')
        playerhand_number <- data.frame(playerhand_number[,1:2], value = str_split("EDCBA", "")[[1]], stringsAsFactors = FALSE)
      straight <- asc(playerhand_number[1,3]) - asc(playerhand_number[5,3]) == 4
    } else
      straight = FALSE

  flush <- nrow(playerhand_suit) == 1

  if (flush)
    {
    if (straight)
      playerhand_number <- data.frame(playerhand_number[,c(1,3)], n = c(5, 0, 0, 0, 0), stringsAsFactors = FALSE) else
      playerhand_number <- data.frame(playerhand_number[,c(1,3)], n = c(3, 1, 1, 2, 0), stringsAsFactors = FALSE)
    } else
    {
    if (straight)
      playerhand_number <- data.frame(playerhand_number[,c(1,3)], n = c(3, 1, 1, 1, 0), stringsAsFactors = FALSE)
    }  

  playerhand_value <- append(append(c(playerhand_number$n), rep("0", 5 - nrow(playerhand_number))), c(playerhand_number$value))
  playerhand_value <- paste(playerhand_value, collapse = '')

  playerhand_value

}

Testing the function with the same hands of above example:

l <- c("8C TS KC 9H 4S", "7D 2S 5D 3S AC", "8C AD 8D AC 9C", '7C 5H 8D TD KS')
t <- as_tibble(l)
t <- t %>% mutate(hand = str_split(value, " ")) %>% select(hand)
t <- t %>% mutate(value = sapply(t[,1]$hand, hand_value)) %>% arrange(desc(value))
paste(t[[1]][[1]], collapse = " ")

Which returns the same result:

[1] "8C AD 8D AC 9C"

Hope it helps.




回答9:


Here's a simple rule-based implementation in Kotlin:

class PokerHand constructor(hand: String) : Comparable<PokerHand> {

companion object {
    const val WIN = 1
    const val TIE = 0
    const val LOSS = -1
}

val cards: List<Card>

val isStraightFlush: Boolean
    get() = isStraight && isFlush

val isFourOfAKind: Boolean
    get() = cards.groupBy { it.weight }.map { it.value }.any { it.size == 4 }

val isFullHouse: Boolean
    get() = cards.groupBy { it.weight }.map { it.value }.size == 2

val isFlush: Boolean
    get() = cards.groupBy { it.suit }.map { it.value }.size == 1

val isStraight: Boolean
    get() = cards.map { it.weight.ordinal } == (cards[0].weight.ordinal..cards[0].weight.ordinal + 4).toList()

val isThreeOfAKind: Boolean
    get() = cards.groupBy { it.weight }.map { it.value }.any { it.size == 3 }

val isTwoPair: Boolean
    get() = cards.groupBy { it.weight }.map { it.value }.filter { it.size == 2 }.count() == 2

val isPair: Boolean
    get() = cards.groupBy { it.weight }.map { it.value }.any { it.size == 2 }

init {
    val cards = ArrayList<Card>()
    hand.split(" ").forEach {
        when (it.length != 2) {
            true -> throw RuntimeException("A card code must be two characters")
            else -> cards += Card(Weight.forCode(it[0]), Suit.forCode(it[1]))
        }
    }
    if (cards.size != 5) {
        throw RuntimeException("There must be five cards in a hand")
    }
    this.cards = cards.sortedBy { it.weight.ordinal }
}

override fun compareTo(other: PokerHand): Int = when {
    (this.isStraightFlush || other.isStraightFlush) ->
        if (this.isStraightFlush) if (other.isStraightFlush) compareByHighCard(other) else WIN else LOSS
    (this.isFourOfAKind || other.isFourOfAKind) ->
        if (this.isFourOfAKind) if (other.isFourOfAKind) compareByHighCard(other) else WIN else LOSS
    (this.isFullHouse || other.isFullHouse) ->
        if (this.isFullHouse) if (other.isFullHouse) compareByHighCard(other) else WIN else LOSS
    (this.isFlush || other.isFlush) ->
        if (this.isFlush) if (other.isFlush) compareByHighCard(other) else WIN else LOSS
    (this.isStraight || other.isStraight) ->
        if (this.isStraight) if (other.isStraight) compareByHighCard(other) else WIN else LOSS
    (this.isThreeOfAKind || other.isThreeOfAKind) ->
        if (this.isThreeOfAKind) if (other.isThreeOfAKind) compareByHighCard(other) else WIN else LOSS
    (this.isTwoPair || other.isTwoPair) ->
        if (this.isTwoPair) if (other.isTwoPair) compareByHighCard(other) else WIN else LOSS
    (this.isPair || other.isPair) ->
        if (this.isPair) if (other.isPair) compareByHighCard(other) else WIN else LOSS
    else -> compareByHighCard(other)
}

private fun compareByHighCard(other: PokerHand, index: Int = 4): Int = when {
    (index < 0) -> TIE
    cards[index].weight === other.cards[index].weight -> compareByHighCard(other, index - 1)
    cards[index].weight.ordinal > other.cards[index].weight.ordinal -> WIN
    else -> LOSS
}

}

Implementation details:

  • Instantiate with a coded hand, eg 2H 3H 4H 5H 6H
  • Methods to evaluate whether the hand is a 'Straight Flush', 'Four of a Kind', 'Full House' etc. These are easy to express in Kotlin.
  • Implements Comparable<PokerHand> to evaluate against another hand using a simple rules approach, eg a straight flush beats four of a kind, which beats a full house, and so forth.

The sources are here.



来源:https://stackoverflow.com/questions/10363927/the-simplest-algorithm-for-poker-hand-evaluation

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!