ダメージ計算もイベント駆動で実装したらスッキリした話

目次

はじめに

先日 jpoke というシングル対戦のシミュレータを公開しました。jpoke ではターン進行や効果の連鎖を表現するためにイベント駆動というアーキテクチャを採用しています。

ところで、jpoke では致死率、すなわち『相手を技で倒すのに何発必要で、何%で倒せるか』を計算する補助機能も提供しています。致死率を正確に計算するには、すべてのダメージとHPの分岐を網羅する必要があります。一方で、対戦シミュレータは乱数で選ばれた単一状態を追い続けるため、そもそも致死率計算とは相性が良くありません。

致死率計算を実装するにあたり、対戦シミュレータからはダメージ計算式だけを拝借し、分布計算などのパーツはすべて外付けすることにしました。この外付けコードの実装や気づいたことについて紹介したいと思います。

致死率計算 = 畳み込み

ダメージ乱数の16分岐をそのまま配列にして計算してしまうと、n回攻撃したときの計算量が 16n に膨れ上がります。そこで、1本の配列を『状態』と『場合の数』に分離すると、計算量を n_hp * n_damage まで抑えることができます。

具体例として、無振りガブが無振りガブにじしんを撃った場合 (確3)、ダメージは75,76,78,79,81,82,84,85,87,88の10通りなので、以下のような計算になります。

1回目の攻撃
計算量: n_hp(1) * n_damage(10) = 10
攻撃後のHP: 95,96,98,99,101,102,104,105,107,108 (10通り)

2回目の攻撃
計算量: n_hp(10) * n_damage(10) = 100
攻撃後のHP: 7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33 (27通り)

3回目の攻撃
計算量: n_hp(27) * n_damage(10) = 270
攻撃後のHP: 0 (1通り)

ほとんどのケースでは同じ HP に到達する組が大量に発生するので、HP分布は計算量よりも小さくなります。 3回目の攻撃での計算量は 270 で、配列のまま計算した場合 (163=4096) の 1/10 以下で済みました。 致死率を計算するには HP とその場合の数をセットで計算する必要があるので、{hp: freq}, {damage: damage_freq} のような辞書で表現すれば、{hp_after: freq_after} = {hp_before-damage: hp_freq*damage_freq} のように計算できます。

HPだけでは状態を表現しきれない

防御側がオボンの実を持っている場合を考えます。

分布を {hp: freq} で表現しただけでは、オボンの実をまだ消費していないのか、消費した結果その HP まで回復したのか区別できません。区別するにはキーに道具の有効状態を記録する必要があります。そこで、キーを不変 (frozen) データクラスに拡張しました。

@dataclass(frozen=True)
class State:
    value: int
    item_enabled: bool = True

    def __add__(self, other: State) -> State:
        return State(
            value=self.value + other.value,
            item_enabled=self.item_enabled and other.item_enabled,
        )

アイテムを消費した枝と未消費の枝は、同じHPでも別のキーとして扱われます。また __add__ で加算を定義することで、HPの畳み込みと item_enabled の AND 合成を自然に統合できました。

同様に、特性ばけのかわの有効判定も ability_enabled フラグを導入すれば解決できます。

分布計算の途中に効果を差し込む

今度は防御側がたべのこしを持っている場合を考えます。たべのこしは毎ターン終了時に発動するので、ダメージ計算のあとに if で効果を差し込めば正しく計算できます。

hp_dist = {State(value=defender.hp): 1}

for i in range(n_attack):
    hp_dist = hp_dist - damage_dist
    # 効果処理
    if defender.item == "たべのこし":
        hp_dist = hp_dist + defender.max_hp // 16

しかし、致死率計算に影響する要素(アイテム・状態異常・天候など)が増えるたびに if を継ぎ足していくとループがどんどん肥大化していきます。

そこで、ループ進行と効果処理を分離することにしました。まず、ループ側は「ダメージを適用してイベントを発火する」ことだけを担当します。

hp_dist = {State(value=defender.hp): 1}

for i in range(n_attack):
    hp_dist = hp_dist - damage_dist
    # イベントを発火
    hp_dist = emit(Event.ON_TURN_END, ctx, hp_dist)

効果側では、イベントと効果処理を紐づけて登録しておきます。

def heal_1_16(ctx, hp_dist):
    return hp_dist + ctx.defender.max_hp // 16

handlers = {
    "たべのこし": {
        # イベント → ハンドラの辞書
        Event.ON_TURN_END: heal_1_16
    }
}

こうしておけば、対応する効果が増えてもループ本体は汚染されません。新しい特性やアイテムに対応するときは handlers に1行足すだけで済みます。ここでは紹介していませんが、実際にはイベントを発火してからハンドラが呼ばれるまでの間に、呼ぶべきハンドラをかき集める処理が必要です。このようにイベントとハンドラで制御する構造をイベント駆動と呼びます。jpoke 本体の対戦シミュレータも基本的に同じ仕組みです。

まとめ

ここまで読んでいただきありがとうございました。簡単ですが jpoke の致死率計算の実装について紹介しました。この文章がダメージ計算ツールを開発されている方の役に立てば幸いです。

jpoke は GitHub で公開しています。興味を持っていただけたらぜひ覗いてみてください。

https://github.com/tmwork1/jpoke

【PokeResearch】初めての AI 丸投げ開発を振り返る

はじめに

ポケモンのプログラミングに関する記事や論文を収集・整理する情報サイト「PokeResearch」を、Claude Code に実装を丸投げする形で構築しました。

https://poke-research.com/

この記事ではその開発プロセスを振り返りたいと思います。なお、この原稿も開発中の進捗ログや git log を Claude Code に整理させたうえで作成しています。記録さえ残しておけば、こういうときにも便利ですね。

開発のきっかけ

X の TL で「ポケモン個人開発界隈の知識が蓄積されていない」といった内容のポストを見て、開発記事のポータル化を考えました。一応私としては、個人開発は自分が作って楽しむことが第一なので新規性がなくてもええやんと思いつつ、一方で記事が使い捨てされていくのはもったいないとも思います。

PokeResearch の概要・仕組み

PokeResearch は、ポケモンのプログラミングに関する技術情報を収集・整理する情報ハブです。ポケモン、ポケカ、ゲームAIなどに関連する記事・論文を以下のソースから自動収集します。

ソース
記事 Qiita, Zennm note, はてなブックマーク, Web 検索 (Brave Search)
論文 arXiv, OpenAlex

収集した記事や論文は AI が 3 行程度に要約してタグを自動付与します。トップページでは新着・人気記事を閲覧でき、タグや技術カテゴリで絞り込んで検索することができます。

開発の構成

技術・サービス
フロントエンド Astro + TypeScript
ホスティング Cloudflare Workers(Pages)
データベース Supabase(PostgreSQL)
CI/CD GitHub Actions
要約・タグ生成 OpenAI API gpt-5-nano

記事の収集と AI 処理は、Cloudflare Workers の cron 機能によって定期実行するサーバーレス構成です。

運用コスト

  • OpenAI API(記事の要約・タグ生成・採否判定): 数ドル/月
  • Brave Search API(個人ブログの発見に使用): $5/月
  • Cloudflare・Supabase: 無料枠の範囲内で運用中

個人開発とはいえ、さすがに完全無料とはいきませんでした。

リポジトリの再利用性

コードは GitHub で公開しています。

https://github.com/tmwork1/poke-research

このリポジトリは、他のトピックにも簡単に転用できる設計になっています。サイト名・収集キーワード・AI レビューの判定基準といった「ポケモン固有の設定」はひとつの設定ファイルに集約してあり、この中身を書き換えるだけで、収集ソース・AI 要約・検索・UI といった仕組みはそのまま、別ジャンルの情報ハブに転用できます。

実際の開発の流れ

初期構想から約1週間で一通り進めました。作業記録から、日ごとのスケジュールを整理すると以下のようになります。

日付 主な作業
7/4 初期構想の整理, 要件・データモデルの検討
7/5 試作版の土台実装 - 検索, UI 設計, ログイン認証
7/6 UI 改善, SEO 対応, 信頼性強化, 記事収集の品質対策
7/7 タグ最適化, DB 重複レビューの cron 化, UI 改善
7/8 コード整理, 収集の取りこぼし対策, RSS フィード追従
7/9 AI 判定精度改善, arXiv 論文の追加, cron 上限問題への対応
7/10 偽陽性記事の非公開化, OpenAlex 論文収集の追加

前半は不慣れなのもあって基盤構築に費やし、後半で収集ソースの拡張と品質改善を並行して行いました。 開発の役割分担としては、要件・仕様・データモデル・実装指示などの設計判断は自分が行い、実際のコーディングやバグ修正はすべて Claude Code が実施しました。

起きた課題と解決方法

AI に実装を任せているとはいえ、運用を開始してから様々な問題に遭遇しました。印象に残っているものをいくつか紹介します。

1. Cloudflare の cron 上限超過による収集のサイレント失敗

記事・論文を毎日自動で収集するために、Cloudflare Workers の cron 機能を使っています。日次で動く収集ジョブが複数あるほか、DB重複チェックのように週次で動くジョブもあり、あわせるとそれなりの数になります。ところが Cloudflare アカウント全体の cron trigger 登録数には5件の上限があり、ジョブごとに個別の cron を用意することができません。そこで複数のジョブを1つの Worker 呼び出しにまとめ、1つの cron trigger で順番に実行する設計にしていました。

この設計が、Cloudflare Workers の「1回の呼び出しあたりの外部通信(subrequest)回数の上限50件」と衝突しました。複数の収集ジョブを1回の呼び出しに集約していたため上限に到達し、しかも失敗記録自体も subrequest であるため書き込めず、外部からは「何も実行されていない」ように見える状態になっていました。

原因特定後は、既に収集済みの記事をスキップする差分検知を全ジョブへ徹底したうえで、1つの cron trigger の発火時刻を5分刻みで複数用意し、発火時刻ごとに実行するジョブを1つだけ選ぶ「時間分割」方式に変更しました。cron trigger の登録数を増やさずに、実質的にジョブごとの呼び出しを分離できます。

2. ブログ検索への攻略サイト・アフィリエイトサイトの混入

個人ブログを発見するための検索で、攻略 wiki や比較サイトなどポケモンと直接関係のないページが記事として混入することがありました。AI レビューによる除外を期待していましたが、実際には AI レビューのみでは防ぎきれない事例があり、該当ドメインを機械的に除外するリストと、AI への指示文の両方による多層防御で対処しました。

3. AI 判定の矛盾と、一度採用した記事が見直されない設計

AI が記事の採否を判定するとき、却下理由をコメントに記述しつつも結果は「採用」になっている、という矛盾が発生することがありました。加えて、一度採用と判定された記事は仕組み上自動では再判定されない設計だったため、古い判定基準のまま公開が続くケースもありました。

矛盾については、却下理由に使われる定型文言が含まれているのに採用判定になっている場合、結果を強制的に「却下」へ倒す後処理チェックを判定ロジックに追加しました。すでに誤って公開された記事は、確認が取れたものから個別に非公開にしています。

今後の開発について

開発自体はひと段落したので、今後は収集ソースの追加や AI 判定の精度向上を中心に、継続的に改善していく予定です。ご意見・ご要望があれば、ぜひお寄せください。

最後に

AI に実装を丸投げする開発は、想像していたよりもかなりスムーズに進みました。一方で、インフラの制約や AI 判定の癖など、実際に運用してみて初めて露見した問題も多く、「任せて終わり」ではなく人間側のチェックや設計判断はしばらく必要かなと感じました。

PokeResearch に興味を持っていただけたら、ぜひサイトも覗いてみてください。

https://poke-research.com/

モバイル版ポケモンチャンピオンズの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. たまに止まる

...

今後の展望

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