扑克手牌评估最简单的算法

29

我正在思考在Java中对扑克牌手(5张牌)进行评估。现在我寻求的是简单和清晰度而不是性能和效率。我可能可以编写一个“幼稚”的算法,但需要大量代码。

我也看到了一些扑克牌评估库,它们使用哈希和位运算,但它们看起来相当复杂。

什么是最“简洁和简单”的扑克牌手评估算法?


1
这篇文章非常简单、清晰易懂:http://nsayer.blogspot.com/2007/07/algorithm-for-evaluating-poker-hands.html - nullpotent
1
右侧的“相关”侧边栏中可能有很多(也许)相关的问题。它们都没有回答你的问题吗? - Oliver Charlesworth
@OliCharlesworth 我看到的大多是现有库的链接。 - Michael
@iccthedral,我不太喜欢这个方法,评估7张牌中的21种组合似乎非常低效。有一些算法可以在不必查看每个组合的情况下确定7张牌中最好的手牌,而且这个算法只比5张牌的算法稍微复杂一点。 - DPM
13个回答

33

这是一段基于直方图的 5 张扑克牌得分函数,使用 Python(2.x)完成。虽然转换为 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'

1
dansalmo 上面的代码无疑是最优雅的版本,但它在 Python 3 中不可用,因为它会抛出一个错误:“TypeError: unorderable types: tuple() < int()”。它可能存在一个错误,因为一个元组被与一个整数进行比较? - Nickpick
1
实际上,在河牌之后通常会有7张牌。这将如何改变代码?此外,桌面上放置哪些手牌以及哪些由玩家持有似乎并不重要,只有这样才能确定哪张牌是踢腿者。 - Nickpick
1
这是用于排名基本的5张牌手牌的,适用于翻牌游戏或德州扑克之外的其他类型游戏。 - dansalmo
1
在Holdem中,需要以何种方式进行通知? - Nickpick
最简单的方法是从7张牌中创建所有可能的5张牌组合,然后使用上述方法对它们进行排名以找到最佳手牌。 - dansalmo
显示剩余6条评论

13

查找表是解决问题最直接、最简单、也是最快的方法。诀窍在于管理表的大小和保持使用模式足够简单,以便能够非常快速地处理(时空权衡)。显然,在理论上,你可以对每个可能出现的手牌进行编码,并拥有一个评估数组,然后一次表查找即可完成。不过,这样的表对大多数机器来说都是巨大且无法管理的,并且随着内存大量交换,你无论如何都会遇到磁盘读写问题。

所谓的二加二方案需要一个大表格(10M),但实际上是每张卡牌只需一次表查找。你不太可能找到一个更快、更易于理解的算法。

其他方案涉及更压缩的表格和更复杂的索引,但它们很容易理解,而且速度相当快(虽然比2+2慢得多)。这就是你看到有关哈希等技巧的语言——用于将表的大小减小为更易管理的尺寸。

总之,查找解决方案比直方图排序舞蹈-在你的头上比较特殊情况-顺便问一下,那是一张什么牌解决方案要快上数量级,几乎没有一个值得第二眼看。


9
您实际上不需要任何高级功能,所有操作都可以通过位运算完成:(源自:http://www.codeproject.com/Articles/569271/A-Poker-hand-analyzer-in-JavaScript-using-bit-math
(这段代码实际上是用JavaScript编写的,但是如果需要的话,您可以从Java中使用evaluate JavaScript,所以这不应该是一个问题。此外,如果要说明方法,它甚至已经非常简短了...):
首先,将您的牌分成两个数组:点数(cs)和花色(ss),为了表示花色,您将使用1、2、4或8(即0b0001、0b0010、...)。
var J=11, Q=12, K=13, A=14, C=1, D=2, H=4, S=8;

现在来看看其中的奥妙:

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];
}

用法:

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

现在简单介绍一下它的作用:当出现2时,它会将1放入s的第3位,当出现3时,将其放在第4位等等。因此,对于上面的例子,s如下所示:0b111110000000000。对于[A, 2, 3, 4, 5],它看起来像这样:0b100 0000 0011 1100,以此类推。v使用四位记录同一张卡的多次出现,因此它有52个位数。如果你有三个A和两个K,它的8个最高有效位看起来像:0111 0011 ...。最后一行检查是否为同花顺或皇家同花顺(0x7c00)。

顺便提一下,我认为包含你的源代码是明智之举:http://www.codeproject.com/Articles/569271/A-Poker-hand-analyzer-in-JavaScript-using-bit-math顺便说一句,这是一篇很棒的文章。 - Albert Hendriks
当然可以,抱歉。我编辑了答案并在其中引用了文章。 - Matěj Balga
1
如果您想确定哪一手牌获胜,仅返回值是不够的。您还需要包括对子、踢脚等信息。 - Hadus
OP 询问了有关扑克牌手牌评估的问题 - 这正是它所做的。它不应包括游戏规则,因为这应该在其他地方处理 - 这样我们才能遵循单一职责原则。 - Matěj Balga

5
import java.util.*; // Arrays, Set
import java.util.Map.Entry;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.*; // counting, groupingBy

public record Hand(Category category, Rank... ranks) implements Comparable<Hand> {
    public static Hand evaluate(Set<Card> cards) {
        if (cards.size() != 5) { throw new IllegalArgumentException(); }
        var flush = cards.stream().map(Card::suit).distinct().count() == 1;
        var counts = cards.stream().collect(groupingBy(Card::rank, counting()));
        var ranks = counts.entrySet().stream().sorted(
            comparing(Entry<Rank, Long>::getValue).thenComparing(Entry::getKey).reversed()
        ).map(Entry::getKey).toArray(Rank[]::new);
        if (ranks.length == 4) {
            return new Hand(ONE_PAIR, ranks);
        } else if (ranks.length == 3) {
            return new Hand(counts.get(ranks[0]) == 2 ? TWO_PAIR : THREE_OF_A_KIND, ranks);
        } else if (ranks.length == 2) {
            return new Hand(counts.get(ranks[0]) == 3 ? FULL_HOUSE : FOUR_OF_A_KIND, ranks);
        } else if (ranks[0].ordinal() - ranks[4].ordinal() == 4) {
            return new Hand(flush ? STRAIGHT_FLUSH : STRAIGHT, ranks[0]);
        } else if (ranks[0].equals(ACE) && ranks[1].equals(FIVE)) { // A-2-3-4-5
            return new Hand(flush ? STRAIGHT_FLUSH : STRAIGHT, FIVE);
        } else {
            return new Hand(flush ? FLUSH : HIGH_CARD, ranks);
        }
    }

    @Override
    public int compareTo(Hand that) { // first compare by category, then compare ranks lexicographically
        return comparing(Hand::category).thenComparing(Hand::ranks, Arrays::compare).compare(this, that);
    }
}

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

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

enum Suit {DIAMONDS, CLUBS, HEARTS, SPADES}

record Card(Rank rank, Suit suit) {}

4
这是dansalmo程序的修改版本,适用于德州扑克手牌:
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()返回获胜手牌的索引列表。在test()示例中,这是[1, 3, 6],因为三个带有A的手牌分开了奖池,或者如果删去flush hand,则为[8]。


Python 3在evaluate函数中出现奇怪的错误:TypeError: '>'不支持'tuple'和'int'实例之间的比较。 - elsni

4

Python 3版本

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'

为了避免int与tuple比较错误,需要将1替换为(1,)


3
我已经用C ++和Javascript编写了一种扑克手牌评估器。该程序会将随机选择的一组纸牌转换为1和0的三维数组。通过将纸牌转换为这种格式,我能够编写函数来测试从最高级别开始的每种类型的手牌。
因此,总结一下,我的程序会生成随机卡牌,并将它们转换为代表红心,方块,黑桃和梅花的三维数组,其中1代表我拥有的其中一张卡。然后,我会测试三维数组,看看是否有皇家同花顺、同花顺、四条等牌型,直到匹配成功。一旦匹配成功,例如在测试同花时,那么我的程序就不必测试顺子、三条等了,因为同花打败了顺子。
以下是我的程序输出的数据: 我的随机卡牌:
Table Cards
{ Value: '9', Suit: 'H' }
{ Value: 'A', Suit: 'H' }
{ Value: '9', Suit: 'D' }
{ Value: '7', Suit: 'S' }
{ Value: '6', Suit: 'S' }

表示我的卡牌的3D数组:

 A  2  3  4  5  6  7  8  9  10 J  Q  K  A

Spades
[ 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0 ]
Diamonds
[ 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0 ]
Clubs
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
Hearts
[ 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]

通过上面的数值,我可以判断我拥有一对9和一张A,7,6的踢脚牌。

你可以看到数组中包含两个A。这是因为您想要从A开始测试顺子。所以是(A,2,3,4,5)。

如果您想要测试七张牌而不是五张牌,您也可以使用此系统。您可以将用户的两张牌与桌子上的五张牌一起使用我的系统进行运算。您还可以对桌上其他玩家做同样的事情并比较结果。

希望这能有些帮助。


2

如果您将一手牌表示为一个数组,例如Card对象,则我将创建循环遍历该数组并确定它是否具有2个相同的、同花等类型的方法;如果是这样,还可以确定其类型。例如,3ofaKind()方法如果一手牌有三个5,则返回5。然后,我会建立一个可能性层级(例如,3张相同的牌比2张相同的牌更高),并从那里开始工作。这些方法本身应该很容易编写。


2
不要忘记,有些三条比其他三条更好。 - Jeffrey

2

由于问题是关于Java的,而答案是关于Python的,所以我想发布一篇新的Java答案。我的目标是让这个函数易于理解。

The Evaluator will score:
Royal flush - 10000
Straight flush - 9000 + highest card
Four of a kind - 8000 + card
Full house - 7000 + card
----- Here we have a smal gap
Flush - 5000 + highest card
Straight - 4000 + highest card
Three of a kind - 3000 + card
Two pair - 2000 + 13*card1 + card2 (card1 > card2)
Pair - 1000 + card

高牌不计分!(如果高牌打平,您需要继续检查下一张牌等等...)如果您想在评估器中对此进行编码,您确实需要位码,而且程序会变得不太容易理解。幸运的是,在两个已排序的手牌上检查高牌很容易,因此您首先要检查得分。如果得分相等,则检查高牌以打破平局;-)

没有更多的话,这里是代码(适用于已排序的手牌):

public static int scoreHand(int[] hand) {
    return scoreSeries(hand) + scoreKinds(hand);
}
public static int scoreStraight(int[] hand) {
    for(int i = 1; i < hand.length; i++) {
        if ((hand[i] % 100 + 1) != (hand[i - 1] % 100)) return 0;
    }
    return 4000;
}
public static int scoreFlush(int[] hand) {
    for(int i = 1; i < hand.length; i++) {
        if ((hand[i] / 100) != (hand[i - 1] / 100)) return 0;
    }
    return 5000;
}
public static int scoreSeries(int[] hand) {
    int score = scoreFlush(hand) + scoreStraight(hand);
    if (hand[0] % 100 == 12 && score == 9000)
        return 10000;
    if (score > 0) return score + hand[0] % 100;
    return 0;
}

public static int scoreKinds(int[] hand) {
    int[] counts = new int[13], highCards = new int[2];
    int max = 1, twoSets = 0;
    for(int i = 0; i < hand.length; i++) {
        counts[hand[i] % 100]++;
        if (max > 1 && counts[hand[i] % 100] == 2) {
            twoSets = 1;
            highCards[1] = hand[i] % 100;
        }
        if (max < counts[hand[i] % 100]) {
            max = counts[hand[i] % 100];
            highCards[0] = hand[i] % 100;
        }
    }
    if (max == 1) return 0;
    int score = (max * 2 + twoSets) * 1000;
    if (score < 7000) score -= 3000;
    if (max == 2 && twoSets == 1) {
        if (highCards[1] > highCards[0]) swap(highCards, 0, 1);
        score += 13 * highCards[0] + highCards[1];
    } else {
        if (counts[highCards[1]] > counts[highCards[0]]) swap(highCards, 0, 1);
        score += highCards[0];
    }
    return score;
}

1

如果您只是想了解它的工作原理,这里有一个简单的算法:

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)
}

这是来自Darse Billings的《计算机扑克中的算法和评估》一书。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接