pythonで書くガチャ-実践編２・ステップアップガチャの基本-

#内容
実際のゲームにおいては、色んな種類のガチャがありますが、
今回は、その中で`ステップアップガチャ`という種類のものを作成します。

**以下のソースを改修して実装しています**
>[pythonで書くガチャ-実践編１-
](https://qiita.com/saib-ryo/items/cb792f90cc24259da7fa)

**【2020-08-11更新】**

- *ステップ数を記録することに変更しましたので、gacha_user_timesテーブルを、gacha_user_stepとテーブル名、カラム名を変更しています*
- *型ヒント、関数の内包表記などを変更しています*

##ステップアップガチャとは？
ステップアップガチャとは、ユーザーが対象となるガチャの`実行回数`によって、**抽選対象アイテム**や、**抽選の設定**が変わっていくガチャになります。
`ステップアップ`という名称の理由は、基本的には実行回数が多くなると、`より良いアイテムが入手しやすく`なるように設定されているからです。
以後、回数を重ねることを`ステップアップ`という言葉で表していきます。

##ステップアップガチャのデータ構造
まず、`ステップアップ`した場合、何が変化するかを決めます。
今回は、[前回](https://qiita.com/saib-ryo/items/cb792f90cc24259da7fa#%E3%82%AC%E3%83%81%E3%83%A3%E6%83%85%E5%A0%B1%E3%83%87%E3%83%BC%E3%82%BF%E6%A7%8B%E9%80%A0%E3%81%AE%E5%A4%89%E6%9B%B4)のデータ構成を元にして、ガチャの引き方(**gacha_lottery**)の設定がステップアップするように改修を行うこととします。

考え方が分かりやすいように、`段階（ステップ）を踏んで`改修を行います

####改修のステップ1
まず、gacha_typeとして**step_up**という設定を追加します。
**実行する**

**gacha**

|  id | start_time | end_time | gacha_group | gacha_type |
|:---:|:--------:|:------:|:------------:|:----:|
|  1  | 2020-05-01 00:00:00 | 2020-05-31 23:59:59 | A | normal       |
|  6  | 2020-06-01 00:00:00 | 2020-06-30 23:59:59 | C | normal       |
|  <font color="Red">10</font>  | 2020-05-01 00:00:00 | 2020-05-31 23:59:59 | A | step_up       |

**gacha_lottery**

|  id | gacha_type |item_type | times | rarity | omake_times | omake_rarity| cost |
|:---:|:----:|:----:|:----:|:----:|:----:|:------:|:------:|
|  normal_1  | normal |  0  | 1  |  0   |  0    |  0    | 10 |
|  normal_11 | normal |  0  | 10 |  0   |  1    |  3   | 100 |
|  step_up  | step_up |  0  | 1  |  0   |  0    |  0    | 10 |

####改修のステップ2
**gacha_lottery**に`ステップ数`毎の設定ができるように、**step_no**という項目を追加して設定を行います。

**gacha_lottery**

|  id | gacha_type | step_no | item_type | times | rarity | omake_times | omake_rarity| cost |
|:---:|:----:|:----:|:----:|:----:|:----:|:----:|:------:|:------:|
|  normal_1  | normal |    |  0  | 1  |  0   |  0    |  0    | 10 |
|  normal_11 | normal |  |  0  | 10 |  0   |  1    |  3   | 100 |
|  step_up_1  | step_up |  1  |  0  | 1  |  0   |  0    |  0    | 10 |
|  step_up_2  | step_up |  2  |  0  | 2  |  0   |  1    |  3    | 30 |
|  step_up_3  | step_up |  3  |  0  | 3  |  0   |  2    |  3    | 50 |
|  step_up_4  | step_up |  4  |  0  | 10  |  0   |  1    |  5    | 100 |

####改修のステップ３
`gacha_type`がnormalのガチャについては、step_noを**0**として`ステップアップの設定が無い`ということにします。

**gacha_lottery**

|  id | gacha_type | step_no | item_type | times | rarity | omake_times | omake_rarity| cost |
|:---:|:----:|:----:|:----:|:----:|:----:|:----:|:------:|:------:|
|  normal_1  | normal |  0  |  0  | 1  |  0   |  0    |  0    | 10 |
|  normal_11 | normal | 0 |  0  | 10 |  0   |  1    |  3   | 100 |
|  step_up_1  | step_up |  1  |  0  | 1  |  0   |  0    |  0    | 10 |
|  step_up_2  | step_up |  2  |  0  | 2  |  0   |  1    |  3    | 30 |
|  step_up_3  | step_up |  3  |  0  | 3  |  0   |  2    |  3    | 50 |
|  step_up_4  | step_up |  4  |  0  | 10  |  0   |  1    |  5    | 100 |

ステップ4まで実行した時点で、このステップアップガチャは終了となります（それ以上、実行できない）

<img width="833" alt="図2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/625647/452010b4-382a-772d-029a-12bae11945f1.png">


##ガチャ履歴の作成
ステップアップガチャは、ユーザの状態（`ユーザのステップ状態`）によってガチャの状態（確率など）が変化します。
つまり、`あるユーザーが、その対象ガチャを実行したことによる変化したステップ状態を記録する必要があります。`

そこで、ユーザ毎のガチャのステップ状態を`記録できる場所（テーブル）`と、`記録をする処理`を作ります。

####ユーザ情報（user）

|  id | nick_name | created_time |
|:---:|:--------:|:----:|
|  u001  | taro | 2020-05-31 23:59:59 |
|  u002  | hana | 2020-05-31 23:59:59 |

####ユーザガチャステップ情報（gacha_user_step）
| user_id | gacha_id | gacha_type | step_no | updated_time |
|:---:|:--------:|:------:|:------------:|:----:|:----:|
|  u001  | 10 | step_up | 2 | 2020-05-11 22:00:00 |
|  u002  | 10 | step_up | 3 | 2020-05-12 09:00:00 |

user_id＋gacha_id＋gacha_typeの複数カラムで一意となり、実行するたびにstep_noを＋１（update）します。
なお、初回（１回目）の実行の場合は、レコードがありませんので初期ステップ（step_no=1）とみなして、実行時に次のステップ状態（step_no=2）として、レコードを作成（insert）します。

## 実装
実装上の仕様をまとめると以下の通りになります

- ステップアップガチャの挙動の違い（normalガチャに対して）
 - **gacha_user_step**に`ステップの状態`を記録する
 - 同じ**gacha_type**について`実行可能なステップ`のgacha_lotteryが抽出される
 - 実行する毎に、`ステップが一つずつ`進行していく
 - ステップ4まで実行した時点で、`終了`となる（それ以上、実行できない）

*ソース上の変更箇所が分かるように以下のコメント記述しています*

- 【追加】：新しく追加した箇所
- 【改修】：改修した箇所 

###ガチャDB情報
構造・データの変更、履歴をリセットしたい場合は、この処理を再度実行してください

```python:gacha_db.py
# -*- coding: utf-8 -*-
import sqlite3
import random

def get_items():
    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},
    }

    return convert_values(items)

def get_gacha_items():
    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},
    }

    return convert_values(items)

def get_gacha():
    items = {
        1: {"start_time": "2020-05-01 00:00:00", "end_time": "2020-05-31 23:59:59", "gacha_group": "A",
            "gacha_type": "normal"},
        3: {"start_time": "2020-05-25 00:00:00", "end_time": "2020-05-31 23:59:59", "gacha_group": "B",
            "gacha_type": "fighter"},
        4: {"start_time": "2020-06-01 00:00:00", "end_time": "2020-06-04 23:59:59", "gacha_group": "C",
            "gacha_type": "omake_2"},
        5: {"start_time": "2020-05-20 00:00:00", "end_time": "2020-05-31 23:59:59", "gacha_group": "A",
            "gacha_type": "omake_fighter"},
        6: {"start_time": "2020-06-01 00:00:00", "end_time": "2020-06-30 23:59:59", "gacha_group": "C",
            "gacha_type": "normal"},
        10: {"start_time": "2020-05-01 00:00:00", "end_time": "2020-05-31 23:59:59", "gacha_group": "A",
            "gacha_type": "step_up"},
    }

    return convert_values(items)

def get_gacha_lottery():
    items = {
        "normal_1":  {"gacha_type": "normal", "step_no": 0, "item_type": 0, "times": 1, "rarity": 0, "omake_times": 0, "omake_rarity": 0, "cost":10},
        "normal_11":  {"gacha_type": "normal", "step_no": 0, "item_type": 0, "times": 10, "rarity": 0, "omake_times": 1, "omake_rarity": 3, "cost":100},
        "fighter":  {"gacha_type": "fighter", "step_no": 0, "item_type": 0, "times": 2, "rarity": 0, "omake_times": 0, "omake_rarity": 0, "cost":30},
        "omake_2":  {"gacha_type": "omake_2", "step_no": 0, "item_type": 0, "times": 9, "rarity": 2, "omake_times": 2, "omake_rarity": 3, "cost":150},
        "omake_fighter":  {"gacha_type": "omake_fighter", "step_no": 0, "item_type": 2, "times": 5, "rarity": 0, "omake_times": 1, "omake_rarity": 3, "cost":100},
        "step_up_1": {"gacha_type": "step_up", "step_no": 1, "item_type": 0, "times": 1, "rarity": 0, "omake_times": 0,
                     "omake_rarity": 0, "cost": 10},
        "step_up_2": {"gacha_type": "step_up", "step_no": 2, "item_type": 0, "times": 2, "rarity": 0, "omake_times": 1,
                      "omake_rarity": 3, "cost": 30},
        "step_up_3": {"gacha_type": "step_up", "step_no": 3, "item_type": 0, "times": 3, "rarity": 0, "omake_times": 2,
                      "omake_rarity": 3, "cost": 50},
        "step_up_4": {"gacha_type": "step_up", "step_no": 4, "item_type": 0, "times": 10, "rarity": 0, "omake_times": 1,
                      "omake_rarity": 5, "cost": 100},
    }

    return convert_values(items)

def get_users():
    items = {
        "u001": {"nick_name": "taro"},
        "u002": {"nick_name": "hana"},
    }

    return convert_values(items)


def convert_values(items: dict):
    values = []
    keys = []
    for id,info in items.items():
        if len(keys) == 0 :
            keys = list(info.keys())
            keys.insert(0,'id')

        value = list(info.values())
        value.insert(0,id)
        values.append(tuple(value))
    return keys,values

def print_rows(rows, keys: list):
    for row in rows:
        result = []
        for key in keys:
            result.append(str(row[key]))
        print(",".join(result))


def main():
    con = sqlite3.connect("data.db")
    con.row_factory = sqlite3.Row
    cursor = con.cursor()

    cursor.execute("DROP TABLE IF EXISTS gacha")

    cursor.execute(
        """CREATE TABLE gacha 
          (id INTEGER PRIMARY KEY AUTOINCREMENT
          ,start_time DATETIME
          ,end_time DATETIME
          ,gacha_group VARCHAR(32)
          ,gacha_type VARCHAR(32)
          )
        """
    )

    cursor.execute("DROP TABLE IF EXISTS gacha_items")
    cursor.execute(
        """CREATE TABLE gacha_items 
          (id INTEGER PRIMARY KEY AUTOINCREMENT
          ,gacha_group VARCHAR(32)
          ,weight INTEGER
          ,item_id INTEGER
          )
        """
    )

    #　改修
    cursor.execute("DROP TABLE IF EXISTS gacha_lottery")
    cursor.execute(
        """CREATE TABLE gacha_lottery 
          (id VARCHAR(32) PRIMARY KEY
          ,gacha_type VARCHAR(32)
          ,step_no INTEGER
          ,item_type INTEGER
          ,times INTEGER
          ,rarity INTEGER
          ,omake_times INTEGER
          ,omake_rarity INTEGER
          ,cost INTEGER
          )
        """
    )

    cursor.execute("DROP TABLE IF EXISTS items")
    cursor.execute(
        """CREATE TABLE items 
          (id INTEGER PRIMARY KEY AUTOINCREMENT
          ,rarity INTEGER
          ,item_name VARCHAR(64)
          ,item_type INTEGER
          ,hp INTEGER
          )
        """
    )

    #　追加
    cursor.execute("DROP TABLE IF EXISTS users")
    cursor.execute(
        """CREATE TABLE users 
          (id VARCHAR(16) PRIMARY KEY
          ,nick_name VARCHAR(64)
          )
        """
    )

    #　追加
    cursor.execute("DROP TABLE IF EXISTS gacha_user_step")
    cursor.execute(
        """CREATE TABLE gacha_user_step 
          (user_id VARCHAR(16) 
          ,gacha_id INTEGER
          ,gacha_type VARCHAR(32)
          ,step_no INTEGER DEFAULT 1
          ,updated_time DATETIME          
          ,PRIMARY KEY(user_id,gacha_id,gacha_type)
          )
        """
    )


    keys, values = get_items()
    sql = "insert into {0}({1}) values({2})".format('items', ','.join(keys), ','.join(['?'] * len(keys)))
    cursor.executemany(sql,values)
    select_sql = "SELECT * FROM items ORDER BY id"
    result = cursor.execute(select_sql)
    print("===items===")
    print_rows(result, keys)

    keys, values = get_gacha_items()
    sql = "insert into {0}({1}) values({2})".format('gacha_items', ','.join(keys), ','.join(['?'] * len(keys)))
    cursor.executemany(sql,values)
    select_sql = "SELECT * FROM gacha_items ORDER BY id"
    result = cursor.execute(select_sql)
    print("===gacha_items===")
    print_rows(result, keys)


    keys, values = get_gacha()
    sql = "insert into {0}({1}) values({2})".format('gacha', ','.join(keys), ','.join(['?'] * len(keys)))
    cursor.executemany(sql,values)
    select_sql = "SELECT * FROM gacha ORDER BY id"
    result = cursor.execute(select_sql)
    print("===gacha===")
    print_rows(result, keys)

    keys, values = get_gacha_lottery()
    sql = "insert into {0}({1}) values({2})".format('gacha_lottery', ','.join(keys), ','.join(['?'] * len(keys)))
    cursor.executemany(sql,values)
    select_sql = "SELECT * FROM gacha_lottery ORDER BY id"
    result = cursor.execute(select_sql)
    print("===gacha_lottery===")
    print_rows(result, keys)

    keys, values = get_users()
    sql = "insert into {0}({1}) values({2})".format('users', ','.join(keys), ','.join(['?'] * len(keys)))
    cursor.executemany(sql,values)
    select_sql = "SELECT * FROM users ORDER BY id"
    result = cursor.execute(select_sql)
    print("===users===")
    print_rows(result, keys)

    con.commit()
    con.close()

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

###ガチャ処理
```python:gacha.py
import random
import sqlite3
from datetime import datetime
from typing import Dict, List, Tuple, Optional

def convert_row2dict(row) -> Dict:
    if row is None:
        return {}
    return {key: row[key] for key in row.keys()}

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_list(cursor, now_time: int) -> Dict:
    select_sql = "SELECT * FROM gacha ORDER BY id"
    rows = cursor.execute(select_sql)
    results = {}
    for row in rows:
        start_time = int(datetime.strptime(row["start_time"], '%Y-%m-%d %H:%M:%S').timestamp())
        end_time = int(datetime.strptime(row["end_time"], '%Y-%m-%d %H:%M:%S').timestamp())
        # 日時の範囲で対象ガチャ情報を絞り込みます
        if start_time <= now_time <= end_time:
            results[row["id"]] = convert_row2dict(row)

    return results

# 【改修】
# 実行可能ガチャ情報リスト（gacha_lottery情報含む）
def get_available_gacha_info_list(cursor, now_time: int, user_id:str) -> Dict:
    gacha_list = get_gacha_list(cursor, now_time)
    for gacha_id, info in gacha_list.items():
        lottery_info_list = get_gacha_lottery_by_type(cursor, info["gacha_type"])
        gacha_user = get_gacha_user_step(cursor, user_id, gacha_id, info["gacha_type"])

        set_list = []
        for lottery_info in lottery_info_list:
            if lottery_info["step_no"] > 0:
                now_step_no = 1
                if len(gacha_user) > 0:
                    now_step_no = gacha_user["step_no"] + 1
                if now_step_no == lottery_info["step_no"]:
                    set_list.append(lottery_info)
            else:
                set_list.append(lottery_info)
        gacha_list[gacha_id]["gacha_lottery_list"] = set_list
    return gacha_list

def get_gacha(cursor, gacha_id: int, now_time: int) -> Dict:
    select_sql = "SELECT * FROM gacha WHERE id = ? ORDER BY id"
    cursor.execute(select_sql, (gacha_id,))
    row = cursor.fetchone()
    start_time = int(datetime.strptime(row["start_time"], '%Y-%m-%d %H:%M:%S').timestamp())
    end_time = int(datetime.strptime(row["end_time"], '%Y-%m-%d %H:%M:%S').timestamp())
    # 日時の範囲で対象ガチャ情報を絞り込みます
    if start_time <= now_time <= end_time:
        return convert_row2dict(row)

    return {}


def get_gacha_lottery(cursor, gacha_lottery_id: str) -> Dict:
    select_sql = "SELECT * FROM gacha_lottery WHERE id = ? ORDER BY id"
    cursor.execute(select_sql, (gacha_lottery_id,))
    return convert_row2dict(cursor.fetchone())


def get_gacha_lottery_by_type(cursor, gacha_type: str) -> List:
    select_sql = "SELECT * FROM gacha_lottery WHERE gacha_type = ? ORDER BY id"
    rows = cursor.execute(select_sql, (gacha_type,))
    return [
        convert_row2dict(row)
        for row in rows
    ]


def get_items_all(cursor) -> Dict:
    select_sql = "SELECT * FROM items ORDER BY id"
    rows = cursor.execute(select_sql)
    return {
        row["id"]: convert_row2dict(row)
        for row in rows
    }


def get_gacha_items(cursor, gacha_group: str) -> Dict:
    select_sql = "SELECT * FROM gacha_items WHERE gacha_group = ? ORDER BY id"
    rows = cursor.execute(select_sql, (gacha_group,))
    return {
        row["id"]: convert_row2dict(row)
        for row in rows
    }

def get_gacha_items_all(cursor) -> Dict:
    select_sql = "SELECT * FROM gacha_items ORDER BY id"
    rows = cursor.execute(select_sql)
    return {
        row["id"]: convert_row2dict(row)
        for row in rows
    }


# 【改修】
def get_gacha_info(cursor, gacha_id: int, gacha_lottery_id: str, now_time: int, user_id: str) -> Tuple[Optional[Dict], Optional[Dict]]:
    gacha = get_gacha(cursor, gacha_id, now_time)
    gacha_lottery = get_gacha_lottery(cursor, gacha_lottery_id)

    if len(gacha) == 0:
        print("===実行できないgacha_id:%s===" % (gacha_id))
        return None, None

    if len(gacha_lottery) == 0:
        print("===存在しないgacha_lottery_id:%s===" % (gacha_lottery_id))
        return None, None

    if gacha["gacha_type"] != gacha_lottery["gacha_type"]:
        print("===gacha_typeが異なる:%s,%s===" % (gacha["gacha_type"],gacha_lottery["gacha_type"]))
        return None, None

    # step_upガチャのチェック
    if gacha_lottery["step_no"] > 0:
        max_step_no = get_max_step_no(cursor, gacha["gacha_type"])
        gacha_user = get_gacha_user_step(cursor, user_id, gacha_id, gacha["gacha_type"])
        step_no = 1
        if len(gacha_user) > 0:
            step_no = gacha_user["step_no"]
        if max_step_no < step_no:
            print("===最大ステップまで実行済みです（最大ステップ:%s、実行しようとしているステップ:%s）===" %(max_step_no,gacha_lottery["step_no"]))
            return None, None

        if gacha_lottery["step_no"] != step_no:
            print("===実行できるステップと異なる（現在のステップ:%s、実行しようとしているステップ:%s）===" %(step_no, gacha_lottery["step_no"]))
            return None, None

    return gacha, gacha_lottery


# 【追加】
def get_gacha_user_step(cursor, user_id: str, gacha_id: int, gacha_type: str) -> Dict:
    select_sql = "SELECT * FROM gacha_user_step WHERE user_id = ? AND gacha_id = ? AND gacha_type = ?"
    cursor.execute(select_sql, (user_id,gacha_id,gacha_type))
    row = cursor.fetchone()

    return convert_row2dict(row)


# 【追加】
def get_max_step_no(cursor, gacha_type: str) -> Dict:
    select_sql = "SELECT MAX(step_no) AS max_step_no FROM gacha_lottery WHERE gacha_type = ?"
    cursor.execute(select_sql, (gacha_type,))
    row = cursor.fetchone()

    row_dict = convert_row2dict(row)
    return row_dict["max_step_no"]


# 【追加】
def update_gacha_user_step(cursor, user_id: str, gacha_id: int, gacha_type: str, update_time: int) -> None:
    row = get_gacha_user_step(cursor, user_id, gacha_id, gacha_type)
    dt = datetime.fromtimestamp(update_time)
    if len(row) == 0:
        keys = ("user_id","gacha_id","gacha_type","updated_time")
        sql = "insert into {0}({1}) values({2})".format('gacha_user_step', ','.join(keys), ','.join(['?'] * len(keys)))
        cursor.execute(sql, (user_id,gacha_id,gacha_type,dt))
    else:
        sql = "UPDATE gacha_user_step SET step_no = step_no + 1, updated_time = ? WHERE user_id = ? AND gacha_id = ? AND gacha_type = ?"
        cursor.execute(sql, (dt,user_id,gacha_id,gacha_type))


# 【改修】
def set_gacha(cursor, now_time: int, user_id:str):
    cursor = cursor
    now_time = now_time
    user_id = user_id
    items = get_items_all(cursor)

    # 抽選対象リストを抽出
    def get_lots(gacha_group: str, lottery_info: dict) -> Tuple[Dict,Dict]:
        gacha_items = get_gacha_items(cursor, gacha_group)
        dic_gacha_items = {}
        for gacha_item_id, gacha_item in gacha_items.items():
            gacha_item["item_info"] = items[gacha_item["item_id"]]
            dic_gacha_items[gacha_item_id] = gacha_item

        lots = {}
        omake_lots = {}
        for id, info in dic_gacha_items.items():
            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(exec_gacha_id: int, exec_gacha_lottery_id: str) -> List:
        gacha_info, gacha_lottery_info = get_gacha_info(cursor, exec_gacha_id, exec_gacha_lottery_id, now_time, user_id)
        if gacha_info is None or gacha_lottery_info is None:
            return []

        print("==%s==:gacha_group:%s" % (gacha_lottery_info["id"], gacha_info["gacha_group"]))
        lots, omake_lots = get_lots(gacha_info["gacha_group"], gacha_lottery_info)
        ids = gacha(lots, gacha_lottery_info["times"])
        if len(omake_lots) > 0:
            ids.extend(gacha(omake_lots, gacha_lottery_info["omake_times"]))

        # ステップアップガチャの場合は実行回数の更新を行う
        if len(ids) > 0 and gacha_lottery_info["step_no"] > 0:
            update_gacha_user_step(cursor, user_id, exec_gacha_id, gacha_info["gacha_type"], now_time)

        return ids

    return exec


def main():
    con = sqlite3.connect("data.db")
    con.row_factory = sqlite3.Row
    cursor = con.cursor()

    # 【追加】
    # 実際には、認証系の情報からユーザIDを取得することになります
    user_id = "u001"

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

    # 【追加】
    # 実行可能なgacha_lottery
    available_list = get_available_gacha_info_list(cursor,now_time,user_id)
    for gacha_id,available in available_list.items():
        print(gacha_id,available)

    # 初期化（実行日時、アイテム等をセット）
    func_gacha = set_gacha(cursor, now_time, user_id)


    items = get_items_all(cursor)
    gacha_items = get_gacha_items_all(cursor)
    # 単発ガチャを実行
    # gacha_idとgacha_lottery_idが実行時に渡される
    ids = func_gacha(1,"normal_1")
    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"]))

    # 11連ガチャを実行
    # gacha_idとgacha_lottery_idが実行時に渡される
    ids = func_gacha(1,"normal_11")
    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"]))

    # 【追加】
    # ステップアップガチャ
    for i in [1, 2, 3, 4, 5]:
        gacha_lottery_id = "step_up_{0}".format(str(i))
        ids = func_gacha(10,gacha_lottery_id)
        if len(ids) > 0:
            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"]))

    # 【追加】
    con.commit()


    con.close()


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

##実行結果
###１回目
```
1 {'id': 1, 'start_time': '2020-05-01 00:00:00', 'end_time': '2020-05-31 23:59:59', 'gacha_group': 'A', 'gacha_type': 'normal', 'gacha_lottery_list': [{'id': 'normal_1', 'gacha_type': 'normal', 'step_no': 0, 'item_type': 0, 'times': 1, 'rarity': 0, 'omake_times': 0, 'omake_rarity': 0, 'cost': 10}, {'id': 'normal_11', 'gacha_type': 'normal', 'step_no': 0, 'item_type': 0, 'times': 10, 'rarity': 0, 'omake_times': 1, 'omake_rarity': 3, 'cost': 100}]}
10 {'id': 10, 'start_time': '2020-05-01 00:00:00', 'end_time': '2020-05-31 23:59:59', 'gacha_group': 'A', 'gacha_type': 'step_up', 'gacha_lottery_list': [{'id': 'step_up_1', 'gacha_type': 'step_up', 'step_no': 1, 'item_type': 0, 'times': 1, 'rarity': 0, 'omake_times': 0, 'omake_rarity': 0, 'cost': 10}]}
==normal_1==:gacha_group:A
ID:9, R, R_魔法使い
==normal_11==:gacha_group:A
ID:1, UR, UR_勇者
ID:10, R, R_神官
ID:9, R, R_魔法使い
ID:5, SR, SR_戦士
ID:5, SR, SR_戦士
ID:8, R, R_戦士
ID:9, R, R_魔法使い
ID:10, R, R_神官
ID:10, R, R_神官
ID:9, R, R_魔法使い
ID:5, SR, SR_戦士
==step_up_1==:gacha_group:A
ID:2, SSR, SSR_戦士
==step_up_2==:gacha_group:A
ID:9, R, R_魔法使い
ID:4, SSR, SSR_神官
ID:3, SSR, SSR_魔法使い
==step_up_3==:gacha_group:A
ID:9, R, R_魔法使い
ID:2, SSR, SSR_戦士
ID:2, SSR, SSR_戦士
ID:3, SSR, SSR_魔法使い
ID:7, SR, SR_神官
==step_up_4==:gacha_group:A
ID:9, R, R_魔法使い
ID:10, R, R_神官
ID:6, SR, SR_魔法使い
ID:9, R, R_魔法使い
ID:2, SSR, SSR_戦士
ID:7, SR, SR_神官
ID:10, R, R_神官
ID:8, R, R_戦士
ID:10, R, R_神官
ID:7, SR, SR_神官
ID:1, UR, UR_勇者
===存在しないgacha_lottery_id:step_up_5===
```
###2回目
```
1 {'id': 1, 'start_time': '2020-05-01 00:00:00', 'end_time': '2020-05-31 23:59:59', 'gacha_group': 'A', 'gacha_type': 'normal', 'gacha_lottery_list': [{'id': 'normal_1', 'gacha_type': 'normal', 'step_no': 0, 'item_type': 0, 'times': 1, 'rarity': 0, 'omake_times': 0, 'omake_rarity': 0, 'cost': 10}, {'id': 'normal_11', 'gacha_type': 'normal', 'step_no': 0, 'item_type': 0, 'times': 10, 'rarity': 0, 'omake_times': 1, 'omake_rarity': 3, 'cost': 100}]}
10 {'id': 10, 'start_time': '2020-05-01 00:00:00', 'end_time': '2020-05-31 23:59:59', 'gacha_group': 'A', 'gacha_type': 'step_up', 'gacha_lottery_list': []}
==normal_1==:gacha_group:A
ID:7, SR, SR_神官
==normal_11==:gacha_group:A
ID:9, R, R_魔法使い
ID:4, SSR, SSR_神官
ID:2, SSR, SSR_戦士
ID:8, R, R_戦士
ID:8, R, R_戦士
ID:5, SR, SR_戦士
ID:10, R, R_神官
ID:7, SR, SR_神官
ID:10, R, R_神官
ID:7, SR, SR_神官
ID:5, SR, SR_戦士
===最大ステップまで実行済みです（最大ステップ:4、実行しようとしているステップ:1）===
===最大ステップまで実行済みです（最大ステップ:4、実行しようとしているステップ:2）===
===最大ステップまで実行済みです（最大ステップ:4、実行しようとしているステップ:3）===
===最大ステップまで実行済みです（最大ステップ:4、実行しようとしているステップ:4）===
===存在しないgacha_lottery_id:step_up_5===
```

##考察
１回目は、ステップ１〜ステップ４まで実行されています。また、ステップに応じて`ガチャの引き方`が変化し、`ステップアップ`していることが分かります。
２回目は、１回目の実行結果が記録されているため（commit）、実行できずにステップアップガチャが終了していることが分かります。
実行時のステップ数の状態によって、それに応じたエラーメッセージを表示するようにしてありますので、呼び出し方を編集して処理を試していただければ、挙動の変化を実感できるかと思います。

今回作成したものは、ステップアップガチャの仕組みを理解することを目的として、`基本的な処理`の実装にとどまっており、`複雑な制御`は実装していません。
色々なゲームのステップアップガチャの挙動を観察し、どのような制御が足りないのかを考えてみると、`ステップアップガチャを応用`して出来ることが見えてくるかと思います。

*履歴を初期化したい場合は`gacha_db.py`を再度実行してください*