【雀魂】升上魂天真的需要靠运气吗?(基于python模拟)
bip__
2021年07月21日 14:39

    作为最难的魂系游戏,雀魂被不少人看做是一款“你立直必反向一发”,“国士听牌被断幺九”的恐怖游戏,但是,不同层次的玩家,在大量对局数后的实力还是有明显差别的。总体上来看雀魂的最终战绩还是运气与实力的综合体现。以下将通过python模拟,对一些情况进行分析,试图证明雀魂就是纯运辣游。

  1. 给定顺位分布,求升上魂天所需要的对局数

  2. 给定顺位分布,进行一定数量对局数所得到的实际顺位分布

  3. 两个不同段位的玩家,进行一定数量对局的胜率


1.给定顺位分布,求升上魂天所需要的对局数

不同段位的数据(from 雀魂牌谱屋)

    

    上图为不同段位的各项数据,我们先以目前魂天的平均顺位为基准来分析,并作如下处理:

1)近似将顺位分布取为0.29,0.27,0.26,0.18

2)不考虑雀豪1之前的对局(一般为100-200半庄),从豪一原点开始全程在玉之间对局,并且1,2,3,4位的最终点数分别为4,3,2,1w

那么,根据雀魂段位Pt机制,可以算出1,2,3,4位的pt分别为[145,65,-5,-30-段位额外扣分]

3)考虑掉段机制,但是默认只会掉回当前的下一个顺位(例如雀圣2掉回雀圣1,不考虑其再掉回豪3)

    首先验证假设3)的合理性:

    取试验次数n=10000,重复3次实验得到结果:

    可以看到,n=10000时得到的结果已经相对稳定(之后的顺位模拟结果更加稳定),综合考虑程序的运行速度,之后无特殊说明,均取n=10000作为试验次数且不做重复检验。

    即使是豪1升豪2,也有大约2%的可能性会掉段,而且总体上来说,掉段次数的期望大约为1500/10000=0.15,从豪1升到魂天,大约有85%的概率会至少掉一次段,因此在模拟时引入掉段机制是有必要的,但也不必引入过于复杂的掉段机制,可以看到掉两次段的概率已经非常之低了,因此假设3)是合理的。

    经过模拟得到,从雀豪1到魂天的对局数分布如下:

    列表从左到右依次对应[0-500,501-1000,1001-1500,1501-2000,2001-2500,>2500]区间,

根据模拟结果得到几个参数:

平均对局数=1116.0(玉场)

对局数落在[501-1500]这个区间的对局数占82%,对其进行细分

列表分别为落在[500-700,700-900,900-1100,1100-1300,1300-1500,1500-1700]的次数,各区间分布数量虽然有一些不同,但总体来看,无论落在哪个区间都是可以接受的。

    由此看到,如果以相同实力开始对局,打上魂天的局数也会有几百场的偏差,因此不能仅仅根据对局数量来判定玩家的实力

    接着我们改一下顺位分布:

    取雀豪2的平均水平[25.6,25.3,25.0,24.1]:同样取n=10000,然后程序竟然卡了,想了好久发现大概是因为对局数过多,导致while无限循环中…,于是将每一段的对局数上限取为1000,一旦对局数超过这个局数,就直接给予升段,以避免永远打不出魂天的情况(什么时候雀魂也能引入类似的保护机制),这里取n=1000,得到结果为

    列表从左到右依次对应[0-500,501-1000,1001-1500,1501-2000,2001-2500,>2500]区间

    也就是说,对于雀豪2的平均水准,且不说在特定的对局数内升魂天,就单单是升魂天本身都是十分困难的

    再来看一下雀圣1的相关情况:

    取顺位为[0.27,0.26,0.255,0.215]进行分析

    考虑到豪2模拟时翻车的情况,这次我们把n设定为1000,重复3次实验

    可以看到,以雀圣1的水平进行对战,只有大约14%的人能在2500局之内升天,而如果以魂天的平均实力,在2500局之内升天的占比为99%。

    综合起来看,对于实力相同的玩家,麻将确实就是纯运辣游,几百场的偏差都是再正常不过的,但是对于不同实力的玩家,实力相差越大,最终所得到成绩的差异就越明显,拿雀豪2和魂天来说,如果都以最终升天为目标,那么只要魂天没有得罪发牌姬,几乎是必胜的。

    理论分析的话,给定特定的顺位分布,多次实验后其总体成绩呈正态分布,当两条分布曲线重合程度越高,二者实力差距就越小,那么对局时的运气成分占比就越大。反之,如果两条曲线重合程度只有1%,那么这时胜负因素的主体就是实力而不是运气了。所以,实力越高,能升上魂天的概率越高以及升上魂天的对局数期望越小。如果说能够达到目前魂天的平均水平,那么2500场对局内几乎必定通关


2.给定顺位分布,进行一定数量对局数所得到的实际顺位分布

其本质就是个独立重复试验,假定样本的顺位分布为[0.25,0.25,0.25,0.25]

对每一特定的对局数,重复10000次实验

2.1:对局数n=100

列表从左到右分别为平均顺位在[<2.25,2.25-2.35,2.35-2.45,2.45-2.55,2.55-2.65,2.65-2.75,>2.75]区间内的频数和频率

2.2:对局数n=1000

    列表含义同上,可以看到,此时全部的样本都落在[2.35-2.65]这个区间上

    此时,看似实际成绩与真实成绩十分接近了,但是

    考虑牌谱屋的数据(见上),相邻段位的平均顺位差距只有0.02-0.03,基于此,特地再拿出2.4-2.61这个区间段,以0.03为间距进行统计,得到如下结果:

    第一项是平顺2.4-2.43的频数,第二项2.43-2.46,以此类推

可以看到:

    即使是n=1000,反映出的水平与真实水平差一两个(甚至更多)段位也是非常正常的事情,如果n=100,那么其成绩分布方差是非常大的,也就是说,100场对局数所得战绩,与你的运气是有很大关联的,其实际成绩与真实水平差别很大,因此拿100多场的战绩来说明实力是没有什么说服力的。

(关于对局数与真实水平关系的讨论在别的专栏也有看到过,并且还有更深入的分析,这里只是粗略地模拟一下)


3.两个不同段位的玩家,进行一定数量对局的胜率

处理方法是:先按照每个玩家的顺位分别得到二者一局的顺位并进行比较,如果相同,则此局作废重开,重复n次实验

3.1:对局数=10

魂天vs普通玩家(顺位为0.25,0.25,0.25,0.25)

列表从左到右依次为魂天获胜[<=3,4,5,6, >=7]场的频数

在实力相差很大的情况下,只进行10个半庄的对局,弱者胜,平,负的频率分别为0.25,0.23,0.52,可以看到,魂天与一个一般玉场玩家对局10个半庄,也有1/4的概率是负的

魂天vs雀圣1(顺位为0.27,0.26,0.255,0.215)

列表含义同上

弱者胜,平,负的概率分别为0.32,0.24,0.44,二者胜率差进一步缩小

3.2:对局数=100: 魂天vs普通玩家

列表为魂天获胜[<40,40-49,50,51-60,>60]场的频数

弱者胜,平,负频率为:0.11,0.04,0.85

此时展现出了还不错的区分度,但弱者仍有0.1的概率战胜强者

魂天vs雀圣1

列表含义同上

弱者胜,平,负概率为:0.26,0.07,0.67

    结论:两个玩家水平越接近,要区分其水平所需要的对局数也就越多。就雀魂本身的运气性而言,区分顶尖玩家和处于中上水平的普通玉之间玩家,哪怕是用10个半庄也不能完全区分出水平。而比赛场玩家每次对局也就1-2个半庄,几乎没有比赛场会以每人10半庄为单位去进行。那么在这种情况下,只要实力差距不是太悬殊,那么运气成分还是相当可观的。对于四象战这种顶尖选手的对局,几乎看的就是选手的运气。


   

    通过以上几个模拟,可以看到,在短期对局中,雀魂的运气成分确实占最终成绩的相当一部分比重。而对于1000场以上的对局,实力还是占主导地位。只要实力足够,那么上魂天也只是时间问题(当然随个体属性(狗力)不同对局数还是会有较大变化)。同时也可以看到,在一段时间的对局中,掉段,战绩特别好或者特别差,这些都是非常普通的现象,只要个人的打法固定,心态没崩,对这些其实不需要特别在意,更不要因为短时间的波动来结果论地改变自己的打法,这样反而会更不利。(当然这并不是说不能改变,只是强调不能根据一两局的结果来草率地否定之前的打法。)麻将的运气成分终究还是很大的,所以在对局的过程中,我们更应该去关注做牌的思路,过程等问题,而不是简单地根据最后和了还是铳了来进行评价,把该做的都做好,成绩自然会得到改观。最后祝大家雀力日进,早日升到魂天!



附源代码:

1.

代码块
Python
自动换行
复制代码
import random
import numpy as np
shunwei_fenbu=eval(input('顺位分布(保留整数位) 如[29,27,26,18]='))
n=eval(input('样本数='))
pt_fenbu=[145]*shunwei_fenbu[0]+[65]*shunwei_fenbu[1]+[-5]*shunwei_fenbu[2]
pt_fenbu1=pt_fenbu+[-195]*shunwei_fenbu[3] #雀豪1,以此类推
pt_fenbu2=pt_fenbu+[-210]*shunwei_fenbu[3]
pt_fenbu3=pt_fenbu+[-225]*shunwei_fenbu[3]
pt_fenbu4=pt_fenbu+[-240]*shunwei_fenbu[3]
pt_fenbu5=pt_fenbu+[-255]*shunwei_fenbu[3]
pt_fenbu6=pt_fenbu+[-270]*shunwei_fenbu[3]
zhanji=[]
for i in range(n):
    pt=1400
    n1=0 #n1:雀豪1升到2的局数,以此类推
    while pt<2800:#雀豪1
        pt+=random.choice(pt_fenbu1)
        n1+=1#不考虑雀豪1的掉段
        if n1>5000:#设置的额外机制,防止循环过多
            break
    pt=1600
    n2=0
    while pt<3200:
        pt+=random.choice(pt_fenbu2)
        n2+=1
        if n2>5000:
            break
        elif pt<0:
            pt=1400
            while pt<2800:#掉段至雀豪1
                pt+=random.choice(pt_fenbu1)
                n1+=1
                if n1>5000:
                    break
            pt=1600
    pt=1800
    n3=0
    while pt<3600:
        pt+=random.choice(pt_fenbu3)
        n3+=1
        if n3>5000:
            break
        elif pt<0:#掉段机制,只设置1次掉段
            pt=1600
            while pt<3200:
                pt+=random.choice(pt_fenbu2)
                n2+=1
                if n2>5000:
                    break
            pt=1800
        
    pt=2000
    n4=0
    while pt<4000:
        pt+=random.choice(pt_fenbu4)
        n4+=1
        if n4>5000:
            break
        elif pt<0:
            pt=1800
            while pt<3600:
                pt+=random.choice(pt_fenbu3)
                n3+=1
                if n3>5000:
                    break
            pt=2000
    pt=3000
    n5=0
    while pt<6000:
        pt+=random.choice(pt_fenbu5)
        n5+=1
        if n5>5000:
            break
        elif pt<0:
            pt=2000
            while pt<4000:
                pt+=random.choice(pt_fenbu4)
                n4+=1
                if n4>5000:
                    break
            pt=3000
    pt=4500
    n6=0
    while pt<9000:
        pt+=random.choice(pt_fenbu6)
        n6+=1
        if n6>5000:
            break
        elif pt<0:
            pt=3000
            while pt<6000:
                pt+=random.choice(pt_fenbu5)
                n5+=1
                if n5>5000:
                    break
            pt=4500
    zhanji.append(n1+n2+n3+n4+n5+n6)
duijushu_fenbu=[len([i for i in zhanji if i<=500]),\
       len([i for i in zhanji if 500<i<=1000]),\
       len([i for i in zhanji if 1000<i<=1500]),\
       len([i for i in zhanji if 1500<i<=2000]),\
       len([i for i in zhanji if 2000<i<=2500]),\
       len([i for i in zhanji if i>2500])]
zhanbi=[round(duijushu_fenbu[i]/sum(duijushu_fenbu),3)\
        for i in range(len(duijushu_fenbu))]
print(duijushu_fenbu)
print(zhanbi)
print(np.mean(zhanji))
复制成功

注:这里用到了第三方模块numpy,需要先安装对应模块才能使用

另外也尝试使用函数递归进行分析,虽然结果确实是精确了那么一些(没有掉段的近似处理),但是程序运行实在是太慢。

本人也只是初学小白,目前想不出更优解,如果有更好的算法欢迎大佬评论指出!

(使用递归函数的代码如下:)

代码块
Python
自动换行
复制代码
import random
import numpy as np
import sys
#这个是没有引入保护机制的模拟,即严格按照雀魂规则升降段(除了不考虑雀豪1掉杰3以外)
sys.setrecursionlimit(10000000) #设置最大递归深度
shunwei=eval(input('顺位='))#[0.29,0.27,0.26,0.18],务必保证求和为1
zhanji=[]
n=eval(input('实验次数='))#只能到100多,再往上的话程序就卡了
for i in range(n):
    pt=1400  #初始pt
    n1=0
    n2=0
    n3=0
    n4=0
    n5=0
    n6=0
    def pt_move(x):
        return int(np.random.choice([145,65,-5,x],size=1,replace=False\
                                    ,p=shunwei))
    def quehao1():
        global pt
        global n1
        pt+=pt_move(-195)
        n1+=1
        #if n1>1000:...(根据需要输入特定机制,这里没有采用)
        if pt>=2800:
            pt=1600
            return quehao2()
        else:
            return quehao1()
    def quehao2():
        global pt
        global n2
        pt+=pt_move(-210)
        n2+=1
        if pt>=3200:
            pt=1800
            return quehao3()
        elif pt<=0:
            pt=1400
            return quehao1()
        else:
            return quehao2()
    def quehao3():
        global pt
        global n3
        pt+=pt_move(-225)
        n3+=1
        if pt>=3600:
            pt=2000
            return quesheng1()
        elif pt<=0:
            pt=1600
            return quehao2()
        else:
            return quehao3()
    def quesheng1():
        global pt
        global n4
        pt+=pt_move(-240)
        n4+=1
        if pt>=4000:
            pt=3000
            return quesheng2()
        elif pt<=0:
            pt=1800
            return quehao3()
        else:
            return quesheng1()
    def quesheng2():
        global pt
        global n5
        pt+=pt_move(-255)
        n5+=1
        if pt>=6000:
            pt=4500
            return quesheng3()
        if pt<=0:
            pt=2000
            return quesheng1()
        else:
            return quesheng2()
    def quesheng3():
        global pt
        global n6
        global zhanji
        pt+=pt_move(-270)
        n6+=1
        if pt>=9000:
            zhanji.append(n1+n2+n3+n4+n5+n6)
        elif pt<=0:
            pt=3000
            return quesheng2()
        else:
            return quesheng3()
    quehao1()
duijushu_fenbu=[len([i for i in zhanji if i<=500]),\
       len([i for i in zhanji if 500<i<=1000]),\
       len([i for i in zhanji if 1000<i<=1500]),\
       len([i for i in zhanji if 1500<i<=2000]),\
       len([i for i in zhanji if 2000<i<=2500]),\
       len([i for i in zhanji if i>2500])]#对局数的分布
zhanbi=[round(duijushu_fenbu[i]/sum(duijushu_fenbu),2) for i in range\
        (len(duijushu_fenbu))]#所占比例
print(duijushu_fenbu)
print(zhanbi)
复制成功

2.

代码块
Python
自动换行
复制代码
import random
#采用顺位分布=[1,1,1,1]进行分析,可更改
n=eval(input('对局数='))#1次试验的对局数量
n0=eval(input('试验数='))#n次重复实验
result=[]
for i0 in range(n0):
    pingshun=0
    zhanji=[]#记录对战的顺位
    for i in range(n):
        zhanji.append(random.choice([1,2,3,4]))
    zhanji_total=[zhanji.count(1),zhanji.count(2),zhanji.count(3),zhanji.count(4)]#统计各个顺位
    pingshun=(sum([(i+1)*zhanji_total[i] for i in range(len(zhanji_total))])/sum(zhanji_total))#平均顺位
    result.append(pingshun)
final=[len([i for i in result if i<2.25]),\
       len([i for i in result if 2.25<=i<2.35]),\
       len([i for i in result if 2.35<=i<2.45]),\
       len([i for i in result if 2.45<=i<2.55]),\
       len([i for i in result if 2.55<=i<2.65]),\
       len([i for i in result if 2.65<=i<2.75]),\
       len([i for i in result if 2.75<=i])]
final1=[len([i for i in result if 2.4<i<2.43]),\
       len([i for i in result if 2.43<=i<2.46]),\
       len([i for i in result if 2.46<=i<2.49]),\
       len([i for i in result if 2.49<=i<2.52]),\
       len([i for i in result if 2.52<=i<2.55]),\
       len([i for i in result if 2.55<=i<2.58]),\
       len([i for i in result if 2.58<=i<2.61])]
print(final)
print(final1)#平顺落在各个区间的对局数量
复制成功

3.

代码块
Python
自动换行
复制代码
import random
shunwei1=eval(input('玩家1的顺位分布='))#eg.[12,31,12,3],务必输入整数
shunwei2=eval(input('玩家2的顺位分布='))
score1=[1]*shunwei1[0]+[2]*shunwei1[1]+[3]*shunwei1[2]+[4]*shunwei1[3]
score2=[1]*shunwei2[0]+[2]*shunwei2[1]+[3]*shunwei2[2]+[4]*shunwei2[3]
n=eval(input('对战数='))
zhanji=[]
while len(zhanji)!=n:
    a1=random.choice(score1)
    a2=random.choice(score2)
    if a1<a2:
        zhanji.append(1)#玩家1获胜
    if a1>a2:
        zhanji.append(2)
final=[zhanji.count(1),zhanji.count(2)]#[1获胜局数,2获胜局数]
print('玩家1获胜局数=%d,玩家2获胜局数=%d'%(final[0],final[1]))
复制成功