ポケモンカード AI Battle Challenge(Macbook環境構築)

Windows

macbookでもポケモンカードAIバトルチャレンジの環境つくれるか?

はい、MacBookでも問題なくポケモンカード AIバトルチャレンジの開発環境は作れます

特にApple Silicon(M1/M2/M3/M4)搭載Macでも参加している人は多く、Dockerを使えばほぼWindows/Linuxと同じように開発できます。ただし、Apple Siliconでは一部Dockerイメージがx86向けのためエミュレーションが必要になるケースがあり、多少遅くなることがあります。

必要なもの

Macなら以下だけでOKです。

  • macOS
  • Python 3.11以上
  • Git
  • VS Code
  • Docker Desktop
  • Kaggleアカウント

Macでのメリット

  • Unix系なのでPython開発が非常にやりやすい
  • Gitが使いやすい
  • Dockerとの相性が良い
  • ターミナル環境が最初から整っている

AI開発者はMacを使っている人もかなり多いです。

注意点

Apple Silicon(Mシリーズ)なら

  • Docker Desktopを起動してから作業する
  • Dockerイメージのダウンロードに20GB以上必要
  • メモリは最低16GBあると快適(8GBでも動くが重くなりやすい)

という点だけ注意が必要

全体の流れ

  1. Homebrewをインストール
  2. Gitをインストール
  3. Docker Desktopをインストール
  4. VS Codeをインストール
  5. Kaggleを設定
  6. サンプルAIをダウンロード
  7. ローカル対戦

Homebrewをインストール

ターミナルを開きます。

インストール済みか確認

brew --version

バージョンが表示されればOKです。

表示されなければ

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

バージョン情報が出ないため、パスを追加

brew --version
zsh: command not found: brew

echo >> ~/.zprofile
echo 'eval "$(/opt/homebrew/bin/brew shellenv zsh)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv zsh)"

バージョン確認

brew --version     
Homebrew 6.0.10

Gitをインストール

brew install git

確認

git --version
git version 2.50.1 (Apple Git-155)

Docker Desktopをインストール

次はDocker Desktopをインストールします。

① Docker Desktopをダウンロード

公式サイトからMac版をダウンロード。

Docker Desktop(公式)
https://www.docker.com/products/docker-desktop/

※ Apple Silicon(M1/M2/M3/M4)なら Apple Chip 版を選びます。
※ Intel Macなら Intel Chip 版です。


② インストール

ダウンロードした .dmg を開き、

Docker.appApplications フォルダへドラッグします。


③ 起動

Applicationsから Docker を起動します。

初回は

  • 利用規約への同意
  • パスワード入力

Skip

④ ターミナルで確認

バージョンの確認

docker --version
Docker version 29.6.1, build 8900f1d

docker run hello-world

を実行すると、初回はイメージをダウンロードしたあと、

Hello from Docker!

と表示されればDockerが正常に動作しています。


VS Codeをインストール

インストールします。

拡張機能は後から追加できます。
ターミナルを開いて実行します。

brew install --cask visual-studio-code

インストールが終わったら起動します。

open -a "Visual Studio Code"

またはSpotlight(⌘ + Space)で「Visual Studio Code」と入力して起動できます。


Kaggleアカウント作成

Kaggleへ登録します。

The Pokémon Company - PTCG AI Battle Challenge Simulation
Build an AI Training Agent to play the Pokémon Trading Card Game

「Pokemon TCG AI Battle」へ参加します。
右上からサインインします。

ここではGoogleアカウントを使うので「Sign in with Google」を選択

次は「ポケモンカードAIバトルチャレンジ」のコードを取得します

まず、VS Codeからターミナルでも作業できるようになっているか確認しましょう。
ターミナルで次を実行。

$ code --version
1.130.0
1b6a188127eeaf9194f945eb6eb89a657e93c54c
arm64
$

表示されればOKです。

Kaggle APIを設定

KaggleのAPIキー(kaggle.json)が必要になります。

APIキーをダウンロード

Kaggleにログインし、

Profile(右上のアイコン)→ Settings → API → Create New Token

Generate New Tokenをクリック

トークン情報が出るので、これは誰にも教えないようにしましょう


% mkdir -p ~/.kaggle && echo KGAT_abcdefghijklmna26ce1dfcf1f6c5bc > ~/.kaggle/access_token && chmod 600 ~/.kaggle/access_token

Docker.appを起動させておきます。

Finder → アプリケーションDocker.app をダブルクリックして起動

docker run –rm \
-v ~/.kaggle:/root/.kaggle \
gcr.io/kaggle-images/python \
kaggle competitions list

実行するとダウンロードにしばらく時間がかかります。

AIデータの取得

コンペ名は

pokemon-tcg-ai-battle-challenge-strategy

ですね。

次はコンペデータを取得します。

先に参加表明をしておきます。「Join Hackathon」をクリック

同意をします。


作業をフォルダを作成

mkdir -p ~/pokemon-ai
cd ~/pokemon-ai

コンペデータの取得

docker run --rm \
  -v ~/pokemon-ai:/work \
  -v ~/.kaggle:/root/.kaggle \
  -w /work \
  gcr.io/kaggle-images/python \
  kaggle competitions download -c pokemon-tcg-ai-battle-challenge-strategy

これでデータのダウンロードが完了しました。
圧縮ファイルを解答します。

% cd ~/pokemon-ai
unzip pokemon-tcg-ai-battle-challenge-strategy.zip
Archive: pokemon-tcg-ai-battle-challenge-strategy.zip
inflating: Card_ID List_EN.pdf
inflating: Card_ID List_EN_.pdf
inflating: Card_ID List_JP.pdf
inflating: Card_ID List_JP_.pdf
inflating: EN Card Data.csv
inflating: EN_Card_Data.csv
inflating: JP Card Data.csv
inflating: JP_Card_Data.csv
Mac pokemon-ai %

Simulation部門にも参加

PTCG AI Battle Challenge Simulation

「Join Competition」を押して、利用規約に同意

下記URLにアクセスして、右上のCopy & Editをクリック

[Beginner Guide] From Deck List to First Valid Sub

[Beginner Guide] From Deck List to First Valid Sub
Explore and run AI code with Kaggle Notebooks | Using data from multiple data sources

② Notebookを実行

自分のNotebookになったら、上から順番にセルを

Shift + Enter または ▶ Run

で実行していきます。

画面の右上にある ⬇ Download をクリック。

するとNotebook(.ipynb)をダウンロードできます。

公式サンプルAIのダウンロード

先ほど開いた

A Sample Rule-Based Agent Mega Lucario ex Deck

の右上にある

Copy & Edit

ではなく、Downloadを押します。

すると

A Sample Rule-Based Agent Mega Lucario ex Deck.ipynb

というNotebookがダウンロードされます。

Kaggleの

A Sample Rule-Based Agent Mega Lucario ex Deck

へ戻る。

そして

Copy & Edit した自分のNotebookで、

一番上から

Run All

を押す。

実行が終わると右側のOutputの中に

submission.tar.gz

が作成されます。

これが提出ファイルです。


このファイルをダウンロード。

submission.tar.gz を解凍します。

この画面にある

submission/
├── cg/
├── deck.csv
└── main.py

これが提出するAIになります。

main.py

ここに

  • どのカードを使うか
  • どのポケモンを前に出すか
  • エネルギーを誰につけるか
  • ワザを使うか
  • ベンチに出すか

などのAIが書かれています。

これから一番編集するファイルです。

main.py
│
├── デッキ読み込み
├── カード情報読込
├── 攻撃計画を立てる
├── 各行動に点数を付ける
└── 一番点数が高い行動を選ぶ

つまり、

「ルールベースAI」

です。AI学習(強化学習)ではありません。

deck.csv

これはデッキです。

例えば

4,Lucario
2,Riolu
...

のようにカード構成が書かれています。


cg

ここはゲームエンジンです。中身を触ることはないです。

1. test_battle.py を作る

submission と同じ階層

pokemon-ai/
├── submission/
│   ├── main.py
│   ├── deck.csv
│   └── cg/
└── test_battle.py

として、test_battle.py にこれを書きます。

from kaggle_environments import make
import sys
import os

SUBMISSION_DIR = os.path.join(os.path.dirname(__file__), "submission")

os.chdir(SUBMISSION_DIR)

sys.path.insert(0, SUBMISSION_DIR)

from main import agent

deck_path = os.path.join(SUBMISSION_DIR, "deck.csv")

with open(deck_path, "r") as f:
    deck = [int(line.strip()) for line in f if line.strip()]

print("deck cards:", len(deck))

env = make(
    "cabt",
    configuration={
        "decks": [deck, deck]
    }
)

env.run([agent, agent])

print("Simulation finished.")
print(env.steps[-1])

with open("result.html", "w", encoding="utf-8") as f:
    f.write(env.render(mode="html"))

print("result.html を作成しました。")

公式サンプルも基本的にこの構成です。configuration={"decks": [deck, deck]} を指定して、env.run([agent, agent]) で2つのAIを戦わせます。

実行結果

% docker run --rm \
  --platform linux/amd64 \
  -v "$PWD":/work \
  -w /work \
  gcr.io/kaggle-images/python \
  python test_battle.py
[kaggle_environments.envs.open_spiel_env.open_spiel_env] INFO: Successfully loaded OpenSpiel environments: 24.
[kaggle_environments.envs.open_spiel_env.open_spiel_env] INFO:    
〜中略〜
hSBgAEMABBgAEEAHMABBgABBACEAHMABBgABCACEAHMABBgABDACEAHMABBgABEACEAHMABBgABFACEAsEAEqgADDIBACBmABQACMgEADEADBnALDAECQAVBADWAWEACFQAWQAI=='}, 'status': 'DONE'}]
Traceback (most recent call last):
  File "/work/test_battle.py", line 34, in <module>
    f.write(env.render(mode="html"))
            ^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/dist-packages/kaggle_environments/core.py", line 373, in render
    window_kaggle, self.html_renderer(*args[: self.html_renderer.__code__.co_argcount])
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/dist-packages/kaggle_environments/envs/cabt/cabt.py", line 197, in html_renderer
    with open(jspath, encoding="utf-8") as f:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: '/usr/local/lib/python3.12/dist-packages/kaggle_environments/envs/cabt/cabt.js'

What's next:
    Debug this container error with Gordon → docker ai "help me fix this container error"

対戦は終わっているが、対戦をブラウザで表示するためのJavaScriptファイルがDockerイメージに入っていない、というだけです。

先程のプログラムの後半を修正

from kaggle_environments import make
import sys
import os

SUBMISSION_DIR = os.path.join(os.path.dirname(__file__), "submission")

os.chdir(SUBMISSION_DIR)

sys.path.insert(0, SUBMISSION_DIR)

from main import agent

deck_path = os.path.join(SUBMISSION_DIR, "deck.csv")

with open(deck_path, "r") as f:
    deck = [int(line.strip()) for line in f if line.strip()]

print("deck cards:", len(deck))

env = make(
    "cabt",
    configuration={
        "decks": [deck, deck]
    }
)

env.run([agent, agent])

print("Simulation finished.")
print(env.steps[-1])


winner = env.steps[-1][0]["reward"]

if winner == 1:
    print("Player1 WIN")
elif winner == -1:
    print("Player2 WIN")
else:
    print("DRAW")

今度は2つのデッキで対戦してみましょう

===============================

まずフォルダを2つに分けます

現在の submission を、元AIと改造AIとしてコピーします。

Macのターミナルで、

cd ~/pokemon-ai

mkdir -p agents

cp -R submission agents/v1
cp -R submission agents/v2

こうなればOKです。

pokemon-ai/
├── agents/
│   ├── v1/
│   │   ├── main.py
│   │   ├── deck.csv
│   │   └── cg/
│   │
│   └── v2/
│       ├── main.py
│       ├── deck.csv
│       └── cg/
│
├── submission/
└── tournament.py   ← これから作る

v1サンプルAIそのままv2これから改造するAIです。


2. tournament.py を作ります

VS Codeで、

~/pokemon-ai/tournament.py

を新規作成して、以下コードを書きます。

from kaggle_environments import make

import importlib.util
import os
import sys
import time


BASE_DIR = os.path.dirname(os.path.abspath(__file__))

V1_DIR = os.path.join(BASE_DIR, "agents", "v1")
V2_DIR = os.path.join(BASE_DIR, "agents", "v2")


def load_deck(agent_dir):
    """deck.csv を読み込む"""
    deck_path = os.path.join(agent_dir, "deck.csv")

    with open(deck_path, "r") as f:
        deck = [
            int(line.strip())
            for line in f
            if line.strip()
        ]

    if len(deck) != 60:
        raise ValueError(
            f"{deck_path}: deck must contain 60 cards "
            f"(actual={len(deck)})"
        )

    return deck


def load_agent(agent_dir, module_name):
    """
    main.py を別々のPythonモジュールとして読み込む。

    v1 と v2 で plan / pre_turn / ability_used などの
    グローバル変数が混ざらないようにする。
    """

    main_path = os.path.join(agent_dir, "main.py")

    # cg を import できるようにする
    if agent_dir not in sys.path:
        sys.path.insert(0, agent_dir)

    # main.py は deck.csv を相対パスで読み込むため、
    # import 中だけカレントディレクトリを変更する
    old_cwd = os.getcwd()

    try:
        os.chdir(agent_dir)

        spec = importlib.util.spec_from_file_location(
            module_name,
            main_path
        )

        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)

    finally:
        os.chdir(old_cwd)

    return module.agent


def run_game(agent1, agent2, deck1, deck2):
    """1試合実行"""

    env = make(
        "cabt",
        configuration={
            "decks": [deck1, deck2]
        },
        debug=False,
    )

    env.run([agent1, agent2])

    final_step = env.steps[-1]

    reward1 = final_step[0]["reward"]
    reward2 = final_step[1]["reward"]

    return reward1, reward2


def main():

    games = 100

    print("=== Pokemon TCG AI Local Tournament ===")
    print(f"Games: {games}")
    print()

    deck_v1 = load_deck(V1_DIR)
    deck_v2 = load_deck(V2_DIR)

    v1_win = 0
    v2_win = 0
    draw = 0
    error = 0

    start_time = time.time()

    for game_no in range(1, games + 1):

        try:
            #
            # 重要:
            # 毎試合モジュールを読み直します。
            #
            # main.py 内には plan / pre_turn / ability_used
            # という状態変数があるためです。
            #

            agent_v1 = load_agent(
                V1_DIR,
                f"agent_v1_game_{game_no}"
            )

            agent_v2 = load_agent(
                V2_DIR,
                f"agent_v2_game_{game_no}"
            )

            # 先攻・後攻による偏りを減らすため、
            # 奇数試合と偶数試合で左右を入れ替える
            if game_no % 2 == 1:

                reward1, reward2 = run_game(
                    agent_v1,
                    agent_v2,
                    deck_v1,
                    deck_v2
                )

                v1_reward = reward1
                v2_reward = reward2

            else:

                reward1, reward2 = run_game(
                    agent_v2,
                    agent_v1,
                    deck_v2,
                    deck_v1
                )

                v2_reward = reward1
                v1_reward = reward2

            if v1_reward > v2_reward:
                v1_win += 1
                result = "v1 WIN"

            elif v2_reward > v1_reward:
                v2_win += 1
                result = "v2 WIN"

            else:
                draw += 1
                result = "DRAW"

            print(
                f"{game_no:3d}/{games} "
                f"{result:7s} "
                f"| v1={v1_win:3d} "
                f"v2={v2_win:3d} "
                f"draw={draw:3d}"
            )

        except Exception as e:

            error += 1

            print(
                f"{game_no:3d}/{games} ERROR: "
                f"{type(e).__name__}: {e}"
            )

    elapsed = time.time() - start_time

    valid_games = v1_win + v2_win + draw

    print()
    print("================================")
    print("RESULT")
    print("================================")

    print(f"Total       : {games}")
    print(f"Completed   : {valid_games}")
    print(f"Errors      : {error}")
    print()

    print(f"v1 wins     : {v1_win}")
    print(f"v2 wins     : {v2_win}")
    print(f"Draw        : {draw}")

    if valid_games > 0:
        print()
        print(
            f"v1 win rate : "
            f"{v1_win / valid_games * 100:.1f}%"
        )
        print(
            f"v2 win rate : "
            f"{v2_win / valid_games * 100:.1f}%"
        )

    print()
    print(f"Time        : {elapsed:.1f} sec")


if __name__ == "__main__":
    main()

3. Dockerで100試合させます

~/pokemon-ai で、

cd ~/pokemon-ai

実行します。

docker run --rm \
  --platform linux/amd64 \
  -v "$PWD":/work \
  -w /work \
  gcr.io/kaggle-images/python \
  python tournament.py

最初はこのまま100試合でOKです。

結果はこんな感じになります。

=== Pokemon TCG AI Local Tournament ===
Games: 100

  1/100 v1 WIN  | v1=  1 v2=  0 draw=  0
  2/100 v2 WIN  | v1=  1 v2=  1 draw=  0
  3/100 v1 WIN  | v1=  2 v2=  1 draw=  0
...
100/100 v2 WIN  | v1= 47 v2= 53 draw=  0

================================
RESULT
================================
Total       : 100
Completed   : 100
Errors      : 0

v1 wins     : 52
v2 wins     : 48
Draw        : 0

v1 win rate : 52.0%
v2 win rate : 48.0%

Time        : 54.4 sec

4. なぜ単純に env.run([agent, agent]) にしないのか

ここはかなり重要です。

今回のサンプル main.py には、

plan = AttackPlan()
pre_turn = 0
ability_used = False

というグローバルな状態変数があります。

さらにターンが変わるたびに、

global plan
global pre_turn
global ability_used

として状態を変更しています。

したがって、単純に

env.run([agent, agent])

とすると、場合によっては両プレイヤーが同じPythonモジュールの状態を共有する可能性があります。

今回の tournament.py は、v1とv2を別モジュールとしてロードしています。さらに各試合でロードし直すため、前の試合の plan 等が次の試合に残らないようにしています。

これは勝率測定ではかなり大事です。


5. 最初の実験

まずはまだ v2/main.py を変更しない状態で100試合してください。

つまり、

v1 = 元AI
v2 = 元AI

です。

理想的には試行回数を増やすと、

v1 約50%
v2 約50%

に近づきます。

いきなりAIを改造するより、まずこれで対戦・集計システム自体が公平そうか確認します。


となるか試すのが非常に分かりやすいです。

まずはv1/v2を変更せずに100試合させてみてください。実行結果を貼ってもらえれば、50:50からどれくらい偏っているか確認したうえで、最初のv2改造を一緒に1か所だけ行いましょう。

最初に改造する場所

このAIは攻撃対象を

def pokemon_score(...)

で決めています。

例えば

score += len(pokemon.energies) * 150

となっています。

つまり

エネルギーが1枚付いている相手

150点


私は最初にここを変更したいです。

例えば

score += len(pokemon.energies) * 250

にすると

エネルギーを大量につけた相手を積極的に倒すAI

になります。

The Pokémon Company - PTCG AI Battle Challenge Simulation
Build an AI Training Agent to play the Pokémon Trading Card Game

Submit Angenをクリック

「Upload Submission」が表示される場合

そこに、

Macで作成した

submission.tar.gz

をドラッグ&ドロップします。

そのまま提出できます。

アップロードが完了しました。

アップロードすると、すぐにほかのデッキとの対戦結果が確認できます。

Episodesのアイコンをクリック

ただし、V2をそのまま提出するには、submission.tar.gz をV2から作り直す必要があります。

今の状況はこうなっています。

pokemon-ai/
├── agents/
│   ├── v1/
│   │   ├── main.py
│   │   ├── deck.csv
│   │   └── cg/
│   │
│   └── v2/
│       ├── main.py   ← ← これを提出したい
│       ├── deck.csv
│       └── cg/

方法①(おすすめ・Macだけで完結)

agents/v2 に移動して、

cd ~/pokemon-ai/agents/v2

次に

tar -czf submission.tar.gz main.py deck.csv cg

これで

submission.tar.gz

が作られます。

中身は

submission.tar.gz
├── main.py
├── deck.csv
└── cg/

となり、提出形式と同じになります。

===============================

ここまで来れば、Kaggle上だけでなくMac上で自由にAIを改良できるようになります。

自分で環境を作りながら、ブログを書いたので、途中抜けがあるかもしれませんが、
皆さんのお役に立てればと思います。

おつかれさまでした。

余裕があれば、AIを使ってさらに勝率をあげられるようにしていきます。

コメント

タイトルとURLをコピーしました