最近一段时间一直在玩某款带有养成元素的休闲手游,其养成体系主要体现在它的装备合成系统上。这段时间,我突然对其装备合成系统产生了一定兴趣,遂尝试着研究该游戏玩家在游玩进度逐渐累积时,各等级的装备数量变化情况。
一 游戏背景
该游戏是一款打怪升级类休闲手游,其装备合成系统主要有以下三个特征:
- 装备仅能通过打开宝箱获取,每次打开宝箱可获得一件随机的基础(Level 0)装备,这里假设打开宝箱时每件装备获取的概率相等。
- 三件相同等级、类型的装备将会被消耗,来合成更高一级的同类型装备,如三件Level0的“镰刀”可以合成一件Level1的“镰刀”,三件Level1的“镰刀”可以合成一件Level2的“镰刀”,以此类推。
- 装备均存入装备库中,不可摧毁。
在游玩过程中,我发现了一个有趣的现象:游戏中后期,随着我打开宝箱数量的增加,装备库的高级装备数量在逐渐增加,但低等级的装备数量却变化不大;并且当游戏进行到一定阶段时,最低几个等级的装备数量变得相差无几。
出于对该现象的好奇,我使用Python模拟了该游戏的宝箱抽取过程,统计了各等级装备的数量变化情况,此外,我还进一步从理论上计算了该数量的期望值。模拟和理论计算结果均表明,当开箱数量逐渐增加时,各等级里的装备数量均值出现了类似正弦函数的波动,并最终收敛到一个固定值上。文章对该现象在一定程度上作出了解释。
二 函数模型
2.1 模拟开箱
为了绘制不同开箱数量时各等级装备数量的变化曲线,我归纳出了三个参数:等级数量(levels),装备种类(equips)和开箱数量(chests)。以我游玩的该游戏为例,武器类装备共有 4 种,分 6 个等级,即 equips=4, levels=6。
进行一整次模拟开箱实验(即打开chests个箱子) 的函数代码如下,该函数接收levels, equips, chests三个整型参数,并在计算得到各等级里每种装备的数量期望后,对装备取均值,返回 各等级里同种装备的数量均值 矩阵。
def equipment(chests, equips, levels):
arms = np.zeros([levels, equips, chests+1])
chest_list = np.random.randint(equips, size=chests+1) # 模拟chests次开箱
# 更新每次开箱后的装备列表
for chest in range(1, len(chest_list)):
# 将此次开箱的装备加入装备库
equip = chest_list[chest]
arms[0, equip, chest] += 1
# 合成装备
for level in range(levels-1):
if arms[level, equip, chest] < 3:
break
arms[level, equip, chest] = 0
arms[level+1, equip, chest] += 1
# 初始化下一次开箱前的装备列表
if chest != chests:
arms[:, :, chest+1] = arms[:, :, chest]
mean = arms.mean(axis=1) #对每一等级的装备数量计算均值
return mean
重复进行开箱实验的函数如下,其中nums参数指重复模拟的次数,该函数返回一个由多次重复实验得到的 各等级里同种装备的数量均值 矩阵。
def test(nums, chests=2000, equips=4, levels=6):
mean = np.zeros([levels, chests+1])
for num in range(nums):
mean += equipment(chests, equips, levels)
mean = mean / nums
return mean
为了在不失一般性的同时简化分析,我在模拟时使用了参数equips=4, levels=6, chests=2000。重复进行1000次试验,将结果转化为DataFrame,并绘制折线图。
a = pd.DataFrame(test(1000).T, columns=['L0','L1','L2','L3','L4','L5'])
a.plot()
折线图如下,其中横轴为开箱数量,纵轴为各等级里的同种装备数量。
从折线图中,可以看到:
① 各等级同种装备数量均值均出现了不同程度的震荡;且等级越高,数量均值震荡的周期和幅度越大。
② 各等级同种装备数量在震荡中逐渐稳定,其中低等级同种装备数量曲线在震荡后很快稳定在 N=1 直线附近。
2.2 计算理论均值
由于事先没有预测到各曲线的震荡性质,因此为了验证模型的正确性,我决定对该均值进行一个理论计算。
通过分析装备合成规则可以知道,在开箱得到 N 件同类的基础装备后,装备库中一件等级为 L 的该装备数量为:
f
l
o
o
r
(
N
%
3
L
+
1
3
L
)
floor(\frac{N\;\%\;3^{L+1}}{3^L})
floor(3LN%3L+1)
其中%表示取余,floor函数表示向下取整,将其转换为python语言表达式即为:
(N % (3**(L+1))) // (3**L)
这是因为,当有
3
L
+
1
3^{L+1}
3L+1件基础装备时,它们会合成等级为 L+1 的更高级装备,故首先需要将
N
N
N 对
3
L
+
1
3^{L+1}
3L+1 取余;之后再除以
3
L
3^L
3L 并取整。举个例子,如果抽到了 17 件基础装备,那么其中 9 件会被用于合成 L2 装备,剩下的 8 件用来合成 2 件 L1 装备;使用公式计算 L1 装备的数量为 (17%9)//3 = 8//3 = 2。
在计算中,可以将连续地打开宝箱看作是伯努利试验,这样便能够得到各种基础装备数量的分布列,从而能够较为轻松地计算装备数量均值了,最终代码如下:
def equipment_theory(chests, equips, levels):
mean = np.zeros([levels, chests+1])
for chest in range(1, chests+1):
# 对每种装备计算基础装备数量的分布
for num in range(chest):
prob = stats.binom.pmf(num, chest, 1/equips)
# 由公式计算装备在每个等级数量的均值
for level in range(levels):
count = (num % (3**(level+1))) // (3**level)
mean[level, chest] += prob * count
return mean
b = pd.DataFrame(equipment_theory(2000, 4, 6).T, columns=['L0','L1','L2','L3','L4','L5'])
b.plot()
绘制的折线图如下,其中横轴为开箱数量,纵轴为各等级里同种装备的数量期望
可以看到,理论计算得到的数量均值曲线与模拟得到的曲线相差无几。
三 结果分析
本章对绘制的数量曲线进行分析,主要目的是解决以下两个问题:
① 当开箱数量足够多时,各等级里同种装备的数量期望最终都会稳定到一个均值上吗?如果是,这个均值是多少?
② 为什么各等级里同种装备的数量期望曲线会出现上下波动情况?波动的规律是什么?
3.1 装备数量期望收敛值
为了回答第一个问题,让我们首先证明这样一条性质:
- 当开箱数量足够大时,用 X 表示得到某件基础装备的数量,那么 X 除以任意正整数 n 后,余数是 0,1,2,…,n-1 的概率都是一样的,均为 1/n。
即 P ( X % n = i ) = 1 / n , i = 0 , 1 , 2 , . . . , n − 1 P(X\%n=i)=1/n, \;i=0,1,2,...,n-1 P(X%n=i)=1/n,i=0,1,2,...,n−1。
- 证明:
为简化描述,此条证明中用符号 P ( i ) P(i) P(i) 来表示 P ( X % n = i ) P(X\%n=i) P(X%n=i)
首先,得到所有余数的概率之和等于1,故有
∑ i = 0 n − 1 P ( i ) = 1 \sum_{i=0}^{n-1}P(i)=1 ∑i=0n−1P(i)=1
另外,当开箱数量趋近于正无穷时,余数为 i ( i ≥ 1 ) i\;(i\ge1) i(i≥1) 等价于以下两种情况之和:a)第一次开出该装备,且之后开出该装备数量的余数为 i − 1 i-1 i−1;b)第一次没有开出该装备,且之后开出该装备数量的余数为 i i i。故有
P ( i ) = 1 n P ( i − 1 ) + n − 1 n P ( i ) i = 1 , 2 , . . . n − 1 P(i)=\frac{1}{n}P(i-1)+\frac{n-1}{n}P(i)\quad i=1,2,...n-1 P(i)=n1P(i−1)+nn−1P(i)i=1,2,...n−1
联立以上两式求解,便可得到该定理。
得到定理1后,便可以计算均值了。
用
N
L
N_L
NL 表示开箱数量趋近于正无穷时,装备库中等级为 L 的装备的数量。由于三件相同装备可以合成更高级的装备,故装备库中任意等级的某种装备数量仅可能取值为0、1 和 2。
因为开箱数量趋近于正无穷,由定理1 易得:
P
(
N
L
=
0
)
=
∑
i
=
0
3
L
−
1
P
(
X
%
3
L
+
1
=
i
)
=
3
L
/
3
L
+
1
=
1
/
3
P(N_L=0)=\sum_{i=0}^{3^{L}-1}P(X\,\%\,3^{L+1}=i) =3^L/3^{L+1}=1/3
P(NL=0)=∑i=03L−1P(X%3L+1=i)=3L/3L+1=1/3
P ( N L = 1 ) = ∑ i = 3 L 2 ⋅ 3 L − 1 P ( X % 3 L + 1 = i ) = 3 L / 3 L + 1 = 1 / 3 P(N_L=1)=\sum_{i=3^{L}}^{2 \cdot 3^{L}-1}P(X\,\%\,3^{L+1}=i) =3^L/3^{L+1}=1/3 P(NL=1)=∑i=3L2⋅3L−1P(X%3L+1=i)=3L/3L+1=1/3
P ( N L = 2 ) = ∑ i = 2 ⋅ 3 L 3 L + 1 − 1 P ( X % 3 L + 1 = i ) = 3 L / 3 L + 1 = 1 / 3 P(N_L=2)=\sum_{i=2 \cdot 3^{L}}^{3^{L+1}-1}P(X\,\%\,3^{L+1}=i)=3^L/3^{L+1}=1/3 P(NL=2)=∑i=2⋅3L3L+1−1P(X%3L+1=i)=3L/3L+1=1/3
可以看到,当开箱数量趋近于正无穷时,装备库中任意等级的某装备数量等可能地取0,1,2三个值,故均值为
E
(
N
L
)
=
0
⋅
1
/
3
+
1
⋅
1
/
3
+
2
⋅
1
/
3
=
1
E(N_L)=0\cdot 1/3+1\cdot 1/3+2\cdot 1/3=1
E(NL)=0⋅1/3+1⋅1/3+2⋅1/3=1
至此,我们可以回答第一个问题了:当开箱数量足够多时,各等级里每种装备的数量分布趋近于均匀分布,其期望为 1 。 这也与我们的图表相符。
3.2 装备数量期望波动及收敛原因
通过以上分析,我们知道了当开箱数量足够多时,各等级里同种装备的数量期望是稳定在 1 附近的。而我们想要进一步回答的问题是,当开箱数量从零开始逐渐增加时,各等级里同种装备的数量期望变化及收敛的原因是什么呢?
由于本人能力所限,本章仅提供经验分析,不进行数学证明。
3.2.1 装备曲线波动原因
由于装备的抽取满足伯努利分布,因此在抽取一定数量的宝箱后,我们便能够大致判断出得到某种基础装备的数量范围。例如,当共有4种装备,即equips=4时,抽 n 次宝箱便可得到某一种装备约 n/4 个。
下面代码用于绘制当分别抽取400, 1000, 2000次时,某一种装备数量的概率分布情况及其均值。
x = np.arange(0,2000)
# 计算概率分布
binom_400 = stats.binom.pmf(x, 400, 1/4)
binom_1000 = stats.binom.pmf(x, 1000, 1/4)
binom_2000 = stats.binom.pmf(x, 2000, 1/4)
# 绘制概率分布
plt.plot(x[0:800],binom_400[0:800], label = '400 chests')
plt.plot(x[0:800],binom_1000[0:800], label = '1000 chests')
plt.plot(x[0:800],binom_2000[0:800], label = '2000 chests')
# 绘制平均值垂线
plt.axvline(x=sum(np.arange(800)*binom_400[0:800]), ls="--", c='blue',alpha=0.5)
plt.axvline(x=sum(np.arange(800)*binom_1000[0:800]), ls="--", c='orange',alpha=0.5)
plt.axvline(x=sum(np.arange(800)*binom_2000[0:800]), ls="--", c='green',alpha=0.5)
plt.legend()
结果如下,从分布列可以很直观得看出,抽得的同种装备数量大概率处在均值附近的一个小范围内。
例如,当抽取1000次宝箱时(如橙线所示),得到的某一种基础装备数量应该在 250 个左右。 我们可以考察当某种基础装备数量在 250 左右时,各等级的装备数量变化情况。
def one_equipment(low, high, levels=6):
count = np.zeros([levels, high+1])
for chest in range(low, high+1):
for level in range(levels):
count[level, chest] += (chest % (3**(level+1))) // (3**level)
return count
c = pd.DataFrame(one_equipment(200, 300, 6).T, columns=['L0','L1','L2','L3','L4','L5'])
c = c.loc[220:280, ['L3','L4','L5']]
c['L3'] += 0.025 #为了更清楚地绘制折线图,对值做轻微地错位处理
c.plot(color = ['red','purple','brown'])
这里我们仅绘制了高等级(L3, L4, L5)装备数量的变化情况,而低等级装备数量变化情况将在下一小节中进行研究。绘制的折线图如下:
可以看到,对于 L3 ~ L5 这样的高等级而言,某种基础装备数量在 250 附近的各等级装备变化趋势,与折线图中抽取宝箱数量在 1000 左右时的各等级装备数据均值变化趋势(见下图)出奇地一致。
综上,我们可以归纳出这样的结论:抽取宝箱时,高等级里某种装备数量期望的变化情况,其规律类似于 抽到该种基础装备的数量在其最有可能的取值附近时,各等级该装备的数量变化情况。
3.2.2 装备曲线收敛原因
刚才我们分析了刚抽取宝箱数量为1000时,L3 ~ L5 的高等级装备数量的变化规律,这节我们将进一步研究 L0 ~ L2 的低等级装备数量的变化趋势。
让我们重新回到抽取宝箱时基础装备数量的概率分布曲线,这次我们在考察均值之外,更主要的是考察其取值区间。
下面代码用于绘制在给定的置信度下,某种基础装备数量分布的置信区间。
# 计算置信区间
def confi_interval(prob, confi=0.8):
sum1 = 0
for i in range(len(prob)):
x = prob[i]
sum1 += x
if sum1 > (1-confi)/2:
rst1 = i - 1
break
sum2 = 0
for j in range(rst1, len(prob)+1):
x = prob[j]
sum2 += x
if sum2 > confi:
rst2 = j
break
return (rst1, rst2)
x = np.arange(0,2000)
# 计算概率分布
binom_400 = stats.binom.pmf(x, 400, 1/4)
binom_1000 = stats.binom.pmf(x, 1000, 1/4)
binom_2000 = stats.binom.pmf(x, 2000, 1/4)
# 绘制概率分布
plt.plot(x[0:800],binom_400[0:800], label = '400 chests')
plt.plot(x[0:800],binom_1000[0:800], label = '1000 chests')
plt.plot(x[0:800],binom_2000[0:800], label = '2000 chests')
# 绘制 80% 情况下数量的取值区间
for i in (0, 1):
plt.axvline(x=confi_interval(binom_400, 0.95)[i], ls="--", c='blue',alpha=0.5)
plt.axvline(x=confi_interval(binom_1000, 0.95)[i], ls="--", c='orange',alpha=0.5)
plt.axvline(x=confi_interval(binom_2000, 0.95)[i], ls="--", c='green',alpha=0.5)
plt.legend()
结果如下,其中置信度取 0.8,同颜色两条虚线内的范围即为该数量 80% 情况下所处的区间。
可以看到,当抽取宝箱数量增多时,其 80% 置信度下的置信区间范围逐步扩大。而越低等级的装备合成所需要用到的基础装备越少,故当基础装备数量的取值区间变广时,低等级装备数量分布的不确定性增加得将比高等级装备更快。
假设装备共有四种,即 equips=4,考虑抽取宝箱数量逐渐增加时,各等级里(不包括最高等级 L5)同种装备数量的变化情况。
取宝箱数量为 25, 80, 1000, 5000,编程绘制各情况下装备数量分布折线图
def possibilities(chests, levels=6):
prob = np.zeros([levels-1, 3])
x = np.arange(chests+1)
binom = stats.binom.pmf(x, chests, 1/4)
for equip in x:
for level in range(levels-1):
count = (equip % (3**(level+1))) // (3**level)
prob[level, count] += binom[equip]
probdf = pd.DataFrame(prob.T, columns=['L0', 'L1', 'L2','L3', 'L4'])
return probdf
nums = [25, 80, 1000, 5000]
fig, axes = plt.subplots(2, 2, figsize=(10,10))
for num in range(len(nums)):
chests = nums[num]
prob = possibilities(chests)
prob.plot(style='o--', ax=axes[num//2][num%2], ylim=[0,1], title='Draw '+str(nums[num])+' times', xticks=[0,1,2])
当抽取不同数量宝箱时,各等级里同种装备数量概率分布的折线图如下:
可以看到:
- 当抽取 25 次宝箱时,任意一种装备数量其80%置信区间为 3 ~ 8 ,此时能较好地预测 L4, L3, L2, L1 装备数量(分别约 0, 0, 0, 1→2 件),而 L0 装备数量分布趋于均匀。
- 当抽取 80 次宝箱时,任意一种装备数量其80%置信区间为 14 ~ 24 ,此时能较好地预测 L4, L3, L2 装备数量(分别约 0, 0, 1→2 件),而 L1, L0 装备数量分布趋于均匀。
- 当抽取 800 次宝箱时,任意一种装备数量其80%置信区间为 183 ~ 214 ,此时能较好地预测 L4, L3 装备数量(分别约 2→0, 2→0 件),而 L2, L1, L0 装备数量分布趋于均匀。
- 当抽取 5000 次宝箱时,任意一种装备数量其80%置信区间为 1210 ~ 1288 ,此时能较好地预测 L4 装备数量(约 0 件),而 L2, L1, L0 装备数量分布趋于均匀。
综上,我们可以归纳出这样的结论:抽取宝箱数量越多,同一置信度下某种基础装备数量取值范围就越大,而 基础装备数量取值范围的变大将从低等级到高等级,逐步使各等级装备的数量分布趋近于均匀分布。当某等级装备数量分布逐渐接近于均匀分布时,其均值便会慢慢地向 1 收敛了。
四 总结
本文研究了某游戏中当抽取宝箱数量变化时,各等级装备数量期望的变化情况,主要结论如下:
- 当抽取宝箱数量增加时,各等级同种装备数量期望会出现类似于正弦函数的波动,该波动源自同种基础装备合成导致的各等级装备数量变化。
- 当抽取宝箱数量足够多时,各等级同种装备数量分布将从低等级到高等级逐步趋近于均匀分布,使得其数量期望逐渐向 1 收敛。
本文仅是出于好奇的研究,行文之初并未考虑到对游戏运营的启示意义,但在绘制出意料之外的波动曲线后,我还是得到了以下两点(不那么有新意的)运营启示:
- 高等级装备数量变化周期较大,故玩家在游戏中后期容易较长一段时间内得不到装备上的提升,因此需对此类玩家设立其他养成方面的激励措施,使其保持游戏乐趣。
- 文章得出了不同阶段玩家各等级装备数量期望的变化情况及其收敛值,为玩家装备库的设计风格提供了重要参考意见。
写在最后: 最开始仅仅是因为突然想到了这个问题,就想随手写一篇随笔来简要分析一下,没想到一下就写了这么多字,也是很震惊了… 撰文的过程中暴露了自己在编程和行文上的很多不足,汲取经验继续加油吧。