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

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

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

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次加概率保底”和“新的卡池前十次出一次保底”功能。
快速跳转下一期:
Python实现《明日方舟》干员寻访模拟器第二期:间接学习变化序列抽取目标事件和序列精确索引内容的经验

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

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

  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值