Python实现《明日方舟》干员寻访模拟器第三期:原始但实用的卡池机制,不过要小心!

上一期文章我们实现了比较完善的单次抽奖函数,成功地在默认卡池中抽取单个结果,并且解决了一个微小但致命的问题,今天我们着手进行《明日方舟》干员寻访模拟器的实现,如果您忘记了上一期的内容,或者是第一次阅读“游戏抽奖模拟器专栏”的文章,您可以快速跳转前几期的文章:

  1. 第一期
  2. 第二期(上一期)

给大家重温一下抽奖规则(如果您已经了解规则,可以跳过这些内容):
1、基准概率
六星干员出率:2%,五星干员出率:8%,四星干员出率:50%,三星干员出率:40%,不会出现一、二星干员。

2、默认卡池
六星干员20名,包括暂时绝版的干员1名;五星干员37名;四星干员29名;三星干员16名。共计102名干员(含绝版一名)。
每次抽奖在101位非绝版干员中随机获取一名,那么各干员在其所属星级内等可能出现。

3、官方每更新一个卡池,该卡池用户前十次抽奖内必有一次是五星或六星干员,具体第几次为随机。

4、官方的标准卡池(而非默认卡池)含有两名特定六星干员,三名特定五星干员,特定干员在所属星级内的出率总和占该星级出率的50%,也就是说,如果你抽中了六星干员,有50%概率抽到两名特定六星中的一个,具体是随机的,并且是等可能的,但也有50%概率抽到别的非特定干员。

5、如果用户在任何一个卡池连续50次都没有抽到六星干员,下一次六星干员的总出率将提高两个百分点,之后每一次都会提升两个百分点,直到抽到六星,将恢复基准概率。这个次数不会因卡池变换而清零。

6、每次抽奖消耗600合成玉,合成玉与另一种货币:至纯源石的换算规律是:1源石=180合成玉

上一期我们实现了单次抽奖函数:

def single():
    global real_no_six,least,do_least
    no_six_times=real_no_six
    if least!=0:
        a=random.randint(1,least)
        if a==1:
            least=0#触发保底机制后清除剩余的次数
            do_least=True
        else:
            least-=1
            do_least=False
    while no_six_times>=50:
        if do_least==True:
            if real_no_six<54:
                least_stars[(no_six_times-50)*2]=6
                least_stars[(no_six_times-50)*2+1]=6
        else:
            stars[(no_six_times-50)*2]=6
            stars[(no_six_times-50)*2+1]=6
        no_six_times=no_six_times-1
    if do_least==True:
        choices=random.choice(least_stars)
    else:
        choices=random.choice(stars)
    if choices==6:
        result=random.choice(six_stars)
        real_no_six=0
        stars=[4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,
               4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,
               3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
               3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,5,5,5,5,6,6]
        do_least=False#无论通过什么方式抽到六星,都要解除保底
        least=0
        print(result)#这里简写了,下同
    elif choices==5:
        result=random.choice(five_stars)
        real_no_six+=1
        do_least=False
        least=0
        print(result)
    elif choices==4:
        result=random.choice(four_stars)
        real_no_six+=1
        print(result)
    else:
        result=random.choice(three_stars)
        real_no_six+=1
        print(result)

今天我们继续实现第四条中的内容:标准卡池

标准卡池中有两名特定六星干员,三名五星干员,具体是什么干员,最好是让用户自定义的,因为谁也不知道官方每两个星期会更新什么卡池,我们应当随着官方的变化而变化,所以在这里卡池机制的自由度非常重要

规则要求是让两名六星干员出率总和占全六星的50%,相应地,其他六星干员的出率就会下降,这和上一期的“50次提概率”的想法是一样的。

但是,这个机制和“50次提概率”的实现细节上有很大区别:“50次提概率”时,为了让六星的出现概率提升,允许将其他事件的概率降至0。而今天的卡池机制不能为了提升特定干员的概率,让其他干员不出现。所有可能的干员都必须出现

第二个区别就是,“50次出一次”机制中六星占全星级概率是随着抽奖情况变化的,而卡池机制中特定干员占该星级干员的比例一旦根据用户设置确定,就不会再改变。

那我们该如何提升特定干员的概率呢?我们先来看个实验:

events=['a','b','c','d','e']
print(events.count('a')/len(events))
#用count输出'a'的出现次数,除以总数量,得到概率
events.append('a')
print(events.count('a')/len(events))

#输出结果:
0.2
0.333333333333333333

这是一个十分基础的东西,是最原始的概率提升的方法,除了特定的a以外,其它事件的出现概率都是均等的,这样的好处是能够等量地降低非特定事件的概率,每个人抽出一点点去提升特定事件的概率,保证了抽奖的公平性。

按照这个思路,如果我们在列表的末端加上需要提升概率的干员的事件,我们就可以实现提升概率。

six_stars=['黑','能天使','莫斯提马','艾雅法拉','伊芙利特','刻俄柏','斯卡蒂','煌','陈','银灰','赫拉格','阿','推进之王','安洁莉娜','麦哲伦','星熊','塞雷娅','夜莺','闪灵','风笛']
five_stars=['守林人','陨星','灰喉','白金','送葬人','普罗旺斯','蓝毒','夜魔','惊蛰','天火','布洛卡','拉普兰德','星极','诗怀雅','幽灵鲨','芙兰卡','狮蝎','食铁兽','崖心','槐琥','红','凛冬','德克萨斯','苇草','初雪','格劳克斯','真理','空','梅尔','雷蛇','临光','可颂','吽','白面鸮','赫默','华法琳','慑砂']
four_stars=['杰西卡','梅','流星','安比尔','白雪','红云','夜烟','远山','角峰','调香师','末药','苏苏洛','蛇屠箱','古米','霜叶','缠丸','猎蜂','慕斯','杜宾','阿消','暗索','砾','地灵','深海色','清道夫','桃金娘','红豆','宴','格雷伊']
three_stars=['炎熔','史都华德','克洛丝','空爆','月见夜','泡普卡','玫兰莎','香草','翎羽','芬','卡缇','米格鲁','斑点','芙蓉','安赛尔','梓兰']
#以上为默认卡池的参考

#以下是处理卡池的方案
cha6_1=input('请输入特定六星干员姓名:')
cha6_2=input('请输入第二六星干员姓名:')
cha5_1=input('请输入特定五星干员姓名:')
cha5_2=input('请输入第二五星干员姓名:')
cha5_3=input('请输入第三五星干员姓名:')
while six_stars.count(cha6_1)+six_stars.count(cha6_2)<0.5*len(six_stars):
    six_stars.extend([cha6_1,cha6_2])
while five_stars.count(cha5_1)+five_stars.count(cha5_2)+five_stars.count(cha5_3)<0.5*len(five_stars)
    five_stars.extend([cha5_1,cha5_2,cha5_3])
#不要用append,否则你得写五行,倘若要是有很多特定干员呢,累死你


#测试结果
请输入特定六星干员姓名:银灰
请输入第二六星干员姓名:艾雅法拉
请输入特定五星干员姓名:可颂
请输入第二五星干员姓名:灰喉
请输入第三五星干员姓名:拉普兰德
>>> (six_stars.count('银灰')+six_stars.count('艾雅法拉'))/len(six_stars)
0.5272413793103449

这样做的好处是非常方便,对于官方标准卡池是足够了,你只需要输入必要的干员的名字!不过你也看到了:这个概率并不是很准,有时候会偏差到0.56,取决于你卡池的大小,卡池项目越多,出现概率就越准确。或者你可以编写一个按概率抽奖的函数,可参考我发布的一篇博客:
学会编一个按概率抽奖的函数!Python3实现为每个随机事件指定一个抽奖概率

缺点:由于Python浮点数计算精度的问题,上述链接编写的函数需要自己另行计算概率,而且可能因为精度问题造成所有概率加起来不等于1,所以无论是哪种方法,任何概率都是不可能完全准确的

代码块中的函数功能限制很大,只能设置两个特定干员和三个五星干员,只适用于官方的标准寻访卡池,而对于官方特别公布限时寻访卡池(可能是更多或者更少特定干员),显然是不适用的。

那我们如何才能做到模拟官方的限时寻访卡池呢?我们需要创建一个自由度更高的函数,用来进行自由度更高的概率提升:

def free_setter():
    num6=input('您要选择几名六星干员?')
    num5=input('您要选择几名五星干员?')
    if num6=='1':
        print('请输入六星干员的姓名')
        name6_1=input()
        while six_stars.count(name6_1)<0.5*len(six_stars):
            six_stars.extend([name6_1])
    elif num6=='2':
        print('请输入两个六星干员的姓名')
        name6_1=input()
        name6_2=input()
        while six_stars.count(name6_1)+six_stars.count(name6_2)<0.5*len(six_stars):
            six_stars.extend([name6_1,name6_2])
#然后是num6=3或者4或者5...
#再后来就是五星的...

方法的缺点非常明显,需要事先知道你要提升多少个干员,自己编写代码也非常麻烦,任何一种情况都要给出不同的提升方法。而且只能支持至多5个干员,支持的数量越多,写的代码也越多。

那有什么办法可以自动检测干员数量,最大限度支持更多干员数目呢?我们可以我们可以自己设计一个特别的函数,用来实行特殊计数方法,并且修改代码块执行条件。

这样的思路是直接让用户输入所有六星干员的姓名,让用户用分号分隔,然后直接对用户输入的字符串按分号进行split分解,分解出来的列表就是需要提升的干员,我们假设它叫b,我们还得计算b中所有元素在a中的出现次数总和,这样就能精准的控制干员的出现概率。能想到这一步比较困难,但是一旦成功,等待你的就是简洁的代码:

ganyuan6=input('请输入需要提升概率的六星干员名单(英文分号分隔):')
ganyuan5=input('请输入需要提升概率的五星干员名单(英文分号分隔):')
special_six_star=ganyuan6.split(';')
special_five_star=ganyuan5.split(';')
#以下九行为作者自行研究而成
def count(b,a):#计算b中所有元素在a中出现次数总和
    counts=0
    for i in b:
        counts+=a.count(i)
    return counts
while count(special_six_star,six_stars)<0.5*len(six_stars):
    six_stars.extend(special_six_star)
while count(special_five_star,five_stars)<0.5*len(five_stars):
    five_stars.extend(special_five_star)

上述方法只用了很少的代码,就实现了自适应干员个数,自适应添加概率和自由度更高的功能,主要是count函数五行代码立了大功,能够有这样的构思,需要较长时间的编程经验和较广的功能知识储备,本人使用这种方法将121行代码缩减为9行。

但是有某些特殊的限时寻访卡池,我们刚才的函数造不出来,比如这个:
在这里插入图片描述
这个是特选干员定向寻访,你在这个卡池只可能抽到特定的六星干员,也就是说,四名六星干员出率占全六星出率的100%,六名五星干员占全五星出率的100%,当然,寻访的根本规则还是不变的,六星2%,五星8%,四星50%,三星40%。

而我们的函数默认是占该星级50%,为了模拟这种卡池,我们让用户自己输入占比,检查合理之后,我们需要让程序判定是否占比为1:

def count(b,a):#计算b中所有元素在a中出现次数总和
    counts=0
    for i in b:
        counts+=a.count(i)
    return counts
def free_setter():
    global six_stars,five_stars
#也可以改成 def free_setter(ganyuan6,ganyuan5,r6=0.5,r5=0.5)
#但是用户不知道你的函数是怎么样的,最终在程序中需要引导用户输入
#所以相比之下,在程序开发初期,使用这里写的函数是对用户非常友好的
    ganyuan6=input('请输入需要提升概率的六星干员名单(英文分号分隔):')
    r6=input('请输入特定六星干员出率总和占全六星的比值:')
    ganyuan5=input('请输入需要提升概率的五星干员名单(英文分号分隔):')
    r5=input('请输入特定五星干员出率总和占全六星的比值:')
    if not (0.3<=r6<=1 and 0.3<=r5<=1):
        print('占比数值设置不合理!')
        return
    special_six_star=ganyuan6.split(';')
    special_five_star=ganyuan5.split(';')
    if r6==1:#如果六星干员占比是1,那么把卡池直接改为用户输入的内容,比较方便
        six_stars=special_six_star
    else:
        while count(special_six_star,six_stars)<0.5*len(six_stars):
            six_stars.extend(special_six_star)
    if r5==1:#五星也是一样
        five_stars=special_five_star
    else:
        while count(special_five_star,five_stars)<0.5*len(five_stars):
            five_stars.extend(special_five_star)
    print('完毕')


#试验结果
>>> free_setter()
请输入需要提升概率的六星干员名单(英文分号分隔):银灰;;艾雅法拉;伊芙利特
请输入特定六星干员出率总和占全六星的比值:1
请输入需要提升概率的五星干员名单(英文分号分隔):白面鸮;德克萨斯;白金;可颂;狮蝎;诗怀雅
请输入特定五星干员出率总和占全六星的比值:1
完毕
>>> six_stars
[银灰,, 艾雅法拉, 伊芙利特]
>>> five_stars
[白面鸮, 德克萨斯, 白金, 可颂, 狮蝎, 诗怀雅]
>>>

这样就能够模拟定向寻访的卡池,更加贴近了游戏的实际。

如果你想了解更多这个游戏卡池的相关信息,你可以查阅以下两个站点:
http://ak.mooncell.wiki/w/卡池一览/常驻标准寻访
http://ak.mooncell.wiki/w/卡池一览/限时寻访
这个网站能够查阅到的东西远比这里推荐的多,如果需要测试函数,进一步模拟游戏的话,你可以拿这些数据练练手。

如果我们要重置这个卡池,恢复到原来的卡池应该怎么办呢?没错,我们应该将初始的卡池先储存在另一个变量中,之后重置的时候只要让卡池变量指向初始卡池所在的变量,就能实现卡池重置:

initial_six=['黑','能天使','莫斯提马','艾雅法拉','伊芙利特','刻俄柏','斯卡蒂','煌','陈','银灰','赫拉格','阿','推进之王','安洁莉娜','麦哲伦','星熊','塞雷娅','夜莺','闪灵','风笛']
initial_five=['守林人','陨星','灰喉','白金','送葬人','普罗旺斯','蓝毒','夜魔','惊蛰','天火','布洛卡','拉普兰德','星极','诗怀雅','幽灵鲨','芙兰卡','狮蝎','食铁兽','崖心','槐琥','红','凛冬','德克萨斯','苇草','初雪','格劳克斯','真理','空','梅尔','雷蛇','临光','可颂','吽','白面鸮','赫默','华法琳','慑砂']
initial_four=['杰西卡','梅','流星','安比尔','白雪','红云','夜烟','远山','角峰','调香师','末药','苏苏洛','蛇屠箱','古米','霜叶','缠丸','猎蜂','慕斯','杜宾','阿消','暗索','砾','地灵','深海色','清道夫','桃金娘','红豆','宴','格雷伊']
initial_three=['炎熔','史都华德','克洛丝','空爆','月见夜','泡普卡','玫兰莎','香草','翎羽','芬','卡缇','米格鲁','斑点','芙蓉','安赛尔','梓兰']
six_stars=initial_six
five_stars=initial_five
four_stars=initial_four
three_stars=initial_three

def reset():
    global no_six_times,least,six_stars,five_stars,four_stars,three_stars
    no_six_times=0#系第二期“50次加概率”
    least=0#系第二期“10次出一次”保底
    six_stars=initial_six
    five_stars=initial_five
    four_stars=initial_four
    three_stars=initial_three
    
def free_setter():
    global six_stars,five_stars
    ganyuan6=input('请输入需要提升概率的六星干员名单(英文分号分隔):')
    r6=input('请输入特定六星干员出率总和占全六星的比值:')
    ganyuan5=input('请输入需要提升概率的五星干员名单(英文分号分隔):')
    r5=input('请输入特定五星干员出率总和占全六星的比值:')
    if not (0.3<=r6<=1 and 0.3<=r5<=1):
        print('占比数值设置不合理!')
        return
    special_six_star=ganyuan6.split(';')
    special_five_star=ganyuan5.split(';')
    if r6==1:#如果六星干员占比是1,那么把卡池直接改为用户输入的内容,比较方便
        six_stars=special_six_star
    else:
        while count(special_six_star,six_stars)<0.5*len(six_stars):
            six_stars.extend(special_six_star)
    if r5==1:#五星也是一样
        five_stars=special_five_star
    else:
        while count(special_five_star,five_stars)<0.5*len(five_stars):
            five_stars.extend(special_five_star)
    print('完毕')
###################################################
def single():#上一期实现的单抽函数
    global real_no_six,least,do_least
    no_six_times=real_no_six
    if least!=0:
        a=random.randint(1,least)
        if a==1:
            least=0#触发保底机制后清除剩余的次数
            do_least=True
        else:
            least-=1
            do_least=False
    while no_six_times>=50:
        if do_least==True:
            if real_no_six<54:
                least_stars[(no_six_times-50)*2]=6
                least_stars[(no_six_times-50)*2+1]=6
        else:
            stars[(no_six_times-50)*2]=6
            stars[(no_six_times-50)*2+1]=6
        no_six_times=no_six_times-1
    if do_least==True:
        choices=random.choice(least_stars)
    else:
        choices=random.choice(stars)
    if choices==6:
        result=random.choice(six_stars)
        real_no_six=0
        stars=[4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,
               4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,
               3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,
               3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,5,5,5,5,6,6]
        do_least=False#无论通过什么方式抽到六星,都要解除保底(如果有)
        least=0
        print(result)#这里简写了,下同
    elif choices==5:
        result=random.choice(five_stars)
        real_no_six+=1
        do_least=False
        least=0
        print(result)
    elif choices==4:
        result=random.choice(four_stars)
        real_no_six+=1
        print(result)
    else:
        result=random.choice(three_stars)
        real_no_six+=1
        print(result)


>>> free_setter()
请输入需要提升概率的六星干员名单(英文分号分隔):银灰;;艾雅法拉;伊芙利特
请输入特定六星干员出率总和占全六星的比值:1
请输入需要提升概率的五星干员名单(英文分号分隔):白面鸮;德克萨斯;白金;可颂;狮蝎;诗怀雅
请输入特定五星干员出率总和占全六星的比值:1
完毕
>>> six_stars
[银灰,, 艾雅法拉, 伊芙利特]
>>> five_stars
[白面鸮, 德克萨斯, 白金, 可颂, 狮蝎, 诗怀雅]
>>> reset()
>>> six_stars
[银灰,, 艾雅法拉, 伊芙利特]
>>> five_stars
[白面鸮, 德克萨斯, 白金, 可颂, 狮蝎, 诗怀雅]
>>>

看起来这个reset函数没什么问题,然而,我们发现reset函数的效果却不理想,还是原来的卡池,并没有重置,其他卡池也无一例外。

这是因为什么呢?因为一开始six_stars指向initial_six,并不是将initial_six的内容复制下来,我们通过free_setter修改了six_stars,就等同于修改了initial_six

下面这个试验可以让你更懂这个原理:

>>> a=[]
>>> b=a
>>> b.append('1')
>>> a
['1']
>>>

这个问题同样发生在我们的程序里,好在这种问题有很多种解决办法:

  1. 将initial_six改为元组,让six_stars=list(initial_six),这样initial_six就不可变了,修改six_stars不会影响到initial_six
  2. 把initial_six的内容抄一遍,在代码中赋值给six_stars(不建议,这样你的代码太长了)
  3. 将initial_six的内容放进一个文件,six_stars从中读取(强烈建议,这样就容易对卡池进行更新,更新的时候只要发布卡池文件就好了,非常易于维护)

那么今天我们对标准寻访卡池和自定义寻访卡池的讲解就到这里结束,欢迎大家在下方留言,拓宽大家的视野。

本文为作者原创,未经作者允许,禁止转载。

----------------------END------------------------

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值