共计 5517 个字符,预计需要花费 14 分钟才能阅读完成。
起源:优达学城 Design Of Computer Programs by Peter Norvig
这是一个 python 实现的德州扑克的程序,而且解说竟然是 Peter Norvig
!然而那些扑克规定真是把我给整懵了 … 在此做些记录以期理顺逻辑。
一、根本设定
- hand: 手牌。一把手牌中有五张扑克牌。
- rank&suit:手牌点数 & 花色。比方一张♦5,花色(suit)是方片(diamond),点数(rank)是 5。
-
牌型 hand rank。牌型有这样几种,大小由大到小排列:
- straight flush:同花顺。同花色的顺子。
- 4-kind:手牌中有 4 张同点数的牌。
- full house: 三张同点数 + 两张同点数,如 10 10 10 6 6。
- flush: 五张同花色。
- straight:顺子。如 5 6 7 8 9。
- 3-kind: 手牌中三张同点数牌。
- two pairs: 两对同点数的牌加一张散牌。
- pair: 一对同点数的牌加三张散牌。
- high card: 不合乎以上任何一种。大小由点数最大的牌决定。(大小程序为 A K Q J 10 9 8 7 6 5 4 3 2)。
二、程序主体
- 次要实现的程序是
poker(hands:list)->hand
。poker
能够承受一个 hand 列表,返回最大的那个 hand。 hand_rank(hand)
。hand_rank
能够指出手牌 hand 的牌型。比方对于一副♥J ♠J ♦2 ♣2 ♦5 的手牌,hand_rank
会指出这是一副two pair。
三、问题
(1) Q: python 的内置函数中有与 poker
函数性能相相似的吗?
A: 有啊,max
。max
中的 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。