我如何加快我的Python扑克牌手对手公平性计算

2024-05-21 07:05:22 发布

您现在位置:Python中文网/ 问答频道 /正文

首先声明:我是一名医学专业人士,业余爱好是玩Python和扑克。我在这两个方面都没有受过正规的培训,我也不知道计算机科学课的课程是什么。 我使用的电脑是台式机i7-47903.6ghz,16gb的RAM和Jupyter笔记本。在

我的目标是编写相当于扑克战略网Equilab或https://www.cardschat.com/poker-odds-calculator.php给我。我只会选择德州霍尔登。在

为了做到这一点,我需要为任何5张牌组合编写求值器。我这样做了,它可以完美地完成工作,考虑手上的每一张牌,并生成一个元组作为输出,例如:

('2h', '3c', '4s', '6s', 'Jh'): (0, 11, 6, 4, 3, 2)
High-card hand, kickers J, 6, 4, 3, 2

('7c', 'Ad', 'Kd', 'Kh', 'Tc'): (1, 13, 14, 10, 7)
One pair, pair of kings, kickers A, T, 7

('2c', '3c', '4c', '5c', 'Ac'): (8, 5)
Straight flush, 5 high

所以它区分了9 8 7 3和9 8 7 5平齐或高手。我用理论上的皇家冲锋次数,四舍五入,满屋数等检查了259860张卡片组合和频率检查(https://www.quora.com/How-many-possible-hands-are-there-in-a-five-card-poker-game

现在我试着从这260万张中找出每一张可能的5张牌组合,结果花了令人失望的51秒。在

我有点期待我的5张卡片评估器不能成为算法竞赛的冠军,当然还有更好的方法(如果有关联,我可以把它贴在这里),但我想没关系。一旦所有的5卡组合被评估,我将把它们保存在字典中,下次我将加载字典,当我有任何5卡组合时,我将简单地查找结果。在

又一次失望。10万(1000万)的董事会搜索大约需要23-24天秒。这个是一个我不明白的部分!!!我基本上有一个260万的数据库。行x 2列,搜索速度太慢了。那么十亿的记录数据库是如何完成的呢?我的整个字典保存到一个文件中需要88MB——这是一个巨大的数据库吗?在

最后,我制作了一个完整的手对手求值器,在伪代码中可以做到:

  • 举双手,例如AhAs vs.6d6h

  • 列出所有可以处理这2张“死”牌的牌,即1 712 304张牌

  • 列出hand1与board 1的所有21种组合,

  • 用这21个组合搜索排名的_hands字典,并返回可能的最佳结果(21个组合,因为在texas holdem,您可以使用手上的一张、两张或不使用手牌上的5张社区卡中的任何一张)

  • 对hand2和board1执行相同的操作

  • 比较hand1的最佳结果与hand2的最佳结果

  • 如果结果有利于第1手、第2手或是平局,则计数

  • 转到下一个板

这个算法大约要查找7100万个字典-每个170万个板x 42(每只手的21个组合两次)。在

现在,这是一场灾难。每手大约80秒对手对决。 以这样的速度,我什么也不能开始。 所以,如果我能把这件事做得更好,我将不胜感激?在

是我和我缺乏适当的计算机科学和算法知识吗?在

是Python吗? 是Chrome内部的Jupyter笔记本吗?在

还有其他建议吗?在

要求的代码:

^{pr2}$

也许一次打印(总计)多份,但最初是在Jupyter笔记本上


Tags: httpscom算法数据库字典www笔记本jupyter
3条回答

我已经仔细阅读了你的代码。我认为预先计算每只手牌的等级的方法是可行的,尽管它看起来很野蛮。你关于在大多数情况下必须评估每一个踢球员的观点似乎是这种方法的合理理由。在

编辑-一些在线研究表明这不是一个小问题,目前最好的解决方案是“2+2”方法,它实际上是基于查找,但有一些重大的优化。 https://web.archive.org/web/20111103160502/http://www.codingthewheel.com/archives/poker-hand-evaluator-roundup#2p2

一些一般要点:

  • 我还没有真正尝试优化你的手-排名-字典功能,因为它只需要运行一次。在
  • 我是这么说的。将itertools迭代器转换为列表没有任何好处,你只是把它们放在周围而占用内存。我在构建字典的运行时中得到了相当大的改进,只需修改一下这个

然而,我一直在努力从代码中挤出更多的速度。事实上,我觉得现在慢了一点!通过尽可能使用集合并删除一些不必要的中间变量,我稍微整理了一下,但从根本上看,运行时的大部分时间都被字典查找消耗掉了。每一个都很快,但有太多的一切加起来。这里值得的是我对你的代码的版本。我没有使用Jupyter,所以我稍微重构了一下,将字典保存到磁盘上。我没有想到比你的算法更好的算法,所以我会继续思考!在

import itertools
import time
import pickle

ranks = ['2','3','4','5','6','7','8','9','T','J','Q','K','A']
names ="Deuces Threes Fours Fives Sixes Sevens Eights Nines Tens Jacks Queens Kings Aces"
cardnames = names.split()
suitsall = "hearts spades diamonds clubs"
suitnames = suitsall.split()
suits = ['h','s','d','c']
cards = set()

# Create all cards from suits and ranks
for suit in suits:
    for rank in ranks:
        cards.add(rank + suit)


# Function dict_hand_rank ranks every board and returns a tuple (board) (value)
def hand_rank_dict(hand):

    suits = []
    ranks_alphabetical = []
    ranks_numerical = []
    ranks_histogram = []
    kickers = []
    kickers_text = []

    isFlush = False
    isStraight = False
    isStraightFlush = False
    handrankValue = 0

    straightHeight = -1
    straightName = "No straight"
    handName = "none yet"

    for card in hand:
        suits.append(card[1])
        ranks_alphabetical.append(card[0])

    # create ranks_histogram where from A 2 ... J Q K A every card has the corresponding number of occurencies, A double counted

    ranks_histogram.append(str(ranks_alphabetical.count('A')))

    for rank in ranks:
        ranks_histogram.append(str(ranks_alphabetical.count(rank)))

    joined_histogram = ''.join(ranks_histogram)

    # create ranks numerical instead of T, J, Q, K A

    for card in hand:
        ranks_numerical.append(ranks.index(card[0])+2)

    # create kickers

    kickers = sorted([x for x in ranks_numerical if ranks_numerical.count(x) <2], reverse = True)

    # check if a hand is a straight

    if '11111' in joined_histogram:
        isStraight = True
        straightHeight = joined_histogram.find('11111') + 5
        straightName = cardnames[straightHeight - 2]
        handName = "Straight"
        handrankValue = (4,) + (straightHeight,)

    # check if a hand is a flush

    if all(x == suits[0] for x in suits):
        isFlush = True
        handName = "Flush " + cardnames[kickers[0] - 2] + " " + cardnames[kickers[1] - 2] \
              + " " + cardnames[kickers[2] - 2] +  " " + cardnames[kickers[3] - 2] + " " + cardnames[kickers[4] - 2]
        handrankValue = (5,) + tuple(kickers)

    # check if a hand is a straight and a flush

    if isFlush & isStraight:
        isStraightFlush = True
        handName = "Straight Flush"
        handrankValue = (8,) + (straightHeight,)

    # check if a hand is four of a kind
    if '4' in  joined_histogram:
        fourofakindcard = (joined_histogram[1:].find('4') + 2)
        handName = "Four of a Kind " + cardnames[fourofakindcard -2] + " " + cardnames[kickers[0] - 2] + " kicker"
        handrankValue = (7,) + ((joined_histogram[1:].find('4') + 2),) + tuple(kickers)

    # check if a hand is a full house
    if ('3' in joined_histogram) & ('2' in joined_histogram):
        handName = "Full house"
        handrankValue = (6,) + ((joined_histogram[1:].find('3') + 2),) + ((joined_histogram[1:].find('2') + 2),) + tuple(kickers)


    # check if a hand is three of a kind
    if ('3' in joined_histogram) & (len(kickers) == 2):
        threeofakindcard = (joined_histogram[1:].find('3') + 2)
        handName = "Three of a Kind " + cardnames[threeofakindcard -2] + " " + cardnames[kickers[0] - 2] + \
            " " + cardnames[kickers[1] - 2]
        handrankValue = (3,) + ((joined_histogram[1:].find('3') + 2),) + tuple(kickers)

    # check if a hand is two pairs
    if ('2' in joined_histogram) & (len(kickers) == 1):
        lowerpair = (joined_histogram[1:].find('2') + 2)
        higherpair = (joined_histogram[lowerpair:].find('2') + 1 + lowerpair)
        handName = "Two pair " + cardnames[higherpair -2] + " and " + cardnames[lowerpair - 2] + " " + \
            cardnames[kickers[0] - 2] + " kicker"
        handrankValue = (2,) + (higherpair, lowerpair) + tuple(kickers)

    # check if a hand is one pair
    if ('2' in joined_histogram) & (len(kickers) == 3):
        lowerpair = (joined_histogram[1:].find('2') + 2)
        handName = "One pair " + cardnames[lowerpair - 2] + " kickers " + cardnames[kickers[0] - 2] \
            + " " + cardnames[kickers[1] - 2] +  " " + cardnames[kickers[2] - 2]
        handrankValue = (1,) + (lowerpair,) + tuple(kickers)


    # evaluate high card hand
    if (len(ranks_numerical) == len(set(ranks_numerical))) & (isStraight == False) & (isFlush == False):
        handName = "High card " + cardnames[kickers[0] - 2] + " " + cardnames[kickers[1] - 2] \
            + " " + cardnames[kickers[2] - 2] +  " " + cardnames[kickers[3] - 2] + " " + cardnames[kickers[4] - 2]
        handrankValue = (0,) + tuple(kickers)

    return {tuple(sorted(hand)) : handrankValue}


def build_hands_dict(cards, path):

    ranked_hands_dict = {}
    t0 = time.time()
    for board in itertools.combinations(cards, 5):
        ranked_hands_dict.update(hand_rank_dict(board))
    t1 = time.time()
    total = t1-t0
    print(total)
    with open(path,'wb') as f:
        pickle.dump(ranked_hands_dict, f)

# Uncomment this to build the pre-calculated dict of hand ranks
# build_hands_dict(cards, r'D:\hands.p')

with open(r'D:\hands.p','rb') as f:
    ranked_hands_dict = pickle.load(f)

# Function that given board and 2 cards gives back tuple of the best possible hand by searching through ranked_hands_dict keys
def find_the_best_hand(board, hand):

    seven_card_hand = set(board) | hand
    evaluated_all_possible_hands = []

    all_possible_hands = itertools.combinations(seven_card_hand, 5)
    for hand in all_possible_hands:
        evaluated_all_possible_hands.append(ranked_hands_dict[tuple(sorted(hand))])

    return max(evaluated_all_possible_hands)


hand1 = {'2h', '7d'}
hand2 = {'Ad', 'Ah'}

# HAND vs. HAND EVALUATOR

t0 = time.time()

one = 0
two = 0
tie = 0

deadcards = hand1 | hand2
possible_boards = itertools.combinations(cards - deadcards, 5)

n = 0
for board in possible_boards:

    hand1rank = find_the_best_hand(board, hand1)
    hand2rank = find_the_best_hand(board, hand2)

    if hand1rank > hand2rank:
        one = one + 1

    elif hand1rank < hand2rank:
        two = two + 1

    else: #hand1rank == hand2rank:
        tie = tie + 1

    n += 1

onepercent = (one/n)*100
twopercent = (two/n)*100
tiepercent = (tie/n)*100

print(onepercent, twopercent, tiepercent)


t1 = time.time()

total = t1-t0

print(total)

您可以使用dbm模块(请参见https://docs.python.org/3/library/dbm.html)或python2.x中的bsddb将整个查找表存储在数据库文件中,如果它太大而无法放入dict中的内存,那么填充该表可能需要一些时间,但只需执行一次。在

我注意到你的方法中有一个广泛的主题,可能是一个值得重新评估的话题,因为它可能会在性能上产生实质性的差异。

听起来你在试图强行解决这个问题。如果我要求你现在比较两只手(没有电脑,只有你的大脑),你会参考你存储在内存中的每一个可能的扑克手的预先计算的列表吗?你有没有看过这样的清单(实际上是坐着读过每一行)??我希望不会,我猜这两个问题的答案都是“不”。

那么,为什么你选择这个策略来解决你的程序中的相同问题呢?相反,您能编写一个程序,其中包括扑克手的每个类型的抽象定义?这样你的程序就能识别“皇室冲水”还是“满屋”?然后只需计算出所涉双手的相对值,并将结果进行比较,以确定更好的手。没有大的查找表可以扫描,我敢打赌,它可以在没有比您已经拥有的代码更多的情况下完成(但是您可能需要废弃您已经拥有的并重新开始)。

如果您仍然想采用使用预先计算的查找表的策略,请提供以下几点建议:

  • 将所有不可播放的手从表中删除。因为您存储的数据无论如何,您只需要做一次。然后,您可以将每次失败的查找默认为零。至少,这将节省空间,并减少您需要对所有元素进行线性扫描所需的时间。说到这里。。。。
  • Python字典基本上使用hash table作为底层实现,将键映射到相关的值:^{{cd1>}。这意味着,当通过指定整个键访问记录,而没有其他任何操作(^{cd2>})访问记录时,操作可以在固定的时间内完成,该时间不会随着表的增长而增加(与列表相反,该列表要求在找到匹配记录或检查所有记录之前,线性遍历该列表没有匹配)。创建字典时,确保密钥的构造方式与以后访问的方式完全匹配。

并且,无论您选择了何种方法:

  • 由于您是作为学习练习来进行的,我强烈建议您not使用^{cd3>},而且可能也^{cd4>}。虽然这些库非常方便,但它们也隐藏了算法设计的一些非常基本和重要的方面。如果这是分配给一个本科计算机科学课程的作业作业,这些图书馆很可能是不允许的。This article我解释得比我好(这是2001年的乔尔,你还能要求什么?)。
  • 同样,由于这是为了学习,我建议您使用Python的调试工具。具体来说,学习如何在执行期间设置断点和暂停程序,这使您能够逐行地通过代码。这将有助于揭示代码中的热点,因此您知道您的时间将在哪里最好地用于提高性能。

编辑

这里有一个类实现了扑克牌,并通过使用“>;”、“<;”和“=”直接比较两个实例,在任意一组手上传递订单。没有查找表。

from collections import Counter, namedtuple

SUITS = ['d', 'h', 's', 'c']
RANKS = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
Card = namedtuple('Card', ['suit', 'rank'])

class Hand:
    def __init__(self, hand):
        self.hand = hand
        self.catg = None
        self.high_card_ranks = []
        self.hand.sort(key=(lambda c: c.rank), reverse=True)
        self._classify_hand()

    def __eq__(self, x_hand):
        return self._comp_hand(x_hand) == 'EQ'

    def __lt__(self, x_hand):
        return self._comp_hand(x_hand) == 'LT'

    def __gt__(self, x_hand):
        return self._comp_hand(x_hand) == 'GT'

    def __repr__(self):
        face_cards = {1: 'A', 11: 'J', 12: 'Q', 13: 'K', 14: 'A'}
        repr_str = ''
        for n in range(0, 5):
            repr_str += str(face_cards.get(self.hand[n].rank,
                                           self.hand[n].rank)) \
                        + self.hand[n].suit + ' '
        return repr_str

    def _classify_hand(self):
        rank_freq = list(Counter(card.rank for card in self.hand).values())
        suit_freq = list(Counter(card.suit for card in self.hand).values())
        rank_freq.sort()
        suit_freq.sort()
        if self._is_straight() and suit_freq[0] == 5:
            self.catg = 'SF'
            self.high_card_ranks = [c.rank for c in self.hand]
            self._wheel_check()
        elif rank_freq[1] == 4:
            self.catg = '4K'
            self.high_card_ranks = [self.hand[2].rank,
                                    (self.hand[0].rank
                                     if self.hand[0].rank != self.hand[2].rank
                                     else self.hand[4].rank)]
        elif rank_freq[1] == 3:
            self.catg = 'FH'
            self.high_card_ranks = [self.hand[2].rank,
                                    (self.hand[3].rank
                                     if self.hand[3].rank != self.hand[2].rank
                                     else self.hand[1].rank)]
        elif suit_freq[0] == 5:
            self.catg = 'F'
            self.high_card_ranks = [c.rank for c in self.hand]
        elif self._is_straight():
            self.catg = 'S'
            self.high_card_ranks = [c.rank for c in self.hand]
            self._wheel_check()
        elif rank_freq[2] == 3:
            self.catg = '3K'
            self.high_card_ranks = [self.hand[4].rank, self.hand[0].rank]
            self.high_card_ranks.append(self.hand[3].rank
                                        if self.hand[1].rank in self.high_card_ranks
                                        else self.hand[1].rank)
        elif rank_freq[2] == 2:
            self.catg = '2K2'
            self.high_card_ranks = [self.hand[0].rank,
                                    self.hand[2].rank,
                                    self.hand[4].rank]
        elif rank_freq[3] == 2:
            self.catg = '2K'
            self.high_card_ranks = list(set(c.rank for c in self.hand))
        else:
            self.catg = None
            self.high_card_ranks = [c.rank for c in self.hand]

    def _is_straight(self):
        return ((False not in [(self.hand[n].rank == self.hand[n+1].rank + 1)
                               for n in (0, 1, 2, 3)])
                or ([c.rank for c in self.hand] == [14, 5, 4, 3, 2]))

    def _wheel_check(self):
        # allows for the correct ordering of low ace ("wheel") straight
        if (self.catg in ['SF', 'S']
                    and self.high_card_ranks == [14, 5, 4, 3, 2]):
            self.high_card_ranks.pop(0)
            self.high_card_ranks.append(1)

    def _comp_hand(self, comp_hand):
        ret_val = 'EQ'
        catg_order = [None, '2K', '2K2', '3K', 'S', 'F', 'FH', '4K', 'SF']
        curr_hand_catg = catg_order.index(self.catg)
        comp_hand_catg = catg_order.index(comp_hand.catg)
        if curr_hand_catg > comp_hand_catg:
            ret_val = 'GT'
        elif curr_hand_catg < comp_hand_catg:
            ret_val = 'LT'
        else:
            for curr_high_card, comp_high_card in \
                        zip(self.high_card_ranks, comp_hand.high_card_ranks):
                if curr_high_card == comp_high_card:
                    continue
                elif curr_high_card > comp_high_card:
                    ret_val = 'GT'
                    break
                else:
                    ret_val = 'LT'
                    break
        return ret_val

^{pr2}$

相关问题 更多 >