モバイル版ポケモンチャンピオンズのUI改善要望

ポケモンチャンピオンズを遊んでいて気になった点や、こうなったらもっと便利そうだと思った点をまとめました。

主にUIや操作性に関する内容です。同じように感じている方もいるかもしれません。

要望の送信先はこちらです。

https://app-pcs.pokemon-support.com/hc/ja/requests/new

目次

ホーム

  • 最後に対戦したルール (ランクマッチ/大会) に移動できるリンクがほしい。
  • 直接スカウトを開始できるリンクがほしい 。

ボックス

  • 種族値も確認したい。
  • メガシンカ後やフォルムチェンジ後の状態も表示したい。
  • ドラッグ&ドロップでポケモンを移動したい。
  • チームにポケモンを再セットしたとき、以前と同じ持ち物を自動的に持たせたい。
  • 左右フリックやボタン操作でチームを切り替えたい。
  • 並び替え・絞り込みがポップアップで分離していて使いづらい。条件入力と結果表示が一体化しているとよい(例えばAmazon商品検索のような)。
  • ポケモン単体の持ち物を入れ替えるUIがほしい。チーム上の持ち物アイコンをクリックしたら右枠に持ち物一覧が表示されて選択できる、など。ランクマッチ使用率順にソートしたり、非対応メガストーンを非表示にする 機能もあるとうれしい。
  • 欲を言えば、使用率を参照した構築サジェスチョンや攻守のタイプ補完率表示など、チーム構築に役立つ情報を表示する機能がほしい。

持ち物選択

  • チームを俯瞰しながら持ち物を分配するというUIだが、所望の持ち物を探すのに時間がかかり使いづらい。
  • ポケモンごとの持ち物変更やポケモン間の持ち物交換がスムーズに行えるのであれば、そもそも必要ないように感じる。

トレーニング

  • 種族値も表示してほしい。
  • メガシンカ後やフォルムチェンジ後の状態も表示したい。
  • デフォルトの持ち物を指定して、チームに設定したときに持ち物を自動設定したい。
  • バトルデータを見ながら技や能力ポイントなどを選びたい。
  • 基礎ポイントを調節するのに逐一別ウィンドウに移動するのが煩わしい。せめて最大最小くらいはトレーニングのトップ画面から設定したい。
  • 育成情報をメモできる自由記入欄がほしい。

わざ選択

  • 技をひとつずつしか変更できず使い勝手が悪い。まとめて変更したい。
  • 並び替え・絞り込みがポップアップで分離していて使いづらい。条件入力と結果表示が一体化しているとよい。
  • バトルデータの使用率順の並び替えがほしい。
  • 「忘れる」ボタンは滅多に使わないので、下部に大きく表示しないでほしい。

能力ポイント選択

  • 最大化、最小化のボタンは残してほしい。
  • 66ポイントを超えた配分設定ができないようになっているが、ポイントを振り直すときに不便なので、一時的なポイント超過は許してほしい (遊戯王マスターデュエルでは編集時の枚数超過は許容されている)。

ショップ

  • プレミアムショップの内容が少ないにもかかわらず、フロンティアショップと分離するための中間画面がありアクセス性が悪い。ショップを統一してタブで分けてはどうか。

ランクマッチ

  • 左右フリックやボタン操作でチームを切り替えたい。
  • チームのポケモンの情報を確認するためにボックスに移動する必要があり3クリックを要する。ポケモンのアイコンをクリックしたら育成情報を見られるようにしたい。
  • 現在設定されている戦闘曲を表示してほしい

スカウト

  • スカウトを開始するまでの画面遷移が多い。ポケモンHOME連携はスカウトとは別概念であり分離すべきではないか。
  • 過去作資産のない初心者にとってスカウトは貴重な資源のはずだが、スカウト費用(2500VP)は試合報酬(300VP)に対して高いように感じる。過去作所持者との格差を助長しているのではないか。

その他

ファストトラベル

  • 多い。
  • せめてアイコンがほしい。

実績

  • まとめて受け取りたい。

全体的に

  • 情報が集約されておらず中間画面が増え、操作に時間がかかる。
  • ポップアップ表示を多用しており操作性が悪い。
  • フリック操作や「枠外タッチ = 戻る」といった一般的なUIが整備されていない。
  • バトルデータをUIと連携させれば情報をより活用できるのではないか。

おわりに

以上、プレイ中に感じた改善要望のまとめでした。

対戦自体は楽しいので、今後のアップデートでより快適に遊べるようになることを期待しています。

【チャンピオンズ】ボタン連打でVPを稼ぐことはできるのか

できました。

爆盛り

お疲れ様です。

普段はシングルバトルしかやらないので、ダブルバトルをボタン連打で周回してVPを稼げるか試してみました。

必要なもの

  • ポケモンチャンピオンズ (Switch 版)
  • マクロコントローラ、または Linux PC

私の場合、Linux PC として Raspberry Pi 5を使用しました。 PCにnxbt というPythonライブラリをインストールすると、Bluetooth経由でプロコンに偽装してSwitchを無線制御できるようになります。 マクロコントローラはないけど Linux PC はあるという方は試してみてください。

github.com

私が使ったnxbtのスクリプトも紹介します。

import nxbt
from nxbt import Buttons

# nxbt接続
nx = nxbt.Nxbt()
idx = nx.create_controller(nxbt.PRO_CONTROLLER)
nx.wait_for_connection(idx)

# ゲーム画面に戻る
nx.press_buttons(idx, [Buttons.A], down=0.5)
nx.press_buttons(idx, [Buttons.HOME], down=0.5)
nx.press_buttons(idx, [Buttons.HOME], down=0.5)

# R -> A 交互に連打
while True:
    nx.press_buttons(idx, [Buttons.R], down=0.5)
    nx.press_buttons(idx, [Buttons.A], down=0.5)

ランクマの受付画面でHOMEボタンを押して、コントーラ選択画面に移動してスクリプトを実行します。 プロコンを使用中の場合は、USBポートの隣にある丸いボタンを押し込んで接続を切ってから実行してください。

使用パーティ

下2匹は見せポケ

1試合で獲得できるVPはおおよそ300VP(勝ち) 100VP(負け) みたいなので、短時間である程度勝ちも狙える構築がよいと思います。 私はスカウトしたポケモンから適当に4匹選びました。 自爆カビゴンはゲームスピードが上がるのでかなりおすすめです。いかく対策でしろいハーブを持たせるのもよいと思います。 ペリッパースタートの雨パが結構多かったので、初手リザY + スカーフ噴火で早期決着を狙うのも面白そうです。

結果

最低ランクからスタートし、30hほど周回して43000VP獲得できました。これはおよそ140勝に相当します。

8000VPからスタート

序盤はさくさくランクアップするもハイパーボール級で停滞していたようです。

一瞬ランク1にもいた

所感

安定動作している
ポケモンSVでは長時間周回していると処理落ちが酷かったのですが、チャンピオンズでは今のところそういった現象は確認していません。

割と勝てる
スカウトポケモンの寄せ集めでもそこそこ勝てたので、ちゃんと構築すればマスターランクに上がるのは難しくないと思います。

勝ちすぎると効率が落ちる
皮肉にも勝てば勝つほど相手も強くなり、ポイントの収集効率は落ちていきます。 ただし負けてもVPが貰えたり、連敗すると弱Botと戦わされることから、ある程度のところで効率が底打ちすると思われます。

ボタン連打周回は悪か

利用規約に則れば当然ダメでしょうが、過去作なしで遊ぼうとすると深刻なVP不足に陥るので、大目に見てほしいというのが本音です。 遅延Botなどは論外ですが、対戦相手に迷惑がかからないように配慮して利用すべきでしょう。

おわり。

【ポケモンSV】ポリゴン2の「2」って全角?半角?(表記揺れ)

自分のアプリ内でバトルデータベースの公開データを使おうとしたときに、全角・半角の表記違いで引っかかった
細かいところだが疎かにすると後で書き直す羽目になる(実際になっている)ので、表記がどうなっているのか備忘録として残しておく

ポケモンの名前

ポケ徹は半角英数字を使用している

ポリゴン2 ポリゴンZ タイプ:ヌル
公式 全角 全角 全角
バトルDB 全角 全角 全角
ポケ徹 半角 半角 全角

わざ

ポケ徹は半角英数字を使用している

10まんボルト テクスチャー2 Vジェネレート DDラリアット 10まんばりき Gのちから 3ぼんのや
公式 全角 全角 全角 全角 全角 全角 全角
バトルDB 全角 全角 全角 全角 全角 全角 全角
ポケ徹 半角 半角 半角 半角 半角 半角 半角

URL, json

ポケ徹の数字(X)は途中まで図鑑番号だが、4桁手前では図鑑番号から少しずれている
フォルム違いの添え字(S)の決め方もポケモンによって異なるように見える

基本フォルム 派生フォルム
公式(図鑑サイト) XXXX XXXX-N
公式(HOMEデータ) XXXX XXXX_NNN
バトルDB XXXX-00 XXXX-NN
ポケ徹 nXXXX nXXXXS

X, N: 半角0~9
S: 半角a~z

外国語

公式は全て半角

結論

公式(ポケモンHOMEの内部データ)の表記に従っていれば、バトルデータベースとの互換には困らない

【ポケモンSV】上位構築のポケモンの型を機械的に分類する

スクリプト

github.com

動機

以前紹介した対戦シミュレータを使っていると、相手のポケモンの型を予想・補完しないといけない場面が結構あります。これまではポケモンHOMEの情報をもとにほぼランダムで生成していましたが、どうしてもリアリティに欠けるので、実際に結果を残した構築から型を抽出できないかと考えました。

シミュレータ以外にも幅広く使えそうな情報が得られたので、簡単ですが共有したいと思います。

データ収集

バトルデータベースが公開している情報と、同サイトにリンクが張られている構築記事の情報を組み合わせて、型の分類に必要なデータを作ります。

sv.pokedb.tokyo

バトルデータベース

ありがたいことに集計結果が公開されており、json/csv形式でダウンロードできます。ただ、ポケモンの特性/技/ステータス関連の情報がないため、これだけで型を識別するのは難しそうです。

バトルデータベースの集計データ
バトルデータベースのランキング

構築記事

さらにありがたいことに、バトルデータベースには過去シーズンの1000位以内の構築記事がまとめられています。構築記事を見ればだいたいわかるはずなので、スクレイピングして情報を取り出します。記事の文章は人によって書き方が違っていて解析が面倒なので、今回はレンタルパーティのスクショを探して、画像認識で特性と技を取得しました。

レンタルパーティ画像 (https://gamewith.jp/pokemon-sv/article/show/394122)

結果

シーズン22のシングルバトルのデータを確認します。執筆時点でバトルデータベースに公開されている構築は216件ですが、画像を取得できた構築は129件と6割程度でした。それでも774体のポケモンのデータが集まりました。129件の構築で最も使われたポケモンはブリジュラス(71件)で、カイリューガチグマがそのすぐ下にいますね。

ブリジュラスの統計

ここまではバトルデータベースの分析ボタンを押せば見れるので、構築記事から集めた技も確認します。エレクトロビームが50%近く採用されており、ランクマッチ全体の統計(30%)よりもパワフルハーブ型が多かったようです。

型分類

単純な例として、「型 = 特性とアイテムの組み合わせ」と定義して分類すると次のようになりました。パワフルハーブ型が5割弱と主流で、そのほとんどが頑丈で採用されています。持久力+回復アイテム型も合計すれば3割に達します。母数は少ないですが、アイテムと特性は住み分けられているようです。

型ごとの技の採用率をプロットしました。 私は昔育てたCSベースのブリジュラスに風船を持たせて使っていたのですが、風船型はステロを撒くのがいいみたいです。

多数派の頑丈パワフルハーブ型ブリジュラスと同時採用されたポケモンの分布も見てみます。珠ミミッキュがずば抜けて多いことや、ほかのメンツからしても対面構築が多いのかもしれません。

無限に遊べそうなので今回はここで終わります。

最後に

ポケモンの型に着目することで、統計データをより実践的に解釈できるように思います。

自動で集められなかった記事や過去の類似シーズンも参照すれば、なかなかリッチなデータベースになりそうです。データを効率よく集めるためにスクレイピングの精度を上げたいところですが、Chat GPTにお願いしたところ性格くらいは拾えたので、いつか(無料になる日がきたら)生成AIなども活用してみたいですね。

また、型どうしの横のつながりを可視化すれば、構築アドバイザーのような新しいアプリができるかもしれません。個人的には、こういった解析を本家バトルデータベースのサイト上でやれたら嬉しいのですが......もしこの記事をご覧になっていたらぜひ検討していただけないでしょうか?

冒頭にあるように一連のソースコードを公開していますので、興味のある方はぜひご自身で解析してみてください。

【ポケモンSV】の対戦システムをPythonで再現して実機AIを作る (後編:実機Bot)

前編をお読みいただいた前提で説明するため、未読の方はまずこちらをご覧ください。

目次

概要

対戦シミュレーションのBattleクラスをベースに、以下の機能を追加してBotモジュールを構築します。

  • ゲーム画面を解析して情報を取得する
  • シミュレータに情報を渡して、方策関数からコマンドをもらう
  • Switchにコマンドを送ってゲームを操作する

Botシステムのイメージ

必要なもの

ポケモンSVは日本語ROMを前提にしています。

Switchを遠隔操作するnxbtモジュールを使うにはLinux環境が必要です。
VirtualBoxの仮想環境でも動くらしいのですが、私はvagrant upのところで躓いて、挫折してラズパイ5を購入しました。 github.com

私の環境

  • PC - Raspberry Pi 5 8GB (1.5万円)
    • 電源 - HT-PD27W-JA (2,200円)
    • micro SD - SDSQUAB-064G-GH3MA (1,200円)
    • 筐体+ファン - SW53-J (3,000円)
    • micro HDMI - HD-007-0.3M (700円)
    • USB SSD - SSD-PST250U3BA/N 250GB (6,000円) 高速化目的、なくてもよい
  • キャプチャーボード - HSV320 (4,700円) Full HD で出力できればなんでもOK
  • Python 3.11.2

nxbtのインストールでトラブルが頻発しているようなので、可能であれば、まず手持ちのLinux環境で試してみてください。
私は当初ラズパイ3Bで開発していましたが、処理能力が低く文字認識に時間がかかりすぎたため、奮発してラズパイ5に乗り換えました。

なお、ラズパイ3Bでは内部のBluetoothアダプタを使うと通信できず、代わりにUSBドングルを外部接続する必要がありました。VirtualBoxでも同様だそうです。

nxbtのインストール

nxbtはsudoでインストールする必要があるため、あらかじめpythonの仮想環境を構築しておきます。

qiita.com

次に、こちらを参考にnxbtをインストールしてください。

github.com

Switchのコントローラ接続画面に移動して、ターミナルに以下のコマンドを入力します。

cd Pokepyのソースコードのディレクトリ
sudo 仮想環境のパス/bin/python ex11_nxbt_connection.py

見慣れないプロコンが接続されたら正しくインストールできています。

nxbtの接続テスト

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. たまに止まる

...

今後の展望

前述の課題に取り組みつつ、並列処理による高速化も試してみたいですね。

【ポケモンSV】の対戦システムをPythonで再現して実機AIを作る (前編:対戦シミュレータ)

(poke-envの存在を知らずに作ってしまった...
手番以降の盤面の部分探索ができる、実機対戦に対応している点で差別化できていると信じたい)

ソースコード】対戦シミュレータ + 実機対戦Bot github.com

ソースコード】ランクマッチの使用率を取得 github.com

目次

できること

  • 対戦シミュレーション
  • 致死率計算 (いわゆるダメージ計算)
  • ポケモンSVの実機制御

概要

「強いAIを作ったので公開します!」という趣旨ではなく、あくまでコンピュータに実機で対戦させる仕組みを作ってみたというものです。

今回の内容は2つの記事に分けて紹介します。

  1. 対戦シミュレータの導入 (本記事)
  2. 実機対戦Botの導入 (後編)

はじめに、コンピュータが先読みできるように、ポケモンSVの対戦システムを再現するシミュレータを導入します。
続いて、このシミュレータに画面解析やコマンド入力などの機能を追加して、実機ポケモンSV上で自立して対戦するBotを作ります。

必要なもの

シミュレータを使うには、

  • PythonをインストールしたPC

さらにBotを作るには、

  • ポケモンSV
  • キャプチャーボード
  • Linux PC (仮想環境でも可…らしい)
  • USB Bluetooth ドングル (環境による)

PCからSwitchを操作するためにnxbtというライブラリを使用しますが、これはLinux(仮想)環境でしか動作しないようです。
形成判断AIのようにSwitchを操作しないアプリを作るのであれば、どのOSでもよいと思います。

シミュレータの導入

筆者の環境

基本的な使い方

2匹のポケモンを戦わせてみます。

"""ex_random_1on1.py"""
from pokepy.pokemon import *

# ライブラリの初期化
Pokemon.init()

# Battleクラスのインスタンスを生成
battle = Battle()

# ポケモンを生成して選出に追加
battle.selected[0].append(Pokemon('カイリュー'))
battle.selected[1].append(Pokemon('ガチグマ(アカツキ)'))

# 勝敗が決まるまで繰り返す
while battle.winner() is None:
    # ターン経過
    battle.proceed()

    # 行動した順にログ表示
    print(f'\nターン{battle.turn}')
    for player in battle.action_order:
        print(f'{player=}', battle.log[player], battle.damage_log[player])

出力結果

ターン0
player=0 ['交代 -> カイリュー'] []
player=1 ['交代 -> ガチグマ(アカツキ)'] []

ターン1
player=0 ['カイリュー', 'HP 166/166', 'コマンド 11', '先手', 'テラスタル ノーマル', 'しんそく PP 7', 'ダメージ 58', '相手HP 130', 'しんそく 成功', 'HP -54', 'D-1'] ['ノーマルテラスタル x1.5']
player=1 ['ガチグマ(アカツキ)', 'HP 188/188', 'コマンド 1', '後手', 'HP -58', 'だいちのちから PP 15', '急所', 'ダメージ 54', '追加効果', '相手HP 112', 'だいちのちから 成功'] ['マルチスケイル x0.50', '急所 x1.5']

ターン2
player=0 ['カイリュー', 'HP 112/166', 'コマンド 1', '先手', 'しんそく PP 6', 'ダメージ 57', '相手HP 73', 'しんそく 成功', 'HP -99'] ['ノーマルテラスタル x1.5']
player=1 ['ガチグマ(アカツキ)', 'HP 130/188', 'コマンド 3', '後手', 'HP -57', 'ハイパーボイス PP 15', 'ダメージ 99', '相手HP 13', 'ハイパーボイス 成功'] []

ターン3
player=1 ['ガチグマ(アカツキ)', 'HP 73/188', 'コマンド 12', '先手', 'テラスタル ノーマル', 'しんくうは PP 47', '急所', 'ダメージ 13', '相手HP 0', '勝ち'] ['急所 x1.5']
player=0 ['カイリュー', 'HP 13/166', 'コマンド 2', '後手', 'HP -13', '負け'] []

0ターン目にカイリューとガチグマが場に出て、殴り合った結果ガチグマが勝ちました。

ポケモンを3匹ずつ選出すれば3対3の試合になります。

"""ex2_random_3on3.py"""
(略)
# ポケモンを3匹ずつ生成して選出に追加
for player in range(2):
    names = random.sample(list(Pokemon.home.keys()), 3)
    print(f'{player=} の選出 {names}')

    for i in range(3):
        battle.selected[player].append(Pokemon(names[i]))

# 勝敗が決まるまで繰り返す
while battle.winner() is None:
    (略)

出力結果

player=0 の選出 ['ルカリオ', 'カジリガメ', 'オーロンゲ']
player=1 の選出 ['エーフィ', 'ルチャブル', 'リザード']

ターン0
player=0 ['交代 -> ルカリオ'] []
player=1 ['交代 -> エーフィ'] []

ターン1
player=1 ['エーフィ', 'HP 140/140', 'コマンド 22', '先手', '交代 -> リザード', '行動不能 交代', 'HP -55'] []
player=0 ['ルカリオ', 'HP 145/145', 'コマンド 0', '後手', 'しんそく PP 7', 'ダメージ 55', '相手HP 78', 'しんそく 成功'] []
(略)

1on1では技を使うだけでしたが、2匹以上選出すると交代もできるようになります。
上の例では、1ターン目にエーフィがリザードに交代しています。

対戦シミュレーションの基本的な流れは次のようになります。

  1. Battleインスタンスを生成する
  2. Pokemonインスタンスを生成して選出に追加する
  3. proceed()メソッドでターンを進める

Battleクラスは試合や盤面などの対戦システム全般を、Pokemonクラスはポケモンの個体や関連情報を表現します。

ポケモンの生成

ポケモンを生成して中身を確認してみます。

"""ex3_generate_pokemon.py"""
from pokepy.pokemon import *

# ライブラリの初期化
Pokemon.init(season=None)

name = 'ガチグマ(アカツキ)'
p = Pokemon(name, use_template=True)

print('-'*50 + '\nインスタンス変数\n' + '-'*50)
print(f'名前\t{p.name}')
print(f'タイプ\t{p.types}')
print(f'体重\t{p.weight}')
print(f'性別\t{p.sex}')
print(f'レベル\t{p.level}')
print(f'性格\t{p.nature}')
print(f'元の/現在の特性\t{p.org_ability} / {p.ability}')
print(f'アイテム\t{p.item}')
print(f'テラスタイプ\t{p.Ttype}')
print(f'種族値\t{p.base}')
print(f'個体値\t{p.indiv}')
print(f'努力値\t{p.effort}')
print(f'ステータス\t{p.status}')
print(f'HP\t{p.hp} ({p.hp_ratio*100}%)')
print(f'わざ\t{p.moves}')
print(f'PP\t{p.pp}')
print(f'能力ランク\t{Pokemon.status_label} = {p.rank}')
#print(f'状態変化\t{p.condition}')

print('-'*50 + '\nランクマッチの使用率\n' + '-'*50)
print(Pokemon.home[name]['nature'])
print(Pokemon.home[name]['ability'])
print(Pokemon.home[name]['item'])
print(Pokemon.home[name]['Ttype'])
print(Pokemon.home[name]['move'])

出力結果

--------------------------------------------------
インスタンス変数
--------------------------------------------------
名前    ガチグマ(アカツキ)
タイプ  ['じめん', 'ノーマル']
体重    333.0
性別    0
レベル  50
性格    ひかえめ
元の/現在の特性        しんがん / しんがん
アイテム
テラスタイプ    ノーマル
種族値  [113, 70, 120, 135, 65, 52]
個体値  [31, 31, 31, 31, 31, 31]
努力値  [0, 0, 0, 0, 0, 0]
ステータス      [188, 90, 140, 155, 85, 72]
HP      188 (100%)
わざ    ['ブラッドムーン', 'だいちのちから', 'しんくうは', 'ハイパーボイス']
PP      [8, 16, 48, 16]
能力ランク      ('H', 'A', 'B', 'C', 'D', 'S', '命中', '回避') = [0, 0, 0, 0, 0, 0, 0, 0]
--------------------------------------------------
ランクマッチの使用率
--------------------------------------------------
[['ひかえめ', 'ずぶとい', 'れいせい', 'おくびょう', 'おだやか', 'がんばりや', 'のんき', 'なまいき', 'わんぱく', 'いじっぱり'], [73.6, 10.8, 6.5, 3.9, 2.4, 2.1, 0.4, 0.1, 0.0, 0.0]]
[['しんがん'], [100.0]]
[['とつげきチョッキ', 'たべのこし', 'シルクのスカーフ', 'オボンのみ', 'きあいのタスキ', 'いのちのたま', 'ゴツゴツメット', 'アッキのみ', 'おんみつマント', 'のどスプレー'], [42.5, 15.6, 13.8, 10.5, 6.3, 2.1, 1.9, 1.8, 1.3, 0.7]]
[['ノーマル', 'どく', 'フェアリー', 'みず', 'ほのお', 'ゴースト', 'でんき', 'かくとう', 'くさ', 'ひこう'], [65.6, 15.8, 12.7, 2.1, 0.9, 0.9, 0.7, 0.6, 0.2, 0.1]]
[['ブラッドムーン', 'だいちのちから', 'しんくうは', 'ハイパーボイス', 'あくび', 'つきのひかり', 'めいそう', 'ムーンフォース', 'ちょうはつ', 'テラバースト'], [96.8, 92.2, 73.8, 55.3, 26.0, 25.6, 11.7, 9.7, 2.6, 2.4]]

名前のみを指定してポケモンを生成したにもかかわらず、それらしい技構成になっています。
これはコンストラクタの引数で use_template=True を指定すると、内部で apply_template() メソッドが呼ばれ、ランクマッチ使用率上位の特性や技がセットされていたためです。

デフォルトの型をカスタマイズしたい場合はオーバーライドするとよいでしょう。

"""ex3_view_pokemon.py"""
# Pokemonクラスを継承
class MyPokemon(Pokemon):
    def __init__(self, name: str = 'ピカチュウ', use_template: bool = True):
        super().__init__(name, use_template)
    
    def apply_template(self):
        """デフォルトの型を設定する"""
        if self.name in Pokemon.home:
            self.nature = Pokemon.home[self.name]['nature'][0][0]
            self.org_ability = Pokemon.home[self.name]['ability'][0][0]
            self.Ttype = Pokemon.home[self.name]['Ttype'][0][0]
            self.moves = Pokemon.home[self.name]['move'][0][:4]

# ライブラリの初期化
MyPokemon.init(season=None)
(略)

行動と方策関数

ポケモンの行動は整数のコマンドに基づいて制御されます。

コマンド 行動 入力するタイミング
0~9 n番目の技を選択 ターン開始時
10~19 ラスタルして(n-10)番目の技を選択 ターン開始時
20~25 (n-20)番目に選出したポケモンに交代 ターン開始・任意交代時*
30 または Battle.STRUGGLE わるあがき ターン開始時
40 または Battle.NO_COMMAND 命令できない ターン開始時
-1 または Battle.SKIP 行動スキップ ターン開始時 (通常発生しない)

任意交代とは、とんぼがえりや死に出しにより発生する、ターン途中の交代のことを指します。
シミュレータ上では技を4つに制限する必要がないため、10通りの技選択コマンドを用意しました。

proceed() メソッドによるターン経過の途中に、方策関数によって入力されるコマンドが決定されます。
方策関数には battle_command() と change_command() の2種類があり、それぞれターン開始時と任意交代時に呼ばれます。

"""pokepy/pokemon.py"""
class Battle:
    (中略)
    def battle_command(self, player: int) -> int:
        """{player}のターン開始時に呼ばれる方策関数"""
        return random.choice(self.available_commands(player))

    def change_command(self, player: int) -> int:
        """{player}の任意交代時に呼ばれる方策関数"""
        return random.choice(self.available_commands(player, phase='change'))

デフォルトでは、現在選択可能なコマンドの一覧を available_commands() メソッドで取得し、その中からランダムに選んでいます。
そのため、最初の1on1や3on3の例では、ポケモンはランダムに行動していました。

battle_command()メソッドが呼ばれるタイミングのイメージ

change_command()メソッドが呼ばれるタイミングのイメージ

先読み

シンプルな先読みの例として「選択可能なコマンドをすべて実行してみて、一番マシな盤面になるコマンドを選ぶ」ような方策関数を実装してみます。

"""ex4_bruteforce_1on1.py"""
from pokepy.pokemon import *

# Battleクラスを継承
class MyBattle(Battle):
    def __init__(self):
        super().__init__()

    def battle_command(self, player):
        """{player}のターン開始時に呼ばれる方策関数"""

        # プレイヤー視点の仮想盤面を生成
        blinded = self.clone(player)

        # 両プレイヤーの選択可能なコマンドの一覧を取得
        # 自分: player (= 0 or 1), 相手: not player (= 1 or 0)
        available_commands_list = [
            blinded.available_commands(pl) for pl in [player, not player]
        ]

        scores = []

        # 自分のコマンドのループ
        for c0 in available_commands_list[0]:
            _scores = []

            # 相手のコマンドのループ
            for c1 in available_commands_list[1]:
                
                # コマンドごとに仮想盤面を複製
                battle = deepcopy(blinded)

                # コマンドを指定して仮想盤面のターンを進める
                battle.proceed(commands=([c0, c1] if player == 0 else [c1, c0]))

                # 行動が有効なら盤面の評価値を計算し、無効なら0を記録する
                if battle.was_valid[player]:
                    _scores.append(battle.score(player))
                else:
                    _scores.append(0)

            # 相手のとりうる行動に対して最低スコアを記録
            scores.append(min(_scores))            

        # スコアが最も高いコマンドを選ぶ
        return available_commands_list[0][scores.index(max(scores))]
        
    def score(self, player: int) -> float:
        """盤面の評価値を返す"""
        # 例: TODスコアの比
        return (self.TOD_score(player) + 1e-3) / (self.TOD_score(not player) + 1e-3)


# ライブラリの初期化
Pokemon.init()

# Battleクラスのインスタンスを生成
battle = MyBattle()
(略)

方策関数が呼ばれている間、コマンド入力待ちのためターン処理は中断されています。
clone() メソッドで現在の盤面を複製し、得られた仮想盤面にコマンドを渡してターンを再開します。
その後、仮想盤面の終状態の評価値に基づいてコマンドを評価しています。
盤面の複製については次節で詳しく見ていきます。

score() メソッドで利用している TOD_score とは、内部的に試合の勝敗判定に用いられる評価値で、次のように定義されます。

ポケモンが全滅するとTOD_scoreは0になり、判定勝負ではTODスコアの大きいプレイヤーが勝利します。

交代の方策関数 change_command() でも同様の探索を行うことができます。

"""ex5_bruteforce_3on3.py"""
from pokepy.pokemon import *

# Battleクラスを継承
class MyBattle(Battle):
    def __init__(self):
        super().__init__()

    def battle_command(self, player: int) -> int:
        """{player}のターン開始時に呼ばれる方策関数"""
        return self.available_commands(player)[0]
    
    def change_command(self, player: int) -> int:
        """{player}の任意交代時に呼ばれる方策関数"""

        # 選択可能なコマンドの一覧を取得
        available_commands = self.available_commands(player, phase='change')

        print('\t'+'-'*30 + ' 交代の方策関数 ' + '-'*30)
        print('\tここまでの展開')
        for pl in self.action_order:
            print(f'\t\tPlayer {pl} {self.log[pl]}')

        scores = []

        # 自分のコマンドのループ
        for cmd in available_commands:
            # コマンドごとに仮想盤面を生成
            battle = self.clone(player)

            # コマンドを指定して、交代の直前から仮想盤面を再開し、ターンの終わりまで進める
            battle.proceed(change_commands=[cmd, None] if player == 0 else [None, cmd])

            print(f'\tコマンド{cmd}を指定して仮想盤面を再開')
            print(f'\t\tPlayer {player} {battle.log[pl]}')

            # 交代後、さらにターンを進めることも可能
            #battle.proceed()
            
            # 盤面の評価値を記録
            scores.append(battle.score(player))

        print('\t'+'-'*76)

        # スコアが最も高いコマンドを返す
        return available_commands[scores.index(max(scores))]

    def score(self, player: int) -> float:
        """盤面の評価値を返す"""
        # 例: TODスコアの比
        return (self.TOD_score(player) + 1e-3) / (self.TOD_score(not player) + 1e-3)


# ライブラリの初期化
Pokemon.init()

# Battleクラスのインスタンスを生成
battle = MyBattle()

# ポケモンを生成して選出に追加
battle.selected[0].append(Pokemon('ママンボウ'))
battle.selected[0][-1].moves = ['クイックターン']
battle.selected[0].append(Pokemon('オーロンゲ'))
battle.selected[0].append(Pokemon('グライオン'))

battle.selected[1].append(Pokemon('カイリュー'))
battle.selected[1].append(Pokemon('ガチグマ(アカツキ)'))
battle.selected[1].append(Pokemon('サーフゴー'))

# 勝敗が決まるまで繰り返す
while battle.winner() is None:
    # ターン経過
    battle.proceed()
    (略)

出力結果

ターン0
player=0 ['交代 -> ママンボウ'] []
player=1 ['交代 -> カイリュー'] []
        ------------------------------ 交代の方策関数 ------------------------------
        ここまでの展開
                Player 1 ['カイリュー', 'HP 166/166', 'コマンド 0', '先手', 'じしん PP 15', 'ダメージ 69', '相手HP 171', 'じしん 成功', 'HP -7']
                Player 0 ['ママンボウ', 'HP 240/240', 'コマンド 0', '後手', 'HP -69', 'クイックターン PP 31', 'ダメージ 7', '相手HP 159', 'クイックターン 成功']
        コマンド21を指定して仮想盤面を再開
                Player 0 ['ママンボウ', 'HP 240/240', 'コマンド 0', '後手', 'HP -69', 'クイックターン PP 31', 'ダメージ 7', '相手HP 159', 'クイックターン 成功', '交代 -> オーロンゲ']
        コマンド22を指定して仮想盤面を再開
                Player 0 ['ママンボウ', 'HP 240/240', 'コマンド 0', '後手', 'HP -69', 'クイックターン PP 31', 'ダメージ 7', '相手HP 159', 'クイックターン 成功', '交代 -> グライオン']
        ----------------------------------------------------------------------------

ターン1
player=1 ['カイリュー', 'HP 166/166', 'コマンド 0', '先手', 'じしん PP 15', 'ダメージ 69', '相手HP 171', 'じしん 成功', 'HP -7'] []
player=0 ['ママンボウ', 'HP 240/240', 'コマンド 0', '後手', 'HP -69', 'クイックターン PP 31', 'ダメージ 7', '相手HP 159', 'クイックターン 成功', '交代 -> オーロンゲ'] ['マルチスケイル x0.50']

ママンボウクイックターンを使用した後、方策関数内で仮想的にオーロンゲとグライオンの両方への交代を試しています。

上の例で見たように、任意交代による中断状態でコマンドを指定して proceed() メソッドを実行すると、交代直前から処理が再開され、ターンの終わり (or 次の交代) まで進みます。この状態はもとの盤面を複製しても持続するため、上記のような方策関数を実装できます。

不完全情報の表現

clone() メソッドはその盤面のdeepcopyを返しますが、引数のplayerを指定すると、そのプレイヤーの視点に相当するように相手の情報を隠蔽します。

相手ポケモンの隠蔽

selectedに格納されたポケモンの情報はそのプレイヤーしか知り得ない、いわば真値です。
proceed() メソッドでターンを進めていくと、場に出たポケモンや試合中に発動した技・アイテムなど、実際にゲーム画面から観測されるような情報はobservedに蓄積されます。
clone() メソッドによるポケモンの隠蔽では、相手のselectedの参照先をobservedのコピーに置き換えます。

このとき、相手の技が一つも観測されていないと後々エラーになる可能性があります。対策として、置き換え後に complement_pokemon() メソッドにより相手ポケモンの情報を補完します。

"""pokepy/pokemon.py"""
class Battle:
    (中略)
    def complement_pokemon(self, pokemon):
        """ポケモンの情報を補完する"""
        # 技の補完
        if not pokemon.moves:
            if pokemon.name in Pokemon.home:
                pokemon.add_move(Pokemon.home[pokemon.name]['move'][0][0])
            else:
                pokemon.add_move('テラバースト')

相手の行動の隠蔽

ターン処理の途中で clone() メソッドを実行すると、相手の行動が隠蔽される場合があります。

  • 先手で任意交代するとき、後手の相手の技選択を complement_move() メソッドで書き換える
  • 相手の場のポケモンが瀕死になったとき、相手の交代コマンドを complement_change_command() メソッドで補完する
"""pokepy/pokemon.py"""
class Battle:
    (中略)
    def complement_move(self, player: int) -> str:
        """相手の行動が開示されていない場合に呼ばれ、{player}が選択した技を返す"""
        available_moves = []
        for cmd in self.available_commands(player):
            if cmd >= 10:
                break
            available_moves.append(self.pokemon[player].moves[cmd])
        return random.choice(available_moves)

    def complement_change_command(self, player: int) -> int:
        """相手の行動が開示されていない場合に呼ばれ、{player}が選択した交代コマンドを返す"""
        return 20 + random.choice(self.changeable_indexes(player))

ログの読み書きと乱数

シミュレーションのログを出力するには、試合終了後に dump() メソッドを使用します。

"""ex2_random_3on3.py"""
(略)
# 勝敗が決まるまで繰り返す
while battle.winner() is None:
    # ターン経過
    battle.proceed()

# ログファイルに書き出す
with open('log/random_3on3.json', 'w', encoding='utf-8') as fout:
    fout.write(battle.dump())

log/random_3on3.json

{
  "seed": 1727528188,
  "0": [
    {Player0が選出したポケモンの情報 (割愛)},...,{}
  ],
  "1": [
    {Player1が選出したポケモンの情報 (割愛)},...,{}
  ],
  "Turn-1": {
    "command": [null, null],
    "change_command_history": [
      [],
      []
    ]
  },
  "Turn0": {
...
}

次の3つの情報があれば試合を復元することができます。

  1. 選出したポケモン
  2. 乱数のシード
  3. ターンごとの入力コマンド

出力したjsonファイルを読み込んで試合を再現してみます。

"""ex6_replay_sim.py"""
from pokepy.pokemon import *

# ライブラリの初期化
Pokemon.init()

with open('log/random_3on3.json', encoding='utf-8') as fin:
    log = json.load(fin)
    
    # シードを指定してBattleインスタンスを生成
    battle = Battle(seed=log['seed'])

    # ポケモンを復元
    for player in range(2):
        for p in log[str(player)]:
            battle.selected[player].append(Pokemon())
            battle.selected[player][-1].__dict__ |= p
            battle.selected[player][-1].show()

    # コマンドに従ってターンを進める
    while (key := f'Turn{battle.turn}') in dict:
        # あらかじめ交代コマンドの履歴をセットしておく
        battle.reserved_change_commands = log[key]['change_command_history']

        # コマンドを指定してターンを進める
        battle.proceed(commands=log[key]['command'])

        # 行動した順にログ表示
        for player in battle.action_order:
            print(f'{player=}', battle.log[player], battle.damage_log[player])

シードが与えられたときにターン処理が一意に定まるよう、Battleクラスは乱数生成器(Battle._random)をメンバ変数に持ちます。ターン処理中に発生するダメージや命中率、追加効果などの確率事象の計算には必ずこの乱数生成器を使用します。一方で、方策関数内での探索といった盤面に干渉しない計算では、メンバ変数以外の乱数生成器 (普通の random とか) を使用してください。

致死率計算

一般的にダメージ計算と呼ばれる機能です。 対戦シミュレーションでは様々な乱数が作用しますが、致死率計算では乱数を排除し、すべてのダメージの組み合わせを網羅的に計算します。

"""ex7_lethal.py"""
from pokepy.pokemon import *

# ライブラリの初期化
Pokemon.init()

# ポケモンを生成
p1 = Pokemon('カイリュー', use_template=False)
#p1.nature = 'いじっぱり'
#p1.ability = ''
#p1.item = 'いのちのたま'
#p1.Ttype, p1.terastal = 'ステラ', True
#p1.rank = [0, 0, 0, 0, 0, 0]
#p1.ailment = 'BRN'
p1.show()

p2 = Pokemon('ガチグマ(アカツキ)', use_template=False)
#p2.nature = 'ずぶとい'
#p2.ability = ''
#p2.item = 'オボンのみ'
#p2.Ttype, p2.terastal = 'フェアリー', True
#p2.rank = [0, 0, 0, 0, 0, 0]
#p2.ailment = 'PSN'
#p2.condition['shiozuke'] = 1
p2.show()

# 攻撃側のプレイヤー
player = 0

# 攻撃技
move_list = ['スケイルショット']
#move_list = ['スケイルショット','じしん'] # 複数なら加算ダメ計
print(move_list)

n_hit = 5 # 連続技のヒット数

# Battleインスタンスを生成
battle = Battle()
battle.pokemon = [p1, p2]

# 盤面の状況を設定
#battle.condition['sandstorm'] = 1
#battle.condition['glassfield'] = 1
#battle.condition['reflector'] = [1, 1]

# 致死率計算
print(battle.lethal(move_list=move_list, player=player, n_hit=n_hit))

# ダメージ計算の詳細を表示
print(battle.damage_log[player])

出力結果

['スケイルショット']
80~105 (42.6~55.9%) 乱2(20.84%)
[]

致死率計算では防御側のHPを {'hp': 場合の数} のように辞書型で管理します。ダメージが累積したときの場合の数は個々の場合の数の積で求まるため、すべての分岐をリストで扱うよりも計算コストを大幅に削減できます。
さらに、

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

のようにキーを加工してアイテムの有無を追跡することで、きのみの発動条件などを考慮して致死率を計算します。

カスタマイズ

以下のメソッドはオーバーライドして使うことを想定しています。

class Battle:
    def battle_command(self, player: int) -> int:
        # 方策関数

    def change_command(self, player: int) -> int:
        # 方策関数

    def choose_damage(self, player: int, damage_list: list[int]) -> int:
        # 乱数で分岐したダメージからひとつ選択する

    def hit_probability(self, player: int, move: str) -> int:
        # 命中率を返す

    def critical_probability(self, player: int, move: str) -> float:
        # 急所率を返す

    def complement_pokemon(self, pokemon) -> None:
        # Clone()したときに相手ポケモンの情報を補完する

    def complement_move(self, player: int) -> str:
        # Clone()したときに相手の技選択を補完する

    def complement_change_command(self, player: int) -> int:
        # Clone()したときに相手の交代先を補完する

    def estimate_attack(self, player: int, name: str, status_index: int, recursive: bool=True) -> bool:
        # これまでに発生したダメージから、相手のA/C実数値と補正アイテムを推定する

    def estimate_defence(self, player: int, name: str, status_index: int, recursive: bool=True) -> bool:
        # これまでに発生したダメージから、相手のH/B/D実数値と補正アイテムを推定する

最後の相手の攻守を推定するメソッドは、方策関数の冒頭などで使うとよいでしょう。

後編へ

hfps4469.hatenablog.com

【ポケモンSV】ポケモンHOME APIでランクマッチの使用率を取得してみる

スクリプト

github.com

はじめに

ポケモンSVのランクマッチ用に、自動ダメージ計算サイトを作りました。 https://pbasv.cloudfree.jp/pbasv.cloudfree.jp 別の記事で紹介していますが、このサイトはポケモンHOMEが提供するランクマッチの統計データがないと機能しません。
ですが、株式会社ポケモンの利用規約を読むと、

5.ユーザーの権利および制約について
本規約は、お客様個人に対し、本サービスのコンテンツを個人的かつ非商業的な形で、お客様がご自宅において利用することに限り許諾するものです。お客様は、如何なる場合であっても、次のような行為をしてはならないものとします。
(中略)
(v) 如何なる理由であれ、大量のコンテンツをデータベースにダウンロードすること

とあり、上記の自動ダメ計サイトや、ポケモン対戦をサポートするサードパーティツールの多くは(この記事含め)規約に則っているとは言いがたいです。
ポケモンホームの情報等を利用したツールを作成する際は、少なくとも商用利用にならないよう気を付けたいと思います。

ポケモンHOMEの統計データを取得する

やり方は剣盾時代に確立されているはずなので、とりあえずググってみる。
1. 剣盾のランクマ情報の取得方法を説明している記事 okuokuch.hatenablog.com 2. SV用のURLも紹介している記事 https://xn--lcss68alkav93n.com/blog/ktts/340/%E6%9C%AA%E5%88%86%E9%A1%9E/%E3%83%9D%E3%82%B1%E3%83%A2%E3%83%B3%E3%83%9B%E3%83%BC%E3%83%A0-api-%E6%8E%A5%E7%B6%9A%E3%83%A1%E3%83%A2/xn--lcss68alkav93n.com 要点としては、まず全シーズンの情報を取得する。

headers = {
    'accept': 'application/json, text/javascript, */*; q=0.01',
    'countrycode': '304',
    'authorization': 'Bearer',
    'langcode': '1',
    'user-agent': 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Mobile Safari/537.36',
    'content-type': 'application/json',
}
data = '{"soft":"Sw"}'
#url = 'https://api.battle.pokemon-home.com/cbd/competition/rankmatch/list' #剣盾
url = 'https://api.battle.pokemon-home.com/tt/cbd/competition/rankmatch/list' #SV
response = requests.post(url, headers=headers, data=data)

次に、取得した全シーズンの情報から特定のシーズンのデータが保存されているパスのID(id,rst,ts1,ts2)を得る。

terms = []
for season in data: #このdataは先ほどのresponse.text()を辞書型に格納したもの
    for id in data[season]:
        if data[season][id]['rule'] == 0: #0:シングル, 1:ダブル
            terms.append({'id': id, 'rst': data[season][id]['rst'], 'ts1': data[season][id]['ts1'], 'ts2': data[season][id]['ts2']})

term = terms[0] # 0:最新, 1:前シーズン, 2:前々シーズン, ...
id = str(term['id'])
rst = str(term['rst'])
ts1 = str(term['ts1'])
ts2 = str(term['ts2'])

これをもとに、ポケモン使用率ランキングや、

#url = 'https://resource.pokemon-home.com/battledata/ranking/'+id+'/'+rst+'/'+ts2+'/pokemon' # 剣盾
url = 'https://resource.pokemon-home.com/battledata/ranking/scvi/'+id+'/'+rst+'/'+ts2+'/pokemon' # SV
response = requests.get(url, headers=headers)

ポケモンの技や持ち物の採用率といった個別データを取得する。

for x in range(1,7):
    #url = 'https://resource.pokemon-home.com/battledata/ranking/'+id+'/'+rst+'/'+ts2+'/pdetail-'+str(x) # 剣盾
    url = 'https://resource.pokemon-home.com/battledata/ranking/scvi/'+id+'/'+rst+'/'+ts2+'/pdetail-'+str(x) # SV
    response = requests.get(url, headers=headers)

取得したデータを確認すると、

使用率ランキング
ポケモン個別データ
のように整数でエンコードされており、どの数字がどの名前に対応しているのか把握する必要がある。

マッピングデータを探す

jsonファイルを取得したURLをブラウザで検索していると、剣盾のポケモンホームのページ?が出てきた。
resource.pokemon-home.com

このページのhtmlの31行目に、

appjs.src = './js/bundle.js?v='+scriptVer;  

とあり、bundle.jsというJavascriptファイルを読み込んでいる。

https://resource.pokemon-home.com/battledata/js/bundle.js
bundle.jsには全国図鑑・タイプ番号・技番号などが直書きされている。
先ほどAPIで取得したjsonファイルは、このbundle.jsと照らし合わせて読み取ることができる。

...と思いきや、アイテムの対応コードが書かれていない。
bundle.jsをもう少し読むと、

getItemnamejson(e,t){this.getjson(e,t,"./json/itemname",{itemname:{}})}  

という関数が定義されており、アイテム名を取得するためにさらに外部ファイルを参照しているようだ。

目的のjsonファイル名がよくわからなかったので、HTTPリクエスト追跡ツールで探してみた。 rakko.tools

上記のサイトに例えばザシアンの詳細ページのURLを入力すると、HTTPリクエスト一覧に
https://resource.pokemon-home.com/battledata/json/itemname_ja.json?v=2861801
と出力され、アイテム番号が itemname_ja.json に格納されていることがわかる。

bundle.js内で参照しているjsonファイルは全部で5つ。