見出し画像

イーオンズ・エンドの自動サプライ作成ツールを作る(1) パソコンで動かす

イーオンズ・エンドを4人でどうしてもやりたいと思い、二年半ぶりに旧友を3人誘って、自治体運営の格安会議室で6時間ぶっ通しで遊びました。結果は合計3戦で1勝2敗。初戦のレイジボーンは余裕の初見撃破だったのですが、2,3戦目の甲殻の女王は2戦続けて群舞による敗北となってしまいました。

全員がテーブルトークRPG、マジック・ザ・ギャザリング、ドミニオンというキャリアパスを経ているため、ハマらない訳がないという目論見でしたが、私が想像していた以上に好評で、1週間後に再戦となりました。

◇ はじめに

実際に何度もプレイするときに意外と面倒に感じたのがサプライ選択でした。イーオンズ・エンド 完全日本語版のルールブックでは「宝石カードから3種類、遺物カードから2種類、そして呪文カードから4種類を、任意、あるいは登場判定カードをランダムに選び出すといいでしょう」という記述があるのですが、「イーオンズ・エンド:終わりなき戦い 完全日本語版」はこれに加えて以下のような「サプライ構成チャート」というカードが付属しており、カード種別の構成枚数やエーテルコストが書かれています。サプライ構成チャートは1から6までの6種類があります。

サプライ構成チャート(全6種類あります)

「サプライ構成チャート」の条件にもとづいてサプライを自動的に選別する仕組みがあれば良いのにと思ったのですが、見つからなかったので作ることにしました。できれば1週間後の集まりで運用したいと思います。

◇ 仕様を考える

今回は「サプライ構成チャート」の条件にもとづいてサプライを選択するという内容のため、データベースを使うことにします。カード一覧、サプライ構成チャートのテーブルを設定すればよさそうです。
やりたいことを挙げてみました。

サプライ構成チャートは6種類ありますが、これもどれを選択するかはシステムに任せてしまいます。また外で遊ぶ場合には持ち出せるカードも限られているので選び出せるカードも絞れるようにします。
これも外出時の考慮ですが、わざわざパソコンを持参したくないのでAndroidスマホで動くようにしたいと思います。

最終的にはAndroidスマホで動かしますが、今回はパソコンでこの仕様で動作するか確認していきます。

◇ 必要なもの

  • パソコン 

    • 今回はWindows 11のパソコンを使用します

  • データべース作成環境

    • SQLiteを導入します。「Precompiled Binaries for Windows」の中にある「64-bit DLL (x64) for SQLite」をダウンロードし、zipファイル内にある sqlite3.dll を C:¥Windows¥System32にコピーします。

    • DB Browser for SQLiteを導入します。「DB Browser for SQLite - Standard installer for 64-bit Windows」をダウンロード、インストールします。

  • Python開発環境

    • Python環境はv3.9.8を使用します

    • 必要なライブラリはpandasです。Python環境のインストール後にコマンドプロンプトで「pip install pandas」を実行するとインストールされます。

    • プログラムエディターはPyScripter v4.1.1.0 x64を使用します。

◇ 開発手順

1. DB Browser for SQLiteでデータベースを設定する

仕様を考えたときにカード一覧、サプライ構成チャートをテーブルにするのが良さそうだと判断しましたので、DB Browser for SQLを起動して早速作っていきます。
「新しいデータベース」を選択してAeons_end.dbを作ります。作ったデータベースのデータベース構造からテーブルを作成を選択して「カード一覧」のテーブルであるcardlistと「サプライ構成チャート」のテーブルであるsupplyを設定します。

DB Browser for SQLの新しいデータベース、テーブルの設定画面

cardlistテーブルのフィールドにはカード名(name)、第何弾のカードであるか(wave)、基本/拡張セット名(card_set)、カード種類(type)、必要エーテル数(cost)で構成しました。すべてのデータは空欄を認めておらず、nameフィールドはデータの重複も認めていません。costフィールドは数値型でそれ以外は文字列型としています。これらの設定をDB Browser for SQLではすべてGUIから設定できます。

DB Browser for SQLのcardlistテーブル設定画面

フィールド内の第何弾(wave)については以下で詳細を書いておりますので併せて見ていただけると幸いです。

supplyテーブルのフィールドにはサプライ構成チャート種類(pattern)、カード種類(type)、エーテル数1(cost_1)、エーテル数2(cost_2)、選択エーテル数に加える条件(以上、以下、等しい)(condition)で構成しました。cost_2フィールド以外すべてのデータは空欄を認めていません。

※エーテル数のフィールド(cost_1、cost_2)が2つ設定されているのは、ほとんどの「サプライ構成チャート」は指定のエーテル数以上、以下、等しいで表現できるのですが、指定のエーテルが4か5というパターンがあるため、このような構成にしました。

DB Browser for SQLのsupplyテーブル設定画面

フィールドの名前を決め、どのようなデータを持つか決めましたら、データを打ち込んでいきます。データ閲覧タブを選び、テーブルをcardlistを選択します。このcardlistテーブルにデータを追加するときは以下の「新しいレコードを現在のテーブルに挿入」を押しますと1行追加します。

cardlistテーブルのデータ入力

同じようにsupplyテーブルのフィールドにも値を入力します。

supplyテーブルのデータ入力

これで検索に使用するデータベースが完成しました。

2. 検索用のSQL文を挙げる

すでに設定したデータベースに対してどのようなSQL文を書けば、欲しいサプライ情報を入手できるか考えます。

サプライ構成チャートも6種類からどの組み合わせを採用するかをランダムで決めたいので、以下のSQL文でsupplyテーブル内にあるpatternフィールドのデータをランダムで1件選びます。ORDER BY RAMDOM() LIMIT 1が「該当したレコードからランダムで1件選択する」という意味です。

SELECT pattern FROM supply ORDER BY RANDOM() LIMIT 1

上のSQL文で選択したpatternフィールドは1~6のいずれかを持っていますので、supplyテーブルから同じpatternフィールドのデータをすべて選びます。
例えばランダムで選択されたpatternフィールドが3であれば以下のように書きます。

SELECT * FROM supply WHERE pattern == 3

選びたいカードの条件はsupplyテーブルから選択しましたので、この条件に合うカードをcardlistテーブルから検索し、複数が当てはまる場合にはランダムで1件選びます。supplyテーブルには大きく分けて4種類の分類があります。これに加えて選択対象を全体とするか、一部とするかで倍になるため、全部で選択条件が8種類になります。

(1) cardlistテーブル内で必要エーテルが4か5(BETWEEN 4 AND 5)の宝石カードの名前をランダムで1件選びます。

SELECT name FROM cardlist WHERE type = '宝石' AND cost BETWEEN 4 AND 5 ORDER BY RANDOM() LIMIT 1

(2) cardlistテーブル内で第1弾のカード(wave = '1st wave')の中から必要エーテルが4か5の宝石カードの名前をランダムで1件選びます。

SELECT name FROM cardlist WHERE type = '宝石' AND cost BETWEEN 4 AND 5 AND wave = '1st wave' ORDER BY RANDOM() LIMIT 1

(3) cardlistテーブル内のカードの中から必要エーテルが4以下(cost <= 4)の宝石カードの名前をランダムで1件選びます。

SELECT name FROM cardlist WHERE type = '宝石' AND cost <= 4 ORDER BY RANDOM() LIMIT 1

(4) cardlistテーブル内で第2弾のカードの中から必要エーテルが4以下の宝石カードの名前をランダムで1件選びます。

SELECT name FROM cardlist WHERE type = '宝石' AND wave = '2nd wave' AND cost <= 4 ORDER BY RANDOM() LIMIT 1

(5) cardlistテーブル内のカードの中から必要エーテルが4以上(cost >= 4)の宝石カードの名前をランダムで1件選びます。

SELECT name FROM cardlist WHERE type = '宝石' AND cost >= 4 ORDER BY RANDOM() LIMIT 1

(6) cardlistテーブル内で第2弾のカードの中から必要エーテルが4以上の宝石カードの名前をランダムで1件選びます。

SELECT name FROM cardlist WHERE type = '宝石' AND wave = '2nd wave' AND cost >= 4 ORDER BY RANDOM() LIMIT 1

(7) cardlistテーブル内のカードの中から必要エーテルが4(cost = 4)の宝石カードの名前をランダムで1件選びます。

SELECT name FROM cardlist WHERE type = '宝石' AND cost = 4 ORDER BY RANDOM() LIMIT 1

(8) cardlistテーブル内で第2弾のカードの中から必要エーテルが4の宝石カードの名前をランダムで1件選びます。

SELECT name FROM cardlist WHERE type = '宝石' AND wave = '2nd wave' AND cost = 4 ORDER BY RANDOM() LIMIT 1

3. コードを書く

ここまでデータベースを作り、その中からどのようなSQL文を使えば、欲しいデータを選択できるか考えてきました。改めてその中から見てみたいと思います。

SELECT name FROM cardlist WHERE type = '宝石' AND wave = '2nd wave' AND cost >= 4 ORDER BY RANDOM() LIMIT 1

上のSQL文を見ますと、SELECT WHERE文で書いている条件に直接条件を書いています。このままですと、例えば選択したいカード種類(type)が'宝石'から'呪文'に変わると別のSQL文を書かないといけません。
そこで上のSQL文の中で'宝石'や4といった値を置き換えるようにすれば何度も使えるのであれば、そこを変数に置き換えます。

type、costフィールドはsupplyテーブルを参照できるので問題ないです。
waveフィールドはないので今回はコード上に直書きします。今後、この部分は見直していきます。
検索条件によっては該当するカードが存在しない可能性があります。このような場合にはエラーを出力し、再度サプライ生成を行ってもらうようエラー処理を追加します。

メイン関数は以下のようにしました。

import sqlite3
import pandas as pd

def main():
    conn = sqlite3.connect('Aeons_end.db') # 'Aeons_end.db'を開く
    cur = conn.cursor()
    supply_set = [] # 選択したカード名を保持する配列変数の初期化
    keys = [] # SQL文で変数化した内容を保持する配列変数の初期化

    supply = pd.read_sql_query('SELECT pattern FROM supply ORDER BY RANDOM() LIMIT 1', conn) # サプライ構成チャート番号をランダムで1件取得する
    keys.append(int(supply.pattern[0])) # 取得したサプライ構成チャート番号をkeysに保持する
    pattern = pd.read_sql_query('SELECT * FROM supply WHERE pattern == ?', conn, params=keys) # supplyテーブルからサプライ構成チャート番号に該当するカード情報を取得する
    applyset = 'all' # 第1弾+第2弾(all)、第1弾(1st wave)、第2弾(2nd wave)を検索対象とする。将来的にGUIから値を渡す

    for i in range(len(pattern)): # サプライ構成チャート番号に該当するカード枚数だけループ
        result = check_condition(pattern,applyset,i) # SQL文を作成する関数

        keys = [pattern.type[i],int(pattern.cost_1[i])] # SQL文の変数を編集する
        if result[0] == True: # supplyテーブルのcost_2フィールドにデータが存在する
            keys.append(int(pattern.cost_2[i])) #SQL文の変数を追加する

        if applyset != 'all': # 第1弾か第2弾のいずれかが検索対象
            keys.append(applyset) #SQL文の変数を追加する

        cardselect = pd.read_sql_query(result[1], conn, params=keys) # 変数keysに基づいてデータベースを検索する

        if len(cardselect) != 0: # データベース内に1件以上の該当データが存在した
            while cardselect.name[0] in supply_set: # 同じカードを選択していないか確認する
                cardselect = pd.read_sql_query(result[1], conn, params=keys) # 同じカードがあれば再度データベース検索する
            supply_set.append(cardselect.name[0]) # 選択したカードリストにカード名を追加する
        else: データベース内に該当データなし
            print('該当するカードが一部存在していません。\nカードの選択範囲を変更して再実施してください。')
            break

    print('サプライ構成チャート' + str(pattern.pattern[0]) + '\n' + str(supply_set))
    conn.close()
    pass

if __name__ == '__main__':
    main()

supplyテーブルの設定に合わせてSQL文を作成する箇所をcheck_conditionという関数にしました。ここで上で書いた8種類のSQL文を条件に合わせて作成しています。

import sqlite3
import pandas as pd

def check_condition(pattern,applyset,i): # 引数でサプライ構成チャート番号、検索範囲、現在検索したいsupplyテーブルのデータ

    cmd = ''
    select_type = False

    cmd = 'SELECT name FROM cardlist WHERE type = ? AND cost ' # 共通のSQL文を保持
    
    if pd.notna(pattern.cost_2[i]): # supplyテーブルのcost_2に値がある
        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 = ? '

    cmd += 'ORDER BY RANDOM() LIMIT 1' 
    
    return select_type,cmd # cost_2フィールドのデータ有無フラグとSQL文を戻り値でmain関数に返す

◇ 完成コード

import sqlite3
import pandas as pd

def check_condition(pattern,applyset,i):

    cmd = ''
    select_type = False

    cmd = 'SELECT name 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 = ? '
    cmd += 'ORDER BY RANDOM() LIMIT 1'

    return select_type,cmd

def main():
    conn = sqlite3.connect('Aeons_end.db')
    cur = conn.cursor()
    supply_set = []
    keys = []

    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 = 'all'

    for i in range(len(pattern)):
        result = check_condition(pattern,applyset,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] in supply_set:
                cardselect = pd.read_sql_query(result[1], conn, params=keys)
            supply_set.append(cardselect.name[0])
        else:
            print('該当するカードが一部存在していません。\nカードの選択範囲を変更して再実施してください。')
            break

    print('サプライ構成チャート' + str(pattern.pattern[0]) + '\n' + str(supply_set))
    conn.close()
    pass

if __name__ == '__main__':
    main()

現状の出力はこのような感じです。

サプライ構成チャート1
['忌まわしきダイアモンド''赤熱ルビー''変化の真珠''ゆらめくプリズム''屈曲ダガー''着火''鋸歯雷''熾火の鞭''飲み込む虚無']

出力を見た感じでは、このコードでやりたいことはできたと思います。しかし現状は検索範囲を変えるときにコードを変更する必要があるため、次回はこのコードにGUIを追加して改善したいと思います。

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

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