モンハンの装備の選び方を数理最適化で考える
この記事は数理最適化 Advent Calendar 2023の10日目の記事です。
モンスターハンターワールドというゲームの装備の組み合わせ最適化を整数計画法のソルバーを用いてチャレンジしてみたので、それについてまとめました。
本記事で作成したオプティマイザーは以下でWebアプリという形で公開しているので、是非試してみてください。
モンハンにおける装備選択問題
モンハンには非常に多くの種類の武器、防具、装飾品、護石が存在します。プレイヤーはこれらが作る無数の組み合わせの中から、狩りを有利に進めるために、自分にあったスキル構成の装備を選ぶ必要があります。普通は自分の感覚に任せて様々な組み合わせを試して装備作成を楽しむわけですが、自分が最強だと思った装備が無数にある組み合わせの中で必ずしも最強であるとは限りません。真の最強装備とは何でしょうか? コンピューターを使ってそれを見つけ、狩りを更に有利に進めよう! というのがこの記事の内容です。
何を最適化するか?
モンハンでは 欲しいスキルの集合 を最初に定め、それを満たすような装備の中で 出来るだけ大きな防御力 を持つような装備が欲しくなることが多いです。
例えば次のような形で欲しいスキルの集合を定義し、これらのスキルを満たすような装備の中で最大の防御力となるような装備の組み合わせを求めます。
スキル名 | スキルレベル |
---|---|
見切り | 7 |
超会心 | 3 |
弱点特攻 | 3 |
攻撃 | 5 |
以降ではこの問題を整数計画問題として定式化し、ソルバーで解ける形にしていきます。
いきなり装飾品を含めて考えるのは難しいので、まずは装飾品なしの場合で考えてみます。
装飾品なしの場合
定式化
欲しいスキルの集合を
次のような変数と定数を導入します。
-
:x_i^{helm} \in \{0, 1\} 番目の頭防具を選ぶことに対応する0-1変数i -
:v_i^{helm} \in \mathbb{N} 番目の頭防具の防御力を表す定数i -
:s_{i,j}^{helm} \in \mathbb{N} 番目の頭防具のi 番目のスキルレベルを表す定数j
ここで
腕防具(arms), 胴防具(chest), 腰防具(waist), 足防具(legs)、護石(charm)についても対応する変数、定数を同様に定義します。
装備の防御力
各装備は1つだけ選びたいので、次のような制約を導入します。
装備が欲しいスキルを持つという制約は次のような
ここでは左辺が装備のスキルレベルの合計を表し、右辺が必要なスキルレベルを表しています。
以上をまとめると、装飾品がない場合の装備最適化は次のような形で定式化出来ます[1]。
以下ではソルバーを用いてこの問題を解いてみます。
Pythonでの実装
pulpを使って上の問題を解いてみます。
# 最大化
problem = pulp.LpProblem("max-defense-skills", pulp.LpMaximize)
# 防御力を最大化する
problem += (
pulp.lpDot(v_helm, x_helm)
+ pulp.lpDot(v_arms, x_arms)
+ pulp.lpDot(v_chest, x_chest)
+ pulp.lpDot(v_waist, x_waist)
+ pulp.lpDot(v_legs, x_legs)
)
# 各防具を1つだけ選ぶ制約
problem += pulp.lpSum(x_helm) == 1
problem += pulp.lpSum(x_arms) == 1
problem += pulp.lpSum(x_chest) == 1
problem += pulp.lpSum(x_waist) == 1
problem += pulp.lpSum(x_legs) == 1
# 護石を1つだけ選ぶ制約
problem += pulp.lpSum(x_charm) == 1
# 装備のスキルレベルの制約
for i in range(len(skill_types)):
problem += (
pulp.lpDot(s_helm[i], x_helm)
+ pulp.lpDot(s_arms[i], x_arms)
+ pulp.lpDot(s_chest[i], x_chest)
+ pulp.lpDot(s_waist[i], x_waist)
+ pulp.lpDot(s_legs[i], x_legs)
+ pulp.lpDot(s_charm[i], x_charm)
>= required_skills[skill_types[i]]
)
result = problem.solve()
ここでは例として必要なスキルの集合を以下のように定めます。
スキル名 | レベル |
---|---|
見切り | 7 |
弱点特効 | 3 |
超会心 | 3 |
また、装備のレア度の範囲を6から8の間にあらかじめ限定しておきます[2]。
プログラムを走らせると以下のような結果が得られました。
Result - Optimal solution found
Objective value: 360.00000000
Enumerated nodes: 0
Total iterations: 0
Time (CPU seconds): 0.00
Time (Wallclock seconds): 0.00
Option for printingOptions changed from normal to all
Total time (CPU seconds): 0.00 (Wallclock seconds): 0.00
objective = 360.0
Build(helm=Armor(name='ゾラマグナヘッドγ',
rarity=8,
skills=[Skill(type=<SkillType.WIND_PROOF: '風圧耐性'>, level=2),
Skill(type=<SkillType.CRITICAL_EYE: '見切り'>, level=2)],
slots=[2, 0, 0],
defense_initial=72,
defense_max=78,
defense_custom=92,
element=Element(fire=4,
water=-3,
thunder=-1,
ice=-2,
dragon=-3)),
arms=Armor(name='カイザーアームγ',
rarity=8,
skills=[Skill(type=<SkillType.CRITICAL_EYE: '見切り'>, level=3)],
slots=[3, 0, 0],
defense_initial=72,
defense_max=78,
defense_custom=92,
element=Element(fire=3, water=-3, thunder=1, ice=-3, dragon=1)),
chest=Armor(name='マムガイラメイルα',
rarity=8,
skills=[Skill(type=<SkillType.STUN_RESISTANCE: '気絶耐性'>,
level=2),
Skill(type=<SkillType.CRITICAL_BOOST: '超会心'>,
level=2)],
slots=[1, 0, 0],
defense_initial=72,
defense_max=78,
defense_custom=92,
element=Element(fire=4,
water=-2,
thunder=3,
ice=-4,
dragon=-2)),
waist=Armor(name='マムガイラコイルγ',
rarity=8,
skills=[Skill(type=<SkillType.CRITICAL_BOOST: '超会心'>,
level=1),
Skill(type=<SkillType.WEAKNESS_EXPLOIT: '弱点特効'>,
level=1)],
slots=[3, 0, 0],
defense_initial=72,
defense_max=78,
defense_custom=92,
element=Element(fire=4,
water=-2,
thunder=3,
ice=-4,
dragon=-2)),
legs=Armor(name='カイザーグリーヴγ',
rarity=8,
skills=[Skill(type=<SkillType.CRITICAL_EYE: '見切り'>, level=2)],
slots=[3, 1, 0],
defense_initial=72,
defense_max=78,
defense_custom=92,
element=Element(fire=3, water=-3, thunder=1, ice=-3, dragon=1)),
decorations=[],
charm=Charm(name='痛撃の護石Ⅱ',
rarity=8,
skills=[Skill(type=<SkillType.WEAKNESS_EXPLOIT: '弱点特効'>,
level=2)]))
出来上がった装備を眺めると確かに必要なスキルが満たされていることが分かります。また、組み合わせの数が膨大なのにも関わらず、非常に高速に解くことが出来ました。
装飾品を入れた場合
定式化
装飾品を入れると問題は少し複雑になりますが、同様に整数計画問題として定式化することが出来ます。
次のような変数と定数を追加で導入します。
-
:y_{i,j} \in \mathbb{N} 番目のレベルi 装飾品を何個選ぶかを表す変数j -
:d_{i,j,k} \in \mathbb{N} 番目のレベルi 装飾品のj 番目のスキルレベルを表す定数k -
:l_{i,j}^{helm} \in \mathbb{N} 番目の頭防具がレベルi 装飾品スロットを何個持っているかを表す定数j
ここで
腕防具(arms), 胴防具(chest), 腰防具(waist), 足防具(legs)、護石(charm)についても対応する変数、定数を同様に定義します。
目的関数は装飾品なしの場合と同じになります[3]。
次に制約ですが、装飾品ありの場合には装飾品スロット数に関する制約が加わるのと、スキルレベルに関する制約に装飾品の効果が加わってきます。
まず、レベル4装飾品スロットの制約は以下のように表せます。
ここで左辺は使おうとしているレベル4装飾品スロット数、右辺は実際に装備できるレベル4装飾品スロット数を表しています[4]。
続いてレベル3装飾品スロットの制約は以下のように表せます。
レベル3装飾品はレベル4装飾品スロットにもつけることが出来るので、制約条件はレベル3装飾品スロット数とレベル4装飾品スロット数の合計が装備のそれを超えないという条件になります。
レベル2、レベル1装飾品スロット数の制約も同様に書いていくことができ、それより上のレベルの装飾品スロット数の合計に関しての制約条件として記述されます。
最後にスキルレベルの制約ですが、以下のように表せます。
装飾品なしの場合と異なるのは、左辺に
以上をまとめると、装飾品がある場合の装備最適化は以下のような形で定式化出来ます。
Pythonでの実装
# 最大化
problem = pulp.LpProblem("max-defense-skills", pulp.LpMaximize)
# 防御力を最大化する
problem += (
pulp.lpDot(v_helm, x_helm)
+ pulp.lpDot(v_arms, x_arms)
+ pulp.lpDot(v_chest, x_chest)
+ pulp.lpDot(v_waist, x_waist)
+ pulp.lpDot(v_legs, x_legs)
)
# 各防具を1つだけ選ぶ制約
problem += pulp.lpSum(x_helm) == 1
problem += pulp.lpSum(x_arms) == 1
problem += pulp.lpSum(x_chest) == 1
problem += pulp.lpSum(x_waist) == 1
problem += pulp.lpSum(x_legs) == 1
# 護石を1つだけ選ぶ制約
problem += pulp.lpSum(x_charm) == 1
# Lv4スロットの合計数
M_4 = (
pulp.lpDot(l_helm[3, :], x_helm)
+ pulp.lpDot(l_arms[3, :], x_arms)
+ pulp.lpDot(l_chest[3, :], x_chest)
+ pulp.lpDot(l_waist[3, :], x_waist)
+ pulp.lpDot(l_legs[3, :], x_legs)
)
# Lv3スロットの合計数
M_3 = (
pulp.lpDot(l_helm[2, :], x_helm)
+ pulp.lpDot(l_arms[2, :], x_arms)
+ pulp.lpDot(l_chest[2, :], x_chest)
+ pulp.lpDot(l_waist[2, :], x_waist)
+ pulp.lpDot(l_legs[2, :], x_legs)
)
# Lv2スロットの合計数
M_2 = (
pulp.lpDot(l_helm[1, :], x_helm)
+ pulp.lpDot(l_arms[1, :], x_arms)
+ pulp.lpDot(l_chest[1, :], x_chest)
+ pulp.lpDot(l_waist[1, :], x_waist)
+ pulp.lpDot(l_legs[1, :], x_legs)
)
# Lv1スロットの合計数
M_1 = (
pulp.lpDot(l_helm[0, :], x_helm)
+ pulp.lpDot(l_arms[0, :], x_arms)
+ pulp.lpDot(l_chest[0, :], x_chest)
+ pulp.lpDot(l_waist[0, :], x_waist)
+ pulp.lpDot(l_legs[0, :], x_legs)
)
# Lv4装飾品スロットの制約
problem += pulp.lpSum(y_lv4) <= M_4
# Lv3装飾品スロットの制約
problem += pulp.lpSum(y_lv4) + pulp.lpSum(y_lv3) <= M_4 + M_3
# Lv2装飾品スロットの制約
problem += (
pulp.lpSum(y_lv4) + pulp.lpSum(y_lv3) + pulp.lpSum(y_lv2) <= M_4 + M_3 + M_2
)
# Lv1装飾品スロットの制約
problem += (
pulp.lpSum(y_lv4)
+ pulp.lpSum(y_lv3)
+ pulp.lpSum(y_lv2)
+ pulp.lpSum(y_lv1)
<= M_4 + M_3 + M_2 + M_1
)
# 装備のスキルレベルの制約条件
for i in range(len(skill_types)):
problem += (
pulp.lpDot(s_helm[i, :], x_helm)
+ pulp.lpDot(s_arms[i, :], x_arms)
+ pulp.lpDot(s_chest[i, :], x_chest)
+ pulp.lpDot(s_waist[i, :], x_waist)
+ pulp.lpDot(s_legs[i, :], x_legs)
+ pulp.lpDot(s_charm[i, :], x_charm)
+ pulp.lpDot(s_lv1[i, :], y_lv1)
+ pulp.lpDot(s_lv2[i, :], y_lv2)
+ pulp.lpDot(s_lv3[i, :], y_lv3)
+ pulp.lpDot(s_lv4[i, :], y_lv4)
>= self.required_skills[skill_types[i]]
)
実行してみましょう。今度はスキルを以下のように盛りだくさんにしてみます。
スキル名 | レベル |
---|---|
見切り | 7 |
弱点特効 | 3 |
超会心 | 3 |
攻撃 | 7 |
体力増強 | 3 |
以下のような結果を得ました。
Result - Optimal solution found
Objective value: 360.00000000
Enumerated nodes: 0
Total iterations: 0
Time (CPU seconds): 0.00
Time (Wallclock seconds): 0.00
Option for printingOptions changed from normal to all
Total time (CPU seconds): 0.00 (Wallclock seconds): 0.00
objective = 360.0
M_1 = 2.0
M_2 = 5.0
M_3 = 2.0
M_4 = 0
sum(y_lv1) = 4.0
sum(y_lv2) = 5.0
sum(y_lv3) = 0
sum(y_lv4) = 0
Build(helm=Armor(name='カイザークラウンγ',
rarity=8,
skills=[Skill(type=<SkillType.CRITICAL_EYE: '見切り'>, level=2)],
slots=[2, 2, 0],
defense_initial=72,
defense_max=78,
defense_custom=92,
element=Element(fire=3, water=-3, thunder=1, ice=-3, dragon=1)),
arms=Armor(name='オーグアームγ',
rarity=8,
skills=[Skill(type=<SkillType.PARTBREAKER: '破壊王'>, level=1),
Skill(type=<SkillType.ATTACK_BOOST: '攻撃'>, level=1)],
slots=[2, 1, 1],
defense_initial=72,
defense_max=78,
defense_custom=92,
element=Element(fire=1, water=1, thunder=-3, ice=1, dragon=-3)),
chest=Armor(name='ドラケンメイルα',
rarity=8,
skills=[Skill(type=<SkillType.CRITICAL_EYE: '見切り'>, level=2),
Skill(type=<SkillType.CRITICAL_BOOST: '超会心'>,
level=1)],
slots=[3, 0, 0],
defense_initial=72,
defense_max=78,
defense_custom=92,
element=Element(fire=-2,
water=-2,
thunder=3,
ice=-2,
dragon=4)),
waist=Armor(name='ドラケンコイルα',
rarity=8,
skills=[Skill(type=<SkillType.CRITICAL_EYE: '見切り'>, level=2),
Skill(type=<SkillType.POWER_PROLONGER: '強化持続'>,
level=1)],
slots=[3, 0, 0],
defense_initial=72,
defense_max=78,
defense_custom=92,
element=Element(fire=-2,
water=-2,
thunder=3,
ice=-2,
dragon=4)),
legs=Armor(name='オーググリーヴγ',
rarity=8,
skills=[Skill(type=<SkillType.ATTACK_BOOST: '攻撃'>, level=3)],
slots=[2, 2, 0],
defense_initial=72,
defense_max=78,
defense_custom=92,
element=Element(fire=1, water=1, thunder=-3, ice=1, dragon=-3)),
decorations=[Decoration(name='体力珠',
rarity=6,
slot=1,
skills=[Skill(type=<SkillType.HEALTH_BOOST: '体力増強'>,
level=1)]),
Decoration(name='体力珠',
rarity=6,
slot=1,
skills=[Skill(type=<SkillType.HEALTH_BOOST: '体力増強'>,
level=1)]),
Decoration(name='体力珠',
rarity=6,
slot=1,
skills=[Skill(type=<SkillType.HEALTH_BOOST: '体力増強'>,
level=1)]),
Decoration(name='達人珠',
rarity=6,
slot=1,
skills=[Skill(type=<SkillType.CRITICAL_EYE: '見切り'>,
level=1)]),
Decoration(name='痛撃珠',
rarity=6,
slot=2,
skills=[Skill(type=<SkillType.WEAKNESS_EXPLOIT: '弱点特効'>,
level=1)]),
Decoration(name='痛撃珠',
rarity=6,
slot=2,
skills=[Skill(type=<SkillType.WEAKNESS_EXPLOIT: '弱点特効'>,
level=1)]),
Decoration(name='痛撃珠',
rarity=6,
slot=2,
skills=[Skill(type=<SkillType.WEAKNESS_EXPLOIT: '弱点特効'>,
level=1)]),
Decoration(name='超心珠',
rarity=8,
slot=2,
skills=[Skill(type=<SkillType.CRITICAL_BOOST: '超会心'>,
level=1)]),
Decoration(name='超心珠',
rarity=8,
slot=2,
skills=[Skill(type=<SkillType.CRITICAL_BOOST: '超会心'>,
level=1)])],
charm=Charm(name='攻撃の護石Ⅲ',
rarity=7,
skills=[Skill(type=<SkillType.ATTACK_BOOST: '攻撃'>, level=3)]))
装飾品が加わっていることが分かると思います。計算してみると装飾品とスロット数は一致し、必要なスキルも満たされていることが分かります。結構スキルをモリモリをしたのに解があることに少し驚きました。
Webアプリの方ではこれと同様の装飾品がある場合の最適化を解くことが出来るので、是非パラメーターを変えて色々試してもらえればと思います。
Webアプリについて
Webアプリとして公開するために今回streamlitというフレームワークを用いました。streamlitはデータサイエンス向けのWebフレームワークで、pandasやmatplotlibなどを使ったデータサイエンスっぽい可視化などを行うWebアプリを爆速で開発することが出来ます。今回は最適化部分に関してはローカルで実行出来るものを事前に作っていたのですが、せっかく記事を書くなら使ってもらえるようにしようということで、streamlitでWebアプリ化してみました。たった1-2時間でそれなりの見た目の動くアプリが作れてしまったのでstreamlit最高!という感じです。とても良いので是非使ってみてください。
Discussion