游戏:三张牌

游戏:三张牌(理论分析+py3模拟)

序言:
本文将全面剖析一款民间风靡游戏,三张牌。

0. 规则
游戏使用一副除去大小司令的扑克牌,即 A, 2, 3, 4, 5 ,6, 7, 8, 9, 10, J, Q, K,共计 13 种牌型,每种牌型 4 种花色,总计 52 张牌。比牌规则为

  • 单牌,A > K > Q > … > 2
  • 牌型, 豹子 > 顺金 > 金花 > 顺子 > 对子 > 散牌
  • 不比花色

注,当牌型完全相同时,在民间多人玩法中判先比牌者负。但本文只看庄,闲两家,对于牌型相同情况,将判平局。

1. 牌型概率的理论计算
从 52 张牌中任取 3 张牌,总共可能出现的情况为 C 52 3 = 22100 C_{52}^{3} = 22100 C523=22100

  • 豹子
    总共 13 ∗ 4 = 52 13 * 4 = 52 134=52 种,概率 P 0 = 0.235 % \red {P_0 = 0.235\%} P0=0.235%
  • 顺金
    枚举,A23, 234, …, QKA
    总共 12 ∗ 4 = 48 12 * 4 = 48 124=48 种,概率 P 1 = 0.217 % \red {P_{1} = 0.217\%} P1=0.217%
  • 金花
    先考虑黑桃金花,即从黑桃牌共计 13 张牌中抽取 3 张,然后减去黑桃顺金部分,最后乘 4 即可:
    总共 ( C 13 3 − 12 ) ∗ 4 = 1096 (C_{13}^{3} - 12) * 4 = 1096 (C13312)4=1096 种,概率 P 3 = 4.959 % \red{P_{3} = 4.959\%} P3=4.959%
  • 顺子
    先考虑 A,2,3 顺,共 4 3 = 64 4^3= 64 43=64 种,除去A, 2, 3顺金部分,最后乘 12 即可:
    总共 ( 4 3 − 4 ) ∗ 12 = 720 (4^3-4) * 12 = 720 (434)12=720 种,概率 P 4 = 3.258 % \red{P_{4} = 3.258\%} P4=3.258%
  • 对子
    先考虑 A 对,共 C 4 2 ∗ ( 12 ∗ 4 ) = 288 C_{4}^{2} * (12 * 4)= 288 C42(124)=288 种,最后乘 13 即可:
    总共 C 4 2 ∗ ( 12 ∗ 4 ) ∗ 13 = 3744 C_{4}^{2} * (12 * 4) * 13 = 3744 C42(124)13=3744 种,概率 P 5 = 16.941 % \red{P_{5} = 16.941\%} P5=16.941%
  • 散牌
    剩下的概率都是拿到散牌,约为 75 % 75\% 75%。散牌的种类不可胜数,一个恰当的统计方法是以散牌中最大的那张牌作为牌种区分的标志。比如,散牌 A,表示当前手牌为散牌,且3张牌中最大的牌为 A。
  1. 散 A
    在这里插入图片描述
    先考虑黑桃散A,即,现在已经确定手上有一张黑桃 A,那么剩下的两张牌不能:
    I. 自成对,例如 两个K,两个Q等,该情况总共有 C 4 2 ∗ 12 = 72 C_{4}^{2} * 12= 72 C4212=72
    II. 与黑桃 A 成顺,即 A23 或 QKA,该情况总共有 4 ∗ 4 ∗ 2 = 32 4 * 4 * 2 = 32 442=32
    III. 与黑桃 A 成花,该情况总共有 C 12 2 = 66 C_{12}^{2} = 66 C122=66
    此外,II 与 III 具有重叠部分,即顺金A23, QKA 两种,需要额外补偿回来。
    因此黑桃散A 的总可能情况应该如此描述, 另外两张牌应该从 2 - K 中去取,并且不能I.自成对,II与黑桃A成顺,也不能III.与黑桃A成花。计算如下:
    C 48 2 − C 4 2 ∗ 12 − 4 ∗ 4 ∗ 2 − C 12 2 + 2 = 960 C_{48}^{2} - C_{4}^{2} * 12 - 4 * 4 * 2 - C_{12}^{2} + 2 = 960 C482C4212442C122+2=960,所有散A的种数为上述结果乘 4,即 3840 种。
    因此,概率 P A = 17.376 % \red {P_{A} = 17.376\%} PA=17.376%
  2. 散 K
    在这里插入图片描述
    类似于散 A 的计算。但是,根据定义,散 K 里一定没有 A,否则它便是散 A。故在计算时应该直接从除去 4 张 A 的牌堆里抽取。计算如下: 4 ∗ ( C 44 2 − C 4 2 ∗ 11 − 4 ∗ 4 − C 11 2 + 1 ) = 3240 4 * (C_{44}^{2} - C_{4}^{2} * 11 - 4 * 4 - C_{11}^{2} + 1) = 3240 4(C442C421144C112+1)=3240
    因此,概率 P K = 14.661 % \red {P_{K} = 14.661\%} PK=14.661%
  3. 散 Q
    总计 2640 种,概率 P Q = 11.946 % \red {P_{Q} = 11.946\%} PQ=11.946%
  4. 散 J
    总计 2100 种,概率 P J = 9.502 % \red {P_{J} = 9.502\%} PJ=9.502%
  5. 散 10
    总计 1620 种,概率 P 10 = 7.330 % \red {P_{10} = 7.330\%} P10=7.330%
  6. 散 9
    总计 1200 种,概率 P 9 = 5.430 % \red {P_{9} = 5.430\%} P9=5.430%
  7. 散 8
    总计 840 种,概率 P 8 = 3.801 % \red {P_{8} = 3.801\%} P8=3.801%
  8. 散 7
    总计 540 种,概率 P 7 = 2.443 % \red {P_{7} = 2.443\%} P7=2.443%
  9. 散 6
    总计 300 种,概率 P 6 = 1.357 % \red {P_{6} = 1.357\%} P6=1.357%
  10. 散 5
    总计 120 种,概率 P 5 = 0.543 % \red {P_{5} = 0.543\%} P5=0.543%

不存在散4及其以下,即不可能存在当前牌为散牌,且最高牌不大于4的情况。

2. 利用python3模拟
除了理论计算的方法,还可以尝试使用编程模拟解决。这里考虑用的是python3进行模拟。要点在于如何判别牌型:

  1. 先对3张手牌按数字大小以降序排序
  2. 若三张牌点数相同,判为豹子,否则步入步骤3
  3. 若三张牌点数成公差 -1 的等差数列,进一步判断,否则步入步骤4
    3.1 若三张牌花色一致,判为顺金
    3.2 否则,判为顺子
  4. 若三张牌花色一致,判为金花,否则步入步骤5
  5. 若第一张牌与二张牌点数相同,或者第二张牌与第三张牌点数相同,判为对子,否则步入步骤6
  6. 其余情况,判为散 x x x,其中 x x x 是第一张牌的点数

在样本总容量取 1 0 7 10^7 107情况下,得到结果如下:

在这里插入图片描述
在这里插入图片描述
每种情况与理论分析的误差不大于一个万分点。

3. 每种牌型的胜率
这里只考虑庄,闲玩法,而不深究多人玩法(概率随人数而变化)。在计算胜率时,理论分析存在很大困难,这是因为对于特定牌型的胜率考察,涉及到条件概率,需要讨论的情况繁多。因此,采用计算机进行模拟以得到一个近似值。要点在于如何比较两副手牌的大小:

  1. 首先判断牌型,按照 豹子 > 顺金 > 金花 > 顺子 > 顺子 > 散牌 的规则进行第一次比较,如果牌型相同,步入步骤2
  2. 如果是对子,则先比较对子大小,如果相同,再比较剩下的那张单牌的大小
  3. 否则,分别对两副手牌排序,按顺序比较即可

同样在样本总容量取 1 0 7 10^7 107情况下,得到胜率的模拟结果如下:
在这里插入图片描述
4. 总结
最后提及很有趣的一点。虽然 豹子 > 顺金,但是豹子出现的概率却略大于顺金;同样,金花 > 顺子,但是金花出现的概率要比顺子大。

附python3代码:

# -*- coding: utf-8 -*-
import random
import matplotlib.pyplot as plot
import numpy

color_book = {1: "♠", 2: "♥", 3: "♣", 4: "♦"}
num_book = {"J": 11, 11: "J", "Q": 12, 12: "Q", "K": 13, 13: "K", "A": 14, 14: "A"}
type_book = {100: "豹子", 101: "顺金", 102: "金花", 103: "顺子", 104: "对子",
             14: "散A", 13: "散K", 12: "散Q", 11: "散J", 10: "散10", 9: "散9", 8: "散8", 7: "散7", 6: "散6", 5: "散5"}
lvl_book = [100, 101, 102, 103, 104, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5]
card_book = list()
'''单张牌'''
class Card:
    def __init__(self, num, color = 1):
        self.num = num
        self.color = color
'''手牌'''
class Hand:
    def __init__(self, cardList):
        self.hand = sorted(cardList, key=lambda x: x.num, reverse=True)
    def parseType(self) -> int:
        #100. 豹子
        if self.hand[0].num == self.hand[1].num and self.hand[0].num == self.hand[2].num:
            return 100

        # 金花 & 顺金
        elif self.hand[0].color == self.hand[1].color and self.hand[0].color == self.hand[2].color:
            # 101. 顺金
            if  (self.hand[2].num + 1 == self.hand[1].num and self.hand[1].num + 1 == self.hand[0].num \
                     or self.hand[2].num == 2 and self.hand[1].num == 3 and self.hand[0].num == 14):
            # 因为 A 是以数字 14 存储的,因此 A23 顺要额外考虑
                return 101
            # 102. 金花
            else:
                return 102
        # 103 顺子
        # 因为上个 if 已经除去了顺金部分,因此只要是公差为1的等差数列则一定是顺子
        elif self.hand[2].num + 1 == self.hand[1].num and self.hand[1].num + 1 == self.hand[0].num \
                     or self.hand[2].num == 2 and self.hand[1].num == 3 and self.hand[0].num == 14:
            return 103
        # 104. 对子, 对子要额外记录一下对子和单牌,以便比较大小
        elif self.hand[2].num == self.hand[1].num or self.hand[1].num == self.hand[0].num:
            if self.hand[2].num == self.hand[1].num:
                self.pair, self.single = self.hand[2].num, self.hand[0].num
            else:
                self.pair, self.single = self.hand[0].num, self.hand[2].num
            return 104
        # 散牌
        else:
            return self.hand[0].num
'''初始化牌堆'''
def initCard():
    # 1 表示 A
    # 13 表示 K
    for i in range(2, 15):
        for j in range(1, 5):
            card_book.append(Card(i, j))
'''打印手牌'''
def printHand(handCard: Hand):
    for h in handCard.hand:
        print(h.num, end='') if 1 < h.num <= 10 else print(num_book[h.num], end='')
        print(color_book[h.color], end=' ')
    print(type_book[handCard.parseType()])
    
'''比较辅助函数'''
def cmpCard(h1: Hand, h2: Hand):
    for i in range(len(h1.hand)):
        if h1.hand[i].num != h2.hand[i].num:
            if h1.hand[i].num > h2.hand[i].num:
                return 0
            else:
                return 2
    return 1

'''比较手牌函数'''  
def cmpHand(h1: Hand, h2: Hand):
    lvl1 = h1.parseType()
    lvl2 = h2.parseType()
    #printHand(h1)
    #printHand(h2)
    # 先比较牌种
    if lvl_book.index(lvl1) < lvl_book.index(lvl2):
        return 0
    elif lvl_book.index(lvl1) > lvl_book.index(lvl2):
        return 2
    # 对子,要先比对子
    elif lvl1 == 104:
        if h1.pair == h2.pair:
            return cmpCard(Hand([Card(h1.single)]), Hand([Card(h2.single)]))
        else:
            return cmpCard(Hand([Card(h1.pair)]), Hand([Card(h2.pair)]))

    # 其余情况,挨个比就行
    else:
        return cmpCard(h1, h2)
    
'''作图函数'''
def plotRects(x_list, y_list):
    rects = plot.bar(range(len(y_list)), y_list, color=[numpy.random.random(3) for i in range(len(y_list))])

    plot.ylabel("概率(%)")
    plot.xticks([i for i in range(len(x_list))], x_list)
    for rect in rects:
        height = rect.get_height()
        plot.text(rect.get_x() + rect.get_width() / 2, height, "{:.3f}%".format(height), ha='center', va='bottom')
    plot.rcParams['font.sans-serif'] = ['Arial Unicode MS']
    plot.rcParams['axes.unicode_minus'] = False
    plot.show()

'''获得全部牌型的概率'''
def getPr(cap: int):
    cnt = dict()
    tot = int(cap)
    for i in range(tot):
        hand1 = Hand(random.sample(card_book, 3))
        type = hand1.parseType()
        cnt[hand1.parseType()] = 1 if type not in cnt else cnt[hand1.parseType()] + 1

    cnt = dict(sorted(cnt.items(), key=lambda x: x[1]))
    '''分组画图'''
    sub1 = dict([(key, cnt[key]) for key in range(100, 105)])
    sub2 = dict([(key, cnt[key]) for key in range(5, 15)])
    name_list1 = [type_book[k] for k in sub1.keys()]
    val_list1 = [v / tot * 100 for v in sub1.values()]
    name_list2 = [type_book[k] for k in sub2.keys()]
    val_list2 = [v / tot * 100 for v in sub2.values()]
    plotRects(name_list1, val_list1)
    plotRects(name_list2, val_list2)

'''获得全部牌型的胜率''' 
def getWinning(cap: int):
    result = {}
    tot = int(cap)
    for i in range(tot):
        hand = random.sample(card_book, 6)
        hand1 = Hand(hand[:3])
        hand2 = Hand(hand[-3:])
        type = hand1.parseType()                            
        if type in result.keys():
            # print(type, cmpHand(hand1, hand2))
            result[type][cmpHand(hand1, hand2)] += 1
        else:
            result[type] = [0] * 3
            result[type][cmpHand(hand1, hand2)] = 1

    '''分组画图'''
    sub1 = dict([(key, result[key]) for key in range(100, 105)])
    sub2 = dict([(key, result[key]) for key in range(5, 15)])

    name_list1 = [type_book[k] for k in sub1.keys()]
    val_list1 = [v[0] / sum(v) * 100 for v in sub1.values()]
    name_list2 = [type_book[k] for k in sub2.keys()]
    val_list2 = [v[0] / sum(v) * 100 for v in sub2.values()]
    plotRects(name_list1, val_list1)
    plotRects(name_list2, val_list2)
if __name__ == '__main__':
    initCard()
    #getPr(1e7)
    getWinning(1e7)
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值