初探幻塔抽卡规则与概率机制
===引子===
最近在研究各类抽卡游戏的机制设计,收集不同系统的优点,以便优化和完善自己的分析工具包,已考察多个游戏的抽卡规则。
来看看幻塔的抽卡机制如何。
官方公布武器概率信息
金核与赤核抽取SSR武器的基础概率为0.75%,综合概率达2%,包含80抽必得保底机制。
金核与赤核抽卡中,SR武器基础出率为1%,计入十连保底后综合概率达12%。
按SSR概率设定,前79次抽卡每次为0.75%,第80次必出,计算得综合概率约为1.990625%,接近官方公布的2%。若将前79次概率微调至0.76%,综合概率则升至2.0005%,更贴近2%。因此推测实际设定可能为0.76%,但出于宣传考虑,取整为0.75%以便与5的倍数对齐,真实机制或略有调整。
对SR不太理解,便观察了一些主播抽卡,人工整理了部分数据。
8 7 7 7 7 8 8 10 10 10 9 9 8 9 9 8 9 10 9 8 9 9 9 9 8 9 9 10 8 9 9 1 1 8 8 9 4 3 8 3 10 8 9 9 9 8 9 8 9 9 8 9 9 8 8 9 10 9 8 8 9 9 9 8 8 9 9 10 9 10 8 8 1 9 9 8 9 9 9 9 10 6 2 9 9 8 8 8 8 8 9 8 8 8 3 5 10 8 9 8 10 9 10 8 9 2 6 8 10 9 8 8 9 9 8 10 10 9 8 8 10 8 9 10 8 9 8 10 9 10 1 8 10 10 10 10 5 3 8 9 10 8 9 10 10 9 8 9 9 9 10 10 7 1 10 10
从第8次抽取起,SR的获取概率开始逐步上升。由于数据有限且关注度不高,大致估算每抽概率增加0.33%,使整体概率达到11.7%,中橙色线所示。
===采用模型===
SSR概率模型与官方公布一致,前79抽每抽0.75%,第80抽必定获得。
SR概率调整:1至7次均为1%,第8次34%,第9次67%。
由于幻塔的抽卡机制具有保底重置特性(抽出SSR后重置),且存在铸金兑换规则,原有基于道具获取即重置保底假设的计算工具已不适用,必须设计专门的动态规划算法,才能准确处理这一复杂情况。
在群友帮助下得知,贴吧已有用户(immortalcow)编写了相关DP代码,并建立了代码仓库。
检查无误,所有情况均已涵盖,稍作修改加入SR概率递增后,直接用于绘图,省去重写步骤。
暂时没写意志抽取部分,现在懒得弄,以后有空再修改完善。
抽卡记录不足,只能分析到这种程度了。
===其他小点===
当SSR数量达到7个后,继续抽取相同SSR仍返还2个铸金。假设SSR综合概率为2%,SR综合概率为12%,且SR已全部集齐。此时的期望指在抽卡次数趋于无穷时,获得每个道具所需的平均抽卡次数。
在限定条件下讨论(现实中每次抽卡次数有限,结果通常比理论更差)。
获取SSR的途径中,42.51%来自80抽保底,32.3%通过120铸金兑换,另有25.19%出自非保底区域。
限定SSR的获取来源中,80抽保底占32.13%,120铸金兑换占48.84%,非保底区域占19.03%。
在限定概率下,每个SSR的期望抽取次数为34次,限定SSR则为51.406次。该数值明显低于实际所需抽卡数,具体情况可参考对应表格。目前发现可能存在细微误差,后续将进行核查修正。
这个限制条件对普通玩家抽卡参考价值不大,建议参考上方图表。若未在120次内抽中保底,将会非常不划算。
===代码===
code
[code=py]
import numpy
import pandas
pandas.set_option("display.max_rows",1000)
import matplotlib
from matplotlib import pyplot as plt
SSR_PROBABILITY_BASIC=0.0075
SR_PROBABILITY_BASIC=0.01
SSR_PROBABILITY_CRITICAL=0.5
SSR_INSURANCE_LIMIT=80
SR_INSURANCE_LIMIT=10
SSR_PRICE_GOLD_CAST=120
SSR_MAX_COUNT=7
def AddOrUpdate(state,key,value):
if key in state:
state[key]+=value
else:
state[key]=value
def ProcessState(ssr_count,ssr_insurance,sr_insurance,gold_cast):
if gold_cast>=SSR_PRICE_GOLD_CAST:
ssr_count+=gold_cast//SSR_PRICE_GOLD_CAST
gold_cast%=SSR_PRICE_GOLD_CAST
return (SSR_MAX_COUNT,0,0,0)
return ssr_count,ssr_insurance,sr_insurance,gold_cast
def Transfer(state):
result={}
for (ssr_count,ssr_insurance,sr_insurance,gold_cast),probability in state.items():
if ssr_count>=SSR_MAX_COUNT:
AddOrUpdate(result,(SSR_MAX_COUNT,0,0,0),probability)
continue
ssr_insurance+=1
sr_insurance+=1
if ssr_insurance>=SSR_INSURANCE_LIMIT:
AddOrUpdate(result,ProcessState(ssr_count+1,0,0,gold_cast+1),probability*SSR_PROBABILITY_CRITICAL)
AddOrUpdate(result,ProcessState(ssr_count,0,0,gold_cast+1),probability*(1-SSR_PROBABILITY_CRITICAL))
continue
AddOrUpdate(result,ProcessState(ssr_count+1,ssr_insurance,0,gold_cast+1),probability*SSR_PROBABILITY_BASIC*SSR_PROBABILITY_CRITICAL)
AddOrUpdate(result,ProcessState(ssr_count,ssr_insurance,0,gold_cast+1),probability*SSR_PROBABILITY_BASIC*(1-SSR_PROBABILITY_CRITICAL))
if sr_insurance>=SR_INSURANCE_LIMIT:
AddOrUpdate(result,ProcessState(ssr_count,ssr_insurance,0,gold_cast+2),probability*(1-SSR_PROBABILITY_BASIC))
continue
sr_rate_now = SR_PROBABILITY_BASIC
if sr_insurance == 9:
sr_rate_now = 0.66+0.01
if sr_insurance == 8:
sr_rate_now = 0.33+0.01
AddOrUpdate(result,ProcessState(ssr_count,ssr_insurance,0,gold_cast+2),probability*(1-SSR_PROBABILITY_BASIC)*sr_rate_now)
AddOrUpdate(result,ProcessState(ssr_count,ssr_insurance,sr_insurance,gold_cast+1),probability*(1-SSR_PROBABILITY_BASIC)*(1-sr_rate_now))
return result
def GetDistribute(state):
result=[0]*(SSR_MAX_COUNT)
for (ssr_count,_,__,___),probability in state.items():
for i in range(ssr_count):
result+=probability
return result
def CalculateSteps(max_step,ssr_insurance=0,sr_insurance=0,gold_cast=0):
results=[{(0,ssr_insurance,sr_insurance,gold_cast):1}]
for _ in range(max_step):
results.append(Transfer(results[-1]))
results[-2]=GetDistribute(results[-2])
for i,j in results[-1].items():
if i[0]==0:
print(i,j)
results[-1]=GetDistribute(results[-1])
return pandas.DataFrame(results,columns=range(1,SSR_MAX_COUNT+1))
final_result=CalculateSteps(MAX_STEPS,SSR_INSURANCE,SR_INSURANCE,GOLD_CAST)
from copy import deepcopy
import matplotlib.cm as cm
import numpy as np
import GGanalysis as gg
from GGanalysis.gacha_plot import quantile_function
def get_most_pull(n):
core = 0
pull = 0
items = 0
while True:
pull += 1
core += 1
if pull % SSR_INSURANCE_LIMIT == 0 or pull % SR_INSURANCE_LIMIT == 0:
core += 1
if core >= SSR_PRICE_GOLD_CAST:
core %= SSR_PRICE_GOLD_CAST
items += 1
if items >= n:
return pull
ans_list = [gg.finite_dist_1D([1])]
for i in range(1, SSR_MAX_COUNT+1):
ans = deepcopy(final_result.values)
ans[1:] = ans[:-1]
dist = final_result.values - ans
dist = dist[:get_most_pull(i)+1]
dist = gg.finite_dist_1D(dist)
dist.p_normalization()
print(i, dist.exp, dist.exp/i)
ans_list.append(dist)
def Hotta_num(x):
return str(x-1)+星
Hotta_fig = quantile_function(
ans_list,
幻塔限定SSR武器抽取概率说明
item_name=限定SSR武器,
本图展示玩家SR角色已全部满破,并计入铸金兑换情况,模型仅供参考,不保证完全准确。因存在铸金机制,期望值参考意义有限。如需估算抽取次数,建议结合图示概率查看。当前条件下,获取单个道具最多需110抽,集齐七个道具最多需764抽。计算由@immortalcow完成,绘图出自@一棵平衡树之手。
text_tail=,
max_pull=675,
mark_func=Hotta_num,
line_colors=0.5*(cm.Blues(np.linspace(0.1, 1, SSR_MAX_COUNT+1))+cm.Greys(np.linspace(0.4, 1, SSR_MAX_COUNT+1))),
y_base_gap=25,
y2x_base=3,
mark_exp=False,
mark_max_pull=False,
is_finite=True)
Hotta_fig.show_figure(dpi=300, savefig=True)
[/code]
评论
更多评论