从向量角度理解游戏《荒野乱斗》

前段时间在玩《荒野乱斗》(Brawl Stars)时,为了尽可能提高获胜的概率,我不断更换尝试不同的英雄,从而探寻哪个英雄最适合当前的模式和地图。有那么一瞬间,仿佛回到了曾经打数学建模竞赛时那些调整参数的夜晚,先选择一组参数,然后运行代码,等结果,分析结果,接着继续调整,此时此刻,恰如彼时彼刻

又因为近期在讲空间向量以及受到《数学之美》第14章“余弦定理和新闻的分类”的启发,我决定从向量出发,谈谈(瞎编)一个一定程度上可以解释《荒野乱斗》的数学模型。

英雄参数化

每个英雄都有一系列不尽相同数据作为参数,例如生命值、攻击力、攻击范围、移动速度等等,把这些数值放到一起,那么每个英雄都可以写作一个向量,若用来表示,即

以9级的艾德加为例,生命值为5940,普通攻击值为2×972,射程为“近”,装弹速度“非常快”,移动速度“非常快”;超级技能攻击值为1350,射程为“中等”,若把描述性词语用之间的一个实数量化(比如“非常快”标记为0.9,“中等”标记为0.5等,当然这样过于主观,想要得到更精确的结果,需要结合实际情况调整参数),则艾德加的参数可表示为一个7维向量

当然在游戏中,还有一些诸如“随身妙具”“极限充能”之类的附属加成,为了简化模型,这部分参数的影响就暂时不考虑了。此外,各种英雄的攻击方式不尽相同,比如有的英雄一次有3发子弹,每次攻击值为500,而有些英雄攻击值虽有1000但却是一次性的,这时就不能简单地用上述仅一个分量来表示攻击值,可以考虑把第二分量拆成3个分量,即

对应两个英雄的参数为

英雄分类

在游戏中,参数相近的英雄会归为同一类,比如生命值较大的英雄一般会归类为“坦克”,而攻击范围(射程)较远一般会归为“射手”,移动速度较快则可能归为“突袭者”。比如上面的“艾德加”就属于“突袭者”,再比如“阿方”也是“突袭者”,而“雅琪”属于“坦克”。仍沿用第一种不细分普通攻击次数的表示方法,把3位英雄参数放到一起(都是9级)对比如下:

若分别将3位英雄两两组合,可以计算他们夹角余弦值,计算公式大家应该是再熟悉不过了,即

其中,表示两位英雄的内积,分别表示两个英雄向量的模。下面用python简单计算一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import numpy as np

def cosine_similarity(A, B):
dot_product = np.dot(A, B)
norm_A = np.linalg.norm(A)
norm_B = np.linalg.norm(B)
cosine_sim = dot_product / (norm_A * norm_B)
return cosine_sim

# 定义向量
\mathbf{H}_艾德加 = np.array([5940, 1944, 0.2, 0.9, 0.9, 1350, 0.5])
\mathbf{H}_阿方 = np.array([7740, 2448, 0.9, 0.9, 0.7, 2160, 0.9])
\mathbf{H}_雅琪 = np.array([9000, 2232, 0.2, 0.5, 0.7, 0, 0.6])

# 计算余弦值
cos_艾德加_阿方 = cosine_similarity(\mathbf{H}_艾德加, \mathbf{H}_阿方)
cos_艾德加_雅琪 = cosine_similarity(\mathbf{H}_艾德加, \mathbf{H}_雅琪)
cos_阿方_雅琪 = cosine_similarity(\mathbf{H}_阿方, \mathbf{H}_雅琪)

# 输出余弦值计算结果,保留四位有效数字
print("艾德加 & 阿方 的余弦值:", round(cos_艾德加_阿方, 4))
print("艾德加 & 雅琪 的余弦值:", round(cos_艾德加_雅琪, 4))
print("阿方 & 雅琪 的余弦值:", round(cos_阿方_雅琪, 4))

计算结果如下:

向量对余弦值
艾德加 & 阿方0.9988
艾德加 & 雅琪0.9748
阿方 & 雅琪0.9644

可以看到,三个结果数值上非常接近,这是因为在前面计算时并没有对不同位置的分量进行加权处理,比如生命值的数值都在量级,远大于那些取值范围在的描述性参数,若将3个英雄向量先进行归一化(我也不知道这个方法的具体名称以及是否严谨,暂时先这样算吧)处理再计算:

1
2
3
4
5
6
7
8
9
10
# 将向量堆叠为矩阵
\mathbf{H}_matrix = np.vstack([\mathbf{H}_艾德加, \mathbf{H}_阿方, \mathbf{H}_雅琪])

# 按列计算比例
\mathbf{H}_normalized = \mathbf{H}_matrix / \mathbf{H}_matrix.sum(axis=0)

# 计算归一化向量之间的余弦值
cos_艾德加_阿方 = cosine_similarity(\mathbf{H}_normalized[0], \mathbf{H}_normalized[1])
cos_艾德加_雅琪 = cosine_similarity(\mathbf{H}_normalized[0], \mathbf{H}_normalized[2])
cos_阿方_雅琪 = cosine_similarity(\mathbf{H}_normalized[1], \mathbf{H}_normalized[2])

得到计算结果如下:

向量对余弦值
艾德加 & 阿方0.8892
艾德加 & 雅琪0.8374
阿方 & 雅琪0.7509

这里余弦值越大表示夹角越小,则两个英雄的特征参数越接近。若定义一个阈值,比如说当两英雄余弦值大于0.85时,我们就可以把这两个英雄认定为同一类英雄。从上面的结果可以看出,艾德加和阿方都是“突袭者”,所以余弦值较大,而雅琪是“坦克”,与前两位英雄的余弦值明显小于他们之间的余弦值。

计算好余弦值后再进行一遍聚类,这样就可以把所有英雄都分好类。除了前面提到的“坦克”“突袭者”“射手”之外,还有“投弹手”“火力手”“控场”和“辅助”。

比赛函数

比赛可以看成是把英雄向量映射到某一实数的函数

这里的实数值越大可以理解为这个英雄在比赛当中获胜的概率越大,或者说在这场比赛当中的竞争力越大,或者说在这场比赛当中的适应性越高。其中中的是英雄向量的维数,前面的例子里。显然不同类型的地图以及模式对应着不同的函数。

但这么说还是有些抽象了,那么有没有办法找到一个确定的具体的函数呢?我的一个解决办法是让不同比赛都对应有一个单位方向向量 ,每个比赛函数就是把英雄向量往这个比赛相应的方向向量 上投影,然后取对应投影向量的长度作为衡量值大小的一个标准,最简单的方式就是直接定义

若一个英雄向量 和比赛方向向量之间的夹角越小,那么就说明这个英雄在这个比赛模式和地图下的适应程度越高,或者说和比赛期望的英雄属性是比较接近的。比如草丛掩体多的地图适合“突袭者”等近身攻击效果好的英雄,而空旷的地带则适合那些射手型英雄。 的选取与两个因素有关——地图和模式。

组队比赛

如果是双人或者三人甚至更多人的组队比赛,这时就需要涉及先把对应所有英雄的向量进行某种运算,比如进行相加,然后用函数映射。但是,如果只是单纯的相加求和向量,那么理论上讲属性越接近的英雄和向量的模长会越大,对应的 值也就越大,然而实际情况却是那些优势互补的组合往往获胜概率更大。

这时我们可以考虑求两英雄向量外积的模长作为 值,即

由于要面对不同比赛,所以我们需要修改一下前面关于比赛函数的定义。现在我们需要把比赛看作是作用于英雄向量的一个线性变换,即,对于单个英雄而言,

对于双人组队的英雄而言,

类似的,对于三人组队的情形,

小结

通过上面的分析可知,玩游戏《荒野乱斗》其实无非就是不断调整参数,去寻找使得特定的一场比赛取到最优解的一组参数(英雄),再包裹上一层炫彩华丽的画面和动感十足的音乐(音效),但其本质和数学建模还是非常相近的。唯一的区别在于游戏中有限数量的英雄的参数都已经被开发者设定好了,因此给人一种受制于人的感觉;而如果是用计算机进行数值模拟,我们的活动范围大大增加;若是进入到数学的理论层面,我们则可以动辄考虑各种无穷的极限情形,这正是数学的迷人之处。

最后,引用一下每个游戏都会有的《健康游戏忠告》:

抵制不良游戏,拒绝盗版游戏。注意自我保护,谨防受骗上当。

适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活!