見出し画像

イーオンズ・エンドの自動サプライ作成ツールを作る(3) オプション検索を追加する

1週間前に「甲殻の女王」に惨敗したわれわれは再び集まって討伐することにしました。しかし最初の1戦はあっさりと完敗、2戦目は中盤から押し返して最終局面で勝ったと油断していたところに2連続ネメシスで群舞による強制ゲーム終了。3戦目はグレイヴホールドの体力が残り3点まで追い込まれたのですが、フェドラクサの「予兆のルーン」が大当たりして辛勝することができました。このゲームバランスがイーオンズ・エンドの魅力です。

爽快呪文「カオスアーク」のイラストになっているが防御系のフェドラクサ

その後、「歪んだ仮面」に初見撃破し、勢いに乗ったわれわれはCore Setの最終ボス(?)の「暴食の公子」に挑んだのですが、サプライを食い尽くされて敗北。この日の戦績は5戦2勝3敗でした。

先週、急いで作成したサプライ自動生成ツールは大変便利でしたが、課題点もありました。それはサプライ自動生成ツールを選ぶときにサプライ構成チャート以外に最低1枚はカードを破壊できるカードをサプライにほしいというというときに対応できない点です。

対応しましょう!

◇ 追加する仕様を考える

自身が持っているイーオンズ・エンド(Core Set)と終わりなき戦いのカードを見て、また実際に遊んだときに重要に思えたカード効果は以下の5点でした。

  • カードを破壊する

  • グレイヴホールドの体力を回復する

  • プレイヤーの体力を回復する

  • 破孔を強化する

  • チャージを取得する

そこで、これらの効果を含んだカードを自動サプライ作成時のオプションとしてチェックボックスで複数選択できるようにします。

実現にあたり作成したカード情報データベースに追加するオプション5点を追加します。

cardlistテーブルにカラムを5点追加

cardlistテーブルの該当する効果を持っているカードに「applicable」を追加していきます。
こうしてみますとCore Setのみの場合は、プレイヤー体力の効果が少ないことが改めて分かります。

追加したカラムに情報を追加

1. 従来の処理

6種類のサプライ構成チャート(supplyテーブル)の中から1つをランダムで選択し、条件に合ったカードをカード一覧(cardlistテーブル)からランダムで9枚選ぶ。

2. 追加する処理

従来の処理で使用したサプライ構成チャート(supplyテーブル)の条件に合ったカードを選択したオプションでフィルターしたカード一覧(cardlistテーブル)からすべて選択し、その中からランダムでカードをオプション1つにつき1枚選択します。
選択したカードを従来の処理で選択したカード一覧と比較して、重複がないようであればカード情報を上書きします。

ただし、今回追加するオプションよりもサプライ構成チャートの条件を優先します。例えばイーオンズエンド(Core Set)だけの場合、プレイヤーの体力を回復するサプライ上のカードは「エッセンス強奪」しかないのですが、サプライ構成チャートにエッセンス強奪のカード種別(呪文)、コスト(5)がなければ選ばれません。

「サプライ構成チャート1」では5エーテルの呪文が選ばれない

またオプションが複数選択された場合でどうしても両方を採用できないサプライ構成チャートの場合には以下の優先度で採用します。上から優先順位を高くします。この順番はもう少しこのゲームをやり込むと変わるでしょう。

  1. カードを破壊する

  2. グレイヴホールドの体力を回復する

  3. プレイヤーの体力を回復する

  4. 破孔を強化する

  5. チャージを取得する

3. 改修を反映する場所を考える

どういった仕様にするか決まりましたら、どこに何を反映するかまとめます。

  • データベース

  • GUI

  • 処理

データベースは今回追加するオプション5種類のカラムを追加します。当初はカードテキストをすべて追加して、文章からどのオプションを持ったカードであるか判定しようと考えましたが、すべてを入力するのが大変ということと、日本語版のカードテキスト(特に破孔の強化関連)が一貫していないことがあり、この対応としました。

GUIはサプライ生成のオプションを追加します。選択できるオプションが1つであればラジオボタンにしますが、オプションを複数選択できた方が良いと思い、チェックボックスにしました。

処理は上記、追加する処理をコードに落としました。
それではこれらの内容を今までのコードに追加して実現したいと思います。

◇ 改修を実装する

1. データベース

「DB Browser for SQLite」というソフトウエアでこれまで作成しましたデータベースファイルを開きます。こちらのソフトウエアにつきましては以前に紹介しましたので、そちらを参照ください。

cardlistテーブルの「テーブルを変更」を選択し、カラムを追加します。すべてデータ型は文字列(TEXT)で、任意の入力項目とします。destoroy_cardカラムが「カードを破壊する」、focus_breachカラムが「破孔を強化する」、gain_chargeカラムが「チャージを取得する」、gain_gravehold_lifeカラムが「グレイブホールドの体力を回復する」、gain_lifeカラムが「プレイヤーの体力を回復する」です。優先度で並べずにアルファベット順にしています。

cardlistテーブルにカラムを追加する

追加した5つのカラムにデータを更新していきます。「データ閲覧」、「cardlist」を選択しましてカード効果が該当するところに該当するを意味する「applicable」を記入します。もし強化オーブのように複数に該当するカードがありましたら両方にapplicableを記入します。

カード効果をcardlistに反映する

すべて更新が終わりましたらデータベースを保存して作業完了です。

2. GUI

仕様を考えたときにオプションはチェックボックスで表示するとしましたので画面レイアウトを考えないといけません。検索条件を設定し(入力)、ボタンを押すと(処理)、選択されたサプライセットが表示される(出力)という動きを考えますと、入力は画面の上、出力は画面の下に配置するのが自然な目線の誘導です。処理は入力と出力の間に入れていたのですが、チェックボックスを追加したことでボタンとチェックボックスを混ぜると煩雑に見えましたので。画面下にボタンを移動しました。

チェックボックスは優先順位が最も高いものを左上に配置し、下方向に次に優先順位が高いオプションを配置しました。すべての項目を同じ列に配置するとバランスが悪いため、2列で配置しています。

変更したGUIのコードは以下のとおりです。

import tkinter as tk
from tkinter import ttk

root = tk.Tk()
root.geometry('360x450')
root.title('イーオンズ・エンド - サプライ自動生成')

# ラベル
txt_lbl = ttk.Label(root, text='検索するカードセット:', width='', state='normal')
txt_lbl.place(relx=0.1, rely=0.03, relheight=0.1, relwidth=0.4)

# コンボボックス
conn = sqlite3.connect('Aeons_end.db')
cur = conn.cursor()
module =[]
cardlist = pd.read_sql_query('SELECT DISTINCT wave FROM cardlist' , conn)
module.append('all')
for i in range(len(cardlist)):
    module.append(cardlist.wave[i])
conn.close()

combobox = ttk.Combobox(root, values=module, state='readonly', width='20', justify = "center")
combobox.current(0)
combobox.place(relx=0.45, rely=0.04, relheight=0.08, relwidth=0.45)

# チェックボタン
destroy_card = tk.BooleanVar()
focus_breach = tk.BooleanVar()
gain_charge = tk.BooleanVar()
gain_gravehold_life = tk.BooleanVar()
gain_life = tk.BooleanVar()

chk_1 = ttk.Checkbutton(root, variable=destroy_card, text='カード破壊')
chk_1.place(relx=0.1, rely=0.13, relheight=0.1, relwidth=0.4)
destroy_card.set(False)

chk_2 = ttk.Checkbutton(root, variable=gain_gravehold_life, text='グレイヴホールド回復')
chk_2.place(relx=0.1, rely=0.2, relheight=0.1, relwidth=0.4)
gain_gravehold_life.set(False)

chk_3 = ttk.Checkbutton(root, variable=focus_breach, text='破孔強化')
chk_3.place(relx=0.55, rely=0.13, relheight=0.1, relwidth=0.4)
focus_breach.set(False)

chk_4 = ttk.Checkbutton(root, variable=gain_charge, text='チャージを得る')
chk_4.place(relx=0.55, rely=0.2, relheight=0.1, relwidth=0.4)
gain_charge.set(False)

chk_5 = ttk.Checkbutton(root, variable=gain_life, text='体力回復')
chk_5.place(relx=0.1, rely=0.27, relheight=0.1, relwidth=0.4)
gain_life.set(False)

# テキストボックス
out = tk.Text(root, width='38', height='20')
out.place(relx=0.08, rely=0.37, relheight=0.5, relwidth=0.85)

# ボタン
btn = ttk.Button(root, text='サプライ選択', command=btn_click)
btn.place(relx=0.3, rely=0.9, relheight=0.08, relwidth=0.4)

root.mainloop()

3. 処理

オプションのチェックボックスの状態を見てサプライ構成チャートと比較するSQL文を作成するためのパラメータを作り、SQL文を実行した結果から該当するカードを適用します。もしオプションとサプライ構成チャートに当てはまるカードが複数存在する場合にはランダムで1枚選んでサプライに反映します。

import sqlite3
import pandas as pd
import random # 配列からランダムで値を取得するライブラリ

option.append(gain_charge.get()) # チャージを得るのチェックボックス状態をセット
option.append(focus_breach.get()) # 破孔強化のチェックボックス状態をセット
option.append(gain_life.get()) # 体力回復のチェックボックス状態をセット
option.append(gain_gravehold_life.get()) # グレイヴホールド回復のチェックボックス状態をセット
option.append(destroy_card.get()) # カード破壊のチェックボックス状態をセット

if len(cardselect) != 0: # サプライ構成チャートに基づいて9枚選ぶことができた
     for i in range(len(option)): # チェックボックスの数だけループする
         if option[i] == True: # チェックボックスが有効化している
             option_index = []
             option_list = []
             priority_mode = i+1 # 優先順位の低いオプションから処理する
             for j in range(len(pattern)): # サプライ構成チャートの条件から当てはまるカードを探す
                 result = check_condition(pattern,applyset,priority_mode,j) # SQL文を作成する関数に処理を渡す
                 keys = [pattern.type[j],int(pattern.cost_1[j])] # SQL文に渡すパラメータを設定する
                 if result[0] == True:
                     keys.append(int(pattern.cost_2[j]))
                 if applyset != 'all':
                     keys.append(applyset)
                 keys.append('applicable') # オプションの項目が有効になっているカードの中から選ぶ
                 cardselect = pd.read_sql_query(result[1], conn,params=keys) # 対象カードを選ぶ
                 if (len(cardselect) != 0 and
                     cardselect.name[0] + ': ' + cardselect.card_set[0] not in option_list):
                     option_index.append(j) # 何番目のカードを置き換えるか保存する
                     option_list.append(cardselect.name[0] + ': ' + cardselect.card_set[0]) # 更新カード名を保存する
                 else:
                     option_list.append('None') # オプション設定とサプライ構成チャートの条件に当てはまらなかった場合にはNoneを保存する
              if len(option_index) != 0: # オプションとサプライ構成チャートに当てはまるカードが存在する
                 update_card = random.choice(option_index) # 複数枚存在する場合には1オプションにつき1枚を選出する
                 if option_list[update_card] not in supply_card: # その選出したカードがサプライになければ上書き更新する
                     supply_card[update_card] = option_list[update_card]

SQL文を編集する関数check_conditionにも処理を追加しました。SQL文を作る際に今回追加したオプションが選ばれているようであれば、選択したオプションに当てはまるカラム名をSELECT ~ FROM ~ WHEREの条件に追加します。

def check_condition(pattern,applyset,priority_mode,i): # 引数priority_modeを追加
    cmd = ''
    select_type = False

    cmd = 'SELECT name, card_set FROM cardlist WHERE type = ? AND cost '

    if pd.notna(pattern.cost_2[i]):
        select_type = True
        cmd += 'BETWEEN ? AND ? '
    elif pattern.condition[i] == '以下':
        cmd += '<= ? '
    elif pattern.condition[i] == '以上':
        cmd += '>= ? '
    elif pattern.condition[i] == '等しい':
        cmd += '= ? '

    if applyset != 'all':
        cmd += 'AND wave = ? '

    if priority_mode == 1: # チェックボックスのオプションごとに対応するカラムでフィルターをかける
        cmd += 'AND gain_charge = ? '
    elif priority_mode == 2:
        cmd += 'AND focus_breach = ? '
    elif priority_mode == 3:
        cmd += 'AND gain_life = ? '
    elif priority_mode == 4:
        cmd += 'AND gain_gravehold_life = ? '
    elif priority_mode == 5:
        cmd += 'AND destroy_card = ? ' # ここまでを追加

    cmd += 'ORDER BY RANDOM() LIMIT 1'
    return select_type,cmd

◇ 完成したコード

import sqlite3
import pandas as pd
import tkinter as tk
from tkinter import ttk
import random

def check_condition(pattern,applyset,priority_mode,i):
    cmd = ''
    select_type = False

    cmd = 'SELECT name, card_set FROM cardlist WHERE type = ? AND cost '

    if pd.notna(pattern.cost_2[i]):
        select_type = True
        cmd += 'BETWEEN ? AND ? '
    elif pattern.condition[i] == '以下':
        cmd += '<= ? '
    elif pattern.condition[i] == '以上':
        cmd += '>= ? '
    elif pattern.condition[i] == '等しい':
        cmd += '= ? '

    if applyset != 'all':
        cmd += 'AND wave = ? '

    if priority_mode == 1:
        cmd += 'AND gain_charge = ? '
    elif priority_mode == 2:
        cmd += 'AND focus_breach = ? '
    elif priority_mode == 3:
        cmd += 'AND gain_life = ? '
    elif priority_mode == 4:
        cmd += 'AND gain_gravehold_life = ? '
    elif priority_mode == 5:
        cmd += 'AND destroy_card = ? '

    cmd += 'ORDER BY RANDOM() LIMIT 1'
    return select_type,cmd

def btn_click():
    conn = sqlite3.connect('Aeons_end.db')
    cur = conn.cursor()
    supply_card = []
    option_index = []
    option_list = []
    keys = []
    option = []
    out.delete(0.'end')

    supply = pd.read_sql_query('SELECT pattern FROM supply ORDER BY RANDOM() LIMIT 1', conn)
    keys.append(int(supply.pattern[0]))
    pattern = pd.read_sql_query('SELECT * FROM supply WHERE pattern == ?', conn, params=keys)

    applyset = combobox.get()
    option.append(gain_charge.get())
    option.append(focus_breach.get())
    option.append(gain_life.get())
    option.append(gain_gravehold_life.get())
    option.append(destroy_card.get())

    for i in range(len(pattern)):
        priority_mode = 0
        result = check_condition(pattern,applyset,priority_mode,i)
        keys = [pattern.type[i],int(pattern.cost_1[i])]
        if result[0] == True:
            keys.append(int(pattern.cost_2[i]))

        if applyset != 'all':
            keys.append(applyset)

        cardselect = pd.read_sql_query(result[1], conn, params=keys)

        if len(cardselect) !0:
            while (cardselect.name[0] + ': ' + cardselect.card_set[0]) in supply_card:
                cardselect = pd.read_sql_query(result[1], conn, params=keys)
            supply_card.append(cardselect.name[0] + ': ' + cardselect.card_set[0])
        else:
            out.insert(0.,'該当するカードが存在しません。''\n' + 'サプライ選択ボタンを押してください。')
            break

    if len(cardselect) !0:
        for i in range(len(option)):
            if option[i] == True:
                option_index = []
                option_list = []
                priority_mode = i+1
                for j in range(len(pattern)):
                    result = check_condition(pattern,applyset,priority_mode,j)
                    keys = [pattern.type[j],int(pattern.cost_1[j])]
                    if result[0] == True:
                        keys.append(int(pattern.cost_2[j]))

                    if applyset != 'all':
                        keys.append(applyset)

                    keys.append('applicable')

                    cardselect = pd.read_sql_query(result[1], conn,params=keys)

                    if (len(cardselect) != 0 and
                        cardselect.name[0] + ': ' + cardselect.card_set[0] not in option_list):
                        option_index.append(j)
                        option_list.append(cardselect.name[0] + ': ' + cardselect.card_set[0])
                    else:
                        option_list.append('None')
                if len(option_index) !0:
                    update_card = random.choice(option_index)
                    if option_list[update_card] not in supply_card:
                        supply_card[update_card] = option_list[update_card]

    for i in range(len(supply_card)):
        if i == len(supply_card)-1:
            out.insert(tk.END,str(i+1) + '. ' + supply_card[i])
        else:
            out.insert(tk.END,str(i+1) + '. ' + supply_card[i] + '\n' + '\n')

    conn.close()

root = tk.Tk()
root.geometry('360x450')
root.title('イーオンズ・エンド - サプライ自動生成')

# ラベル
txt_lbl = ttk.Label(root, text='検索するカードセット:', width='', state='normal')
txt_lbl.place(relx=0.1, rely=0.03, relheight=0.1, relwidth=0.4)

# コンボボックス
conn = sqlite3.connect('Aeons_end.db')
cur = conn.cursor()
module =[]
cardlist = pd.read_sql_query('SELECT DISTINCT wave FROM cardlist' , conn)
module.append('all')
for i in range(len(cardlist)):
    module.append(cardlist.wave[i])
conn.close()

combobox = ttk.Combobox(root, values=module, state='readonly', width='20', justify = "center")
combobox.current(0)
combobox.place(relx=0.45, rely=0.04, relheight=0.08, relwidth=0.45)

# チェックボタン
destroy_card = tk.BooleanVar()
focus_breach = tk.BooleanVar()
gain_charge = tk.BooleanVar()
gain_gravehold_life = tk.BooleanVar()
gain_life = tk.BooleanVar()

chk_1 = ttk.Checkbutton(root, variable=destroy_card, text='カード破壊')
chk_1.place(relx=0.1, rely=0.13, relheight=0.1, relwidth=0.4)
destroy_card.set(False)

chk_2 = ttk.Checkbutton(root, variable=gain_gravehold_life, text='グレイヴホールド回復')
chk_2.place(relx=0.1, rely=0.2, relheight=0.1, relwidth=0.4)
gain_gravehold_life.set(False)

chk_3 = ttk.Checkbutton(root, variable=focus_breach, text='破孔強化')
chk_3.place(relx=0.55, rely=0.13, relheight=0.1, relwidth=0.4)
focus_breach.set(False)

chk_4 = ttk.Checkbutton(root, variable=gain_charge, text='チャージを得る')
chk_4.place(relx=0.55, rely=0.2, relheight=0.1, relwidth=0.4)
gain_charge.set(False)

chk_5 = ttk.Checkbutton(root, variable=gain_life, text='体力回復')
chk_5.place(relx=0.1, rely=0.27, relheight=0.1, relwidth=0.4)
gain_life.set(False)

# テキストボックス
out = tk.Text(root, width='38', height='20')
out.place(relx=0.08, rely=0.37, relheight=0.5, relwidth=0.85)

# ボタン
btn = ttk.Button(root, text='サプライ選択', command=btn_click)
btn.place(relx=0.3, rely=0.9, relheight=0.08, relwidth=0.4)

root.mainloop()

Androidスマホでも問題なく使えることを確認しました。


この記事が参加している募集

この記事が気に入ったらサポートをしてみませんか?