#内容
これまでガチャ処理のみについて考えてきましたが、実際のゲームにおいて
- ガチャを実行する画面を表示
- ユーザが選択してガチャを実行
以上を考慮したデータ設計、処理に対応できる改修を考えてみます。
また、今回からガチャに関するデータの格納にDBを利用します。
以下のソースを改修して実装しています
##冗長な設定情報をまとめる
####ガチャ情報
id | start_time | end_time | gacha_group | gacha_lottery_id |
---|---|---|---|---|
1 | 2020-05-01 00:00:00 | 2020-05-31 23:59:59 | A | normal_1 |
2 | 2020-05-01 00:00:00 | 2020-05-31 23:59:59 | A | normal_11 |
6 | 2020-06-01 00:00:00 | 2020-06-30 23:59:59 | C | normal_1 |
7 | 2020-06-01 00:00:00 | 2020-06-30 23:59:59 | C | normal_11 |
####ガチャ情報の要素 |
- 実施期間
- 対象ガチャアイテムのグループ
- ガチャの引き方
ガチャ情報の役割は、実施期間
における対象ガチャアイテムのグループ
を設定し、ガチャの引き方
に紐付けることです。IDの1と2(又は6と7)の違いはガチャの引き方
が、単発(1回)、または11連の違いのみです。
冗長な情報の設定は、手間が増え運用時の設定ミスにも繋がります。できれば同じ情報を複数設定することは避けたいので、データ構造の改修を行います。
####ガチャ情報データ構造の変更
gacha情報からgacha_lottery_idの代わりにgacha_type
という項目を追加します。
gacha_lotteryにもgacha_type
を追加し、ガチャ情報とガチャ方式定義情報の紐付けを行います
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 |
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 |
実際のゲーム内のガチャ画面において以下のように表示されることをイメージしてください
##実装
今回の実装から各種情報はDBより取得しますので、まずテーブルの作成とデータのセットを行います
###ガチャDB情報(構造、データの変更があれば実行)
# -*- 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"},
}
return convert_values(items)
def get_gacha_lottery():
items = {
"normal_1": {"gacha_type": "normal", "item_type": 0, "times": 1, "rarity": 0, "omake_times": 0, "omake_rarity": 0, "cost":10},
"normal_11": {"gacha_type": "normal", "item_type": 0, "times": 10, "rarity": 0, "omake_times": 1, "omake_rarity": 3, "cost":100},
"fighter": {"gacha_type": "fighter", "item_type": 0, "times": 2, "rarity": 0, "omake_times": 0, "omake_rarity": 0, "cost":30},
"omake_2": {"gacha_type": "omake_2", "item_type": 0, "times": 9, "rarity": 2, "omake_times": 2, "omake_rarity": 3, "cost":150},
"omake_fighter": {"gacha_type": "omake_fighter", "item_type": 2, "times": 5, "rarity": 0, "omake_times": 1, "omake_rarity": 3, "cost":100}
}
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()
# gacha
cursor.execute("DROP TABLE IF EXISTS gacha")
#id
#start_time
#end_time
#gacha_group
#gacha_lottery_id
cursor.execute(
"""CREATE TABLE gacha
(id INTEGER PRIMARY KEY AUTOINCREMENT
,start_time DATETIME
,end_time DATETIME
,gacha_group VARCHAR(32)
,gacha_type VARCHAR(32)
)
"""
)
# 1: {"gacha_group": "A", "weight": 3, "item_id": 5101},
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
)
"""
)
# "normal_1": {"item_type": 0, "times": 1, "rarity": 0, "omake_times": 0, "omake_rarity": 0, "cost":10},
cursor.execute("DROP TABLE IF EXISTS gacha_lottery")
cursor.execute(
"""CREATE TABLE gacha_lottery
(id VARCHAR(32) PRIMARY KEY
,gacha_type VARCHAR(32)
,item_type INTEGER
,times INTEGER
,rarity INTEGER
,omake_times INTEGER
,omake_rarity INTEGER
,cost INTEGER
)
"""
)
# 5101: {"rarity": 5, "item_name": "UR_勇者", "item_type": 1, "hp": 1200},
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
)
"""
)
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)
con.commit()
con.close()
if __name__ == '__main__':
main()
###ガチャ処理
import random
import sqlite3
from datetime import datetime
def convert_row2dict(row) -> dict:
keys = row.keys()
return {key: row[key] for key in 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) -> 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_list[gacha_id]["gacha_lottery_list"] = lottery_info_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,))
row = cursor.fetchone()
return convert_row2dict(row)
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,))
results = []
for row in rows:
row_dict = convert_row2dict(row)
results.append(row_dict)
return results
def get_items_all(cursor) -> dict:
select_sql = "SELECT * FROM items ORDER BY id"
rows = cursor.execute(select_sql)
results = {}
for row in rows:
row_dict = convert_row2dict(row)
results[row["id"]] = row_dict
return results
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,))
results = {}
for row in rows:
row_dict = convert_row2dict(row)
results[row["id"]] = row_dict
return results
def get_gacha_items_all(cursor) -> dict:
select_sql = "SELECT * FROM gacha_items ORDER BY id"
rows = cursor.execute(select_sql)
results = {}
for row in rows:
row_dict = convert_row2dict(row)
results[row["id"]] = row_dict
return results
def get_gacha_info(cursor, gacha_id: int, gacha_lottery_id: str, now_time: int):
gacha = get_gacha(cursor, gacha_id, now_time)
gacha_lottery = get_gacha_lottery(cursor, gacha_lottery_id)
if gacha["gacha_type"] != gacha_lottery["gacha_type"]:
return None, None
return gacha, gacha_lottery
def set_gacha(cursor, now_time: int):
cursor = cursor
now_time = now_time
items = get_items_all(cursor)
# 抽選対象リストを抽出
def get_lots(gacha_group: str, lottery_info: 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)
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"]))
return ids
return exec
def main():
con = sqlite3.connect("data.db")
con.row_factory = sqlite3.Row
cursor = con.cursor()
# 動作確認のため、ガチャ実行日時を指定
now_time = int(datetime.strptime("2020-05-01 00:00:00", '%Y-%m-%d %H:%M:%S').timestamp())
# 初期化(実行日時、アイテム等をセット)
func_gacha = set_gacha(cursor, now_time)
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"]))
#con.commit()
con.close()
if __name__ == '__main__':
main()
###実行結果
==normal_1==:gacha_group:A
ID:8, R, R_戦士
==normal_11==:gacha_group:A
ID:5, SR, SR_戦士
ID:8, R, R_戦士
ID:3, SSR, SSR_魔法使い
ID:10, R, R_神官
ID:9, R, R_魔法使い
ID:9, R, R_魔法使い
ID:8, R, R_戦士
ID:1, UR, UR_勇者
ID:10, R, R_神官
ID:10, R, R_神官
ID:7, SR, SR_神官
##補足
ガチャの実行時にgacha_idとgacha_lottery_idを引数として渡している事に疑問を持たれた方もいるかと思います。ガチャを実行するのに必要なのはgacha_lottery_idで、紐付けられたgacha_typeで、その処理が行われる日時に該当するgacha_idを取得すれば、ガチャ処理としては問題ありません。
しかし、ガチャの実施期間の切り替わるタイミング
でガチャを実行した場合、ユーザは自分が引きたい対象アイテムとは、異なる対象アイテムのガチャを実行することになるかもしれません。
####例
ユーザは5/31 23:59:59に画面に表示されていたガチャを実行、しかし処理のタイミングで6/1に変わり、ガチャ対象アイテムがAからCに変わってしまった
このような事を避けるために、ガチャ実行時にgacha_idも引数として渡して、もしそのgacha_idが実施期間対象外であれば、そのガチャは終了しましたということでユーザに通知して、ガチャ画面を再度表示します。
ガチャは、課金が絡むことを前提とした処理のため、厳密にユーザの意図と異なる処理が行われないようにしなければなりません。
今回のソースでは、若干エラー処理を省いていますので、アプリケーションとして必要なエラー処理を考え実装してください