pythonで書くガチャ-期間設定機能の追加-

#内容
実際にガチャのサービスが運用されると、定期的に新しいアイテムを追加していく必要があります。
[前回](https://qiita.com/saib-ryo/items/c6bb58fab256722a0a21)までのデータ構成では、アイテムを追加したい時間になったら、`ガチャアイテム情報`を書き換えるという作業が必要でした。

そこで、以下の機能要望に対応するデータ構成を考えてみます。

- ガチャの実施期間を設定したい
- ガチャの実施期間が切り替わったら抽選対象アイテムを自動的に追加したい


##設定情報の改修
###ガチャ情報
**gacha**

|  id | start_time | end_time | gacha_group | gacha_lottery_id |
|:---:|:--------:|:------:|:------------:|:----:|
|  <font color="Blue">1</font>  | 2020-05-01 00:00:00 | 2020-05-31 23:59:59 | A | normal_1       |
|  <font color="Blue">2</font> | 2020-05-01 00:00:00 | 2020-05-31 23:59:59 | A | normal_11      |
|  3  | 2020-05-25 00:00:00 | 2020-05-31 23:59:59 | B | fighter_2      |
|  4</font>  | 2020-06-01 00:00:00 | 2020-06-04 23:59:59 | C | omake_2_11     |
|  5  | 2020-05-20 00:00:00 | 2020-05-31 23:59:59 | A | omake_fighter_6| 100|
|  <font color="Red">6</font>  | 2020-06-01 00:00:00 | 2020-06-30 23:59:59 | <font color="Orange">C</font> | normal_1       |
|  <font color="Red">7</font>  | 2020-06-01 00:00:00 | 2020-06-30 23:59:59 | <font color="Orange">C</font> | normal_11      |
<font color="Blue">青字（1と2）</font>と<font color="Red">赤字（6と7）</font>のIDは、それぞれ同じgacha_lottery_idを使っていますが、使用する抽選グループ対象が、以下のように異なります。

- 2020年5月31日までの抽選対象グループ：`A`
- 2020年6月1日 からの抽選対象グループ：`C`

このように期間毎の設定を行っておけば、その時間にガチャアイテム情報を更新せずに、切り替えが行なえます。
設定内容をまとめると以下のようになります

- ガチャの実施期間の開始と終了（start_time,end_time）
- 期間内における抽選対象グループ（gacha_group）
- 対応するガチャ方式定義ID（gacha_lottery_id）

###ガチャ方式定義情報
**gacha_lottery**

|  id | item_type | times | rarity | omake_times | omake_rarity| cost |
|:---:|:----:|:----:|:----:|:----:|:------:|:------:|
|  normal_1  |  0  | 1  |  0   |  0    |  0    | 10 |
|  normal_11  |  0  | 10 |  0   |  1    |  3   | 100 |
|  fighter_2  |  0  | 2  |  0   |  0    |  0   | 30 |
|  omake_2_11  | 0  | 9 |  2   |  2    |  3    | 150 |
|  omake_fighter_6 |  2  | 5 |  0   |  1    |  3 | 100 |

抽選対象グループ（gacha_group）の管理は、`ガチャ情報`に移管し、gacha_lottery_idによって互いの情報の紐付けを行います。
`抽選対照グループ（gacha_group）の項目を持たなくなったために、IDからグループの接頭語（AとかB）を除去しました`

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

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]

# ガチャ情報の辞書
def get_gacha_info(now_time: int) -> dict:
    gachas = {
        1: {"start_time": "2020-05-01 00:00:00", "end_time": "2020-05-31 23:59:59", "gacha_group": "A",
            "gacha_lottery_id": "normal_1"},
        2: {"start_time": "2020-05-01 00:00:00", "end_time": "2020-05-31 23:59:59", "gacha_group": "A",
            "gacha_lottery_id": "normal_11"},
        3: {"start_time": "2020-05-25 00:00:00", "end_time": "2020-05-31 23:59:59", "gacha_group": "B",
            "gacha_lottery_id": "fighter_2"},
        4: {"start_time": "2020-06-01 00:00:00", "end_time": "2020-06-04 23:59:59", "gacha_group": "C",
            "gacha_lottery_id": "omake_2_11"},
        5: {"start_time": "2020-05-20 00:00:00", "end_time": "2020-05-31 23:59:59", "gacha_group": "A",
            "gacha_lottery_id": "omake_fighter_6"},
        6: {"start_time": "2020-06-01 00:00:00", "end_time": "2020-06-30 23:59:59", "gacha_group": "C",
            "gacha_lottery_id": "normal_1"},
        7: {"start_time": "2020-06-01 00:00:00", "end_time": "2020-06-30 23:59:59", "gacha_group": "C",
            "gacha_lottery_id": "normal_11"}
    }

    results = {}
    for gacha_id, info in gachas.items():
        start_time = int(datetime.strptime(info["start_time"], '%Y-%m-%d %H:%M:%S').timestamp())
        end_time = int(datetime.strptime(info["end_time"], '%Y-%m-%d %H:%M:%S').timestamp())
        # 日時の範囲で対象ガチャ情報を絞り込みます
        if start_time <= now_time <= end_time:
            results[gacha_id] = info

    return results

# ガチャ方式定義情報の辞書
def get_gacha_lottery_info(gacha_lottery_id: str) -> dict:
    # gacha_groupを移管しました
    gacha_lottery = {
        "normal_1":  {"item_type": 0, "times": 1, "rarity": 0, "omake_times": 0, "omake_rarity": 0, "cost":10},
        "normal_11":  {"item_type": 0, "times": 10, "rarity": 0, "omake_times": 1, "omake_rarity": 3, "cost":100},
        "fighter_2":  {"item_type": 0, "times": 2, "rarity": 0, "omake_times": 0, "omake_rarity": 0, "cost":30},
        "omake_2_11":  {"item_type": 0, "times": 9, "rarity": 2, "omake_times": 2, "omake_rarity": 3, "cost":150},
        "omake_fighter_6":  {"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, now_time: int=0) -> list:
        if not now_time: now_time = int(datetime.now().timestamp())
        # 実行可能なガチャ情報を取得
        gachas = get_gacha_info(now_time)
        lottery_info = get_gacha_lottery_info(gacha_lottery_id)

        ids = []
        for gacha_id, gacha_info in gachas.items():
            if gacha_lottery_id == gacha_info["gacha_lottery_id"]:
                lottery_info["gacha_group"] = gacha_info["gacha_group"]
                print("==%s==:gacha_group:%s" % (gacha_lottery_id, lottery_info["gacha_group"]))
                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},
        # 以下、追加
        4101: {"rarity": 4, "item_name": "SSR_勇者", "item_type": 1, "hp": 1000},
        5201: {"rarity": 5, "item_name": "UR_戦士", "item_type": 2, "hp": 1300},
        5301: {"rarity": 5, "item_name": "UR_魔法使い", "item_type": 3, "hp": 1000},
    }

    # ガチャアイテム情報
    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},
        # 以下、追加
        14: {"gacha_group": "C", "weight": 1, "item_id": 5101},
        15: {"gacha_group": "C", "weight": 1, "item_id": 5201},
        16: {"gacha_group": "C", "weight": 1, "item_id": 5301},
        17: {"gacha_group": "C", "weight": 9, "item_id": 4101},
        18: {"gacha_group": "C", "weight": 6, "item_id": 4201},
        19: {"gacha_group": "C", "weight": 6, "item_id": 4301},
        20: {"gacha_group": "C", "weight": 6, "item_id": 4401},
        21: {"gacha_group": "C", "weight": 20, "item_id": 3201},
        22: {"gacha_group": "C", "weight": 20, "item_id": 3301},
        23: {"gacha_group": "C", "weight": 20, "item_id": 3401},
        24: {"gacha_group": "C", "weight": 40, "item_id": 2201},
        25: {"gacha_group": "C", "weight": 40, "item_id": 2301},
        26: {"gacha_group": "C", "weight": 40, "item_id": 2401},
    }

    # 実施するガチャのタプル
    gacha_lottery_ids = (
        "normal_1","normal_11","fighter_2","omake_2_11","omake_fighter_6"
    )

    # 動作確認のため、ガチャ実行日時を指定
    now_time = int(datetime.strptime("2020-06-01 00:00:00", '%Y-%m-%d %H:%M:%S').timestamp())

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

    # gacha_lottery_idの設定にてガチャを実行
    for gacha_lottery_id in gacha_lottery_ids:
        ids = func_gacha(gacha_lottery_id,now_time)
        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`を指定してガチャを実行するようにプログラムを記述しています。

##実行結果
```
==normal_1==:gacha_group:C
ID:26, R, R_神官
==normal_11==:gacha_group:C
ID:25, R, R_魔法使い
ID:22, SR, SR_魔法使い
ID:26, R, R_神官
ID:24, R, R_戦士
ID:25, R, R_魔法使い
ID:21, SR, SR_戦士
ID:15, UR, UR_戦士
ID:24, R, R_戦士
ID:23, SR, SR_神官
ID:24, R, R_戦士
ID:21, SR, SR_戦士
==omake_2_11==:gacha_group:C
ID:21, SR, SR_戦士
ID:25, R, R_魔法使い
ID:24, R, R_戦士
ID:21, SR, SR_戦士
ID:23, SR, SR_神官
ID:25, R, R_魔法使い
ID:23, SR, SR_神官
ID:18, SSR, SSR_戦士
ID:25, R, R_魔法使い
ID:23, SR, SR_神官
ID:17, SSR, SSR_勇者
```

###追記
実際のゲームにおいては以下のフローのように、`gacha_id`を指定して、ガチャを実行することになりますので、少々プログラムの構成が違ってきます。

1. ゲーム画面（アプリ）側からgacha_idを指定してガチャの実行依頼
2. 指定されたgacha_idが有効期限内であることを判定
3. ガチャ情報（gacha_idにて取得した情報）を用いて、ガチャを実行