見出し画像

(遊戯王OCG)シミュレーションで挑むシングルor箱問題

皆さんこんにちは。沙里葉(Sariha)です。
今回は遊戯王のあれこれをシミュレーションする企画第二弾として、OCGプレイヤーならきっと経験があるであろう「シングル買いと箱買いどちらの方が得か」問題にプログラミングで立ち向かおうと思います。
全文無料です。
最後までお付き合いください。



はじめに

遊戯王OCGプレイヤーなら誰しも悩む「シングル買いか、パック(あるいは箱、カートン)買いか」という問題を、プログラミングのシミュレーションを用いて解決する試みを紹介します。
この記事では、Pythonプログラムを使ったシミュレーションの方法を解説します。
プログラミングに興味がある初心者でも理解しやすい内容にするつもりです。

忙しい人用

忙しい人(プログラミングどうでもいいから結果だけ知りたい人)はこちらをご覧ください。


シミュレーションの概要

シミュレーションとは何か?

以前のnoteでも解説していますが、念のため今回も解説します。

「シミュレーション」は、ある現実の出来事や現象を、コンピュータや数学の力を借りて「仮想的に再現すること」を意味します。たとえば、天気予報や交通システム、ゲームの結果など、非常に多くの場面でシミュレーションは使われています。

シミュレーションは、何かを「試してみたい」ときや「どんな結果になるのか知りたい」ときに非常に役立ちます。現実世界で何かを試すのは、時間や費用がかかることもありますが、シミュレーションを使えば、現実に起こることをコンピュータの中で手軽に再現できます。これにより、「もしこうしたらどうなるか?」という質問に対して、素早く結果を知ることができるのです。

遊戯王OCGのシミュレーションに挑戦

今回のテーマは「遊戯王OCG(オフィシャルカードゲーム)」のカードを集める際、シングルで買う方が得なのか、パックで買う方が得なのか、という問題をシミュレーションで解決することです。
遊戯王OCGプレイヤーなら、欲しいカードをどうやって集めるか悩むことはきっと多いですよね。1枚ずつシングルで買うのか、それともパックを開封して運試しをするのか――。

このシミュレーションでは、「欲しいカードがシングルでいくらで売られているのか」と、「パックでランダムに引いたときにそのカードが出るまでにどれくらいお金がかかるのか」を計算し、どちらが得かを比較します。

シミュレーションの手順

  1. シングル販売コストの計算
    シミュレーションの最初のステップは、欲しいカードをシングルで購入した場合のコストを計算することです。各カードにはレアリティ(ノーマル(N)、スーパーレア(SR)、ウルトラレア(UR)など)があり、レアリティごとに値段が異なります。シミュレーションでは、この価格データをもとに、欲しいカードを必要な枚数だけ買うためにかかるお金を計算します。

  2. パック購入コストのシミュレーション
    次に、パックをランダムに開封して、欲しいカードが揃うまでにどれだけのパックやボックスを開封する必要があるかを計算します。ここがこのシミュレーションの面白いところです。パックは運次第で当たり外れがあり、場合によっては多くのパックを開けても欲しいカードが出ないこともあります。パックに入っているカードはランダムなので、シミュレーションでは何度もパックを開けて欲しいカードが揃うまでを再現します

  3. 結果の比較
    最後に、シングルで買った場合のコストと、パックで買った場合のコストを比較して、どちらが得かを判断します。シングルは確実に欲しいカードが手に入る一方、パックは運次第で思いがけないレアカードが手に入ることもあるかもしれません。シミュレーションを何度も繰り返すことで、どちらが効率的かをデータで明らかにします。

プログラムでシミュレーションを作る

シミュレーションはただコンピュータに任せるのではなく、自分でプログラムを作ることもできます。というより今回のようなピンポイントな話題では自力でプログラムを作るのが一番いいです。
今回、私はPythonというプログラミング言語を使って、遊戯王OCGのカードを集める際に「シングルで買うか、パックを開封するか」をシミュレーションしました。
Pythonは、プログラミング初心者にも優しい言語であり、短いコードで複雑な計算やシミュレーションができるため、今回のような実験にはピッタリです。

Pythonプログラムでシミュレーションしてみよう


「プログラムを作る」とは?

私たちが「シミュレーションをしてみよう」と思ったときに、コンピュータを使うことで現実の世界を仮想的に再現できます。
そして、その再現をプログラムという手段でコンピュータに命令します。

今回はPython(パイソン)というプログラミング言語を使って、遊戯王OCGの「シングルで買う」か「パックを開ける」かを比較するシミュレーションを作成します。

まずは準備しよう

シミュレーションを行うには、まず Pythonのプログラムを書く環境 を準備します。Google Colabを使えば、難しい初期設定を不要としてすぐにPythonプログラムが動かせます。

次に、遊戯王のカード情報が入ったCSVファイル(これはカード名、レアリティ、価格が書かれた表のようなもの)を準備します。これをプログラムに読み込んで、欲しいカードの価格や、パックに含まれるカードを確認できるようにします。今回,8月に発売された新弾「デッキビルドパック クロスオーバー・ブレイカーズ」を対象としました.
以下はCSVファイルの中身です.左から順に,

  • 名前

  • 読み方

  • レアリティ

  • 販売価格

  • 最終更新日

を意味しています.
(本当は自動で価格を取得するようにしたかったのですが,今回は力技でやりました…)

$$
\begin{array}{l|l|c|c|c}
name & voice & rare & price & date \\ \hline
ソード・ライゼオル & ソード・ライゼオル & SR & 780 & 240901 \\
ノード・ライゼオル & ノード・ライゼオル & N & 30 & 240901 \\
ノード・ライゼオル & ノード・ライゼオル & P & 80 & 240901 \\
ノード・ライゼオル & ノード・ライゼオル & SE & 380 & 240901 \\
アイス・ライゼオル & アイス・ライゼオル & SR & 680 & 240901 \\
エクス・ライゼオル & エクス・ライゼオル & SR & 780 & 240901 \\
パルマ・ライゼオル & パルマ・ライゼオル & N & 30 & 240901 \\
パルマ・ライゼオル & パルマ・ライゼオル & P & 80 & 240901 \\
ライゼオル・デュオドライブ & ライゼオル・デュオドライブ & SR & 120 & 240901 \\
ライゼオル・デュオドライブ & ライゼオル・デュオドライブ & SE & 480 & 240901 \\
ライゼオル・デッドネーダー & ライゼオル・デッドネーダー & UR & 480 & 240901 \\
ライゼオル・デッドネーダー & ライゼオル・デッドネーダー & QCSE & 6780 & 240901 \\
ライゼオル・プラグイン & ライゼオル・プラグイン & N & 30 & 240901 \\
ライゼオル・プラグイン & ライゼオル・プラグイン & P & 80 & 240901 \\
ライゼオル・クロス & ライゼオル・クロス & N & 30 & 240901 \\
ライゼオル・クロス & ライゼオル・クロス & P & 80 & 240901 \\
ライゼオル・ホールスラスター & ライゼオル・ホールスラスター & N & 30 & 240901 \\
ライゼオル・ホールスラスター & ライゼオル・ホールスラスター & P & 80 & 240901 \\
No.103 神葬零嬢ラグナ・ゼロ & ナンバーズ103 しんそうれいじょうラグナ・ゼロ & N & 30 & 240901 \\
No.103 神葬零嬢ラグナ・ゼロ & ナンバーズ103 しんそうれいじょうラグナ・ゼロ & P & 80 & 240901 \\
深淵に潜む者 & しんえんにひそむもの & N & 30 & 240901 \\
深淵に潜む者 & しんえんにひそむもの & P & 80 & 240901 \\
竜巻竜 & トルネードラゴン & N & 30 & 240901 \\
竜巻竜 & トルネードラゴン & P & 80 & 240901 \\
クロノダイバー・リダン & クロノダイバー・リダン & N & 30 & 240901 \\
クロノダイバー・リダン & クロノダイバー・リダン & P & 80 & 240901 \\
M∀LICE<P>White Rabbit & マリス<ポーン>ホワイト・ラビット & SR & 1680 & 240901 \\
M∀LICE<P>White Rabbit & マリス<ポーン>ホワイト・ラビット & QCSE & 27800 & 240901 \\
M∀LICE<P>Cheshire Cat & マリス<ポーン>チェシャ・キャット & SR & 580 & 240901 \\
M∀LICE<P>Dormouse & マリス<ポーン>ドーマウス & SR & 1480 & 240901 \\
M∀LICE<Q>RED RANSOM & マリス<クイーン>レッド・ランサム & N & 50 & 240901 \\
M∀LICE<Q>RED RANSOM & マリス<クイーン>レッド・ランサム & P & 80 & 240901 \\
M∀LICE<Q>RED RANSOM & マリス<クイーン>レッド・ランサム & SE & 1780 & 240901 \\
M∀LICE<Q>WHITE BINDER & マリス<クイーン>ホワイト・バインダー & N & 50 & 240901 \\
M∀LICE<Q>WHITE BINDER & マリス<クイーン>ホワイト・バインダー & P & 80 & 240901 \\
M∀LICE<Q>WHITE BINDER & マリス<クイーン>ホワイト・バインダー & SE & 2280 & 240901 \\
M∀LICE<Q>HEARTS OF CRYPTER & マリス<クイーン>ハーツ・オブ・クリプター & UR & 480 & 240901 \\
M∀LICE IN UNDERGROUND & マリス イン アンダーグラウンド & SR & 1380 & 240901 \\
M∀LICE<C>MTP-07 & マリス<コード>MTP-07 & SR & 180 & 240901 \\
M∀LICE<C>GWC-06 & マリス<コード>GWC-06 & N & 30 & 240901 \\
M∀LICE<C>GWC-06 & マリス<コード>GWC-06 & P & 80 & 240901 \\
M∀LICE<C>TB-11 & マリス<コード>TB-11 & N & 50 & 240901 \\
M∀LICE<C>TB-11 & マリス<コード>TB-11 & P & 120 & 240901 \\
ドットスケーパー & ドットスケーパー & N & 30 & 240901 \\
ドットスケーパー & ドットスケーパー & P & 80 & 240901 \\
孤高除獣 & ココウノケモノ & N & 30 & 240901 \\
孤高除獣 & ココウノケモノ & P & 80 & 240901 \\
トポロジック・ゼロヴォロス & トポロジック・ゼロヴォロス & N & 30 & 240901 \\
トポロジック・ゼロヴォロス & トポロジック・ゼロヴォロス & P & 80 & 240901 \\
闇の誘惑 & やみのゆうわく & N & 30 & 240901 \\
闇の誘惑 & やみのゆうわく & P & 80 & 240901 \\
サイバネット・バックドア & サイバネット・バックドア & N & 30 & 240901 \\
サイバネット・バックドア & サイバネット・バックドア & P & 80 & 240901 \\
次元の裂け目 & じげんのさけめ & N & 30 & 240901 \\
次元の裂け目 & じげんのさけめ & P & 80 & 240901 \\
恐巄竜華-㟴巴 & きょうろうりゅうげ-かいば & SR & 120 & 240901 \\
海瀧⻯華-淵巴 & かいろうりゅうげ-えんば & SR & 120 & 240901 \\
幻朧⻯華-霸巴 & げんろうりゅうげ-はくば & SR & 120 & 240901 \\
創星⻯華-光巴 & そうせいりゅうげ-みつば & UR & 480 & 240901 \\
創星⻯華-光巴 & そうせいりゅうげ-みつば & QCSE & 120 & 240901 \\
⻯華界闢 & りゅうげかいびゃく & N & 30 & 240901 \\
⻯華界闢 & りゅうげかいびゃく & P & 80 & 240901 \\
⻯華界闢 & りゅうげかいびゃく & SE & 380 & 240901 \\
登⻯華転生紋 & とうりゅげてんせいもん & N & 30 & 240901 \\
登⻯華転生紋 & とうりゅげてんせいもん & P & 80 & 240901 \\
登⻯華転生紋 & とうりゅげてんせいもん & SE & 180 & 240901 \\
登⻯華恐巄⾨ & とうりゅげきょうろうもん & N & 30 & 240901 \\
登⻯華恐巄⾨ & とうりゅげきょうろうもん & P & 80 & 240901 \\
登⻯華海瀧⾨ & とうりゅうげかいろうもん & N & 30 & 240901 \\
登⻯華海瀧⾨ & とうりゅうげかいろうもん & P & 80 & 240901 \\
登⻯華幻朧⾨ & とうりゅうげげんろうもん & N & 30 & 240901 \\
登⻯華幻朧⾨ & とうりゅうげげんろうもん & P & 80 & 240901 \\
⻯華三界流転 & りゅうげさんかいるてん & N & 30 & 240901 \\
⻯華三界流転 & りゅうげさんかいるてん & P & 80 & 240901 \\
センジュ・ゴッド & センジュ・ゴッド & N & 30 & 240901 \\
センジュ・ゴッド & センジュ・ゴッド & P & 80 & 240901 \\
惑星探査⾞ & プラネット・パスファインダー & N & 30 & 240901 \\
惑星探査⾞ & プラネット・パスファインダー & P & 80 & 240901 \\
超次元ロボ ギャラクシー・デストロイヤー & ちょうじげんロボ ギャラクシー・デストロイヤー & N & 30 & 240901 \\
超次元ロボ ギャラクシー・デストロイヤー & ちょうじげんロボ ギャラクシー・デストロイヤー & P & 80 & 240901 \\
星神器デミウルギア & せいしんきデミウルギア & N & 30 & 240901 \\
星神器デミウルギア & せいしんきデミウルギア & P & 80 & 240901\\ \end{array}
$$

ファイル本体も置いておきます.

プログラムの全体像

先に,プログラムの全体像について公開します.
全体のプログラムは以下です.

import pandas as pd
import random
import sys
import datetime
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QListWidget, QComboBox, QLineEdit, QPushButton, QMessageBox, QTextEdit
import numpy as np
def get_target_cards(df):
    app = QApplication([])

    window = QWidget()
    window.setWindowTitle('収集カード設定')
    layout = QVBoxLayout()

    label1 = QLabel('収集したいカードを選択してください')
    list_widget = QListWidget()
    list_widget.addItems(df['name'].unique().tolist())

    label2 = QLabel('レアリティを選択してください')
    combo_box = QComboBox()

    label3 = QLabel('必要枚数を入力してください')
    line_edit = QLineEdit()

    button_add = QPushButton('追加')
    button_done = QPushButton('完了')
    added_cards_list = QListWidget()


    layout.addWidget(label1)
    layout.addWidget(list_widget)
    layout.addWidget(label2)
    layout.addWidget(combo_box)
    layout.addWidget(label3)
    layout.addWidget(line_edit)
    layout.addWidget(button_add)
    layout.addWidget(button_done)
    # 追加されたカードの状態を表示するためのリストボックス

    layout.addWidget(added_cards_list)
    window.setLayout(layout)
    target_cards = {}
    def update_added_cards_list():
        added_cards_list.clear()
        for card, rarities in target_cards.items():
            for rarity, count in rarities.items():
                added_cards_list.addItem(f"{card} ({rarity}): {count}枚")
    def update_rarity_options():
        card_name = list_widget.currentItem().text() if list_widget.currentItem() else None
        if card_name:
            rarities = df[df['name'] == card_name]['rare'].unique().tolist()
            combo_box.clear()
            combo_box.addItems(rarities)

    def add_card():
        card_name = list_widget.currentItem().text() if list_widget.currentItem() else None
        rarity = combo_box.currentText()
        count = line_edit.text()
    
        # CSVファイル内に選択されたカード名とレアリティの組み合わせが存在するか確認
        if card_name and rarity and count.isdigit():
            if df[(df['name'] == card_name) & (df['rare'] == rarity)].empty:
                QMessageBox.warning(window, 'エラー', f"エラー: {card_name} にレアリティ {rarity} は存在しません。")
            else:
                if card_name not in target_cards:
                    target_cards[card_name] = {}
                target_cards[card_name][rarity] = int(count)
                QMessageBox.information(window, '追加成功', f"追加しました: {card_name} ({rarity}): {count}枚")
                update_added_cards_list()  # 追加されたカードのリストを更新
    def done():
        window.close()

    list_widget.currentItemChanged.connect(update_rarity_options)
    button_add.clicked.connect(add_card)
    button_done.clicked.connect(done)

    window.show()
    app.exec_()
    
    return target_cards


def simulate_purchase(df, target_cards, num_simulations):
    # --------- パラメータ設定 ---------
    pack_price = 176
    box_price = 2640
    cards_per_pack = 5
    packs_per_box = 15
    total_packs = 180
    qcse_probability = 1 / total_packs
    sr_per_box = 5
    ur_per_box = 1
    se_inclusion_probability = 0.50
    se_double_probability = 0.05

    # --------- タイムスタンプの取得 ---------
    timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    output_filename = f'simulation_output_{timestamp}.txt'

    # --------- シングル販売コスト計算 ---------
    total_cost_single = 0
    with open(output_filename, 'w') as f:
        sys.stdout = f
        print(f"シングル販売コストの計算を開始します...")

        for card, rarities in target_cards.items():
            for rarity, count in rarities.items():
                price_series = df[(df['name'] == card) & (df['rare'] == rarity)]['price']
                if not price_series.empty:
                    price = price_series.values[0]
                    subtotal = price * count
                    total_cost_single += subtotal
                    print(f"カード '{card}' ({rarity}) の小計: {subtotal} 円 (単価: {price} 円, 枚数: {count})")
                else:
                    print(f"警告: カード '{card}' のレアリティ '{rarity}' の価格情報が見つかりません。")

        print(f"シングル販売の合計コスト: {total_cost_single} 円")

        # --------- パック購入コストシミュレーション ---------
        total_cost_pack = 0
        pack_costs = []

        print(f"パック購入コストのシミュレーションを開始します...")

        for simulation in range(num_simulations):
            obtained_cards = {}
            boxes_opened = 0
            packs_opened = 0
            all_collected = False

            while not all_collected:
                boxes_opened += 1
                if random.random() < se_inclusion_probability:
                    se_per_box = 2 if random.random() < se_double_probability else 1
                else:
                    se_per_box = 0
                current_sr = sr_per_box
                current_ur = ur_per_box
                current_se = se_per_box
                qcse_obtained_in_box = False

                for _ in range(packs_per_box):
                    packs_opened += 1
                    print(f"シミュレーション {simulation+1}: {boxes_opened} BOX目の {packs_opened} パック目を開封中...")

                    pack_contents = []
                    for _ in range(4):
                        card = df[df['rare'] == 'N'].sample(1)['name'].values[0]
                        pack_contents.append((card, 'N'))
                        obtained_cards.setdefault(card, {}).setdefault('N', 0)
                        obtained_cards[card]['N'] += 1

                    rarity = random.choices(
                        population=['P', 'SR', 'UR', 'SE', 'QCSE'],
                        weights=[1, current_sr, current_ur, current_se, qcse_probability if not qcse_obtained_in_box else 0],
                        k=1
                    )[0]

                    available_cards = df[df['rare'] == rarity]
                    if not available_cards.empty:
                        card = available_cards.sample(1)['name'].values[0]
                        pack_contents.append((card, rarity))
                        obtained_cards.setdefault(card, {}).setdefault(rarity, 0)
                        obtained_cards[card][rarity] += 1

                        if rarity == 'SR':
                            current_sr -= 1
                        elif rarity == 'UR':
                            current_ur -= 1
                        elif rarity == 'SE':
                            current_se -= 1
                        elif rarity == 'QCSE':
                            qcse_obtained_in_box = True

                    print(f"パック {packs_opened} の内容: {pack_contents}")

                    all_collected = all(
                        obtained_cards.get(card, {}).get(rarity, 0) >= count
                        for card, rarities in target_cards.items()
                        for rarity, count in rarities.items()
                    )

                    if all_collected:
                        print(f"目標のカードがすべて揃いました!")
                        print(f"シミュレーション: {simulation+1}, パック数: {packs_opened}, BOX数: {boxes_opened}")
                        break

                if boxes_opened > 24:
                    print(f"警告: 必要なカードが揃わないためシミュレーションを中止します。")
                    break

            total_cost_pack += boxes_opened * box_price
            pack_costs.append(boxes_opened * box_price)

        avg_cost_pack = total_cost_pack / num_simulations
        std_dev_pack = np.std(pack_costs)

        print(f"パック購入の平均コスト: {avg_cost_pack:.2f} 円")
        print(f"パック購入の標準偏差: {std_dev_pack:.2f} 円")

    sys.stdout = sys.__stdout__

    result = {
        'single_purchase_cost': total_cost_single,
        'pack_purchase_average_cost': avg_cost_pack,
        'pack_purchase_std_dev': std_dev_pack,
    }

    result_summary = (f"シングル販売での合計コスト: {result['single_purchase_cost']} 円\n"
                      f"パック購入での平均コスト: {result['pack_purchase_average_cost']:.2f} 円\n"
                      f"パック購入での標準偏差: {result['pack_purchase_std_dev']:.2f} 円\n")

    print(f"シングル販売での合計コスト: {result['single_purchase_cost']} 円")
    print(f"パック購入での平均コスト: {result['pack_purchase_average_cost']:.2f} 円")
    print(f"パック購入での標準偏差: {result['pack_purchase_std_dev']:.2f} 円")

    if result['single_purchase_cost'] < result['pack_purchase_average_cost']:
        print(f"結論: シングル販売で購入する方が得です。")
        result_summary += "結論: シングル販売で購入する方が得です。"
    else:
        print(f"結論: パック購入で入手する方が得です。")
        result_summary += "結論: パック購入で入手する方が得です。"

    app = QApplication.instance()
    if not app:
        app = QApplication([])

    QMessageBox.information(None, 'シミュレーション結果', result_summary)
    
    return result


if __name__ == "__main__":
    # CSVファイルの読み込み
    df = pd.read_csv('./list.csv')
    print(f"CSVファイルの読み込みが完了しました。")

    # GUIで収集したいカードの設定を行う
    target_cards = get_target_cards(df)
    print(f"収集したいカードの設定: {target_cards}")
    
    # シミュレーションの実行
    result = simulate_purchase(df, target_cards, num_simulations=1000)

以下では,GUI(マウスで操作する部分)など細かい話は省いてシミュレーションの本質に関わる部分についてのみ解説します.

Pythonでプログラムを書いてみよう

Pythonでのシミュレーションプログラムの基本的な流れは以下です.

  • データを読み込む

まず、カードのデータが入ったCSVファイルを読み込みます。これはPythonのライブラリである pandas を使うことで簡単にできます。

import pandas as pd
df = pd.read_csv('cards_data.csv')
  • シングル購入のコストを計算する
    欲しいカードの名前とレアリティ、そして必要な枚数を指定すると、その合計金額を計算します。

total_cost_single = 0
for card, rarities in target_cards.items():
    for rarity, count in rarities.items():
        price = df[(df['name'] == card) & (df['rare'] == rarity)]['price'].values[0]
        total_cost_single += price * count
print(f"シングル販売の合計コストは {total_cost_single} 円です。")
  • パックを開封してカードを揃えるシミュレーション
    次に、パックを開封して欲しいカードが出るまでのプロセスをシミュレーションします。ランダムにカードを引いて、どれだけのパックを開けたら欲しいカードが揃うのかを試してみます。ここではPythonの random というライブラリを使います。

import random

packs_opened = 0
obtained_cards = {}
while not all_cards_collected(obtained_cards, target_cards):
    packs_opened += 1
    # ランダムにパックからカードを引く
    drawn_card = random_card_from_pack()
    update_obtained_cards(obtained_cards, drawn_card)
print(f"{packs_opened} パックで目標のカードが揃いました!")

この「パック開封のモデル化」の部分が非常に重要になるので,もう少し詳細に解説します.

  • レアリティごとの排出率とは?

まず、遊戯王OCGのパックにはいくつかのレアリティ(カードの希少度)が存在します。一般的に、ノーマルカードが一番多く出て、レア度が高いカードほど出にくくなります。この確率をプログラムに設定することで、実際のパック開封のようなシミュレーションを行います。

プログラムでは以下のレアリティを扱っています:

  • N(ノーマル): パックに4枚含まれる通常のカード

  • P(パラレル): 少し珍しいカードで、ノーマルに比べて出る確率が低い

  • SR(スーパーレア): より珍しいカードで、通常1パックに1枚出る可能性がある

  • UR(ウルトラレア): さらに希少で、1BOXに1枚ほどの確率で出現

  • SE(シークレットレア): 非常に珍しいカードで、出る確率がかなり低い

  • QCSE(クォーターセンチュリーシークレットレア): 最高にレアなカードで、半カートン(12BOX)に1枚出るかどうかという確率

排出率のプログラム内での設定

プログラム内では、`random.choices`という関数を使って、カードのレアリティをランダムに決定します。その際に、以下のような排出率が設定されています:

rarities = ['P', 'SR', 'UR', 'SE', 'QCSE']
probabilities = [0.50, 0.33, 0.10, 0.05, 0.01]

この確率に基づいて、パックの中でノーマルカード4枚と1枚のレアカードを引きます。たとえば、SR(スーパーレア)が33%の確率で出るように設定されています。

また、QCSE(クォーターセンチュリーシークレットレア)の排出率は非常に低く、1パックに出る確率は0.01(1%)です。これを半カートン(12BOX、180パック)で考えると、12BOXに1枚出るかどうかの確率になります。
この「出るかどうか」というのがポイントで,「半カートン購入すれば確実に出る」ことは保証されていない点に注意しましょう.

また,確率を決めるだけでなく,「1BOXにどのレアリティが何枚入っているのか」も追跡する必要があります.

1BOX内のレアリティ追跡管理

プログラム内で各BOXごとにどのレアリティが何枚含まれているかを追跡するために、`random.choices`関数を使用し、各レアリティの出現確率を基にカードを引いています。その際、特定のレアリティのカードが引かれると、BOX内の残りの該当レアリティのカード枚数を減らすように設定されています。

具体的には、以下のようにBOX内の各レアリティのカウントを管理しています:

  • 1.SRとURの残り枚数を減らす処理

各BOXには5枚のSRと1枚のURが含まれるため、current_srとcurrent_urという変数でそれぞれの枚数を管理しています。これらの枚数はカードが引かれるたびに減少し、SRやURがなくなると、それ以上そのレアリティのカードが引けないようになります。

if rarity == 'SR':
    current_sr -= 1
elif rarity == 'UR':
    current_ur -= 1
  • 2.SEの追跡

SE(シークレットレア)は1BOXあたり50%の確率で封入され、稀に2枚封入されることもあります。このSEの枚数もcurrent_seという変数で管理しており、1BOX内でどれだけのSEが引かれたかを追跡しています。

if rarity == 'SE':
    current_se -= 1
  • 3.QCSEの特殊処理

QCSE(クォーターセンチュリーシークレットレア)は特殊なレアリティであり、半カートン(12BOX)あたり1枚の確率で封入されます。この確率はプログラム内でqcse_probability = 1 / 180として設定されており、各BOXで1枚だけ引かれるかどうかが決まります。また、1BOX内でQCSEが出た場合、そのBOXでは再度引けないようにqcse_obtained_in_boxというフラグを設定しています。

if rarity == 'QCSE':
    qcse_obtained_in_box = True

このようにして、プログラム内では1BOXにどのレアリティが何枚含まれているかをリアルタイムで追跡し、カードの排出を管理しています。

プログラムのシミュレーションの流れ

実際にシミュレーションの中で、パックを開封する際のモデル化は次のように行われています:

  1. パックの初期設定
    1パックには5枚のカードが入っており、そのうち4枚は必ずノーマルカードです。残りの1枚は、上記の排出率に従ってレアリティを決定します。

  2. 目標カードが揃うまでのシミュレーション
    パックを開封し続け、目標のカードが揃うまでシミュレーションを繰り返します。このシミュレーションでは、複数のパックやBOXを開けて、何枚目で目標のカードが揃うか、どれだけのコストがかかるかを計算します。

パック購入コストのシミュレーション

ここで,改めてプログラムを確認しましょう.プログラムでは、実際にどれだけのパックを開けたのか、何枚目で目標のカードが出たのかを計算し、それにかかったコストを求めます。例えば、次のように、1回のシミュレーションでパックを開封するたびにカードの内容を記録し、目標のカードが出るまで続けます。

while not all_collected:
    boxes_opened += 1
    # パックの内容をシミュレーション
    for _ in range(packs_per_box):
        packs_opened += 1
        rarity = random.choices(rarities, probabilities, k=1)[0]
        # ... ここでカードを記録し、目標カードが揃ったかを確認

こうしてシミュレーションを何回も繰り返し、パック購入で目標のカードが揃った場合のコストを平均して算出します。

  • シングル vs パックのコストを比較する
    最後に、シングルで買った場合の合計コストと、パックを開封してカードを揃えた場合の合計コストを比較します。これにより、どちらの方法がより効率的であるかがわかります。

if total_cost_single < total_cost_pack:
    print("シングル販売で買ったほうが得です。")
else:
    print("パックを開封するほうが得です。")

プログラムでシミュレーションの流れを追う

プログラムの流れをもう少し詳しく見てみましょう。

  1. シングル購入
    シミュレーションでは、まずシングルでカードを買う場合の合計金額を計算します。各カードの価格と、必要な枚数をもとに、小計を計算し、その合計を出します。なお,価格情報はカードラッシュ通販の販売価格から手動入力しているので,最新の情報を知るには適宜更新する必要があります.

  2. パック開封シミュレーション
    パック購入では、実際にパックを開けるように、プログラムがランダムにカードを選びます。欲しいカードが出るまでパックを開け続け、かかったお金を計算します。

  3. コスト比較
    最後に、シングル購入のコストとパック購入のコストを比較します。結果として、どちらがより少ない金額でカードを集められたかがわかります。

実際に試してみよう

プログラミングが初めての人でも、このシミュレーションは比較的簡単に作ることができます。Pythonの `random` や `pandas` のような便利なライブラリを使えば、少ない行数で複雑な計算やシミュレーションができるのです。今回は利便性をある程度確保するためにGUI(マウスクリックで操作できる画面)を追加する関係で少しプログラムが長くなりました.

自分でシミュレーションを作ってみると、どちらの購入方法が得なのかがデータとしてはっきりわかるので、実際に遊戯王OCGをプレイしているときに、効率的な戦略を立てられるようになります。

シングル買い vs パック買い:どちらがお得?

プログラムの概観

完成したプログラムを実際に動作させている様子です.
諸事情により,後述する結果とは別のカードに対して計算していますがあまり気にしないでください.
「こういう見た目で動くのね」程度に捉えてください.

シミュレーションの方針

さて、今回のシミュレーションでは、クロスオーバー・ブレイカーズ新規テーマであるライゼオル、M∀LICE、竜華のテーマごとにシングル買いとパック買いのコストを比較し、それぞれのテーマでどちらの方法が経済的に有利かを検証します。
さらに、パック購入のコストに対する標準偏差も計算し、ランダム性の影響についても掘り下げて考察しています。
なお,購入するカードは各テーマのUR/SRカード3枚ずつとしました.

ライゼオルの場合

ライぜオルのシミュレーション結果


  • カード 'ソード・ライゼオル' (SR) の小計: 2340 円 (単価: 780 円, 枚数: 3)

  • カード 'アイス・ライゼオル' (SR) の小計: 2040 円 (単価: 680 円, 枚数: 3)

  • カード 'エクス・ライゼオル' (SR) の小計: 2340 円 (単価: 780 円, 枚数: 3)

  • カード 'ライゼオル・デッドネーダー' (UR) の小計: 1440 円 (単価: 480 円, 枚数: 3)

  • シングル販売の合計コスト: 8160 円

  • パック購入の平均コスト: 33345.84 円

  • パック購入の標準偏差: 10748.67 円

シングル買いでは合計コストが8160円でした。これに対し、パック買いの平均コストは33345.84円で、シングル買いに比べて大幅に高くなっています。さらに、パック購入の標準偏差は10748.67円であり、これは試行回数ごとにコストが大きく変動する可能性を示しています。つまり、相当運が良ければパックで得する可能性はありますが、平均するとパック買いはシングル買いよりもかなり高くつくという結果です。

M∀LICEの場合

M∀LICEのシミュレーション結果
  • カード 'M∀LICE<P>White Rabbit' (SR) の小計: 5040 円 (単価: 1680 円, 枚数: 3)

  • カード 'M∀LICE<P>Cheshire Cat' (SR) の小計: 1740 円 (単価: 580 円, 枚数: 3)

  • カード 'M∀LICE<P>Dormouse' (SR) の小計: 4440 円 (単価: 1480 円, 枚数: 3)

  • カード 'M∀LICE<Q>HEARTS OF CRYPTER' (UR) の小計: 1440 円 (単価: 480 円, 枚数: 3)

  • カード 'M∀LICE IN UNDERGROUND' (SR) の小計: 4140 円 (単価: 1380 円, 枚数: 3)

  • カード 'M∀LICE<C>MTP-07' (SR) の小計: 540 円 (単価: 180 円, 枚数: 3)

  • シングル販売の合計コスト: 17340 円

  • パック購入の平均コスト: 35645.28 円

  • パック購入の標準偏差: 8982.98 円

シングル買いでは合計で17400円という結果でした。これに対し、パック買いでは平均コストが35645.28円となり、シングル買いに比べてパック買いのほうが約2倍のコストがかかります。パック購入の標準偏差は8982.98円で、ライゼオルと比較して若干小さいものの、依然として運に左右される度合いが強いです。この結果からも、M∀LICEデッキのカードを集める際はシングル買いのほうが予算管理しやすく、経済的であることがわかります。

竜華の場合

竜華のシミュレーション結果
  • カード '恐巄竜華-㟴巴' (SR) の小計: 360 円 (単価: 120 円, 枚数: 3)

  • カード '海瀧⻯華-淵巴' (SR) の小計: 360 円 (単価: 120 円, 枚数: 3)

  • カード '幻朧⻯華-霸巴' (SR) の小計: 360 円 (単価: 120 円, 枚数: 3)

  • カード '創星⻯華-光巴' (UR) の小計: 1440 円 (単価: 480 円, 枚数: 3)

  • シングル販売の合計コスト: 2520 円

  • パック購入の平均コスト: 33232.32 円

  • パック購入の標準偏差: 10872.27 円

シングル買いの合計コストはわずか2520円であり、これに対し、パック買いの平均コストは33232.32円と、非常に大きな差が出ています。標準偏差も10872.27円と非常に高く、パック購入の結果が極端にばらつくことを示しています。竜華の場合も、シングルでカードを揃えるのが圧倒的にコストパフォーマンスが良いと言えるでしょう。

全部やった場合

ちなみに,全部同時にターゲットにしたらこうなりました.

全てターゲットにした場合のシミュレーション結果
  • カード 'ソード・ライゼオル' (SR) の小計: 2340 円 (単価: 780 円, 枚数: 3)

  • カード 'アイス・ライゼオル' (SR) の小計: 2040 円 (単価: 680 円, 枚数: 3)

  • カード 'エクス・ライゼオル' (SR) の小計: 2340 円 (単価: 780 円, 枚数: 3)

  • カード 'ライゼオル・デッドネーダー' (UR) の小計: 1440 円 (単価: 480 円, 枚数: 3)

  • カード 'M∀LICE<P>White Rabbit' (SR) の小計: 5040 円 (単価: 1680 円, 枚数: 3)

  • カード 'M∀LICE<P>Cheshire Cat' (SR) の小計: 1740 円 (単価: 580 円, 枚数: 3)

  • カード 'M∀LICE<P>Dormouse' (SR) の小計: 4440 円 (単価: 1480 円, 枚数: 3)

  • カード 'M∀LICE<Q>HEARTS OF CRYPTER' (UR) の小計: 1440 円 (単価: 480 円, 枚数: 3)

  • カード 'M∀LICE IN UNDERGROUND' (SR) の小計: 4140 円 (単価: 1380 円, 枚数: 3)

  • カード 'M∀LICE<C>MTP-07' (SR) の小計: 540 円 (単価: 180 円, 枚数: 3)

  • カード '恐巄竜華-㟴巴' (SR) の小計: 360 円 (単価: 120 円, 枚数: 3)

  • カード '海瀧⻯華-淵巴' (SR) の小計: 360 円 (単価: 120 円, 枚数: 3)

  • カード '幻朧⻯華-霸巴' (SR) の小計: 360 円 (単価: 120 円, 枚数: 3)

  • カード '創星⻯華-光巴' (UR) の小計: 1440 円 (単価: 480 円, 枚数: 3)

  • シングル販売の合計コスト: 28020 円

  • パック購入の平均コスト: 43734.24 円

  • パック購入の標準偏差: 9253.43 円

この場合でも,シングル買いの方が安価という結果になりました.しかも,ターゲットが多すぎて計算上限(1カートン24Box以内)にターゲットを集め切らず,計算を打ち切っているケースもあるので実際にはもっと費用がかかると考えられます.

また,標準偏差が9253.43円ということは、パック購入におけるコストが平均値の前後9253円の範囲で変動することを示しています。例えば、ある試行では30000円程度でカードを集められるかもしれませんが、別の試行では53000円近くかかることもあります。この大きな変動は、パックを開封する際の運に強く依存しているため、予算を立てる際にはリスクとして考慮する必要があります。

標準偏差について

標準偏差は、パック購入コストのばらつきを表す指標です。今回のシミュレーションでは、各テーマにおいてパック購入の標準偏差が9000円から11000円程度という結果になりました。この値が示すのは、シミュレーションごとのコストが平均値を中心に±9000円から11000円程度の範囲で大きく変動する可能性があるということです。つまり、標準偏差が高いほど、運に左右される度合いが大きく、結果の不確実性が高いと言えます。

一方、シングル買いは予測が容易で、価格は固定されているため標準偏差がなく、確実に必要なコストでカードを揃えることができます。このため、シングル買いは予算管理がしやすく、安定的に目標のカードを手に入れたい場合には有利です。

結論:どちらが得か?

今回のシミュレーション結果から、明らかにシングル買いのほうがコストパフォーマンスが優れていることが示されました。シングル買いでは、欲しいカードを確実に、かつ予定通りの金額で揃えられるため、予算の不確実性がありません。特に、テーマごとに見てもパック買いの平均コストはシングル買いの何倍にもなるケースが多く、経済的に考えるとシングル買いが圧倒的に有利です。

一方で、パック買いには運の要素が大きく関与し、標準偏差が高いため、コストが大きくぶれる可能性があります。これがリスクとして捉えられる一方、パックを開ける楽しさや、想定外のレアカードを引き当てたときの喜びを重視するプレイヤーにとっては、パック買いも魅力的な選択肢と言えるでしょう。

最終的に、コストを重視する場合はシングル買いが得策ですが、運試しや楽しさを求める場合はパック買いも一つの楽しみ方です。目的や予算に応じて、自分に合った方法を選びましょう。

今後の課題

今回のシミュレーションを通して、「シングル買い」と「パック買い」のどちらが経済的かを検討しましたが、まだまだ改善できる点があります。以下では、今後の課題や改善の余地がある部分について解説します。

価格情報の手入力

現在のシミュレーションでは、カードの価格情報を手動で入力しており、いくつかの課題が残っています。手動入力は時間がかかるだけでなく、誤入力のリスクもあります。また、カードの価格は市場の動向により変動するため、最新の情報を常に反映することが難しく、シミュレーションの精度にも影響を与えかねません。

この課題を解決するためには、価格情報を自動で取得する仕組みを導入することが理想的です。たとえば、カードショップのAPIを利用して最新の価格データを取得することや、価格情報を提供するウェブサイトと連携することで、シミュレーションにリアルタイムの価格情報を反映させることが可能です。

この仕組みを取り入れることで、シミュレーションの精度が向上し、プレイヤーはより現実的なコストを把握できるようになります。また、自動化による効率化により、シミュレーションの手間を大幅に軽減できるというメリットもあります。

不要カードを売る選択肢の考慮

パックを購入すると、どうしても自分が必要としていないカードが手元に残ってしまいます。これらの不要なカードは、カードショップに売却したり、トレードすることで一定の金額を回収できる可能性があります。

現状のシミュレーションでは、この「不要カードを売却する」選択肢を考慮していません。しかし、これをシミュレーションに加えることで、パック買いが実際にはもう少しお得になる可能性もあります。今後は、不要カードの売却額を見積もり、総コストに反映することも検討すべきでしょう。

スタンドアローンではない点

現在のプログラムは、手動でCSVファイルを読み込み、手動でプログラムを実行する必要があります。これでは、使い勝手が悪く、特にプログラミング初心者やシミュレーションに慣れていないユーザーにとっては操作が難しいかもしれません。

理想としては、スタンドアローンのアプリケーションとして、誰でも簡単に操作できるGUI(グラフィカルユーザーインターフェース)を備えたプログラムに発展させたいです。例えば、カードの選択やシミュレーションの結果がリアルタイムで表示されるインターフェースを提供することで、プログラミングの知識がない人でもシミュレーションを簡単に行える環境を提供できます。また、ウェブアプリケーションとして公開すれば、インターネット上で誰でもアクセス可能になり、スタンドアローンのソフトウェアとしての利便性が向上するでしょう。

複数のシミュレーションパターンの導入

現在のシミュレーションは、特定のカードを揃えるための「シングル買い」と「パック買い」の比較に焦点を当てていますが、他のシミュレーションパターンも考慮すべきです。例えば、「一部カードをシングル買いし、残りをパック買いで補う」などの複合的な戦略も現実的な選択肢です。今後の発展として、複数のシミュレーションシナリオを設定し、それぞれの結果を比較できる機能を追加することが期待されます。

市場変動を考慮した動的シミュレーション

カードの価格は、需要と供給に応じて変動します。たとえば、あるカードが急に高騰することや、人気が落ちて価格が下がることがあります。今後の課題としては、こうした市場の価格変動をシミュレーションに組み込み、将来の価格を予測しながらシミュレーションできる機能を開発することです。これにより、ユーザーはより現実に近い判断が可能になります。

プログラム初学者でもできるシミュレーションの魅力

シミュレーションを聞くと、特にプログラム初心者にとっては「難しそう」という印象を受けるかもしれません。しかし、実際にはシミュレーションの鍵は「モデル化」にあります。モデル化とは、現実の問題や状況をプログラムで再現するためのルールを定義することです。このモデル化さえクリアできれば、シミュレーション自体は比較的簡単です。

たとえば、今回のシミュレーションで扱った「パックを開封してカードを集める」というテーマも、パックに含まれるカードの種類とレアリティをルールとして定義すれば、あとはそれをプログラムで反映するだけです。

シンプルなモデル化の手順

  1. データの準備
    シミュレーションの第一歩は、データを用意することです。今回の場合、パックに含まれるカードの名前、レアリティ、価格をリスト化し、プログラムに読み込ませます。このデータがシミュレーションのベースとなります。Pythonでは「pandas」というライブラリを使って、簡単にCSVファイルを読み込み、扱うことができます。

  2. パック開封のモデル化
    パックの中には、レアリティごとにカードが含まれています。この部分をプログラムで再現するために、Pythonの`random.choices`関数を使ってランダムにカードを引き、レアリティに応じてカードを決定します。例えば、スーパーレア(SR)のカードは33%の確率で出るように設定し、パックを開封するたびにその確率に基づいてカードを選びます。

  3. 反復処理の仕組み
    シミュレーションでは、パックを何度も開封して、目標のカードが出るまで繰り返し試行します。ここでプログラムのループ処理を使い、欲しいカードが揃うまでパックを開け続けるシナリオを設定します。Pythonの`while`ループや`for`ループを使うことで、この処理は簡単に実現できます。

難しい部分とその克服方法

難しいと感じる部分は、おそらくモデル化をどのように設計するかという部分でしょう。しかし、今回のように「パックにカードが入っていて、ランダムに引く」というシンプルなルールを設定すれば、あとはそのルールに基づいてコードを書くことになります。具体的なコード例やライブラリ(`pandas`や`random`)を活用することで、複雑な計算も簡単に実装できます。

繰り返し処理による強力なシミュレーション

シミュレーションの最大の利点は、現実では非常にコストや時間がかかる繰り返し作業をプログラムで短時間に行える点です。例えば、実際に何百回もパックを開けるのは現実的に不可能ですが、プログラムを使えば数秒で何千回もの試行を再現できます。今回は1000回のシミュレーションを実行して、統計的に「シングル買い」か「パック買い」のどちらが得かを確認しました。

このように、プログラム初心者でもモデル化さえクリアすれば、シミュレーションを自作して、簡単にデータを収集し、結果を分析することが可能です。

おわりに

今回のシミュレーションを通じて、「シングル買い」と「パック買い」のどちらが経済的かを検討し、結論としてシングル買いの方が圧倒的にコスト面で優れているという結果に至りました。このシミュレーションは、遊戯王OCGプレイヤーが直面する「どの方法でカードを集めるべきか」という悩みに、データに基づいた判断材料を提供するものであり、特にプログラムを通じて視覚的かつ簡単に結果を確認できる点が非常に魅力的です。

プログラミング初心者でも、このようなシミュレーションを作成することは可能であり、実際に自分でシミュレーションを試すことで、現実の問題に対してデータから合理的な結論を導く力を養うことができます。また、Pythonという使いやすい言語を使うことで、短時間でシミュレーションの基盤を作成し、そこから様々な応用ができることも今回の取り組みで実感できたかと思います。

今後、さらなる改善点として、価格情報の自動取得や、不要カードの売却を考慮したシミュレーションなど、より現実的なシナリオに対応できるよう発展させていくことが期待されます。また、スタンドアローンのアプリケーションやウェブアプリとしての提供も視野に入れ、多くのプレイヤーが気軽に利用できるツールへと進化させたいと考えています。

最後に、このシミュレーションを通して、プログラム初心者の方にもプログラミングの魅力が少しでも伝わったなら幸いです。遊戯王OCGの世界で楽しむと同時に、プログラムによるシミュレーションの力を使って、より深く戦略を考えてみてください。

引き続き、あなたの遊戯王ライフがより楽しく、効果的なものになりますように。

それではこの辺で.Good luck.

謝辞

本記事は本公開前に複数名からレビューをいただきました.
この場を借りて,厚く御礼申し上げます.
ありがとうございました.

  • SSM君

  • BTB溶液君

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