Python：キンスレPT構成最適化

# はじめに

　本投稿では趣味で遊んでいる [King's Raid](https://kings-raid.com/) のPT構成を最適化しようと組んだ実行コードを紹介する。趣味にプログラムを応用するのはモチベーションも高くできて、いい勉強になるしおススメ.+.(ﾉ･ω･)ﾉ*.

# 1. 準備
## 1.1 Import

はじめのおまじないを唱えます(∩｡･ｏ･｡)っ.ﾟ☆｡'`

```python
import itertools
import os

import numpy as np
from matplotlib import pyplot as plt
import pandas as pd
```

## 1.2 関数定義
今回使う関数を定義しておく

```python
def list2dic(x):
    """ 数値のリストを辞書に変換（キャラとか攻撃上昇などのラベルを付ける）
    :param x: リスト
    :return: 辞書
    """
    dic = {
        'キャラ': x[0],
        '攻撃上昇': x[1],
        '攻撃速度': x[2],
        '攻撃値': x[3],
        'クリダメ': x[4],
        '防御貫通': x[5],
        '与ダメ': x[6],
        '被ダメ': x[7],
        '防御低下': x[8]
    }
    return dic


def damage_rate(df, a=1.0):
    """ DataFrame（PT構成）からダメージ上昇率をざっくり計算する
    ※ 攻撃値加算がダメージに与える影響が不明だったので係数aで調整できるようにしておいた(´っ･ω･)っ

    :param df: DataFrame（PT構成）
    :param a: 攻撃値加算がダメージに与える影響
    :return: ダメージ上昇率
    """
    f = lambda x: sum(x) # 上昇率を計算する
    return (1 + f(df['攻撃上昇']) + f(df['攻撃値']) * a) * (2 + f(df['クリダメ'])) * (1 + f(df['与ダメ'])) * (1 + f(df['被ダメ']))


def damage_rate_check(df, a=1.0):
    """ DataFrame（PT構成）から各上昇率をざっくり計算する
    ※ 攻撃値加算がダメージに与える影響が不明だったので係数aで調整できるようにしておいた(´っ･ω･)っ

    :param df: DataFrame（PT構成）
    :param a: 攻撃値加算がダメージに与える影響
    :return: 各上昇率
    """
    f = lambda x: sum(x) # 上昇率を計算する
    data = np.array([(1 + f(df['攻撃上昇']) + f(df['攻撃値']) * a), (2 + f(df['クリダメ'])), (1 + f(df['与ダメ'])), (1 + f(df['被ダメ']))])
    a = np.array([1/2, 1/4, 1])
    plt.figure(figsize=(10, 4))
    plt.bar(['ATK', 'CD', 'GSD', 'TD'], data, width=0.3)
    plt.bar(['ATK', 'CD', 'GSD'], data[:3]*a, width=0.3)
    plt.show()
    return dict(zip(['ATK', 'CD', 'GSD', 'TD'], data))


def pick(chara, df):
    """ 指定したキャラをピックしてDataFrame（PT構成）を作る
    
    :param chara: ピックするキャラ
    :param df: キャラ全体
    :return: PT構成
    """
    return df[[c in chara for c in df['キャラ']]]


def chara_combinations(chara, num):
    """ キャラ全体からnum人ピックしてできるPTの全パターンを作る

    :param chara: キャラ全体
    :param num: ピック人数
    :return: PTの全パターン
    """
    return list(itertools.combinations(chara, num))


def concat_skill(df, skill):
    """ スキルを疑似的なPTメンバとして追加する

    :param df: PT構成
    :param skill: スキル
    :return: PT構成＋スキル
    """
    dic = list2dic(['サポスキル']+skill.tolist())
    return pd.concat([df, pd.DataFrame([dic])], ignore_index=True)


def analysis(df, base, relics, skill, num=6):
    """ 最適PTを分析する

    :param df: PT構成
    :param base: 基礎バフ
    :param relics: DD遺物
    :param skill: DDスキル
    :return: 全パターンのDataFrame, 全パターンの倍率, 倍率高い順のインデックス
    """
    pt_df = [pick(pt, df) for pt in chara_combinations(df['キャラ'], num=num)]
    all_df = [pd.concat([pt, base, r, s]) for pt in pt_df for r in relics for s in skill]
    all_rate = np.array([damage_rate(df) for df in all_df])
    sorted_index = all_rate.argsort()[::-1]
    return all_df, all_rate, sorted_index


# スキル(atk:攻撃上昇, cd:クリダメ, gsd:与ダメ)
atk = np.array([0.05, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00])
cd = np.array([0.00, 0.00, 0.00, 0.10, 0.00, 0.00, 0.00, 0.00])
gsd = np.array([0.00, 0.00, 0.00, 0.00, 0.00, 0.025, 0.00, 0.00])


def add_skill(df, skill_num):
    """ 不足分を考慮し最適なスキルを追加する

    :param df: スキルを付与する構成
    :param skill_num: 追加するスキル数
    :return: 最適なスキルを付与した構成
    """
    skill = np.array([0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00])
    for i in range(skill_num):
        out = [damage_rate(concat_skill(df, skill+op)) for op in [atk, cd, gsd]]
        skill += [atk, cd, gsd][np.argmax(out)]
    return concat_skill(df, skill)


# リストから辞書を生成
list2dict = lambda x: dict(zip([data[0] for data in x], range(len(x))))
```

---
# 2. DDの定義
## 2.1 基礎バフ

常時のっているバフはほぼほぼこんな感じなのかな…

```python
# 0.キャラ（名前）, 1.攻撃上昇, 2.攻撃速度, 3.攻撃値, 4.クリダメ, 5.防御貫通, 6.与ダメ, 7.被ダメ, 8.防御低下

buff = [
    ['オディ', 0.35, 0.00, 0.00, 1.10, 0.35, 0.00, 0.25, 0.00], # オディ君は固定バフ扱い∑(･`ω･ﾉ)ﾉ
    ['魔導セット', 0.25, 0.00, 0.00, 0.00, 0.00, 0.25, 0.00, 0.00],
    ['クラスバフ', 0.25, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
    ['DD超越', 0.65, 0.00, 0.00, 0.50, 0.20, 0.00, 0.00, 0.00]
]

pt_relics = [
    ['羽', 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.25, 0.00],
    ['鞭', 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.50, 0.00],
    ['王冠', 0.75, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
    ['優秀賞', 0.20, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
    ['かぼちゃ', 0.50, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
    ['ユノデザ', 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.25, 0.00],
    ['吐息', 0.00, 0.00, 0.00, 0.00, 0.00, 0.125, 0.00, 0.00],
]

base = pd.DataFrame(map(list2dic, buff + pt_relics))
```

## 2.2 魔導スキル
DDの魔導スキルは攻撃特化(防御25攻撃50×4), 与ダメ特化(9525×4), バランスを準備(防御25攻撃50×2, 9525×2)。

```python
# 0.キャラ（名前）, 1.攻撃上昇, 2.攻撃速度, 3.攻撃値, 4.クリダメ, 5.防御貫通, 6.与ダメ, 7.被ダメ, 8.防御低下

dd_skill_list = [
    ['攻撃特化：スキル', 2.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
    ['与ダメ特化：スキル', 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 1.00, 0.00],
    ['バランス：スキル', 1.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.50, 0.00],
]

dic_dd_skill = list2dict(dd_skill_list)

dd_skill = [pd.DataFrame([list2dic(skill)]) for skill in dd_skill_list]
```

## 2.3 遺物

DDの遺物候補をいくつか用意

```python
# 0.キャラ（名前）, 1.攻撃上昇, 2.攻撃速度, 3.攻撃値, 4.クリダメ, 5.防御貫通, 6.与ダメ, 7.被ダメ, 8.防御低下

dd_relics_list = [
    ['大福', 1.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00],
    ['爪飾り', 0.00, 0.00, 0.00, 0.00, 0.00, 0.50, 0.00, 0.00],
    ['火の扉', 0.00, 0.00, 0.00, 0.00, 0.00, 0.25, 0.00, 0.00],
    ['獣人本', 0.00, 0.00, 0.00, 0.00, 0.00, 0.30, 0.00, 0.00],
    ['コイン', 0.50, 0.00, 0.00, 0.50, 0.00, 0.00, 0.00, 0.00],
    # ['シャク剣', 0.00, 0.00, 0.00, 0.50, 0.00, 0.50, 0.00, 0.00], # 使いにくいのでコメントアウト
]

dic_dd_relics = list2dict(dd_relics_list)

dd_relics = [pd.DataFrame([list2dic(relics)]) for relics in dd_relics_list]
```

# 3. PT検討
## 3.1. キャラリストの作成

キャラリストはCSVファイルから読み込めるようにした。（一応元のリストも残しておく）

```python
# chara = [
#     ['ジェーン', 0.05, 0.00, 0.00, 0.00, 0.00, 0.15, 1.95, 0.50],
#     ['プリシラ', 0.40, 0.10, 0.33, 1.20, 0.00, 0.00, 0.00, 0.00],
#     [  '堕フレ', 0.40, 0.70, 0.07, 0.00, 0.50, 0.15, 1.10, 0.00],
#     ['ラブリル', 0.90, 0.00, 0.00, 2.75, 0.30, 0.00, 1.31, 0.25],
#     [    'シア', 0.60, 0.40, 0.05, 1.00, 0.00, 0.20, 0.15, 0.20],
#     ['エステル', 0.25, 0.45, 0.00, 0.90, 0.15, 0.45, 0.00, 0.00],
#     ['ベロニカ', 0.15, 0.00, 0.00, 1.50, 0.00, 0.95, 0.75, 0.00],
#     ['アネット', 0.30, 0.20, 0.05, 0.60, 0.00, 0.00, 1.05, 0.00],
#     # ['オディ',   0.35, 0.00, 0.00, 1.10, 0.35, 0.00, 0.25, 0.00],
#     [   'メイ',  0.90, 1.20, 0.00, 0.00, 0.96, 0.00, 0.25, 0.00],
#     ['メディ',   0.60, 0.00, 0.60, 0.50, 0.00, 0.00, 0.00, 0.00],
#     ['エスカー', 0.99, 0.00, 0.00, 0.00, 0.35, 0.04, 0.75, 0.00]
# ]

# chara = [
#     ['シャクメ', 0.05, 0.00, 0.00, 0.00, 0.00, 0.20, 2.11, 0.00],
#     [ 'ロマン',  0.05, 0.00, 0.00, 0.00, 0.00, 0.00, 1.65, 0.00],
#     ['プリシラ', 0.40, 0.10, 0.33, 1.20, 0.00, 0.00, 0.00, 0.00],
#     [  '堕フレ', 0.40, 0.70, 0.07, 0.00, 0.50, 0.15, 1.10, 0.00],
#     ['ラブリル', 0.90, 0.00, 0.00, 2.75, 0.30, 0.00, 0.00, 0.00],
#     [    'シア', 0.60, 0.40, 0.05, 1.00, 0.00, 0.20, 0.15, 0.20],
#     ['エステル', 0.25, 0.45, 0.00, 0.90, 0.15, 0.45, 0.70, 0.00],
#     ['ベロニカ', 0.15, 0.00, 0.00, 1.50, 0.00, 0.95, 0.75, 0.00],
#     # ['オディ',   0.35, 0.00, 0.00, 1.10, 0.35, 0.00, 0.00, 0.00],
#     [   'メイ',  0.90, 1.20, 0.00, 0.00, 0.96, 0.00, 0.00, 0.00],
#     ['メディ',   0.60, 0.00, 0.70, 0.50, 0.00, 0.00, 0.45, 0.00],
#     ['エスカー', 0.99, 0.00, 0.00, 0.00, 0.35, 0.04, 0.00, 0.00],
#     ['ルシアス', 0.65, 0.00, 0.00, 0.00, 0.00, 0.20, 0.25, 0.00]
# ]

# df = pd.DataFrame(map(list2dic, chara))

df = pd.read_csv('MagicPT.csv')
# df = pd.read_csv('PhysicalPT.csv')
df
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365961/c8a420ce-6644-28cc-8f6f-645c4491eb1e.png)

## 3.2. 最適PTの確認
最適なPTを取得する。`best, second, third = [all_df[i] for i in sorted_index[:3]]`　こんな感じで、上位3位まで取ってくるとかもできる。`print(damage_rate(best))`でどれだけ強いかスコアを表示する。この構成は2327.4らしい。また、棒グラフは 攻撃力（ATK）, クリダメ（CD）, 与ダメ（GSD）, 被ダメ（TD）を可視化したもの。

### 補足
　与ダメや攻撃、クリダメなどは盛りやすさで補正してどれだけ盛るのが最適かが変わります。例えば与ダメを+150%すると基礎値の100%+150%=250%で250%になります。サポの魔導スキルで与ダメは1箇所2.5%、クリダメは1箇所10.0%上がることを考慮すると、与ダメ+150%した場合はクリダメ+800%し、基礎値の200%+800%=1000%にする必要があります。わかりやすく表にして、最適（2.5倍×10倍）から与ダメスキルをクリダメスキルに変更すると下がる様子を表にしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365961/3c836062-9849-9804-d6aa-6d709b989165.png)
　このことを考慮して、単純な攻撃やクリダメ、与ダメの倍率では何が足りないか判断が難しい…（棒グラフの青いバー）。そこで！この盛りやすさで補正をかけたのがオレンジのバーになります.+.(ﾉ*･ω･)ﾉ*.この高さが等しくなるとつよい！つまり、現状クリダメが足りていないことがわかる！

```python
num = 6 # ピック人数
all_df, all_rate, sorted_index = analysis(df, base, dd_relics, dd_skill, num=num)
best, second, third = [all_df[i] for i in sorted_index[:3]]
print(damage_rate(best))
print(damage_rate_check(best))
best

>>> 2327.4137737499996
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365961/f2b01e74-6a5a-0d69-c5fa-57ed42be9249.png)


## 3.3. スキルを割り振る

最高倍率目指して魔導のスキルを割り振るよ。とりあえずピックした全キャラ4枠使う想定… (`add_skill(best, (num+1)*4)`の`(num+1)*4`を書き換えれば追加するサポのスキル数を変更可能)。この構成は2995.8らしい。（さっきの棒グラフで確認した通り、クリダメが不足しているのでサポのスキルはクリダメ全盛になった）

```python
best_add_skill = add_skill(best, (num+1)*4)
print(damage_rate(best_add_skill))
print(damage_rate_check(best_add_skill))
best_add_skill

>>> 2995.79926775
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365961/7246de7e-f088-6997-94f6-0f73e2053afd.png)


## 3.4. オリジナル構成

このキャラは必須、このDD遺物やスキルは使えないなどのためにある程度操作できるようにした。試しにDDを与ダメ特化のスキルにして、大福持たせてみる。2595.3…よわい(´・ω・｀)

```python
my_pt = list(best['キャラ'][:num+1])
my_pt = pick(my_pt, df)
my_dd_skill = dd_skill[dic_dd_skill['与ダメ特化：スキル']]
my_dd_relics = dd_relics[dic_dd_relics['大福']]
my_pt = pd.concat([my_pt, base, my_dd_skill, my_dd_relics])

my_pt_add_skill = add_skill(my_pt, (num+1)*4)
print(damage_rate(my_pt_add_skill))
print(damage_rate_check(my_pt_add_skill))
my_pt_add_skill

>>> 2595.30555025
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/365961/36cafab6-89f3-7956-838b-4c5c54257793.png)

# 4. おわりに

ある程度綺麗に整理できたのでなんとなーく投稿してみた。たまにはこういう簡単なのもいいね(´っ･ω･)っ