模拟游戏的抽奖对于揣测游戏稀有物资的出率有较大的意义,通过编写模拟抽奖之类的程序,我们也能够对概率之类的东西有更深的了解,今天我们将开始编写对游戏《明日方舟》的干员寻访进行模拟的一个程序。
首先为大家讲述一下这个游戏的抽奖规则:
1、基准概率
六星干员出率:2%,五星干员出率:8%,四星干员出率:50%,三星干员出率:40%,不会出现一、二星干员。
2、卡池
六星干员20名,包括暂时绝版的干员1名
五星干员37名,无绝版,四星干员29名,无绝版,三星干员16名,无绝版
共计102名干员(含绝版一名)
每次抽奖在101位非绝版干员中随机获取一名,如果是默认卡池,那么各干员在其所属星级内等可能出现。
3、官方每更新一个卡池,该卡池用户前十次抽奖内必有一次是五星或六星干员,但是十次内一旦抽到五或六,将恢复原来的概率。
4、官方的标准卡池含有两名特定六星干员,三名特定五星干员,特定干员在所属星级内的出率总和占该星级出率的50%,也就是说,如果你抽中了六星干员,有50%概率抽到两名特定六星中的一个,具体是哪个是随机的,并且是等可能的,但也有50%概率抽到别的非特定干员。
5、如果用户在任何一个卡池连续50次都没有抽到六星干员,下一次六星干员的总出率将提高两个百分点,每一次都会提升两个百分点,直到抽到六星,将恢复基准概率。这个次数不会因卡池变换而清零。
6、每次抽奖消耗600合成玉,合成玉与另一种货币:至纯源石的换算规律是:1源石=180合成玉
在编写程序之前,我们需要一个干员的数据库,用于抽取干员,我从网上抄下来了一些数据,并且编写了一个微型抽卡机制:
import random
#100个事件,2个是六星,8个是五星,50个是4星,40个是三星
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]
#为节省篇幅,把一个星级所有干员信息放在了一行,需要查看的话往右边拉
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)
这种机制比较原始,基本思路是先从100种星级事件中选出一个星级事件,决定结果的稀有度,然后因为默认卡池各干员等可能出现(默认卡池是没有“特定干员”的),我们再从对应的星级中抽取一个干员作为最终结果。
random.choice()接受一个参数,这个参数可以是一个非空列表、元组甚至是字符串,但是不能是字典、集合或者数字,这个函数会遍历这个参数对象,随机抽取出一个项,概率是等可能的。
这样子的好处就是,我们不需要考虑每名干员相对于整个卡池的概率,这些数据本身就深入地贯彻着基准概率,但是这样的坏处就是我们想要打破等可能事件将非常困难,需要手动创造事件列表,就像上文的stars一样,你得写100个,那要是以后一万个,一百万个呢?这是不可能让程序员一个个写进代码或者文件的,还很占用空间。
所以,我给大家稍微总结一下两种抽奖方式的优缺点:
逐个分支抽取结果 | 一步到位从直接按照总体概率抽一个 | |
---|---|---|
优点 | 不用考虑每一项相对于整个卡池的总体出率 | 操作方便,省代码,只要抽一次 |
缺点 | 得出一个结果需要对每一分支进行抽取,还需要很多判断语句判断在哪个分支 | 需要人工或者计算器计算每一个结果的总出率,更改事件出现概率需要重新计算总出率,很麻烦 |
建议的使用场合 | 各分支等可能出现,每个分支内各结果也等可能出现时 | 没有多个级别分支时 |
逐个分支抽取结果是我经常使用的,因为这种方法的优点给我们带来了很大便利,但是同级内必须等可能出现,否则要想提高某一事件的概率,比如六星从2%提高到4%,必须把四星的概率降低2%(因为四星原本的概率最高),让“6”覆盖“4”,以达到提高六星出率,降低四星出率的效果,但这样,你总得在处理列表上花功夫。
所以我编写了一个简单的函数,可以以一个字典作为参数输入,其中键为事件,值为出现的概率,修改出现概率也非常容易,只要dict[key]=n 就可以完成,但是修改后的概率和加起来可能就不是1了,需要对其他项的出率进行修改,代码有点复杂:
from random import *
def sampling(dictionary):
#本函数的原理是代数化的几何概率
'''按照参数所指定的概率随机抽取一个元素,传入的参数始终是一个字典,其中键为事件,值为事件发生的概率,
所有的值必须是介于0和1的浮点数且所有值的和应为1,若不是这样,则抛出异常。不支持无法化为有限小数的分数
Returns an random element according to the probability specified by the argument.
The argument passed in is always a dictionary,where the key is the event and the value is the probability of the event.
All values must be float numbers between 0 and 1 and the sum of all values should be 1, raise an exception otherwise.
Fractions that cannot be converted to finite decimals are not supported.'''
events=[]
values=[]
for i in dictionary:
events.append(i)#将事件从字典中分离出来,因为dictionary.keys()返回的不是一个列表,比较难处理
values.append(dictionary[i])#将每个事件的概率也分离出来,与事件一一对应
if sum(values)!=1:#如果概率加起来并不是1,则抛出异常
raise ValueError('All values must be float numbers between 0 and 1 and the sum of all values should be 1.')
points=[]
for i in values:
point=0
a=values.index(i)
while a>=0:#将参数中提供的每个事件的概率转换为一个个类似数轴上的点,之后要判断点之间的区间
point+=values[a]
a-=1
points.append(point)
points.insert(0,0)#把数轴原点0也加入列表
#假设在数轴上,第一个点是0,第一个点和第二个点的距离是第一个事件的概率
#第二个点到第三个点的距离是第二个事件的概率,所以第三个点在数轴上的值是前两个事件概率的和,以此类推
result=random()#在0到1之间选取一个数
for num in points:
if num<=result<points[points.index(num)+1]:#这里必须是小于等于和小于,这样保证不会正好抽到1引发特殊情况而产生异常
return events[points.index(num)]#从events中获取result所在区间对应的事件
上述代码还是非常方便的,只是有点小小的瑕疵,就是不建议使用无限循环小数,否则可能因为浮点数计算问题,概率加起来不等于1,然后报错,比如,数学上明确的1-2/3=1/3,但是在python中输入1-2/3==1/3会返回False:
>>> 1-2/3==1/3
False
>>>
这是由于浮点数精度有限,是我们无法改变的,也正是因为这个原因,我们不能使用像1/3,1/7这样的出现概率,这种概率最好还是做一个包含若干个事件的列表,用random.choice函数从序列中随机抽取一个,这样原始的方法反而比上述函数实用而稳定。
我们借助上述函数和random.choice函数,把他们组合起来,当需要三分之一之类概率,或者多个分支时,就使用原始方案,当需要不等可能事件,且各概率都是有限小数时,就是用新函数,这个函数的特点就是只要概率加起来不等于1,就会自动报错,有利于发现卡池其他缺陷和错误,还可以自己改编,扩展这个函数,让它发挥更大的作用。
我们接下来的任务是先实现默认卡池(无任何特定干员)的单次抽奖,按照规则抽取干员,暂时不考虑“10次中一次保底”和“50次升出率保底”。
#为节省篇幅,省略了卡池的内容
def single():
choices=random.choice(stars)
if choices==6:
result=random.choice(six_stars)
print('恭喜寻访到六星干员!')
elif choices==5:
result=random.choice(five_stars)
print('恭喜寻访到五星干员!')
elif choices==4:
result=random.choice(four_stars)
print('你抽到了四星干员')
else:
result=random.choice(three_stars)
print('你真菜,抽到了三星干员')
hechengyu=hechengyu+600
print('寻访结果:%s'%result)
那么我们对于各情况的分支判断和随机抽奖就完成了。下一期我们将加入“50次加概率保底”和“新的卡池前十次出一次保底”功能。
快速跳转下一期:
实现《明日方舟》干员寻访模拟器第二期:间接学习变化序列抽取目标事件和序列精确索引内容的经验
本文为作者原创,未经作者允许,禁止转载
---------------END----------------
转载:https://blog.csdn.net/weixin_46847476/article/details/105371764