【Python】05 当我旁观诈鸡时,我在想些什么

0 前言

每逢春节,诈金花1作为一种休闲益智类扑克游戏,深受亲朋好友的热爱,以小博大,紧张刺激。下面尝试模拟诈金花游戏,比较各牌型胜率,给充满不确定性的游戏做个参考。

1 游戏规则

诈金花又称诈鸡、三张牌,偏重于运气和心理对抗2,游戏使用一副除去大小王的扑克牌,即 A , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , J , Q , K A, 2, 3, 4, 5 ,6, 7, 8, 9, 10, J, Q, K A,2,3,4,5,6,7,8,9,10,J,Q,K共计 13 种牌型,每种牌型 4 种花色,总计 52 张牌。比牌规则3

  1. 单牌: A > K > Q > . . . > 2 A>K>Q>...>2 A>K>Q>...>2
  2. 牌型:豹子(三个头) > > >顺金(同花顺) > > >金花 > > >顺子(拖拉机) > > >对子 > > >散牌(单张)
  3. 不比花色
  4. 对于牌型相同情况,将判平局。

2 概率计算

从一副牌中任意抽取三张牌,牌型共有 C 52 3 = 22100 C_{52}^3=22100 C523​=22100种可能,下面利用古典概型计算各牌型概率4

  • 豹子(三个头): P 0 = 13 ∗ C 4 3 C 52 3 = 52 22100 = 0.2353 % P_0=\frac{13*C_4^3}{C_{52}^3}=\frac{52}{22100}=0.2353\% P0​=C523​13∗C43​​=2210052​=0.2353%
  • 顺金(同花顺): P 1 = 12 ∗ C 4 1 C 52 3 = 48 22100 = 0.2172 % P_1=\frac{12*C_4^1}{C_{52}^3}=\frac{48}{22100}=0.2172\% P1​=C523​12∗C41​​=2210048​=0.2172%
  • 金花: P 2 = C 13 3 C 4 1 − 12 ∗ C 4 1 C 52 3 = 1096 22100 = 4.9593 % P_2=\frac{C_{13}^3C_4^1-12*C_4^1}{C_{52}^3}=\frac{1096}{22100}=4.9593\% P2​=C523​C133​C41​−12∗C41​​=221001096​=4.9593%
  • 顺子(拖拉机): P 3 = 12 ∗ ( C 4 1 ∗ C 4 1 ∗ C 4 1 − C 4 1 ) C 52 3 = 720 22100 = 3.2579 % P_3=\frac{12*(C_4^1*C_4^1*C_4^1-C_4^1)}{C_{52}^3}=\frac{720}{22100}=3.2579\% P3​=C523​12∗(C41​∗C41​∗C41​−C41​)​=22100720​=3.2579%
  • 对子: P 4 = C 13 1 ∗ C 4 2 ∗ C 12 1 ∗ C 4 1 C 52 3 = 3744 22100 = 16.9412 % P_4=\frac{C_{13}^1*C_4^2*C_{12}^1*C_4^1}{C_{52}^3}=\frac{3744}{22100}=16.9412\% P4​=C523​C131​∗C42​∗C121​∗C41​​=221003744​=16.9412%
  • 单张: P 单 = C 52 3 − [ 13 ∗ C 4 3 + 12 ∗ C 4 1 + ( C 13 3 C 4 1 − 12 ∗ C 4 1 ) + 12 ∗ ( C 4 1 ∗ C 4 1 ∗ C 4 1 − C 4 1 ) + C 13 1 ∗ C 4 2 ∗ C 12 1 ∗ C 4 1 ] C 52 3 = 16440 22100 = 74.3891 % P_单=\frac{C_{52}^3-[13*C_4^3+12*C_4^1+(C_{13}^3C_4^1-12*C_4^1)+12*(C_4^1*C_4^1*C_4^1-C_4^1)+C_{13}^1*C_4^2*C_{12}^1*C_4^1]}{C_{52}^3}=\frac{16440}{22100}=74.3891\% P单​=C523​C523​−[13∗C43​+12∗C41​+(C133​C41​−12∗C41​)+12∗(C41​∗C41​∗C41​−C41​)+C131​∗C42​∗C121​∗C41​]​=2210016440​=74.3891%
  • 单A: P A = C 4 1 ∗ C 12 2 ∗ C 4 1 ∗ C 4 1 − 2 ∗ C 4 1 ∗ C 4 1 ∗ C 4 1 − ( C 4 1 ∗ C 12 2 − 2 ∗ C 4 1 ) C 52 3 = 3840 22100 = 17.3756 % P_A=\frac{C_4^1*C_{12}^2*C_4^1*C_4^1-2*C_4^1*C_4^1*C_4^1-(C_4^1*C_{12}^2-2*C_4^1)}{C_{52}^3}=\frac{3840}{22100}=17.3756\% PA​=C523​C41​∗C122​∗C41​∗C41​−2∗C41​∗C41​∗C41​−(C41​∗C122​−2∗C41​)​=221003840​=17.3756%
  • 单K: P K = C 4 1 ∗ C 11 2 ∗ C 4 1 ∗ C 4 1 − C 4 1 ∗ C 4 1 ∗ C 4 1 − ( C 4 1 ∗ C 11 2 − C 4 1 ) C 52 3 = 3240 22100 = 14.6606 % P_K=\frac{C_4^1*C_{11}^2*C_4^1*C_4^1-C_4^1*C_4^1*C_4^1-(C_4^1*C_{11}^2-C_4^1)}{C_{52}^3}=\frac{3240}{22100}=14.6606\% PK​=C523​C41​∗C112​∗C41​∗C41​−C41​∗C41​∗C41​−(C41​∗C112​−C41​)​=221003240​=14.6606%
  • 单Q: P Q = C 4 1 ∗ C 10 2 ∗ C 4 1 ∗ C 4 1 − C 4 1 ∗ C 4 1 ∗ C 4 1 − ( C 4 1 ∗ C 10 2 − C 4 1 ) C 52 3 = 2640 22100 = 11.9457 % P_Q=\frac{C_4^1*C_{10}^2*C_4^1*C_4^1-C_4^1*C_4^1*C_4^1-(C_4^1*C_{10}^2-C_4^1)}{C_{52}^3}=\frac{2640}{22100}=11.9457\% PQ​=C523​C41​∗C102​∗C41​∗C41​−C41​∗C41​∗C41​−(C41​∗C102​−C41​)​=221002640​=11.9457%
  • 单J: P J = C 4 1 ∗ C 9 2 ∗ C 4 1 ∗ C 4 1 − C 4 1 ∗ C 4 1 ∗ C 4 1 − ( C 4 1 ∗ C 9 2 − C 4 1 ) C 52 3 = 2100 22100 = 9.5023 % P_J=\frac{C_4^1*C_{9}^2*C_4^1*C_4^1-C_4^1*C_4^1*C_4^1-(C_4^1*C_{9}^2-C_4^1)}{C_{52}^3}=\frac{2100}{22100}=9.5023\% PJ​=C523​C41​∗C92​∗C41​∗C41​−C41​∗C41​∗C41​−(C41​∗C92​−C41​)​=221002100​=9.5023%
  • 单10: P 10 = C 4 1 ∗ C 8 2 ∗ C 4 1 ∗ C 4 1 − C 4 1 ∗ C 4 1 ∗ C 4 1 − ( C 4 1 ∗ C 8 2 − C 4 1 ) C 52 3 = 1620 22100 = 7.3303 % P_{10}=\frac{C_4^1*C_{8}^2*C_4^1*C_4^1-C_4^1*C_4^1*C_4^1-(C_4^1*C_{8}^2-C_4^1)}{C_{52}^3}=\frac{1620}{22100}=7.3303\% P10​=C523​C41​∗C82​∗C41​∗C41​−C41​∗C41​∗C41​−(C41​∗C82​−C41​)​=221001620​=7.3303%
  • 单9: P 9 = C 4 1 ∗ C 7 2 ∗ C 4 1 ∗ C 4 1 − C 4 1 ∗ C 4 1 ∗ C 4 1 − ( C 4 1 ∗ C 7 2 − C 4 1 ) C 52 3 = 1200 22100 = 5.4299 % P_9=\frac{C_4^1*C_{7}^2*C_4^1*C_4^1-C_4^1*C_4^1*C_4^1-(C_4^1*C_{7}^2-C_4^1)}{C_{52}^3}=\frac{1200}{22100}=5.4299\% P9​=C523​C41​∗C72​∗C41​∗C41​−C41​∗C41​∗C41​−(C41​∗C72​−C41​)​=221001200​=5.4299%
  • 单8: P 8 = C 4 1 ∗ C 6 2 ∗ C 4 1 ∗ C 4 1 − C 4 1 ∗ C 4 1 ∗ C 4 1 − ( C 4 1 ∗ C 6 2 − C 4 1 ) C 52 3 = 840 22100 = 3.8009 % P_8=\frac{C_4^1*C_{6}^2*C_4^1*C_4^1-C_4^1*C_4^1*C_4^1-(C_4^1*C_{6}^2-C_4^1)}{C_{52}^3}=\frac{840}{22100}=3.8009\% P8​=C523​C41​∗C62​∗C41​∗C41​−C41​∗C41​∗C41​−(C41​∗C62​−C41​)​=22100840​=3.8009%
  • 单7: P 7 = C 4 1 ∗ C 5 2 ∗ C 4 1 ∗ C 4 1 − C 4 1 ∗ C 4 1 ∗ C 4 1 − ( C 4 1 ∗ C 5 2 − C 4 1 ) C 52 3 = 540 22100 = 2.4434 % P_7=\frac{C_4^1*C_{5}^2*C_4^1*C_4^1-C_4^1*C_4^1*C_4^1-(C_4^1*C_{5}^2-C_4^1)}{C_{52}^3}=\frac{540}{22100}=2.4434\% P7​=C523​C41​∗C52​∗C41​∗C41​−C41​∗C41​∗C41​−(C41​∗C52​−C41​)​=22100540​=2.4434%
  • 单6: P 6 = C 4 1 ∗ C 4 2 ∗ C 4 1 ∗ C 4 1 − C 4 1 ∗ C 4 1 ∗ C 4 1 − ( C 4 1 ∗ C 4 2 − C 4 1 ) C 52 3 = 300 22100 = 1.3575 % P_6=\frac{C_4^1*C_{4}^2*C_4^1*C_4^1-C_4^1*C_4^1*C_4^1-(C_4^1*C_{4}^2-C_4^1)}{C_{52}^3}=\frac{300}{22100}=1.3575\% P6​=C523​C41​∗C42​∗C41​∗C41​−C41​∗C41​∗C41​−(C41​∗C42​−C41​)​=22100300​=1.3575%
  • 单5: P 5 = C 4 1 ∗ C 3 2 ∗ C 4 1 ∗ C 4 1 − C 4 1 ∗ C 4 1 ∗ C 4 1 − ( C 4 1 ∗ C 3 2 − C 4 1 ) C 52 3 = 120 22100 = 0.5430 % P_5=\frac{C_4^1*C_{3}^2*C_4^1*C_4^1-C_4^1*C_4^1*C_4^1-(C_4^1*C_{3}^2-C_4^1)}{C_{52}^3}=\frac{120}{22100}=0.5430\% P5​=C523​C41​∗C32​∗C41​∗C41​−C41​∗C41​∗C41​−(C41​∗C32​−C41​)​=22100120​=0.5430%
    不存在单4及其以下的单张牌型。

3 程序模拟

采用随机抽样的方式,当样本容量足够大时,计算各牌型频率,可近似为概率。
程序设计的关键在于识别各种牌型,基本算法如下:

  1. 定义两个列表,分别存放牌号和花色;
  2. 从两个列表随机抽取一个数字,组合成一张牌
  3. 按此方法抽取不完全相同的三张牌
  4. 判断牌型
  5. 记录牌型到频数字典中
  6. 重复步骤2-5,直到达到设定的样本容量
  7. 统计频数,计算各牌型频率
  8. 绘制频率分布直方图
    模拟1000万次,各牌型出现频率如下图
    【Python】05 当我旁观诈鸡时,我在想些什么

4 牌型胜率

每种牌型胜率的理论计算涉及到条件概率,较为复杂,采用程序模拟。
基本算法如下:

  1. 设定玩家人数
  2. 随机发牌
  3. 记录场上出现的牌型到出场频数字典中,重复的牌型都要计入
  4. 按照规则比较场上牌型大小,记录下最大的牌型到胜利频数字典中,相同牌型只记录一次
  5. 胜利频数字典对应牌型频数除以出场频数字典对应牌型频数,即为该牌型胜率
  6. 绘制各牌型胜率直方图
    随机发牌1000万次6位玩家各牌型胜率如下:
    【Python】05 当我旁观诈鸡时,我在想些什么
    8位玩家各牌型胜率如下:
    【Python】05 当我旁观诈鸡时,我在想些什么
    10位玩家各牌型胜率如下:
    【Python】05 当我旁观诈鸡时,我在想些什么

5 结论

  • 豹子比顺金出现的概率大,金花比顺子出现的概率大,但是豹子>顺金,金花>顺子
  • 所有牌型中,单A是最容易出现的,其次是对子,再次是单K
  • 单张牌型中,从单A到单5,出现的概率是递减的
  • 随着玩家人数增多,各种牌型胜率都在减小,金花的胜率约为80%,存在较大不确定性,这可能是该游戏叫做诈金花的原因

6 代码

#创建时间:2021/2/20
#修改时间:2021/2/23
#程序目标:计算牌型出现概率及胜率
import random
import matplotlib.pyplot as plt
import numpy
import copy

name = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] #牌名,分别表示A,2,3...J,Q,K
suit = [1, 2, 3, 4] #花色,分别表示红桃、梅花、方块、黑桃
poker = []  #一副牌,共52张,由牌名和花色组成
for i in name:
    for j in suit:
        poker.append([i,j])
poker_type = ["单A", "单2", "单3", "单4", "单5", "单6", "单7", "单8", "单9", "单10", '单J', '单Q', '单K']
card_1 = []
card_2 = [] 
card_3 = [] 
cards = [] #三张牌
Frequency = {"单5":0, "单6":0, "单7":0, "单8":0, "单9":0, "单10":0, "单J":0, "单Q":0, "单K":0, "单A":0, "对子":0, "顺子":0, "金花":0, "顺金":0, "豹子":0} #频数字典
FrequencyGroup = copy.deepcopy(Frequency) #出场频数字典
FrequencyVictory = copy.deepcopy(Frequency) #胜利频数字典
cards_type_rate = copy.deepcopy(Frequency)  #胜率字典
rate = {}  #频率字典
times = int(1e7) #抽牌次数
N = 8 #玩家数量

#随机抽取三张牌
def DrawCards(poker):
    cards = random.sample(poker, 3)
    return cards

#多人抽取三张牌
def DrawCardsGroup(N):
    poker_temp = poker[:] #临时扑克牌组  注意:这里不能直接用poker_temp = poker,否则只是引用
    cards_group = [] #多人的三张牌
    for j in range(N):
        cards_group.append(DrawCards(poker_temp))  #存放各人的三张牌列表
        for card in cards_group[-1]: #去掉已抽取的牌
            poker_temp.remove(card)
    return cards_group

#判断牌型
def ClassifyCards(cards):
    cards_name = [cards[0][0], cards[1][0], cards[2][0]]  #牌名
    cards_suit = [cards[0][1], cards[1][1], cards[2][1]]  #花色
    cards_name.sort()
    cards_suit.sort()
    cards_type = ""
    if cards_suit[0] == cards_suit[2]:  #三张牌花色相同,为顺金或金花  
        if (cards_name[2]-cards_name[1]==1 and cards_name[1]-cards_name[0]==1) or (cards_name[0]==1 and cards_name[1]==12 and cards_name[2]==13): #三张牌成等差数列即为顺金,否则为金花
            cards_type = "顺金"
        else:
            cards_type = "金花"
    elif (cards_name[2]-cards_name[1]==1 and cards_name[1]-cards_name[0]==1) or (cards_name[0]==1 and cards_name[1]==12 and cards_name[2]==13): #三张牌成等差数列,但花色不同
        cards_type = "顺子"
    elif (cards_name[0] == cards_name[1]) or (cards_name[1] == cards_name[2]): #牌名存在对子
        if cards_name[0] == cards_name[2]:   #三张牌牌名相同,即为豹子
            cards_type = "豹子"
        else:
            cards_type = "对子"
    else:
        if cards_name[0] == 1:  #单A
            cards_type = "单A"
        else:
            cards_type = poker_type[cards_name[2]-1]
    return cards_type

def CountCards(cards_type):  #计算频数
    Frequency[cards_type] = Frequency.get(cards_type, 0) + 1 #记录牌型频数到频数字典中
    return Frequency

def CardsPower(cards_type): #确定牌型牌力
    cards_power = list(Frequency.keys()).index(cards_type)  #以列表索引代表牌力
    return cards_power

def CardsType(cards_power): #根据牌力确定牌型
    cards_type = list(Frequency.keys())[cards_power]
    return cards_type


def CountCardsGroup(cards_group):
    cards_power_group = []  #多人牌力列表
    for cards in cards_group:  #遍历所有人的手牌
        cards_type = ClassifyCards(cards)  #判断牌型
        FrequencyGroup[cards_type] = FrequencyGroup.get(cards_type, 0) + 1 #记录牌型频数到出场频数字典中 
        cards_power = CardsPower(cards_type) #计算牌力
        cards_power_group.append(cards_power) #添加牌力到列表中
    cards_power_group.sort()  #牌力排序,默认从小到大
    cards_type = CardsType(cards_power_group[-1])  #判断胜利牌型
    FrequencyVictory[cards_type] = FrequencyVictory.get(cards_type, 0) + 1 #记录胜利牌型频数到胜利频数字典中
    return FrequencyGroup, FrequencyVictory

def CardsTypeRate(FrequencyGroup, FrequencyVictory): #计算牌型胜率
    for key in FrequencyGroup:
        if FrequencyGroup[key] != 0:
            cards_type_rate[key] = FrequencyVictory[key] / FrequencyGroup[key] *100 #计算胜率
    return cards_type_rate


#绘制各牌型频率分布直方图
def DrawRate(Frequency):
    for key in Frequency: 
        rate[key] = Frequency[key] / times *100  #计算频率
    card_type = list(rate.keys())  #牌型
    card_rate = list(rate.values())  #频率
    plt.rcParams["font.sans-serif"] = ["KaiTi"]  #中文乱码处理
    plt.rcParams['axes.unicode_minus'] = False
    plt.bar(range(len(card_rate)),card_rate, align = "center",color = [numpy.random.random(3) for i in range(len(card_rate))],alpha = 0.6)
    plt.ylabel("概率(%)")
    plt.ylim([0,20])
    plt.xticks(range(len(card_type)), card_type)
    plt.xlabel("牌型")
    plt.title("牌型频率分布直方图")
    for x,y in enumerate(card_rate):
        plt.text(x,y+0.5,'%s' %round(y,4),ha='center')# y+0.5 标签的坐标
    plt.savefig(r"D:\program\MyPythonSpace\rate.png")
    plt.show()

#绘制牌型胜率直方图
def DrawVictoryRate(rate):
    card_type = list(rate.keys())  #牌型
    card_rate = list(rate.values())  #胜率
    plt.rcParams["font.sans-serif"] = ["KaiTi"]  #中文乱码处理
    plt.rcParams['axes.unicode_minus'] = False
    plt.bar(range(len(card_rate)),card_rate, align = "center",color = [numpy.random.random(3) for i in range(len(card_rate))],alpha = 0.6)
    plt.ylabel("胜率(%)")
    plt.ylim([0,100])
    plt.xticks(range(len(card_type)), card_type)
    plt.xlabel("牌型")
    plt.title("牌型胜率直方图")
    for x,y in enumerate(card_rate):
        plt.text(x,y+3,'%s' %round(y,4),ha='center')# y+300 标签的坐标
    plt.savefig(r"D:\program\MyPythonSpace\victory_rate.png")
    plt.show()

#主函数
for i in range(times):
    cards = DrawCards(poker) #从一副扑克牌中随机抽取三张牌
    cards_type = ClassifyCards(cards) #确定手牌牌型
    Frequency = CountCards(cards_type) #记录频数

    cards_group = DrawCardsGroup(N) #从一副扑克牌中随机抽取N个人的三张牌
    FrequencyGroup, FrequencyVictory = CountCardsGroup(cards_group) #记录出场频数字典和胜利频数字典

#绘制直方图
cards_type_rate = CardsTypeRate(FrequencyGroup, FrequencyVictory)  #计算牌型胜率  
DrawRate(Frequency) #绘制牌型频率分布直方图
DrawVictoryRate(cards_type_rate) #绘制牌型胜率直方图

参考资料


  1. 诈金花百度百科 ↩︎

  2. 炸金花游戏(1)–炸金花游戏的模型设计和牌力评估 ↩︎

  3. 游戏:炸金花(理论分析+py3模拟) ↩︎

  4. 排列组合百度百科 ↩︎

上一篇:python系列教程52


下一篇:52条SQL语句性能优化策略,建议收藏