pythonで書くガチャ-基本的データ構造における実装-

#内容
ガチャのデータ設計（[基本的構造１](https://qiita.com/saib-ryo/items/771b5839e9e34d9872ff)、[基本的構造２](https://qiita.com/saib-ryo/items/c1b7837fe04022b6864d)）に対応した実装を行います。

こちらのソースコードを改良
> [Pythonで書くガチャ-データ設計-](https://qiita.com/saib-ryo/items/1309c4c94d9f6629ba6d)


##設定情報一覧
**gacha_lottery**

|  id | gacha_group | item_type | times | rarity | omake_times | omake_rarity | cost |
|:---:|:----:|:----:|:----:|:----:|:----:|:------:|:------:|
|  A_normal_1  |  A  |0  | 1  |  0   |  0    |  0    | 10 |
|  A_normal_11  |  A  |  0  | 10 |  0   |  1    |  3    | 100|
|  B_fighter_2  |  B  |  0  | 2  |  0   |  0    |  0    | 30 |
|  A_omake_2_11  |  A  |  0  | 9 |  2   |  2    |  3    | 200|
|  A_omake_fighter_6  |  A  |  2  | 5 |  0   |  1    |  3    | 100|

ガチャを実行する際に使用するIDという位置づけになりますので、具体的に分かりやすい文字列のIDにします

**gacha_items**

|  id | gacha_group | weight | item_id |
|:---:|:----:|:----:|:------:|
|  1  |A |  3   |  5101    |
|  2  |A |  9   |  4201    |
|  3  |A |  9   |  4301    |
|  4  |A |  9   |  4301    |
|  5  |A |  20  |  3201    |
|  6  |A |  20  |  3301    |
|  7  |A |  20  |  3401    |
|  8  |A |  40  |  2201    |
|  9  |A |  40  |  2301    |
|  10 |A |  40  |  2401    |
|  11  |B |  15 |  4201   |
|  12  |B |  30 |  3201   |
|  13  |B |  55 |  2201   |

**items**

|  id | rarity |   item_name    |item_type|  HP  |
|:---:|:------:|:--------------:|:-------:|:----:|
| 5101  |   5    |   UR_勇者      | 1 | 1200 |
| 4201  |   4    |   SSR_戦士     | 2  | 1000 |
| 4301  |   4    |   SSR_魔法使い  | 3  | 800  |
| 4401  |   4    |   SSR_神官     | 4  | 800  |
| 3201  |   3    |   SR_戦士      | 2  | 600 |
| 3301  |   3    |   SR_魔法使い   | 3  | 500 |
| 3401  |   3    |   SR_神官      | 4  | 500 |
| 2201  |   2    |   R_戦士       | 2  | 400 |
| 2301  |   2    |   R_魔法使い    | 3  | 300 |
| 2401  |   2    |   R_神官       | 4  | 300 |
| 3199  |   3    |   SR_勇者      | 1  | 600 |

**rarity**

|  id |  rarity_name      |
|:---:|:---------------------:|
|  5  |  UR  |
|  4  |  SSR |
|  3  |  SR  |
|  2  |  R   |
|  1  |  N   |

##改修の観点
###機能要件
[基本的構造２](https://qiita.com/saib-ryo/items/c1b7837fe04022b6864d)のデータ構造で実現できるガチャの仕組みとして、以下の要件を満たすようにします。
> - ガチャ対象アイテムをグループ（母集団）として複数設定
- ガチャ実行時に属性の絞り込み（item_type）
- ガチャ実行時に通常抽選のレアリティ以上絞り込み（rarity）
- ガチャ実行時に通常抽選の回数指定（times）
- ガチャ実行時におまけのレアリティ以上絞り込み（omake_rarity）
- ガチャ実行時におまけの抽選回数指定（omake_times）

###データ構造の変更点
前回（[Pythonで書くガチャ-データ設計-](https://qiita.com/saib-ryo/items/1309c4c94d9f6629ba6d)）の実装におけるデータ構造と大きく異なる点として**ガチャ情報**と**アイテム情報**が分離されていることがあります。そのため、情報の紐付けが必要となります。


##実装
```python:gacha.py
import random

def gacha(lots, times: int=1) -> list:
    return random.choices(tuple(lots), weights=lots.values(), k=times)

def get_rarity_name(rarity: int) -> str:
    rarity_names = {5: "UR", 4: "SSR", 3: "SR", 2: "R", 1: "N"}
    return rarity_names[rarity]

# ガチャのIDと設定の辞書
def get_gacha_lottery_info(gacha_lottery_id: str) -> dict:
    gacha_lottery = {
        "A_normal_1":  {"gacha_group": "A", "item_type": 0, "times": 1, "rarity": 0, "omake_times": 0, "omake_rarity": 0, "cost":10},
        "A_normal_11":  {"gacha_group": "A", "item_type": 0, "times": 10, "rarity": 0, "omake_times": 1, "omake_rarity": 3, "cost":100},
        "B_fighter_2":  {"gacha_group": "B", "item_type": 0, "times": 2, "rarity": 0, "omake_times": 0, "omake_rarity": 0, "cost":30},
        "A_omake_2_11":  {"gacha_group": "A", "item_type": 0, "times": 9, "rarity": 2, "omake_times": 2, "omake_rarity": 3, "cost":200},
        "A_omake_fighter_6":  {"gacha_group": "A", "item_type": 2, "times": 5, "rarity": 0, "omake_times": 1, "omake_rarity": 3, "cost":100}
    }
    return gacha_lottery[gacha_lottery_id]

def set_gacha(items: dict, gacha_items: dict):
    # ガチャの設定に必要なアイテム情報をガチャアイテム情報に含める
    dic_gacha_items = {}
    for gacha_item_id, info in gacha_items.items():
        info["item_info"] = items[info["item_id"]]
        dic_gacha_items[gacha_item_id] = info

    # 抽選対象リストを抽出
    def get_lots(lottery_info: dict):
        lots = {}
        omake_lots = {}
        for id, info in dic_gacha_items.items():
            if lottery_info["gacha_group"] != info["gacha_group"]:
                continue
            if lottery_info["item_type"] and lottery_info["item_type"] != info["item_info"]["item_type"]:
                continue

            if not(lottery_info["rarity"]) or lottery_info["rarity"] <= info["item_info"]["rarity"]:
                lots[id] = info["weight"]

            if lottery_info["omake_times"]:
                if not(lottery_info["omake_rarity"]) or lottery_info["omake_rarity"] <= info["item_info"]["rarity"]:
                    omake_lots[id] = info["weight"]

        return lots, omake_lots

    # ガチャ実行
    def exec(gacha_lottery_id: str) -> list:
        lottery_info = get_gacha_lottery_info(gacha_lottery_id)
        lots, omake_lots =get_lots(lottery_info)
        ids = gacha(lots, lottery_info["times"])
        if len(omake_lots) > 0:
            ids.extend(gacha(omake_lots, lottery_info["omake_times"]))
        return ids

    return exec

def main():
    # アイテム情報
    items = {
        5101: {"rarity": 5, "item_name": "UR_勇者", "item_type": 1, "hp": 1200},
        4201: {"rarity": 4, "item_name": "SSR_戦士", "item_type": 2, "hp": 1000},
        4301: {"rarity": 4, "item_name": "SSR_魔法使い", "item_type": 3, "hp": 800},
        4401: {"rarity": 4, "item_name": "SSR_神官", "item_type": 4, "hp": 800},
        3201: {"rarity": 3, "item_name": "SR_戦士", "item_type": 2, "hp": 600},
        3301: {"rarity": 3, "item_name": "SR_魔法使い", "item_type": 3, "hp": 500},
        3401: {"rarity": 3, "item_name": "SR_神官", "item_type": 4, "hp": 500},
        2201: {"rarity": 2, "item_name": "R_戦士", "item_type": 2, "hp": 400},
        2301: {"rarity": 2, "item_name": "R_魔法使い", "item_type": 3, "hp": 300},
        2401: {"rarity": 2, "item_name": "R_神官", "item_type": 4, "hp": 300},
        3199: {"rarity": 3, "item_name": "SR_勇者", "item_type": 1, "hp": 600}
    }

    # ガチャアイテム情報
    gacha_items = {
        1:  {"gacha_group": "A", "weight": 3, "item_id": 5101},
        2:  {"gacha_group": "A", "weight": 9, "item_id": 4201},
        3:  {"gacha_group": "A", "weight": 9, "item_id": 4301},
        4:  {"gacha_group": "A", "weight": 9, "item_id": 4401},
        5:  {"gacha_group": "A", "weight": 20, "item_id": 3201},
        6:  {"gacha_group": "A", "weight": 20, "item_id": 3301},
        7:  {"gacha_group": "A", "weight": 20, "item_id": 3401},
        8:  {"gacha_group": "A", "weight": 40, "item_id": 2201},
        9:  {"gacha_group": "A", "weight": 40, "item_id": 2301},
        10: {"gacha_group": "A", "weight": 40, "item_id": 2401},
        11: {"gacha_group": "B", "weight": 15, "item_id": 4201},
        12: {"gacha_group": "B", "weight": 30, "item_id": 3201},
        13: {"gacha_group": "B", "weight": 55, "item_id": 2201}
    }

    # 実施するガチャのタプル
    gacha_lottery_ids = (
        "A_normal_1","A_normal_11","B_fighter_2","A_omake_2_11","A_omake_fighter_6"
    )

    #アイテム等をセット
    func_gacha = set_gacha(items, gacha_items)

    # gacha_lottery_idの設定にてガチャを実行
    for gacha_lottery_id in gacha_lottery_ids:
        print("==%s==" % gacha_lottery_id)
        ids = func_gacha(gacha_lottery_id)
        for id in ids:
            item_info = items[gacha_items[id]["item_id"]]
            print("ID:%d, %s, %s" % (id, get_rarity_name(item_info["rarity"]), item_info["item_name"]))


if __name__ == '__main__':
    main()
```

##実行結果
**gacha_lottery**のID分ガチャを実行して、挙動を確認します

```
==A_normal_1==
ID:10, R, R_神官
==A_normal_11==
ID:8, R, R_戦士
ID:10, R, R_神官
ID:10, R, R_神官
ID:6, SR, SR_魔法使い
ID:5, SR, SR_戦士
ID:8, R, R_戦士
ID:5, SR, SR_戦士
ID:8, R, R_戦士
ID:10, R, R_神官
ID:5, SR, SR_戦士
ID:7, SR, SR_神官
==B_fighter_2==
ID:13, R, R_戦士
ID:13, R, R_戦士
==A_omake_2_11==
ID:7, SR, SR_神官
ID:10, R, R_神官
ID:10, R, R_神官
ID:6, SR, SR_魔法使い
ID:8, R, R_戦士
ID:7, SR, SR_神官
ID:9, R, R_魔法使い
ID:9, R, R_魔法使い
ID:6, SR, SR_魔法使い
ID:4, SSR, SSR_神官
ID:1, UR, UR_勇者
==A_omake_fighter_6==
ID:8, R, R_戦士
ID:5, SR, SR_戦士
ID:5, SR, SR_戦士
ID:8, R, R_戦士
ID:5, SR, SR_戦士
ID:2, SSR, SSR_戦士
```
##考察
機能要件は、満たすことができまました。
実際の運用に耐えうる仕組みとしては、不十分な箇所がありますが、
まずは、この構成が基本形となります。
今後は、**運用上必要な機能要件**を満たすために、新しい情報の追加、情報の再配置といったデータ構造の変更を行い、それに合わせて実装を改修していくことになります。

今回の実装において、改めて技術的に難しいことは特段導入していません。
端的に言えば、**データ構造を再検討し実装を改修した**だけです。
実際の開発現場に置いてサービス設計者には、企画者の**原案**（機能要件）を表現するために、文章の構成を見直し**ストーリー**を書き直す**脚本家**のような能力が求められます。（*大事なことなんですがイマイチ理解されていない現実があります*）


###追記
情報量（マスタ）が多くなり、またユーザの状態も考慮したガチャを行うため、次回以降はDBを用いた実装を行います