上一期的文章我们实现了《明日方舟》干员寻访模拟器单次抽奖的功能,了解了random.choice这个函数的特点,通过原始奖池机制把等可能转化为不等可能,上一期我们还编写了一个便捷的不等可能事件抽取函数,也比较了原始机制和该函数机制的优缺点,如果你忘记了上一期的内容,或者第一次看此栏目文章,可以选择移步上一期的文章:
Python实现复杂规则游戏抽奖模拟器第一期:间接学习random模块函数
给大家重温一下抽奖规则:
1、基准概率
六星干员出率:2%,五星干员出率:8%,四星干员出率:50%,三星干员出率:40%,不会出现一、二星干员。
2、默认卡池
六星干员42名,包括暂时绝版的干员6名;五星干员83名(33名不能寻访获得);四星干员47名(7名不能寻访获得);三星干员17名(1名无法寻访)。共计150名干员可被寻访到。
每次抽奖在150位非绝版干员中随机获取一名,那么各干员在其所属星级内等可能出现。
3、官方每更新一个卡池,该卡池用户前十次抽奖内必有一次是五星或六星干员,具体第几次为随机。
4、官方的标准卡池(而非默认卡池)含有两名特定六星干员,三名特定五星干员,特定干员在所属星级内的出率总和占该星级出率的50%,也就是说,如果你抽中了六星干员,有50%概率抽到两名特定六星中的一个,具体是随机的,并且是等可能的,但也有50%概率抽到别的非特定干员。
5、如果用户在任何一个卡池连续50次都没有抽到六星干员,下一次六星干员的总出率将提高两个百分点,之后每一次都会提升两个百分点,直到抽到六星,将恢复基准概率。这个次数不会因卡池变换而清零。
6、每次抽奖消耗600合成玉,合成玉与另一种货币:至纯源石的换算规律是:1源石=180合成玉
今天我们要继续实现模拟器的功能:“50次提概率”和“十次出一次”的两种机制。
首先我们回顾一下,上一期我们的抽奖机制是这样的:
import random
#100个事件,2个是六星,8个是五星,50个是4星,40个是三星
stars=[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,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,5,5,5,5,5,5,5,5,6,6]
#为节省篇幅,把一个星级所有干员信息放在了一行,需要查看的话往右边拉
six_stars=['黑','能天使','莫斯提马','艾雅法拉','伊芙利特','刻俄柏','斯卡蒂','煌','陈','银灰','赫拉格','阿','推进之王','安洁莉娜','麦哲伦','星熊','塞雷娅','夜莺','闪灵','风笛']
five_stars=['守林人','陨星','灰喉','白金','送葬人','普罗旺斯','蓝毒','夜魔','惊蛰','天火','布洛卡','拉普兰德','星极','诗怀雅','幽灵鲨','芙兰卡','狮蝎','食铁兽','崖心','槐琥','红','凛冬','德克萨斯','苇草','初雪','格劳克斯','真理','空','梅尔','雷蛇','临光','可颂','吽','白面鸮','赫默','华法琳','慑砂']
four_stars=['杰西卡','梅','流星','安比尔','白雪','红云','夜烟','远山','角峰','调香师','末药','苏苏洛','蛇屠箱','古米','霜叶','缠丸','猎蜂','慕斯','杜宾','阿消','暗索','砾','地灵','深海色','清道夫','桃金娘','红豆','宴','格雷伊']
three_stars=['炎熔','史都华德','克洛丝','空爆','月见夜','泡普卡','玫兰莎','香草','翎羽','芬','卡缇','米格鲁','斑点','芙蓉','安赛尔','梓兰']
a=random.choice(stars)
if a==3:
result=random.choice(three_stars)
elif a==4:
result=random.choice(four_stars)
elif a==5:
result=random.choice(five_stars)
else:
result=random.choice(six_stars)
print(result)
1、“50次加概率”的实现
如果想要实现50次没抽到六星干员就提升六星干员概率的话,我们应该给每次抽奖计数,如果这次没抽到六星,计数加1,如果抽到了六星,那么计数将清零。
#为节省篇幅,省略了卡池信息
import random
no_six_times=0
#先决定结果的稀有度,如果不清楚为什么这么做,请参照第一期
a=random.choice(stars)
if a==3:
result=random.choice(three_stars)
no_six_times+=1
elif a==4:
result=random.choice(four_stars)
no_six_times+=1
elif a==5:
result=random.choice(five_stars)
no_six_times+=1
else:
result=random.choice(six_stars)
print(result)
我们来推算一下,按照抽奖规则,当你已经有50次没抽到时,下一次六星的概率将是4%,51次对应下一次6%,52次对应下一次8%…以此类推,直到98次未抽到时,下一次六星的概率为100%(建议使用初中一次函数的知识计算,比较方便)。也就是说,第99次必定抽到六星干员。这个逻辑一定要清楚,一个是98次之后的下一次抽奖六星的概率,另外一个是第99次必中,不是100的原因是因为六星本身就有2%的概率。
我们推算完以后,比较困难的是通过数据的变化体现概率提升,我打算这么做:50次以上时,每次没有抽到六星,就把stars里面的两个非6数字改成6,这样,提升了六星的概率,等量降低了低星的出现概率。
这个时候,stars中各项的顺序便尤为重要,如果把stars打乱,那么如果我们用stars[0]=6 stars[1]=6的话,可能因为这一项原来就是6,然后就覆盖掉了,这样也许不能达到提升概率的效果。所以我们要以星级升序排列,决不能让5星排在前面,这样会抽不到5星的,对客户是很不公平的。
我们可以这样提升概率:
while no_six_times>=50:
stars[(No_six_times-50)*2]=6
stars[(No_six_times-50)*2+1]=6
no_six_times-=1
在这里给大家一些关于精确索引内容的经验:
1、要常常使用列表,而不是元组,集合,因为对列表的排序是原地进行的,而且它是有序序列,这样我们可以直接取第零项获取升序或降序的最值。
2、当你始终需要改变列表的最后一项时,常用-1索引。
3、通常在两个两个一改变的时候,最好想我使用上面的思路,这样只要小小的计算就行了。
4、在处理文件需要大型列表时,通常读取各行,形成由各行组成的列表,然后再在每一行识别段落、句子之类的,把每一行转换为列表,嵌套进原列表,也就是一个大列表包含各行,每一行又是一个列表,这样识别目标内容非常容易,比如file_lines[10][5]代表的就是第十行第五个字符或者单词。
假设你50次没抽到六星了,下一次六星的概率应该是4%,提升完概率真实的stars应该是这样:
stars=[6,6,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,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,5,5,5,5,5,5,5,5,6,6]
#前两个原本是3,提升概率后成了6,总的算下来是4%,符合预期
可是我们下一次抽奖的时候就出现了问题:提升完概率以后,no_six_times按照计算就变成49了(实际的次数应该是50),那如果用户按照提升后的概率还是没抽到六星,no_six_times变成了50(实际应次数为51),按照算法,提升后的stars还是上面的样子(实际应该是6%),之后次数又变回了49,这完全不符合我们的意愿,第51次没抽到之后,应该是第三项第四项变为6,我们应该将no_six_times分离为两个变量,一个用来提升概率(就是现在的no_six_stars),还有一个是用来反映真实次数的变量(我们把它命名为real_no_six)。
no_six_times=real_no_six
while No_six_times>=50:
stars[(No_six_times-50)*2]=6
stars[(No_six_times-50)*2+1]=6
No_six_times=No_six_times-1
相应地,我们在抽奖的时候要把no_six_times+=1改成real_no_six+=1,因为前者仅仅用于控制卡池,给它加1没有实际意义,也和我们的预期不符。方便起见,我们把前面决定结果稀有度的变量a改为“choices”,注意:我们抽到六星干员后还要重置六星的概率,这个比较简单,只要还原初始那个列表就行了。如下:
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]
print('已获得:%s\n恭喜寻访到六星干员!'%result)
elif choices==5:
result=random.choice(five_stars)
real_no_six+=1
print('已获得:%s,恭喜寻访到五星干员\n距离上次寻访到六星干员已有%s次'%(result,real_no_six))
elif choices==4:
result=random.choice(four_stars)
real_no_six+=1
print('已获得:%s,你寻访到了四星干员\n距离上次寻访到六星干员已有%s次'%(result,real_no_six))
else:
result=random.choice(three_stars)
No_six_stars_times=No_six_stars_times+1
print('已获得:%s,真菜,你寻访到了三星干员\n距离上次寻访到六星干员已有%s次'%(result,real_no_six))
改完之后我们就可以正常运行程序,效果符合预期,也实现了一个变量用于控制概率,另一个变量反应真实数据。
2、“10次出一次”的实现
这里的“出一次”是指出一次五星或者六星,我们称之为“保底”,在这个程序里用“least”这个英文单词表明这个东西与保底有关,保底的这一次是没有三星四星干员的,那么到了那时,原本的stars池应该是这样:
least_stars=[5,5,5,5,5,5,5,5,6,6]
保底的那一次抽奖我们称为“触发保底机制”,10次中必有一次触发保底机制,一旦触发了,保底将结束,除非重新设置保底,否则保底机制不再会被触发。那我们该如何在十次内抽取一次触发保底呢?我们应该在“变化事件集”中,以可变概率抽取目标事件。什么意思呢?就是这样:
n=10#这个不能随便放在这里,应当放入一个函数,按照我们的控制设置保底
#我是为了让大家明白而放这里的
a=random.randint(1,n)#randint函数是两头都包含在内的,1和10都包含
#randint在所给两个参数之间随机抽取一个整数
if a!=1:#始终以1作为目标事件,也就是触发保底机制
do_least=True
else:
do_least=False
n-=1
if do_least:
choices=random.choice(least_stars)
else:
choices=random.choice(stars)
#接下来就是之前介绍过的抽奖机制了
我们这里始终以1触发保底机制的原因很明确,假设你设置的不是1,而是5,那你还剩四次的时候,肯定是抽不到5的,那不就不能保证10次中一次了吗?
那当我们即拥有若干次数的保底,又有50次没抽到六星干员呢?(这种情况是可能的,就比如说你已经抽了50次,再去设置保底)如果下一次正好触发了保底机制,那我们不能按照stars里面进行常规概率提升了,我们应该对least_stars进行概率提升,由于least_stars里面的六星出率是20%,如果还是按照2%一加很麻烦且不太符合逻辑,所以在least_stars内我们可以20%地加。
我们需要给原来的代码进行改进,加上保底中六星概率提升的功能(此处将n重命名为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:#这一块就是需要添加的东西
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:
choices=random.choice(least_stars)
else:
choices=random.choice(stars)
我们再把它整合成一个函数,定名single,表示单抽。
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:
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)
设置和使用保底比前面的概率提升要简单的多,但是当有一个特殊情况时,会出现一个致命漏洞,我在某一次抽奖后遇到了报错:
>>> single()
Traceback(most recent call last):
File "<pyshell#10>", line 19, in <module>:
single()
File H:\明日方舟干员寻访模拟器\主程序.py, line 14, in single:
least_stars[(no_six_times-50)*2]=6
IndexError: list index out of range.
>>> real_no_six
56
>>> do_least
True
我们触发了保底机制,同时确实有50次没抽到六星了,结果却无法抽奖,我们来推算一下,56次没抽到六星,下次对应的将是stars[12]和stars[13]改为6,然后因为触发了保底,所以应该是least_stars[12]=6,然而,least_stars只有十项,所以会触发这个错误。经计算,54次以后的下一次如果触发保底,应该是100%出六星。所以我们只要做个判断,当real_no_six小于(等号可加可不加)54时就能避免错误且保证其他功能不受影响(以下是修正后的片段):
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
那么single函数的基本任务就完成了,我们可以着手其他的功能了。下一期我们将继续实现标准卡池特定干员概率提升的相关功能,欢迎关注我的专栏:游戏抽奖模拟器专栏。我们下期再见。