本文介绍的算法仅对 v1.4.0 版本手动打牌时的推荐算法适用,对于 Contest、GvG 等自动打牌算法不适用。
对于最新版本 Contest、GvG 等自动打牌时 AI 的行为逻辑,请参照 「学園アイドルマスター」Contest 自动打牌算法解析。
由于三年前在解析爱普拉时曾经发现过 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
其中 rn 是计算最终 evaluation
值的 r1
~ r18
中任意变量。
vn 是下标与
rn 相对应的游戏里实际的状态值,也就是我们实际在游戏画面中能够看到的各项状态值,含义分别如下:
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×331
第二个 entry 表示这是在全自动打牌(目前只有 Contest)时,主打效果是やる気的偶像在还剩下 2 回合的时候每有 1 回合消费体力增加则减少 585 点 evaluation,即:
r11=min(v11, 2)×(−585)
注意这里因为只剩下 2 回合,所以即使 v11 大于 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(danceBonusPermil+vocalBonusPermil+visualBonusPermilv1×3000+0.0000999999975, 6)
注意最后要将得到的结果四舍五入取 6 位小数。
r19 特殊参数
特殊参数用于计算非直接效果的持续效果的 evaluation,比如「至高のエンタメ」「天真爛漫」这种不便于直接评估的卡片效果。
r19
由两个 multiplier 变量计算而得:
r19=n∑⌊m1n×m2n+0.0001⌋
其中,
m1n 是 multiplier1,
m2n 是 multiplier2,
下标 n 表示玩家所持有的第 n 个持续效果。
注意在累加之前要舍弃每个值的小数点后的值。
multiplier1
multiplier1
由如下计算方式而来:
m1=1000Pcoeff×T
其中,
Pcoeff 是主数据库 ProduceExamAutoTriggerEvaluation
中 coefficientPermil
的千分数,
T 是当前剩余回合数。
如果主数据库中不存在对应的 coefficientPermil
,则默认赋值 coefficientPermil = 1
。
multiplier2
multiplier2
由如下计算方式而来:
m2=1≤n≤18∑(rn′×1000Penchant)
其中,
rn′ 是持续效果在当前回合发动的实际效果由前述一般参数计算而得的 evaluation,
Penchant 是主数据库 ProduceExamAutoEvaluation
中对应的 examStatusEnchantCoefficientPermil
的千分数。
整个算法到此为止。
只看理论可能比较抽象,我们用几个游戏中实际场景来举例补充说明。
实例
例 1
这是 SP lesson 场,所以对应的 ExamPlayType
是 ManualPlayLessonHard
,剩余 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
这是 audition 场,所以对应的 ExamPlayType
是 ManualPlayAudition
,剩余 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 结束后的统计期间很有可能就是服务器在批量验证是否有异常举动的客户端的运算时间。