「学マス」自动出牌算法解析

本文介绍的算法根据 v1.4.0 版本游戏 APK 逆向解析获取,不排除人为解析失误以及后续版本算法被修改的情形。
实际上本算法在游戏内从 v1.0.0 至今也经过了数次变化修改。
如果您在实践中发现游戏内行为出现与本文介绍的算法相违的情况,则很有可能是出现了以上的情形,欢迎提供样本联系我进行进一步解析与修正。

由于三年前在解析爱普拉时曾经发现过 QA 有将 live 得分运算放在服务端执行的前科,所以导致我从学玛仕开服以来一直以为学玛仕 contest 的自动打牌运算也是在服务端执行,客户端所做的仅仅是将服务端计算好的结果展示出来而已,但最近却发现事情并不是想象中的那样。
最初注意到这个问题是在 Produce 中打牌的时候。细心的 P 们会发现,在出牌阶段如果将画面静止不动放置 5 秒以上,会有一张手牌边框出现橙色的闪烁特效,这张卡即是游戏自动打牌算法推荐使用的卡片。然而通过观察流量可以发现,每场 lesson(或 audition)只在进入时和结算时会产生与服务器的通信,这也就是说自动打牌算法必定在客户端有实现,于是我便开始在游戏代码中寻找线索,最终确定目标方法是 Campus.InGame.Exam.ExamRuleCalculator.Evaluate

预备知识

在解析核心算法之前首先需要了解自动打牌的实现方法。
在 QA 设计的自动打牌机制里有一个十分重要的变量叫做 evaluation,代表着该局游戏整体状态的评估值,这个值越大说明当前整场游戏的状况越好,反之则越差。
QA 设计的自动打牌机制很简单:预先分别计算各枚手牌打出后的 evaluation,哪个值越大则打哪张。
所以整个算法的核心其实是在于如何求出 evaluation 的值。

算法

v1.4.0 版本中 evaluation 的值由 19 个参数的和确定,为了方便这里我们将这 19 个参数取名为 r1 ~ r19

再次强调,本算法的实现方式与游戏版本强相关。例如 v1.0.0 版本中 evaluation 的参数就只有 15 个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
evaluation = math.floor(
r1_JudgeParameter
+ r2_Block
+ r3_Stamina
+ r4_LessonBuff
+ r5_Review
+ r6_Aggressive
+ r7_min_remain_ParameterBuffTurn
+ r8_ParameterBuffTurn
+ r9_min_remain_ParameterBuffMultiplePerTurn
+ r10_min_remain_StaminaConsumptionDownTurn
+ r11_min_remain_StaminaConsumptionAddTurn
+ r12_min_remain_BlockAddDown
+ r13_LessonDebuff
+ r14_min_remain_ParameterDebuff
+ r15_BlockAddDownFix
+ r16_min_remain_SlumpTurn
+ r17_PlayableCards
+ r18_ExtraTurn
+ r19
+ 0.0000999999975
)

其中我们将 r1 ~ r18 称为一般参数,r19 称为特殊参数。

一般参数(r1 ~ r18)

一般参数计算方式如下:

rn=vn×ProduceExamAutoEvaluation.evaluation\boxed{r_n = v_n \times \mathrm{ProduceExamAutoEvaluation.evaluation}}

其中 rnr_n 是计算最终 evaluation 值的 r1 ~ r18 中任意变量。

vnv_n 是下标与 rnr_n 相对应的游戏里实际的状态值,也就是我们实际在游戏画面中能够看到的各项状态值,含义分别如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
v1: 当前已获得的总分
v2: 当前元气值
v3: 当前体力值
v4: 当前集中值
v5: 当前好印象值
v6: 当前やる気值
v7: min(当前好调回合数, 剩余回合数)
v8: 当前好调回合数
v9: min(当前绝好调回合数,剩余回合数)
v10: min(当前体力消费减少回合数, 剩余回合数)
v11: min(当前体力消费增加回合数, 剩余回合数)
v12: min(当前不安回合数, 剩余回合数)
v13: 当前集中减少值
v14: min(当前不调回合数, 剩余回合数)
v15: 当前弱气值
v16: 当前スランプ回合数
V17: 当前可使用卡片次数
v18: 额外追加回合数

ProduceExamAutoEvaluation.evaluation 是主数据库 ProduceExamAutoEvaluation 中与当前状况相对应的 field 名为 evaluation 的值。这里随机从主数据中抽选两个 entry 作为例子进行解说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
- type: ExamPlayType_ManualPlayAudition
examEffectType: ProduceExamEffectType_ExamLessonBuff
remainingTerm: 6
evaluationType: ProduceExamAutoEvaluationType_Parameter
evaluation: 331
examStatusEnchantCoefficientPermil: 1000
...
- type: ExamPlayType_AutoPlay
examEffectType: ProduceExamEffectType_ExamCardPlayAggressive
remainingTerm: 2
evaluationType: ProduceExamAutoEvaluationType_ExamStaminaConsumptionAdd
evaluation: -585
examStatusEnchantCoefficientPermil: 0
...

第一个 entry 表示这是手动玩 audition 时,主打效果是集中的偶像在还剩下 6 回合的时候每获得 1 分则增加 331 点 evaluation,即:

r1=v1×331r_1 = v_1 \times 331

第二个 entry 表示这是在全自动打牌(目前只有 Contest)时,主打效果是やる気的偶像在还剩下 2 回合的时候每有 1 回合消费体力增加则减少 585 点 evaluation,即:

r11=min(v11, 2)×(585)r_{11} = \min(v_{11},\ 2) \times (-585)

注意这里因为只剩下 2 回合,所以即使 v11v_{11} 大于 2,最大也只取 2。

关于 ExamPlayType,目前游戏中定义了四种类型:

1
2
3
4
5
6
7
enum ExamPlayType {
Unknown = 0;
AutoPlay = 1;
ManualPlayLesson = 2;
ManualPlayLessonHard = 3;
ManualPlayAudition = 4;
}

分别是全自动(Contest),手动 lesson,手动 SP lesson,手动 audition。

细心的读者可能注意到还有一个 examStatusEnchantCoefficientPermil 没有用到,这个值我们留在特殊参数中介绍。

r1 在 battle 中的特殊情况

当且仅当计算 v1 时,会有一个补充条件分支。
如果该局游戏是 lesson,则直接使用当前已获分数;如果该局游戏是 audition 或者 contest 之类的 battle,则由如下公式计算:

r1=round(v1×3000danceBonusPermil+vocalBonusPermil+visualBonusPermil+0.0000999999975, 6)\boxed{r_1 = \mathrm{round(}\frac {v_1 \times 3000} {\mathrm{danceBonusPermil} + \mathrm{vocalBonusPermil} + \mathrm{visualBonusPermil}} + 0.0000999999975 \mathrm{,\ 6)}}

注意最后要将得到的结果四舍五入取 6 位小数。

特殊参数(r19)

特殊参数用于计算非直接效果的持续效果的 evaluation,比如「至高のエンタメ」「天真爛漫」这种不便于直接评估的卡片效果。

r19 由两个 multiplier 变量计算而得:

r19=multiplier1×multiplier2+0.0001\boxed{r_{19} = \mathrm{multiplier1} \times \mathrm{multiplier2} + 0.0001}

其中,multiplier1 由主数据库 ProduceExamAutoTriggerEvaluationcoefficientPermil 千分比的值与当前剩余回合数的积而来:

multiplier1=coefficientPermil1000×remainingTurns\boxed{\mathrm{multiplier1} = \frac {\mathrm{coefficientPermil}} {1000} \times \mathrm{remainingTurns}}

如果主数据库中不存在对应的 coefficientPermil,则默认赋值 coefficientPermil = 1

multiplier2 由所有持续效果在当前回合发动实际效果的 evaluation 与前文提到的主数据库 ProduceExamAutoEvaluationexamStatusEnchantCoefficientPermil 的千分比的积而来:

multiplier2=1n18(rn×examStatusEnchantCoefficientPermil1000)\boxed{\begin{equation} \begin{split} \mathrm{multiplier2} &=\sum_{\mathclap{1\le n\le 18}}(r_n \times \frac {\mathrm{examStatusEnchantCoefficientPermil}} {1000}) \end{split} \end{equation} }

最后,根据前述的算式把 r1 ~ r19 全部加起来再加一个常量后舍弃小数点以下的位即得出最终 evaluation。

整个算法到此为止。
只看理论可能比较抽象,我们用几个游戏中实际场景来举例补充说明。

实例

例 1

image

这是 SP lesson 场,所以对应的 ExamPlayTypeManualPlayLessonHard,剩余 5 回合,角色主打效果是好印象。从以上信息找到主数据库 ProduceExamAutoEvaluation 中对应的 entries:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- type: ExamPlayType_ManualPlayLessonHard
examEffectType: ProduceExamEffectType_ExamReview
remainingTerm: 5
evaluationType: ProduceExamAutoEvaluationType_Parameter
evaluation: 301
examStatusEnchantCoefficientPermil: 1000
- type: ExamPlayType_ManualPlayLessonHard
examEffectType: ProduceExamEffectType_ExamReview
remainingTerm: 5
evaluationType: ProduceExamAutoEvaluationType_Block
evaluation: 86
examStatusEnchantCoefficientPermil: 12
- type: ExamPlayType_ManualPlayLessonHard
examEffectType: ProduceExamEffectType_ExamReview
remainingTerm: 5
evaluationType: ProduceExamAutoEvaluationType_Stamina
evaluation: 100
examStatusEnchantCoefficientPermil: 1000
- type: ExamPlayType_ManualPlayLessonHard
examEffectType: ProduceExamEffectType_ExamReview
remainingTerm: 5
evaluationType: ProduceExamAutoEvaluationType_ExamReview
evaluation: 2158
examStatusEnchantCoefficientPermil: 834
- type: ExamPlayType_ManualPlayLessonHard
examEffectType: ProduceExamEffectType_ExamReview
remainingTerm: 5
evaluationType: ProduceExamAutoEvaluationType_ExamCardPlayAggressive
evaluation: 290
examStatusEnchantCoefficientPermil: 3
- type: ExamPlayType_ManualPlayLessonHard
examEffectType: ProduceExamEffectType_ExamReview
remainingTerm: 5
evaluationType: ProduceExamAutoEvaluationType_PlayableValueAdd
evaluation: 6900
examStatusEnchantCoefficientPermil: 1000

再看当前状态。
画面提示还差 60 分 clear,也就是说已获得分是 25,好印象 17,やる気 4,体力 14,元气 8。

然后计算打出各张卡后的 evaluation。

左卡

如果打出左卡,已获得分变为 39,元气变为 5。这时 r1 ~ r19 中不为 0 的值有:

1
2
3
4
5
r1 = 39 * 301 = 11739
r2 = 5 * 323 = 1615
r3 = 14 * 1000 = 14000
r5 = 17 * 2158 = 36686
r6 = 4 * 543 = 2172

所以最终 evaluation 值为:

1
2
evaluation = math.floor(r1 + r2 + r3 + r5 + r6 + 0.0000999999975)
= 66212

中卡

如果打出中间卡,やる気降为 1,元气变为 10,追加使用卡片次数 + 1,同时会赋予特殊效果。这时 r1 ~ r18 中不为 0 的值有:

1
2
3
4
5
6
r1 = 25 * 301 = 7525
r2 = 10 * 323 = 3230
r3 = 14 * 1000 = 14000
r5 = 17 * 2158 = 36686
r6 = 1 * 543 = 543
r17 = 1 * 6900 = 6900

特殊值 r19 由赋予的效果决定。輝くキミへ+++赋予的特殊效果是「以降、スキルカード使用時、好印象の50%分パラメータ上昇」。「スキルカード使用時」对应的 coefficientPermil 在主数据库中不存在(是的,不存在!),这种情况下的逻辑是赋予 coefficientPermil = 1

那么对应的 multiplier1 就等于:

1
multiplier1 = 1 / 1000 * 5 = 0.005

赋予的「好印象の50%分パラメータ上昇」在当前回合如果发动,获得的得分是:

1
2
v1 = math.ceiling(17 * 0.5)
= 9

multiplier2 等于:

1
2
3
multiplier2 = r1 * examStatusEnchantCoefficientPermil / 1000
= 9 * 301 * 1000 / 1000
= 2709

然后可得出 r19

1
2
r19 = multiplier1 * multiplier2 + 0.0001
= 13.5451

最终 evaluation 结果:

1
2
evaluation = math.floor(r1 + r2 + r3 + r5 + r6 + r17 + r19 + 0.0000999999975)
= 68897

由以上计算可以看出,由于主数据库中目前不存在「輝くキミへ」对应的 coefficientPermil,所以导致该效果的评价值大大降低,也就是说系统很难自动选择打出该卡。尚不清楚这是 QA 故意设计还是 bug。

右卡

如果打出右卡,好印象将为 15,やる気变为 8,追加使用卡片次数 + 1。这时 r1 ~ r19 中不为 0 的值有:

1
2
3
4
5
6
r1 = 25 * 301 = 7525
r2 = 8 * 323 = 2584
r3 = 14 * 1000 = 14000
r5 = 15 * 2158 = 32370
r6 = 8 * 543 = 4344
r17 = 1 * 6900 = 6900

所以最终 evaluation 值为:

1
2
evaluation = math.floor(r1 + r2 + r3 + r5 + r6 + r17 + 0.0000999999975)
= 67723

比较

最终 68897 > 67723 > 66212,中间的卡最大,所以系统提示打中间的卡。

例 2

image

这是 audition 场,所以对应的 ExamPlayTypeManualPlayAudition,剩余 9 回合,角色主打效果是集中。从以上信息找到主数据库 ProduceExamAutoEvaluation 中对应的 entries:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- type: ExamPlayType_ManualPlayAudition
examEffectType: ProduceExamEffectType_ExamLessonBuff
remainingTerm: 9
evaluationType: ProduceExamAutoEvaluationType_Parameter
evaluation: 197
examStatusEnchantCoefficientPermil: 1000
- type: ExamPlayType_ManualPlayAudition
examEffectType: ProduceExamEffectType_ExamLessonBuff
remainingTerm: 9
evaluationType: ProduceExamAutoEvaluationType_Block
evaluation: 362
examStatusEnchantCoefficientPermil: 801
- type: ExamPlayType_ManualPlayAudition
examEffectType: ProduceExamEffectType_ExamLessonBuff
remainingTerm: 9
evaluationType: ProduceExamAutoEvaluationType_Stamina
evaluation: 1000
examStatusEnchantCoefficientPermil: 1000
- type: ExamPlayType_ManualPlayAudition
examEffectType: ProduceExamEffectType_ExamLessonBuff
remainingTerm: 9
evaluationType: ProduceExamAutoEvaluationType_ExamLessonBuff
evaluation: 2518
examStatusEnchantCoefficientPermil: 1000
- type: ExamPlayType_ManualPlayAudition
examEffectType: ProduceExamEffectType_ExamLessonBuff
remainingTerm: 9
evaluationType: ProduceExamAutoEvaluationType_ExamParameterBuff
evaluation: 1250
examStatusEnchantCoefficientPermil: 1000

再看当前状态。
已得分 1092,集中 12,好调 4,得分 50% 增加效果不被评估所以无视掉。
截图中没有显示但该场的三属性得分奖励千分率分别是 Vo 7620‰,Da 14840‰, Vi 15530‰。

然后计算打出各张卡后的 evaluation。

左卡

如果打出左卡(至高のエンタメ+),集中降低为 10,但会赋予特殊效果。这时
r1 ~ r18 中不为 0 的值有:

1
2
3
4
5
6
7
8
9
10
11
r1 = [1092 * 3000 / (7620 + 14840 + 15530) + 0.0000999999975] * 197
= 86.233319 * 197
= 16987.963896
r3 = 21 * 1000
= 21000
r4 = 10 * 2518
= 25180
r7 = 4 * 1250
= 5000
r8 = 4 * 1
= 4

特殊值 r19 由赋予的效果决定。至高のエンタメ+赋予的效果是「次のターン、スキルカードを引く。以降、アクティブスキルカード使用時、パラメータ+5」。抽卡不在评估效果中所以无视掉。「アクティブスキルカード使用時」对应的 coefficientPermil 可以在主数据库中找到:

1
2
3
- type: ExamPlayType_ManualPlayAudition
examStatusEnchantProduceExamTriggerId: e_trigger-exam_card_play-p_card_search-active_skill-playing-0_1
coefficientPermil: 900

那么对应的 multiplier1 就等于:

1
multiplier1 = 900 / 1000 * 9 = 8.1

赋予的「パラメータ+5」在当前回合如果发动,获得的得分是:

1
2
v1 = math.ceiling(math.ceiling((5 + 10) * 1.5 * 1.5) * 15.53)
= 529

注意这里乘了两个 1.5,一个是好调效果,另一个是 50% 得分上升的效果。

multiplier2 等于:

1
2
3
multiplier2 = r1 * examStatusEnchantCoefficientPermil / 1000
= 529 * 197 * 1000 / 1000
= 104213

然后可得出 r19

1
2
r19 = multiplier1 * multiplier2 + 0.0001
= 844125.3001

最终 evaluation 结果:

1
2
evaluation = math.floor(r1 + r3 + r4 + r7 + r8 + r19 + 0.0000999999975)
= 912297

注意这里当持续效果中包含得分效果时,虽然该局游戏是 battle,但 QA 的算法中没有对其进行三属性得分奖励平衡处理计算。这使得如果某一张卡片中一旦具有持续得分的效果,那么在 battle 游戏局中该卡的 evaluation 将会变得异常大,也就是说系统选择打出该卡的概率会非常之高。
暂不清楚这是 QA 故意设计的还是 bug。

中卡

打出后集中变为 17层,体力变为 23。这时 r1 ~ r19 中不为 0 的值有:

1
2
3
4
5
6
7
8
9
10
11
r1 = [1092 * 3000 / (7620 + 14840 + 15530) + 0.0000999999975] * 197
= 86.233319 * 197
= 16987.963896
r3 = 23 * 1000
= 23000
r4 = 17 * 2518
= 42806
r7 = 4 * 1250
= 5000
r8 = 4 * 1
= 4

最终 evaluation 值为:

1
2
evaluation = math.floor(r1 + r3 + r4 + r7 + r8 + 0.0000999999975)
= 87797

右卡

打出后得分变为 1729,体力 17,集中变为 15 层。这时 r1 ~ r19 中不为 0 的值有:

1
2
3
4
5
6
7
8
9
10
11
r1 = [1729 * 3000 / (7620 + 14840 + 15530) + 0.0000999999975] * 197
= 136.536031 * 197
= 26897.598107
r3 = 17 * 1000
= 17000
r4 = 15 * 2518
= 37770
r7 = 4 * 1250
= 5000
r8 = 4 * 1
= 4

最终 evaluation 值为:

1
2
evaluation = math.floor(r1 + r3 + r4 + r7 + r8 + 0.0000999999975)
= 86671

比较

最终 912297 > 87797 > 86671,左卡最大,所以系统提示打左卡。

个人感想

由以上分析我们可以看出,整个自动打牌算法采用了类似机器学习的模式,为每种效果赋予了一定的权重,最后将所有效果与权重的积加起来即得到打出某张卡后的评价值。
但遗憾的是,(至少目前来看)整个算法漏洞百出,从例一的解说可以解释为什么很多玩家注意到在 Contest 里「輝くキミへ」很少被 AI 打出的原因。之前还看到 QA 在某个 presentation 里吹「社内开发的自动打牌系统打得比社内员工手动还高」,只能说 QA 社内员工打牌太菜(。

另外,虽然自动打牌的运算是在客户端进行的,但这并不代表我们可以随便乱搞黑科技。实际上每一局开始时服务端会随机生成一个 seed 值,并将这个 seed 发给客户端,客户端会根据这个值生成发牌顺序。也就是说发牌顺序是由服务端预先决定的,同时客户端和服务端应该会共享一套自动打牌算法,一旦客户端做出与服务端计算结果不同的出牌举动服务端是能够感知的。每期 Contest 结束后的统计期间很有可能就是服务器在批量验证是否有异常举动的客户端的运算时间。

 Comments
Comment plugin failed to load
Loading comment plugin