【ポケモンSV】自動ダメージ計算サイトの仕組みと高速化について

はじめに

ポケモンSV向けに自動ダメージ計算サイトを作りました。

キャプチャーボードを接続したPCからアクセスすると、ゲーム画面を勝手に解析してダメージ計算してくれます。

pbasv.cloudfree.jp

1年前くらいにPythonで同じようなアプリを作ったのですが、スクリプトや実行ファイルは配布に不向きでメンテナンスも面倒だったため、ウェブに移植したいと考えていました。

ただ、ウェブ開発の経験がなかったこともあり、なかなか着手できず、昨年末に参考書を購入してようやく作り始めました。

Pythonアプリの下地があったため、1ヶ月あればできるだろうと舐めてかかった結果、土日をほぼすべて費やした上で3ヶ月もかかってしまいました...

せっかくなので、アプリの仕組みや工夫した点について記事にしようと思います。

ユーザーや、これからアプリを開発をしたい方にとって参考になれば幸いです。

コンセプト

自動ダメージ計算アプリ

私がポケモン対戦をまともにやったのはXY以来で、SVもかなりのライトユーザーです。

移り変わる環境について行くのは大変で、特にダメージ感覚がまったくありません。

とはいえ、対戦中にダメージ計算を叩くのも面倒なので、画像解析して自動化しようと考えました。

対戦中にアプリを使うこと自体が負担にならないよう、以下の点に注意して設計しました。

  • キャプチャ映像を表示する (ゲーム画面とアプリ画面を往復しなくてよい)
  • キャプチャ映像の上にモノを置かない (背景が動くと目が疲れる)
  • 自分と相手のパーティを常に表示する (対戦中に確認する手間を減らす、死に出し時に確認できる)
  • ダメージ計算と致死率は数字で明確化する。情報量は色濃度と表示桁数で調整
  • 相手の持ち物・テラスといった未知の情報は、数を限定して表示する (情報過多にしない)
  • 種族値などの慣れてくると分かる情報は、カーソルを合わせた時に注釈として表示する (情報過多にしない)

自動ダメージ計算アプリ

ポケモン管理アプリ

ポケモンを一から育成する場合、仮想敵を考えてステータスを調整することが多いです。

そのため、育成情報と仮想敵とのダメージ計算は常にセットで管理したいと考えました。

前述の自動ダメージ計算アプリと併用する前提のため、PCの大画面で効率よく管理できるように設計しました。

  • 育成情報とダメージ計算を同時に表示する
  • 複数のダメージ計算を並列に表示する
  • 耐久調整 (◯◯耐え) の自動化
  • 最低限のダメージ加算機能

ポケモン管理アプリ

使用言語

一部のバックエンドを除き、アプリの大部分は Javascript で実装しました。

したがって、ダメージ計算などの処理はほぼブラウザで処理されています。

標準ライブラリのほかに、以下の外部ライブラリを利用しました。

  • Tesseract.js : 文字認識
  • OpenCV.js : テンプレートマッチング
  • guessLanguage.js : 言語識別

文字認識(OCR)とは、文字通り画像から文字を読み取る機能です。

対戦中の場のポケモンを認識するときなど、多くの場面で利用しています。

残念ながら一度の読み取りに約0.1秒と時間がかかるため、多用するとアプリの使い勝手が悪くなってしまいます。

テンプレートマッチングとは、二つの画像の類似度や類似箇所を判定する機能です。

選出画面での相手パーティの認識や、試合場面の識別などに利用しています。

文字認識と比べて高速ですが、比較対象となるテンプレート画像を持っておく必要があります。

このアプリでは、相手パーティを認識するために全ポケモンの立ち絵をあらかじめ用意しています。

言語識別については、場のポケモンを認識する際に文字認識と組み合わせて使用しています。

ダメージ計算

ダメージ計算には自作したライブラリを使用しています。

図鑑や技の情報はあらかじめ外部ファイルに集約しておき、起動時に読み込んでいます。

計算式はポケモンWikiを参照しました。

ダメージ計算式 - ポケモン対戦考察まとめWiki|最新世代(スカーレット・バイオレット)

また、検算にはポケモンソルジャー様のダメージ計算サイトを使用しました。

ダメージ計算ツールSV byポケソル

ダメージ計算の高速化

ダメージには乱数があり、85%, 86%, ..., 100%の16通りの中からランダムに選ばれます。

ダメージによる致死率を正確に求めるためには、すべてのパターンを網羅しなければいけません。

1発だけなら16通りで済みますが、例えばスケショ5発のダメージを計算すると16の5乗 (約100万) 通りに分岐します。

分岐が増えるに従って計算時間も増加します。

それでも5発程度であれば計算可能ですが、スケショ+神速といった加算計算や、スライドバーでステータスを変えながら繰り返し計算する場合、この計算量はもはや現実的ではありません。

開発途中で確認したところ、6発計算 (16の6乗) に0.5秒かかり、7発計算しようとするとブラウザが停止しました。

これの対策として、実際には16通りの中に重複するダメージが含まれることを利用して、同じダメージをまとめて計算しています。

ダメージの重複は、1発あたりのダメージが小さい連続技において特に顕著になります。

例えば、無振カイリューが無振オーガポンにスケショ1発で与えるダメージは22, 24, 25, 27の4通りしかなく、5発でも24通り(全分岐計算時の4万分の1以下)の計算量で済みます。

実装については、配列ではなく、{"ダメージ値" : 分岐の数} の連想配列 (Pythonでいう辞書型) として扱っています。

技によるダメージを計算した後の致死率計算では、防御側のポケモンのHPの分岐も {"残りHP" : 分岐の数} として管理します。

木の実による回復

前述の方法だけでは、あるHPに至る経緯は保持されないため、オボンの実などの回復実をすでに使ったかどうか判別できません。

そこで、

  • {"100" : 3} → 木の実を保持しているHP100の分岐が3通りある
  • {"100.0" : 3} → 木の実を保持していないHP100の分岐が3通りある

のようにHPを表すキーを少数表示にして区別しています。

キーを整数化すれば同等に扱うことができるため、実装を煩雑化せずに回復実の有無を識別できます。

この方法を用いて、きのみの発動条件を考慮して正確な確定数を計算しています。

アプリの仕組み

データの保存

ユーザーが登録したポケモンや最後に入力した情報は記録され、アプリを再開したときに読み込まれます。

データはブラウザのローカルストレージに保存されているため、アプリにログインしてアカウントで紐づける必要がありません。

一方で、異なるブラウザソフトや端末間では情報が共有されないデメリットや、Cookieを削除するとデータも消えるリスクもあります。

この対策として、ポケモン管理アプリには、登録したポケモンjson形式で出力・入力できるバックアップ機能を実装しています。

キャプチャ映像の取得・描画

キャプボもカメラとして扱われるため、navigator.mediaDevices プロパティを使って映像を取得できます。

html の video 要素で描画すると画面のアスペクト比がなぜか微妙に崩れてしまうため、まわりくどいですが video 要素の映像を canvas 要素に再描画しています。

画面からポケモン読み込み

ありがたいことに、ボックスの右側のステータス表示で情報が完結しているため、これを解析するだけでポケモンを読み込めます。

性格は六角形の頂点にある◯印をテンプレートマッチングで識別し、それ以外は文字認識しています。

ボックス画面のステータス表示

試合場面の判定

自動ダメージ計算アプリの起動中は、キャプチャ映像を一定時間ごとに確認し、現在のゲームの場面を以下の3つに分類します。

  • 見せ合い画面
  • 対戦画面
  • 該当なし

見せ合い画面は、自分のパーティ表示の左下に表示されるモンスターボールの有無で判定しています。

見せ合い画面の判定箇所

対戦画面は、自分のポケモンの「Lv.」という文字の有無で判定しています。

対戦画面の判定箇所

見せ合い画面での処理

見せ合い画面に移行すると、まず試合をリセットします。

次に、相手のパーティを読み込みます。

ポケモンの立ち絵をテンプレート画像と比較して識別しますが、この時、

することで高速化しています。

画面のロードが間に合わず結果がおかしくなる場合があるため、2回連続で同じ結果を得られるまで繰り返し読み取ります。

相手パーティが読み込めたら、自分と相手のポケモンの相性を評価します。

評価値は以下の式で与えています。

自分と相手の評価が相互に影響するため、有効打が多い相手に有利なポケモンがより評価されます。

攻撃技が少ない受けポケモンは評価されにくいですが、そもそも受け性能の評価は単純ではないため諦めました。

見せ合い画面の残り時間で繰り返し評価を行うことで評価値を収束させます。

対戦画面での処理

対戦画面に移行したら、試合時間のタイマーを起動します。

以降の処理は、場のポケモンが変わるたびに繰り返し行います。

まず、場に出ているポケモンの名前を文字認識します。

文字認識の関数は言語を指定して呼び出しますが、ポケモンの名前は日本語とは限りません。

そこで、まず日本語と仮定して読み取ります。

実際の名前がハングルだとうまく読み取れないため、一度目の文字認識で該当するポケモンがいなければ、ハングルを指定して再び読み取ります。

次に、場のポケモンのHPを読み取ります。

自分のポケモンについては、HPバー内に書かれている文字を読み取っています。

相手のポケモンの正確なHPはわからないため、HPバーの有色箇所のピクセル数を数えて割合に換算しています。

場のポケモンとHPをすべて読み終えたらダメージ計算を行います。

被ダメージ計算では公開されている使用率上位の技を考慮しています。

バランスチェック機能

使用率上位のポケモンのうち、今のパーティが苦手とする相手をハイライトできます。

攻・守をクリックすると、すべての相手に対してダメージ計算を行い、与えるダメージ割合の合計値を評価します。

受け性能の評価では、ダメージを技の使用率で重みづけしたものの合計で評価しています。

バランスチェック機能

最後に

今後もバグ修正などは細々と続けていきます。

レギュFを全くプレイできていないので、伝説環境が始まる前にランクマ復帰したいですね。

【ポケモンSV】収穫ナッシーでキラフロルにTOD勝ちできるのか?- モンテカルロ法による生存率推定

はじめに

DLC後編 藍の円盤にて(アローラ)ナッシーが解禁されました。

このポケモンは「とおせんぼう」と「ねをはる」を覚えるため、吹き飛ばし持ちのディンルーやカバルドンさえもキャッチしてTODできます。
周知のとおりチオンジェンでも同じことができます。


さて、初手の起点作成ポケモンとしてはキラフロルも有名ですね。
チオンジェンは特防が高く、毒テラス+バークアウトで容易にTODできます。しかしながら、(アローラ)ナッシーの種族値はH95D75と高いとはいえず、残飯+根をはる+守るでは受けきれません。

そこで、夢特性の収穫を使って耐久したいところですが、
相手の攻撃の追加効果や急所に加えて、50%の収穫という運要素も絡んでくると、もはや受けきれるのかどうか判別できません。

TOD成功率を解析的に求めるのは難しいため、今回はモンテカルロ法(乱数シミュレーション)を用いてナッシーの生存率を推定してみました。

型紹介

オボンの実とタラプの実(D上昇)を持たせた型を考えます。

オボン収穫型

  • 毒テラス
  • H202(252) D139(252+)
  • とおせんぼう/ねをはる/まもる/攻撃技

タラプ収穫型

  • ゴーストテラス
  • H199(228) D139(252+)
  • とおせんぼう/ねをはる/ねむる/攻撃技

タラプ型ではHP実数値を199に下げて、毒ダメージを25→24に軽減すると生存率がわずかに上がります。
誤差の範囲内かもしれませんが、いずれにせよ余った努力値をBに振ったほうが効率的でしょう。

先に結論

  • オボン型:C182キラフロルすら受けきれない
  • タラプ型:C200キラフロルにも78%でTOD勝ちできる

キラフロルについて

シーズン13(12月24日時点)の使用率は以下のようになっています。
https://sv.pokedb.tokyo/pokemon/show/0970-00?season=13&rule=0
キラースピンの採用率が5割を超えており、先発の起点作成型にはおおよそ搭載されていると思われます。
また、過去に結果を残した中には、ほぼC特化のアタッカー型も見られます。
オボン型は毒の定数ダメージ(1/8)を許容できないため、毒or鋼テラスにする必要があります。
タラプ型は眠るで毒を解除できるため、最高威力のヘドロウェーブを半減するゴーストテラスを採用しました。

オボン型の生存率

以下の仮定のもと計算しました。

  • 1ターン目のナッシーは毒テラスしてHP 100%の状態
  • キラフロルはC182(無補正252振り)、火力アップなし
  • 大地の力をくらう → まもる → ...を16回繰り返す
  • 大地の力のダメージ:90~106 (44.6~52.5%) 乱数2発(17.18%)
  • 1ターン目から根をはるで回復
  • 追加効果のDダウン(10%)、急所(4.1%)を考慮する

ナッシー vs キラフロル対面を10000回計算した結果が以下です。

ナッシーは平均7.8ターンしか生存できませんでした。
追加効果のDダウンと急所を考慮しない場合でも、平均11ターンしか生存できません。
残念ながら、特殊耐久が全く足りていません。

タラプ型

以下の仮定のもと計算しました。

  • 1ターン目のナッシーはゴーストテラスしてHP 100%の状態
  • キラフロルはC200(特化)、火力アップなし
  • ナッシーが毒状態でなければキラースピン、毒状態ならパワージェムをくらう
  • パワージェムのダメージ:66~78 (32.7~38.7%) 乱数3発(97.85%)
  • 1ターン目から根をはるで回復
  • 急所(4.1%)を考慮する
  • 次のターンにパワージェム最大乱数(急所非考慮)+毒ダメで倒される場合にねむる
  • ねむるの最大PP8

ナッシー vs キラフロル対面を10000回計算した結果が以下です。

ナッシーは平均25.6ターン生存しました。
TOD勝ちの目安となる、パワージェム16回+ねむる4回 = 20ターン以上持ちこたえる確率は78.3%でした。
ナッシーの努力値配分をHD特化にしても、勝率は78.1%とほぼ変わりませんでした。

最後に

統計誤差は算出していません m(__)m
タラプ型では、眠るタイミングの見極めが重要になるかと思います。
実際には火力を落としたキラフロルも多く存在するため、TOD成功率は80%以上と予想します。
初手チオンジェンのTODは実際に読まれることがあったので、レギュレーションFのナッシーには期待しています。

【ポケモンSV】パソコン1台で自動化いろいろ

概要

【ポケモンSV】パソコン1台でテラピース自動収集 の記事で紹介した手法を用いて、他にも対戦に役立つ作業を自動化する。

準備

前述の記事を参照。
必須ではないが、Joycontrolを実行するコマンドが長いので、aliasを設定しておくと便利。
ターミナル(端末)を立ち上げて、適当なエディタで.bashrcを開く。

nano ~/.bashrc

末尾に以下を追記する。

alias joycon='sudo joycontrol-pluginloader -r 04:03:D6:21:42:93'

04:03:D6:21:42:93のところは自分のスイッチのMACアドレスに置き換える。
端末を再起動するか、ターミナルで

source ~/.bashrc

を実行して.bashrcを再度読み込むと、Joycontrolを実行する際、

sudo joycontrol-pluginloader -r 04:03:D6:21:42:93 hoge.py

と入力しなくとも、

joycon hoge.py

のように短いコマンドで実行できる。

自動化スクリプト

学校最強大会

Aボタンを連打するマクロは、Joycontrolのプリセットとして用意されている。 学校最強大会の受付で以下のコマンドを実行する。

sudo joycontrol-pluginloader -r 04:03:D6:21:42:93 ~/joycontrol-pluginloader/plugins/utils/RepeatA.py

テラレイド(オンライン)

【ポケモンSV】パソコン1台でテラピース自動収集 でも書いたが、ハピナスレイド等の期間中はボタン連打で簡単に周回できる。
新しくPythonスクリプトを作成する。

nano raid.py

raid.pyに以下をコピペする。

import logging
from JoycontrolPlugin import JoycontrolPlugin
class raid(JoycontrolPlugin):
    # 特定のボタンをt秒ごとにn回押す関数
    async def push(self, button, n=1, t=0.1):
        for _ in range(n):
            await self.button_push(button)
            await self.wait(t)

    async def run(self):
        while True:
            await self.push('x', t=1.0)
            await self.push('a', t=0.5)
            await self.push('r', t=0.5)                                                                                      
            await self.push('a', t=0.5)

インターネットに接続してオンラインのテラレイドのページに移動し、以下を実行する。

sudo joycontrol-pluginloader -r 04:03:D6:21:42:93 raid.py

ファイル名(raid.py)とクラス名(raid)が一致していないと動作しない点に注意。

競り

日付変更あり。
本体設定から"インターネットで時間をあわせる"をオフにする。
新しくPythonスクリプトを作成する。

nano auction.py

auction.pyに以下をコピペする。

import logging
from JoycontrolPlugin import JoycontrolPlugin

logger = logging.getLogger(__name__)

class auction(JoycontrolPlugin):
    # 特定のボタンをt秒ごとにn回押す関数
    async def push(self, button, n=1, t=0.1):
        for _ in range(n):
            await self.button_push(button)
            await self.wait(t)

    # セーブする関数
    async def save(self):
        logger.info('Saving.')
        await self.push('x', t=0.5)
        await self.push('r', t=1)
        await self.push('a', t=3)
        await self.push('b', n=2, t=0.5)

    # 日付を変更する関数
    async def change_date(self, d=1, m=0, y=0):
        logger.info('Changing date.')
        # Close game                                                                                                          
        await self.push('home', t=0.5)
        await self.push('x', t=0.5)
        await self.push('a', t=5)

        # Move to setting                                                                                                     
        await self.push('down')
        await self.push('right', n=5)
        await self.push('a', t=1)

        # Move to current date/time setting                                                                                   
        await self.push('down', n=15, t=0.07)
        await self.push('a')
        await self.push('down', n=9, t=0.07)
        await self.push('a', t=0.2)
        await self.push('down', n=2, t=0.07)
        await self.push('a')

        # Change date                                                                                                         
        cmd = 'up' if y >= 0 else 'down'
        for i in range(y):
            await self.push(cmd, n=y)
        await self.push('a')

        cmd = 'up' if m >= 0 else 'down'
        for i in range(m):
            await self.push(cmd, n=m)
        await self.push('a')

        cmd = 'up' if d >= 0 else 'down'
        for i in range(d):
            await self.push(cmd, n=d)
        await self.push('a')

        await self.push('a', n=3, t=0.2)
        await self.push('home', t=1)

        # Launch game                                                                                                         
        await self.push('a', t=1)
        await self.push('a', t=20)
        await self.push('a', t=25)

    # オークション自動化. Aボタン連打と日付変更を繰り返す
    async def run(self):
        count = 0
        while True:
            await self.push('a', n=120, t=0.5)
            await self.push('b', n=2, t=0.5)
            await self.save()
            await self.change_date()
            count += 1
            logger.info('%d finished.' % count)

マリナードタウンの販売員の前で以下を実行する。

sudo joycontrol-pluginloader -r 04:03:D6:21:42:93 auction.py

コレクレーのコイン集め

日付変更あり。
パルデア十景の看板にいる徒歩コレクレーを周回する。
新しくPythonスクリプトを作成する。

nano coin.py

coin.pyに以下をコピペする。

import logging
from JoycontrolPlugin import JoycontrolPlugin

logger = logging.getLogger(__name__)

class coin(JoycontrolPlugin):
    # 特定のボタンをt秒ごとにn回押す関数
    async def push(self, button, n=1, t=0.1):
        for _ in range(n):
            await self.button_push(button)
            await self.wait(t)

    # Lスティックをt秒間入力する関数
    async def l_stick(self, direction, t=0.1):
        await self.left_stick(direction)
        await self.wait(t)

    # セーブする関数
    async def save(self):
        await self.push('x', t=0.5)
        await self.push('r', t=1)
        await self.push('a', t=3)
        await self.push('b', n=2, t=0.5)

    # 日付を変更する関数
    async def change_date(self, d=1, m=0, y=0):
        # Close game                                                                                                          
        await self.push('home', t=0.5)
        await self.push('x', t=0.5)
        await self.push('a', t=5)

        # Move to setting                                                                                                     
        await self.push('down')
        await self.push('right', n=5, t=0.05)
        await self.push('a', t=1)

        # Move to current date/time setting                                                                                   
        await self.push('down', n=15, t=0.07)
        await self.push('a')
        await self.push('down', n=9, t=0.07)
        await self.push('a', t=0.2)
        await self.push('down', n=2, t=0.07)
        await self.push('a')

        # Change date                                                                                                         
        cmd = 'up' if y >= 0 else 'down'
        for i in range(y):
            await self.push(cmd, n=y)
        await self.push('a')

        cmd = 'up' if m >= 0 else 'down'
        for i in range(m):
            await self.push(cmd, n=m)
        await self.push('a')

        cmd = 'up' if d >= 0 else 'down'
        for i in range(d):
            await self.push(cmd, n=d)
        await self.push('a')

        await self.push('a', n=3, t=0.2)
        await self.push('home', t=1)

        # Launch game                                                                                                         
        await self.push('a', t=1)
        await self.push('a', t=20)
        await self.push('a', t=25)

    # Lスティックで移動する関数
    async def move(self, x=0, y=0):
        if x > 0:
            await self.l_stick('right', t=x)
        elif x < 0:
            await self.l_stick('left', t=-x)
        if y > 0:
            await self.l_stick('up', t=y)
        elif y < 0:
            await self.l_stick('down', t=-y)
        await self.l_stick('center', t=0.5)

    # マップ移動する関数
    async def map_move(self, x=0, y=0):
        await self.push('y', t=2)
        await self.move(x, y)
        await self.push('a', t=0.5)
        await self.push('a', t=1)
        await self.push('a', t=10)

    # パルデア十景巡り
    async def run(self):
        count = 0
        while True:
            # マップ上で一番左下に移動                                                                                           
            await self.push('y', t=2)
            await self.move(x=-10, y=-10)

            # オリーブ農園                                                                                                      
            await self.move(x=2.73, y=2.08)
            await self.push('a', t=0.5)
            await self.push('a', t=1)
            await self.push('a', t=10)

            await self.push('l', t=0.5)
            await self.move(x=-0.7, y=0.3)
            await self.move(x=-0.05)
            await self.push('a', t=0.5)
            await self.push('a', t=1)

           # ビシャビシャの斜塔                                                                                                      
            await self.map_move(x=-1.25, y=-0.8)
            await self.move(x=0.95, y=0.5)
            await self.push('a', t=0.5)
            await self.push('a', t=1)

            # ありがた岩                                                                                        
            await self.map_move(x=-0.31, y=4.88)
            await self.push('l', t=0.5)
            await self.move(x=1)
            await self.push('a', t=0.5)
            await self.push('a', t=1)

            # オージャの大滝                                                                                                  
            await self.map_move(x=1.74, y=-0.46)
            await self.move(x=-0.7, y=0.45)
            await self.push('a', t=0.5)
            await self.push('a', t=1)

            # ナッペの手                                                                                                      
            await self.map_move(x=0.45, y=-0.23)
            await self.push('l', t=0.5)
            await self.move(x=-0.8, y=0.18)
            await self.push('a', t=0.5)
            await self.push('a', t=1)

           # パルデア最高峰                                                                                                   
            await self.map_move(x=1.27, y=-0.23)
            await self.move(x=-0.45, y=0.7)
            await self.push('a', t=0.5)
            await self.push('a', t=1)

            # 100万ボルトの夜景                                                                                                     
            await self.map_move(x=1.46, y=-1.76)
            await self.move(x=0.42, y=0.5)
            await self.push('a', t=0.5)
            await self.push('a', t=1)

            # ひそやかビーチ                                                                                                
            await self.map_move(x=-0.46, y=-2.48)
            await self.move(y=1)
            await self.push('a', t=0.5)
            await self.push('a', t=1)

            await self.save()

            count += 1
            logger.info('%dth' % count)

            await self.change_date()

マップ移動ができる場所で以下を実行する。

sudo joycontrol-pluginloader -r 04:03:D6:21:42:93 coin.py

【ポケモンSV】パソコン1台でテラピース自動収集

動機

  • テラピースを50個集めるのが面倒で、とにかく自動化したい。
  • 複数のソフトやマイコン等を使う方法は確立されているが、テラピースさえ集められればよいので、もっとお手軽に実現したい。

手法

オンラインのテラレイドバトルをボタン連打で自動周回する。

注意事項

  • バトル中は基本的にAボタン連打のため、★6レイドなどの高難易度レイドは難しい。
  • そのため、簡単にクリアできるイベントレイド期間中に周回するなど、味方に迷惑がかからないように努める。

必要なもの

おおまかな手順

  1. Linux(仮想)環境を用意する
  2. LinuxにJoycontrolを導入する
  3. レイド周回用のマクロを作成する

Joycontrolとは

ニンテンドースイッチのコントローラをエミュレートし、Bluetooth経由でPCとスイッチが通信できる。

github.com

つまり、PCがコントローラになる。理屈上なんでも自動化可能。

USBドングルが必要なケース

PCの内蔵Bluetoothアダプタが機能しない場合がある。
筆者のMac miniでも機能しなかったため、TP-Link UB400 (Amazonで1000円) を使用した。

後継品のUB500はMac miniに対応していなかった (1敗)

Linux環境構築とJoycontrol導入

こちらのサイトを参照し、"4. 動作テスト" まで完了する。
すでにLinux環境がある場合はJoycontrolのインストールのみでOK。

qiita.com

なお、上記サイトにあるVirtual Box, Ubuntuのリンク先のバージョンは検証時より新しくなっている。
検証当時のバージョンはこちら。

レイド周回マクロ作成

上記の動作テストで使用したスクリプトの中身を書き変える。
ターミナル(端末)を立ち上げて、適当なエディタでスクリプトを開く。

nano plugins/tests/TestControllerButtons.py

一番下のrun関数

   async def run(self):
    logger.info('TEST Controller Buttons Plugin')
        await self.push_all_buttons()
        await self.pushing_button_simultaneous()
        await self.long_press_button()

を以下のように修正する。

   async def run(self):
        while True:
            await self.button_push('x')
            await self.wait(1.0)
            await self.button_push('a')
            await self.wait(0.5)
            await self.button_push('r')
            await self.wait(0.5)
            await self.button_push('a')
            await self.wait(0.5)

0.5秒間隔で X→A→A→R→A の順にボタンを押し続ける。

  • Xでメニューを開いてテラレイドバトルの画面に移動する。
  • Rでバトル中にテラスタルする。

レイドを周回する

事前準備

  • Aボタン連打で勝てるポケモンを用意する。
  • バッグ内のボールを空にする。自動で捕まえてしまうため。
  • おまかせレポートをONにする。通信エラーによるフリーズ対策。

実行

  1. スイッチをインターネットに接続する
  2. メニュー > ポケポータル > テラレイドバトル
  3. スクリプトを実行する
    (01:23:45:67:89:AB は自分のスイッチのMACアドレスに置き換える)
sudo joycontrol-pluginloader -r 01:23:45:67:89:AB plugins/tests/TestControllerButtons.py 

結果

ハピナスレイド最高。