小言_互联网的博客

实现复杂规则游戏抽奖模拟器第一期:间接学习random模块函数

298人阅读  评论(0)

模拟游戏的抽奖对于揣测游戏稀有物资的出率有较大的意义,通过编写模拟抽奖之类的程序,我们也能够对概率之类的东西有更深的了解,今天我们将开始编写对游戏《明日方舟》的干员寻访进行模拟的一个程序。

首先为大家讲述一下这个游戏的抽奖规则:

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
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场