乐趣区

关于python:Design-Of-Computer-Programs1A-Poker-Program

起源:优达学城 Design Of Computer Programs by Peter Norvig

这是一个 python 实现的德州扑克的程序,而且解说竟然是 Peter Norvig
!然而那些扑克规定真是把我给整懵了 … 在此做些记录以期理顺逻辑。

一、根本设定

  • hand: 手牌。一把手牌中有五张扑克牌。
  • rank&suit:手牌点数 & 花色。比方一张♦5,花色(suit)是方片(diamond),点数(rank)是 5。
  • 牌型 hand rank。牌型有这样几种,大小由大到小排列:

    1. straight flush:同花顺。同花色的顺子。
    2. 4-kind:手牌中有 4 张同点数的牌。
    3. full house: 三张同点数 + 两张同点数,如 10 10 10 6 6。
    4. flush: 五张同花色。
    5. straight:顺子。如 5 6 7 8 9。
    6. 3-kind: 手牌中三张同点数牌。
    7. two pairs: 两对同点数的牌加一张散牌。
    8. pair: 一对同点数的牌加三张散牌。
    9. high card: 不合乎以上任何一种。大小由点数最大的牌决定。(大小程序为 A K Q J 10 9 8 7 6 5 4 3 2)。

二、程序主体

  • 次要实现的程序是 poker(hands:list)->handpoker 能够承受一个 hand 列表,返回最大的那个 hand。
  • hand_rank(hand)hand_rank能够指出手牌 hand 的牌型。比方对于一副♥J ♠J ♦2 ♣2 ♦5 的手牌,hand_rank会指出这是一副two pair

三、问题

(1) Q: python 的内置函数中有与 poker 函数性能相相似的吗?

A: 有啊,maxmax中的 key 参数容许对被比拟的主体进行映射,比方用 abs 将原来的值变成绝对值。这里能够用让 poker 返回 max(hands, key=hand_rank),将手牌映射为牌型。这样一来,poker 的逻辑就能够被简洁地示意为:

def poker(hands):
    "return the best hand: poker([hand,...]) => hand"
    return max(hands, key=hand_rank)

(2)如何示意一手牌(hand)?

如果用字符串示意一张牌,字符串列表就不错。["TC","9D","10D","JH","5C"]中,"TC"示意 club 10,"JH"示意 jack heart。

(3) Q: 如何示意牌型,使得牌型能被 max 函数比拟?比方,将 straight flush 赋值为 8,4-kind赋值为 7,…,high card赋值为 0。这样就能够了吗?

A: 不行。4-kind之间(10 10 10 10 7 和 9 9 9 9 6)怎么比拟呢?能够用一个元组示意牌型。比方 10 10 10 10 7 示意为 (7,10,7),9 9 9 9 9 6 示意为(7,9,6)。python 中元组间也是能够相互比拟的!比拟办法是对每一个元素顺次进行比拟。比方(7,10,7) 就大于 (7,9,6),因为第 1 个地位上 10>9。
如何将其余的牌型用相似的形式表达出来?

  • straight flush:”straight flush, jack high!” 也就是说 J 是同花顺中最大的牌。晓得了最大的牌就能够比拟两手同花顺的大小。(8,11)就能齐全形容手牌状况。
  • 4-kind:”four aces, and a queen kicker!” 四张 A 带一张 Q,示意为(7,14,12)。
  • full house:”full house, eight over kings!” (6,8,13)示意三张 8 带两张 K。
  • flush:这个就须要列出所有牌的点数能力比大小。(5,[10,8,7,5,3])示意同花色、点数为的 10,8,7,5,3 手牌。
  • straight:”straight, jack high!” 这个也只须要最大点数的手牌就能齐全比拟大小——(4,11)。
  • 3-kind:(3, 7, [7, 7, 7, 5, 2]) 示意 7 7 7 5 2。
  • two pair:(2, 11, 3, [13, 11, 11, 3, 3]) 示意 11 11 3 3 13。
  • pair:(1, 2, [11, 6, 3, 2, 2]) 示意 2 2 3 11 6。
  • high card:(0,7,5,4,3,2) 示意 7 5 4 3 2。

这些规定用代码示意就是:

def hand_rank(hand):
    "Return a value indicating the ranking of a hand"
    ranks = card_ranks(hand)  # ranks 指所有手牌点数
    if straight(ranks) and flush(hand):
        return (8, max(ranks))
    elif kind(4, ranks):  # 即手牌中有 4 张点数雷同的牌
        return (7, kind(4, ranks), kind(1, ranks))
    elif kind(3, ranks) and kind(2, ranks):
        return (6, kind(3, ranks), kind(2, ranks))
    elif flush(hand):
        return (5, ranks)
    elif straight(ranks):
        return (4, max(ranks))
    elif two_pair(ranks):
        return (2, two_pair(ranks), ranks)
    elif kind(2, ranks):
        return (1, kind(2, ranks), ranks)
    else:
        return (0, ranks)

#### (4)Q: 如何实现 card_ranks, 给出手牌的点数?
A:card_ranks 给出手牌中牌的点数。简略而言,只有提取出 card 字符串的第一个字符即可。比方:

def card_ranks(hand):
    ranks = [r for r,s in cards]
    ranks.sort(reverse=True)
   return ranks

但对于 ”AC”(梅花 A)这种牌该怎么办呢?
最开始的想法是这样的:弄个字典,贮存 TJQKA 对应的数值。

def card_ranks(cards):
    "Return a list of the ranks, sorted with higher first."
    modifier_dict={
        'T':10,
        'J':11,
        'Q':12,
        'K':13,
        'A':14
    }
    def modifier(x):
        if x in modifier_dict.keys():
            return modifier_dict[x]
        else:
            return int(x)
    ranks = [r for r,s in cards]
    ranks = list(map(modifier, ranks))
    ranks.sort(reverse=True)
    return ranks            

就完事儿。而后还想,这种“找失去键返回键对应值,找不到就返回另一个值”不是很像 dict.get() 吗,那就改成:

def modifier(x):
    modifier_dict={
        'T':10,
        'J':11,
        'Q':12,
        'K':13,
        'A':14
    }
    return modifier_dict.get(x, int(x))

诶他就不行了。会报错不存在 int('A') 这种操作。因为无论找不找失去键 get 都会把 int 给执行了,自作聪明。

然而 Peter 只用了一行:

def card_ranks(hand):
    "Return a list of the ranks, sorted with higher first."
    ranks = ['--23456789TJQKA'.index(r) for r,s in hand]
    ranks.sort(reverse=True)
    return ranks

用列表中的地位对应手牌点数,就很秀。

(4)如何判断 straight(顺子)和 flush(同花色)?

顺子,也就是说点数是 5 个间断整数。比拟奇妙的办法是判断这 5 个数互不雷同,且最大值减去最小值为 4。
max(ranks) - min(ranks) == 4 and len(set(ranks)) == 5
同花色,也就是取转化成汇合后只有一个元素。
len(set(suit))==1

这套规定不仅能用来判断扑克里的同花顺,还能够判断五子棋的输赢:五个棋子横坐标雷同,纵坐标为五个间断值,那就算是赢了。

(5) 如何实现 kind(ranks, n)函数,给出手牌中正好有 n 张的牌?

最简略的方法,遍历 + list.count()。我的想法也差不多,用 collections.Counter 数一遍。然而没有必要,不须要把所有的元素个数都数进去。

def kind(n, ranks):
    for r in ranks:
        if ranks.count(r) == n:
            return r
    return None

(6) 如何实现 two_pair, 判断手牌里是否有两组成对的牌, 有则返回这两组牌的点数?

我的想法:先对手牌用一次 kind(ranks, 2), 失去返回值 r0, 而后从列表里弹出所有点数为 r0 的牌,对剩下的列表再用一次 kind(ranks, 2), 失去返回值 r1。
Peter 的想法:对手牌用一次 kind(ranks, 2)失去 r0,将列表逆序,再用一次 kind(ranks, 2)失去 r1,r0 若不等于 r1 则是 two_pair。
嗯,充分利用数据和函数的个性 … 个性在于 kind 只会返回第一个手里恰好有 n 张牌的点数,ranks 又是有序排列的。

(7) 如何解决平局?

poker函数应用了 max 来比拟手牌大小,然而 max 只会返回第一个最大值,这在平局的状况下是不偏心的。所以咱们要实现本人的max,它能返回所有的最大值。这并不难做到:

def allmax(iterable, key=None):
    result, maxval = [], None
    key = key or (lambda x: x)
    for x in iterable:
        xval = key(x)
        if not result or xval > maxval:
            result, maxval = [x], xval
        elif xval == maxval:
            result.append(x)
    return result

(8)如何发牌?

咱们须要给每个玩家发随机的 n 张牌。
简单的写法:

def deal(numhands, n=5, deck=None):
    deck = copy(deck) or [r + s for r in '23456789TJQKA' for s in 'SHDC']
    hands = []
    for num in range(0, numhands):
        hand = []
        for _ in range(0, n):
            card = random.choice(deck)
            if deck:
                deck.remove(card)
                hand.append(card)
        hands.append(hand)
    return hands

简洁的写法:

def deal(numhands, n=5, deck=None):
    deck = copy(deck) or [r + s for r in '23456789TJQKA' for s in 'SHDC']
    random.shuffle(deck)
    return [deck[n*i: n*(i+1)] for i in range(numhands)]

上面这种写法不仅是在算法实现上简洁,也更合乎日常生活中的状况:谁打牌会每次抽牌的时候都随机选取啊,必定是先把整个牌组打乱而后每人给 n 张牌嘛。

四、测试

wikipedia 给出了每一种牌型呈现的概率。其中最常见的同花顺概率为 0.0015%,最常见的杂牌呈现概率为 50.11%。咱们能够通过屡次运行程序失去所有牌型的呈现频率,如果与 wikipedia 的后果统一,则阐明咱们的程序是正确的。那么,咱们应该运行程序多少次呢?
只管次数多多益善,但理论中必须思考对机器的累赘。要害是让最常见的牌型也呈现足够多的次数,以此保障后果的稳健性。咱们能够冀望让同花顺呈现 10 次左右,则程序应运行 10 / 0.0015% 次,即大概 700,000 次。

五、重构

hand_rank函数反复用到了好屡次 rank,犯了自我反复的禁忌:有没有四张同点数的牌?哦,没有,那有没有三张同点数的牌?哦,也没有 … 那不如一开始就算分明手里有哪些点数的牌,各自有几张。

def group(items):
    groups = [(items.count(x), x) for x in set(items)]
    return sorted(groups, reverse=True) # 把数量最多的放在最后面,数量雷同的话,将点数最大的放在后面


def hand_rank(hand):
    groups = group(['--23456789TJQKA'.index(r) for r,s in hand])
    counts, ranks = unzip(groups)  #7 10 7 9 7 => (3, 1, 1), (7, 10, 9)
    if ranks == (14, 5, 4, 3, 2):
        ranks = (5, 4, 3, 2, 1)  # 非凡状况下的规定
    straight = (len(ranks) == 5) and (max(ranks) - min(ranks) == 4)
    flush = (len(set([s for r, s in hand])) == 1)
    return (9 if (5,) == counts else
            8 if straight and flush else
            7 if (4, 1) == counts else
            6 if (3, 2) == counts else
            5 if flush else
            4 if straight else
            3 if (3, 1, 1) == counts else
            2 if (2, 2, 1) == counts else
            1 if (2, 1, 1, 1) == counts else
            0), ranks

这样做不仅效率更高,还分明地展现了德州扑克的特点:它有序地拆分了数字 5。
5 = 4 + 1
   = 3 + 2
   = 3 + 1 + 1
   = 2 + 2 + 1
   = 2 + 1 + 1 + 1
   = 1 + 1 + 1 + 1 + 1。

退出移动版