【ポケモンSV】の対戦システムをPythonで再現して実機AIを作る (後編:実機Bot)
前編をお読みいただいた前提で説明するため、未読の方はまずこちらをご覧ください。
目次
概要
対戦シミュレーションのBattleクラスをベースに、以下の機能を追加してBotモジュールを構築します。
- ゲーム画面を解析して情報を取得する
- シミュレータに情報を渡して、方策関数からコマンドをもらう
- Switchにコマンドを送ってゲームを操作する

必要なもの
ポケモンSVは日本語ROMを前提にしています。
Switchを遠隔操作するnxbtモジュールを使うにはLinux環境が必要です。
VirtualBoxの仮想環境でも動くらしいのですが、私はvagrant upのところで躓いて、挫折してラズパイ5を購入しました。
github.com
私の環境
- PC - Raspberry Pi 5 8GB (1.5万円)
- キャプチャーボード - HSV320 (4,700円) Full HD で出力できればなんでもOK
- Python 3.11.2
nxbtのインストールでトラブルが頻発しているようなので、可能であれば、まず手持ちのLinux環境で試してみてください。
私は当初ラズパイ3Bで開発していましたが、処理能力が低く文字認識に時間がかかりすぎたため、奮発してラズパイ5に乗り換えました。
なお、ラズパイ3Bでは内部のBluetoothアダプタを使うと通信できず、代わりにUSBドングルを外部接続する必要がありました。VirtualBoxでも同様だそうです。
nxbtのインストール
nxbtはsudoでインストールする必要があるため、あらかじめpythonの仮想環境を構築しておきます。
次に、こちらを参考にnxbtをインストールしてください。
Switchのコントローラ接続画面に移動して、ターミナルに以下のコマンドを入力します。
cd Pokepyのソースコードのディレクトリ
sudo 仮想環境のパス/bin/python ex11_nxbt_connection.py
見慣れないプロコンが接続されたら正しくインストールできています。

nxbtはプロコンに偽装するため、Switchに接続する前に本物のプロコンの接続を解除する必要があります。無線接続している場合は、USB-Cポートの隣にある小さいボタンを押し込むことで切断できます。
事前準備
キャプチャーボードの確認
ターミナルに以下を入力します。
v4l2-ctl --list-devices
出力結果
(略)
MiraBox Capture: MiraBox Captur (usb-xhci-hcd.1-2.4):
        /dev/video1
        /dev/video2
        /dev/media0
表示されたVide ID (上の例だと1または2) をconfig.txtに書いておきます。
VideoID 1
キャプチャーボードを差し替えるとVideo IDが変わってしまうことがあるので、キャプボの接続に失敗することがあれば設定を見直してみてください。
遅延時間の測定
必須ではありませんが、画面を移動するためにコマンドを入力してから、遷移後の画面をキャプチャできるようになるまでの時間を計測しておくと、Botの動作精度や速度改善に役立ちます。
野生ポケモンとの戦闘画面に移動して、次のスクリプトを実行します。
sudo 仮想環境のパス/bin/python ex12_latency_meas.py

パーティ登録
実戦で使うパーティを登録します。
ボックス画面の手持ちまたはバトルボックスにパーティをセットし、右側にステータスを表示した状態で以下を実行します。
sudo 仮想環境のパス/bin/python ex13_party_registration.py

Bot構築
前編で例にあげた方策関数をそのまま流用した、Botのサンプルスクリプトを用意しました。
"""ex14_sample_bot.py""" from pokepy.pokebot import * from distutils.util import strtobool import random import sys # Pokebotクラスを継承 class MyBot(Pokebot): def __init__(self): super().__init__() def selection_command(self, player=0) -> list[int]: """{player}の選出画面で呼ばれる方策関数 n=0~5 : パーティのn番目のポケモンを選出 選出する順番に数字を格納したリストを返す """ # ランダム選出 return random.sample( list(range(len(self.party[player]))), 3 ) def battle_command(self, player): """{player}のターン開始時に呼ばれる方策関数 ex4_bruteforce_1on1.py から流用 """ (略) # スコアが最も高いコマンドを選ぶ return available_commands_list[0][scores.index(max(scores))] def change_command(self, player: int) -> int: """{player}の任意交代時に呼ばれる方策関数""" # ランダム交代 return random.choice(self.available_commands(player, phase='change')) def score(self, player: int) -> float: """盤面の評価値を返す""" # 例: TODスコアの比 return (self.TOD_score(player) + 1e-3) / (self.TOD_score(not player) + 1e-3) # ライブラリの初期化 Pokemon.init(season=None) # Botを生成、実行 bot = MyBot() bot.main_loop(vs_NPC=strtobool(sys.argv[1]))
最初にPokebotクラスを継承してBotを作成します。 PokebotクラスはBattleクラスを継承したものです。
前編のシミュレータと比べると、選出画面でコマンドを返す方策関数 selection_command() が追加されていますが、それ以外はほぼ変わりません。
試運転 vs NPC
作成したBotで学校最強大会を周回してみます。
大会に参加した状態で、ターミナルに以下を入力します。
sudo 仮想環境のパス/bin/python ex14_sample_bot.py 1
引数に1を指定すると対NPC戦のモードで走ります。簡易的なデバッグに便利です。
対人戦
フリーマッチまたはランクマッチの待機画面に移動して、ターミナルに以下を入力します。
sudo 仮想環境のパス/bin/python ex14_sample_bot.py 0
引数に0を渡すと対人戦モードで動作します。
現状の課題
1. 表示テキストの誤読が多い
対戦中、画面に表示されるテキストから相手の技やアイテムを取得していますが、 全く関係のないテキストを誤読することが多いです。 これは、ノイズとなるテキストを手動で除外しているのですが、文章が多岐にわたり全てをカバーできていないためです。
2. ダメージを観測していない
前述のテキスト解析を優先して、ダメージを観測する仕組みをまだ実装できていません。 実際にやると、どこまでが技によるダメージでどこからが回復なのか、など識別に多少苦労しそうです。
3. たまに止まる
...
今後の展望
前述の課題に取り組みつつ、並列処理による高速化も試してみたいですね。