| name | nash |
| description | ナッシュ均衡ソルバー。零和対戦 (ポケモン選出、構築マッチアップ、じゃんけん型メタ) の最適混合戦略、選出最適化 (pkdx select)、メタ乖離分析 (pkdx meta-divergence) を提供。「最適選出」「選出分布」「構築メタ」「対面ジャンケン」「使用率 vs 最適」等の質問時に使用。 |
| allowed-tools | Bash, Read, AskUserQuestion |
Nash Equilibrium Skill
零和 2 人ゲームの混合戦略ナッシュ均衡ソルバーと、その上に構築された選出最適化・メタ乖離分析。
パス定義
SKILL_DIR=(このSKILL.mdが置かれたディレクトリ)
REPO_ROOT=$SKILL_DIR/../../../..
PKDX=$REPO_ROOT/bin/pkdx
VIZ=$REPO_ROOT/scripts/select_grid_viz.sh # pkdx select の進捗可視化
$VIZ は pkdx select --progress=json の stderr (JSON Lines) を受け取り、stdout が TTY なら 60×60 グリッドを live 描画、TTY 以外では phase 境界 + dp ノードサマリの数行ログに自動切替する。pkdx select を呼ぶときは原則として 2> >($VIZ) を付ける。Phase 2b では実行主体 (エージェント / ユーザー手動) を AskUserQuestion で選ばせる。
用語
| 用語 | 意味 |
|---|
| value | 行プレイヤーから見たゲーム値 (期待利得) |
| exploitability | 現在の戦略 σ に対する最良応答で得られる追加利得。零和で 0 なら σ は Nash 均衡 |
| support | 確率 > 0 の純戦略 index 集合 |
| TeamPayoffModel | 利得の作り方 (switching_game / screened_switching_game:<trials>:<seed>:<keep_top>)。turn_limit 既定は MC=5 / DP=5 (switching_game:<N> で個別上書き可) |
| BattleFormat | single = lead-aware 3 体選出 (60x60、6 × C(5,2) = 60 通りで先頭 (lead) を含む順序付きタプル) のみ対応 (double は現状未サポート) |
詳細はまず references/ を参照:
references/theory.md — 零和 LP / Simplex / Fictitious play / MWU の数式と根拠
references/exploitability.md — exploitability / NashConv / KL / L1 の定義と使い分け
references/payoff_semantics.md — SwitchingGame / ScreenedSwitchingGame の仕様と選択基準
Phase 0: 初期化
0-1: pkdx 存在確認
$PKDX nash --help >/dev/null 2>&1 && echo "OK" || echo "NOT_FOUND"
NOT_FOUND の場合は以下を案内してスキルを終了:
pkdx CLI が見つかりません。リポジトリルートで以下を実行:
./setup.sh
moon build --target native src/main
Phase 1: タスク選択 (AskUserQuestion)
| # | 質問 | header | オプション |
|---|
| 1 | 何を計算しますか? | 計算種別 | 既知の行列を解く (nash solve), 選出最適 (select), メタ乖離分析 (meta-divergence), 構築マッチアップ DOT グラフ (nash graph) |
選択に応じて Phase 2 以降に分岐。
Phase 2a: pkdx nash solve — 行列 / monocycle characters を解く
入力形式の決定
| # | 質問 | header | オプション |
|---|
| 1 | 入力の種類は? | 入力形式 | 行列を直接入力 (matrix), monocycle character (p, v) リスト (characters) |
matrix 形式: n×n の実数行列。対称・反対称を問わない。零和にしたい場合は A[j,i] = -A[i,j] を自前で設定するか、monocycle 形式を使う。
characters 形式: 各キャラに label (名前)、power (スカラー p)、v (2D ベクトル {x, y})。利得は A[i,j] = (pᵢ − pⱼ) + vᵢ × vⱼ で自動生成。
実行
cat <<'JSON' | $PKDX nash solve
{
"matrix": [[0, 1, -1], [-1, 0, 1], [1, -1, 0]],
"labels": ["R", "P", "S"]
}
JSON
または:
cat <<'JSON' | $PKDX nash solve
{
"characters": [
{"label": "A", "power": 0, "v": {"x": 2, "y": 0}},
{"label": "B", "power": 0, "v": {"x": -1, "y": 1.7}},
{"label": "C", "power": 0, "v": {"x": -1, "y": -1.7}}
]
}
JSON
出力 JSON:
{
"value": 0,
"row_strategy": [0.333, 0.333, 0.333],
"col_strategy": [0.333, 0.333, 0.333],
"exploitability": 0,
"support": {"row": [0, 1, 2], "col": [0, 1, 2]},
"labels": ["R", "P", "S"]
}
結果整形
## Nash 均衡結果
- **ゲーム値**: {value}
- **exploitability**: {exploitability} (< 1e-6 なら厳密解とみなしてよい)
### 行プレイヤーの混合戦略
| index | label | 確率 |
|---|---|---|
| 0 | {labels[0]} | {row_strategy[0]:.3f} |
...
### 列プレイヤーの混合戦略
(同上)
### support
- 行: {labels of support.row}
- 列: {labels of support.col}
Phase 2b: pkdx select — 選出最適化
入力の収集
team (6 体), opponent (6 体), format (single のみ対応), team_payoff_model を取得する。
データソース
box/teams/*.meta.json (推奨) — team-builder Phase 8 または Champions スクショ取り込みで生成される。.meta.json の members 配列がそのまま combatant として使える (types[] + base_stats{} 形式を pkdx select が直接受け付ける)。
- ユーザー直接入力 — 上記がない場合、ポケモン名・ステータス・技を対話で収集する。
.meta.json からの読み込み手順
ls box/teams/*.meta.json
重要: .meta.json の members にはステータスが種族値 (base_stats) の場合と実数値 (hp/atk/...) の場合がある。Champions スクショ取り込み経由なら実数値が揃っているが、skill 手順で作成した場合は種族値のみの可能性がある。足りないデータ (実数値、priority、stat_effects 等) がある場合はユーザーに補完を促す。
モデル選択肢 (team_payoff_model フィールド)
"switching_game" (既定) — 交代込み extensive-form ゲーム木。DP turn_limit=5 既定 (先制技 / ランク補正技に対応)。長期戦評価が必要なら "switching_game:<N>" で turn_limit を上書き可
"screened_switching_game:<trials>:<seed>:<keep_top>" — MC で選出行列を screening (rollout turn_limit=5)、下位を quantile cutoff で枝刈り、残存 sub-matrix だけ SwitchingGame DP (refine turn_limit=5 既定)。例: "screened_switching_game:1000:42:0.3"。パラメータを細かくチューニングしたいときだけ明示指定する
CLI 側の自動フォールバック: switching_game:<N> で N >= 5 のとき、pkdx select は内部で ScreenedSwitchingGame(trials=1000, seed=42, keep_top=0.3, refine_turn_limit=N) に自動昇格する。skill 側で screening をどう使うか判断する必要はなく、常に switching_game:<N> で turn_limit だけ指定すればよい。明示的に screened_switching_game:... を書いた場合はそのまま使われる。
詳細は references/payoff_semantics.md。
読みターン数の決定 (AskUserQuestion)
team_payoff_model 文字列を構築する直前に、ユーザーに「何ターン先まで読むか」を AskUserQuestion で必ず確認する。選択結果を switching_game:<N> / screened_switching_game:<trials>:<seed>:<keep_top>:<N> の <N> に埋め込む。
| # | 質問 | header | オプション |
|---|
| 1 | 何ターン先まで読みますか? | 読みの深さ | おまかせ(5ターン先まで・通常対戦向け), じっくり読む(10ターン先まで・積み展開も評価/時間長め), サクッと(3ターン先まで・パーティ調整中の素早い確認) |
ユーザー視点の言い換え(質問・選択肢に出す表現はこちら、内部で扱う turn_limit 値は次の対応表):
| ラベル | turn_limit | こう案内する |
|---|
| おまかせ(5ターン先まで) | 5 | 通常の対戦想定。ほとんどの構築・選出で十分な精度 |
| じっくり読む(10ターン先まで) | 10 | 積み技や全抜き展開、長期戦の見極めに。計算は数倍〜10 倍ほど時間がかかります |
| サクッと(3ターン先まで) | 3 | パーティを調整しながら大まかな選出傾向だけ早く確認したいとき |
ユーザーが「Other」で任意の正整数を入力した場合はそれを <N> に埋め込む。負値・0 は弾く。「ターン」を「先読み」「深さ」と言い換えても OK だが、turn_limit / DP などの内部用語はユーザー向け文面に出さないこと。
エンコード:
- 基本は
"switching_game:<N>" を組み立てるだけでよい。N >= 5 のとき CLI が内部で screening 版に自動昇格するので、skill 側で screened_switching_game:... を構築する必要はない
- screening パラメータ (
trials/seed/keep_top) をユーザーが明示的に変更したい場合のみ "screened_switching_game:<trials>:<seed>:<keep_top>:<N>" を直接指定する
実行
1. 入力 JSON をファイルへ保存
実行主体に関わらず、入力 JSON はまず /tmp/pkdx_select_input.json に書き出す (ユーザー手動ルートでも同じファイルを参照させるため heredoc ではなくファイル経由に統一する)。技の priority / stat_effects は pkdx moves 出力をそのまま流せる。省略時はデフォルト (priority=0 / stat_effects=[]) 扱い。
cat > /tmp/pkdx_select_input.json <<'JSON'
{
"team": [
{"name":"P0","type1":"ノーマル","type2":"","hp":100,"atk":100,"def":80,"spa":80,"spd":80,"spe":100,
"ability":"","item":"","tera":"",
"moves":[{"name":"のしかかり","type":"ノーマル","category":"物理","power":85,"priority":0,"stat_effects":[]}]},
...
],
"opponent": [...],
"format": "single",
"stat_system": "champions",
"team_payoff_model": "switching_game:<N>"
}
JSON
<N> は直前の AskUserQuestion で得た値を埋める (おまかせ=5 / じっくり読む=10 / サクッと=3、Other 入力時はその正整数)。screened_switching_game を選んだ場合は "screened_switching_game:1000:42:0.3:<N>" のように 4 番目のフィールドとして同じ値を付ける。
2. 実行主体の決定 (AskUserQuestion)
| # | 質問 | header | オプション |
|---|
| 1 | 計算を誰が実行しますか? | 実行主体 | エージェントが実行(結果だけ受け取る), ターミナルで自分で実行(計算過程をグリッドで見たい) |
ユーザー視点の言い換え(質問・選択肢の文面はこちら、内部分岐は下記):
| ラベル | 挙動 | こう案内する |
|---|
| エージェントが実行 | skill が直接 Bash 実行 | サクッと結果だけ欲しいとき。進捗は [viz] phase ... done の数行ログのみ |
| ターミナルで自分で実行 | コマンドを提示してユーザーが別ターミナルで実行 | 計算過程を 60×60 グリッドで眺めたいとき。長めの計算で進捗を視覚的に追える |
3a. 「エージェントが実行」を選んだ場合
$PKDX select --parallel=auto --progress=json 2> >($VIZ) > /tmp/pkdx_select_result.json < /tmp/pkdx_select_input.json
cat /tmp/pkdx_select_result.json
進捗フィードバックを止めたい場合: --progress=off (既定値) を明示するか、2> >($VIZ) を外す。長い --progress-every を渡せば dp ノードサンプル頻度を間引ける (例: --progress-every=5000)。
--parallel フラグ:
- 既定値
1 は in-process 計算で、damage cache (box/cache/damage_cache.sqlite) の L2 を活用する。同じチーム / 相手で複数回連続呼ぶ場合はこちらの方が速い (ヒット率が高いと 2 回目以降が大幅短縮)。
auto は nproc() (Windows は 64 で clamp) でシャード並列実行する。turn_limit が大きい (5+) で初回計算かつ単発で結果を欲しい場合に最速。シャードでは damage cache (disk) を使わないため L2 ヒット率は下がるが、CPU を全コア使い切れる利得が遥かに大きい。
- 明示的に整数を渡すこともできる (
--parallel=8 等)。Windows 上では MAXIMUM_WAIT_OBJECTS=64 の制約があり、それを超える明示指定はエラーになる。
- 緊急時は
PKDX_NO_PARALLEL=1 環境変数で auto 指定を強制 1 に落とせる (テスト・デバッグ用 escape hatch)。
bit-exact parity は保たれているため、結果 JSON は --parallel 値に依存しない。
3b. 「ターミナルで自分で実行」を選んだ場合
-
ユーザーへコマンドを提示する。<REPO_ROOT> は pwd で取得した実体パス (例: /Users/.../pkdx) に展開し、コードブロックでそのままコピペできる形で渡すこと:
別ターミナルで以下を実行してください。完了したら次の選択肢で教えてください。
```bash
cd <REPO_ROOT> && cat /tmp/pkdx_select_input.json \
| bin/pkdx select --parallel=auto --progress=json 2> >(scripts/select_grid_viz.sh) \
> /tmp/pkdx_select_result.json
```
グリッドビジュアライザは並列パスでも動くが、子プロセスは `--progress=off` を強制するため細粒度の `cell_start` イベントは出ない。`PhaseStart` / `PhaseEnd` レベルの進捗だけ流れる。細粒度表示を見たい場合は `--parallel=1` を明示する (代わりに数倍遅くなる)。
-
提示後、AskUserQuestion で完了確認を取る:
| # | 質問 | header | オプション |
|---|
| 1 | 実行は完了しましたか? | 実行状況 | 完了したので結果を見せて, やっぱりエージェントに実行させる, キャンセル |
-
分岐:
- 完了したので結果を見せて:
/tmp/pkdx_select_result.json を Read で読み込み、結果整形フェーズへ進む。ファイル不在 / 空 / value キー欠落のときはエラーを伝えて再実行 or フォールバックを促す
- やっぱりエージェントに実行させる: 3a へフォールバック
- キャンセル: スキルを終了
出力:
{
"format": "single",
"value": 0.0,
"exploitability": 0.0,
"selections": [[0,1,2], [0,1,3], ..., [1,0,2], ...],
"selection_names": [["P0","P1","P2"], ...],
"opp_selections": [...],
"opp_selection_names": [...],
"row_strategy": [...],
"col_strategy": [...]
}
各 selection は [lead, b1, b2] の 順序付きタプルで、位置 0 が先頭選出 (lead)、後ろ 2 体は控え (昇順)。シングル 6v6 では 6 × C(5,2) = 60 通り列挙されるので row_strategy / col_strategy も長さ 60。同じメンバー集合でも先頭が違えば別エントリ扱いになる点に注意 (例: [0,1,2] と [1,0,2] は別物)。
結果整形
確率 > 1% の選出のみ表示。先頭の Pokémon は 太字で強調する:
## 選出分布 ({format})
- **期待勝率 (value)**: {value:.3f}
- **exploitability**: {exploitability:.6f}
### 採用すべき選出
| 確率 | 先頭 → 控え |
|---|---|
| {p:.1%} | **{names[0]}** → {names[1]}, {names[2]} |
...
### 相手の最適選出
(同上)
Phase 2c: pkdx meta-divergence — メタ乖離分析
入力の収集
usage: 各ポケモン/構築の使用率 (合計 1)
matrix: 対応するマッチアップ行列
labels: 名前
実行
cat <<'JSON' | $PKDX meta-divergence
{
"usage": [0.4, 0.3, 0.3],
"matrix": [[0, 1, -1], [-1, 0, 1], [1, -1, 0]],
"labels": ["R", "P", "S"]
}
JSON
出力:
{
"exploitability": 0.0,
"expected_value": 0.0,
"regrets": [0.0, 0.0, 0.0],
"over_used": [],
"under_used": [],
"labels": ["R", "P", "S"]
}
結果整形
## メタ乖離レポート
- **期待利得 σᵀAσ**: {expected_value:.3f}
- **exploitability**: {exploitability:.3f}
### 各要素の regret
| label | 使用率 | regret |
|---|---|---|
| {labels[i]} | {usage[i]:.1%} | {regrets[i]:+.3f} |
### 過剰使用 (over_used)
{names — 使用率 > 0 だが負の regret = 本来避けるべき}
### 過少使用 (under_used)
{names — 負の regret だが使用率 < ε = 本来選ぶべき}
Phase 2d: pkdx nash graph — DOT 可視化
3 種類の入力を受け付ける:
- matrix 形式: 既知の利得行列を直接渡す
- characters 形式: monocycle (p, v) リストから自動生成
- team + opponent 形式:
pkdx select と同じ Combatant JSON。box/teams/*.meta.json の members をそのまま流せる
cat <<'JSON' | $PKDX nash graph --threshold 0.5
{"matrix": [[0, 1, -1], [-1, 0, 1], [1, -1, 0]], "labels": ["R", "P", "S"]}
JSON
jq -n \
--slurpfile team box/teams/<自>.meta.json \
--slurpfile opp box/teams/<相手>.meta.json \
'{team: $team[0].members, opponent: $opp[0].members}' \
| $PKDX nash graph --threshold 0.2 > matchup.dot
dot -Tpng matchup.dot -o matchup.png
team + opponent 形式の詳細
- 入力フィールド:
team / opponent (必須、Combatant 配列)、stat_system (任意、既定 "champions")、turn_limit (任意、既定 1)
- 行列サイズ:
(n + m) × (n + m) の零和拡張。上半 [0..n) が自チーム、下半 [n..n+m) が相手チーム。同陣営ブロックは可視化用ゼロ埋め
- ノード ID:
team_<i> / opp_<j> の安定 ID + label="..." 属性。両陣営に同名ポケモンが居ても DOT で衝突しない
labels フィールドは指定不可(メンバー名から自動生成)。明示するとエラー
matrix / characters との同時指定もエラー
turn_limit の使い分け
turn_limit | 評価方式 | コスト |
|---|
1 (既定) | 攻撃技の平均削り率差 (fast path) | 36セルで <1秒 |
>=2 | switching_game_winrate([a],[b],N) 1v1 DP | turn_limit に応じて秒〜数十秒 |
turn_limit を上げると積み技や交代評価が反映されるが、6×6=36 セルで全1v1 DP を解くため 5以上は本物の 6体構築では数十秒以上かかる。先に 1 で傾向を見てから必要なら上げる。
threshold で |A[i,j]| がその値以下のエッジを削除。大きな行列の可視化で有効。
エラーハンドリング
| 状況 | 対応 |
|---|
invalid JSON: ... | 入力 JSON の構文エラー。再入力を依頼 |
missing field: ... | 必須フィールド不足。仕様を再提示して再入力 |
both matrix and characters provided; pick one | どちらか一方を削除 |
matrix is not square: NxM | 正方行列に修正 |
usage does not sum to 1 (got X) | 使用率を正規化し直す |
game is infeasible / unbounded | 入力が退化 (LP が解けない)。対角/反対称性を確認 |
計算条件の注意
- stat_system:
pkdx select は Champions SP 既定 ("stat_system": "champions")。トップレベル JSON の "stat_system" フィールドで "champions" (既定) / "standard" を切り替え可能。旧バージョン (SV 等) のデータを使う場合は "stat_system": "standard" を明示する。
- 天候・フィールド・ランク: 現バージョンは 0 固定。動的状態を含む選出最適化は将来対応。
- tera_type:
combatant.tera フィールドで指定可能。攻撃側 STAB のみに作用し、防御側タイプ書換は未実装。