ルールエージェントの戦闘評価とサイドレース¶
src/pca/rule_agents/combat.py
の設計ドキュメント。ルールエージェントの「今ターン KO できるか」「相手に KO されるか」「サイドレースはどちらが速いか」「今ターン既に何をしたか」を deck 非依存の共有関数として提供する。
関連ドキュメント¶
- rule-agents-redesign —問題分析 (look-ahead ゼロ / 相手モデル不在 / ターン計画欠如) と 3 層構想
- rule-agent-config — params.yaml 経由の閾値注入
- rule-agent-tuning — prize_race パラメータの tuning
なぜ新モジュールか (heuristics.py に足さない理由)¶
heuristics.py は凍結 (SHA-256 lock) された notebook 由来 agent が import *
で読む共有モジュールであり、既存関数の挙動変更は凍結境界の侵犯になる。combat.py は heuristics の既存部品 (apply_damage_modifiers
/ is_game_winning_ko / prize_value / CARD_DB / ALL_ATTACKS) を 利用する側
として新設し、凍結 agent からは一切参照されない。
デッキ固有の打点計算を渡す仕組み¶
デッキ固有の動的打点 (きょくらいごう = 70
× 場の基本エネ、まんようしぐれ 等) は combat.py には持ち込まない。agent の View が base_damage_fn
として注入する:
# raging_bolt_ogerpon/main.py — View.attacker_ko_route
return best_ko_attack(
attacker,
self.opp_active,
base_damage_fn=self.base_damage, # 動的打点は View 側の知識
energy_type_fn=attack_energy_type, # 弱点計算用の攻撃タイプ
)
これで combat.py は deck 非依存のまま、各デッキの正確な打点で KO 判定できる。注入しない場合は
ALL_ATTACKS の静的ダメージ (default_base_damage) を使う。
公開API¶
KO 判定¶
usable_attacks(attacker, *, extra_energy=0) -> list[int]
# エネ数 >= コスト長 の簡易判定。extra_energy は「このターン手張りできる」+1
best_ko_attack(attacker, target, *, base_damage_fn=..., energy_type_fn=...,
extra_energy=0, stadium_dmg_penalty=0) -> (attack_id, damage) | None
# target.hp に届く最大打点のワザ。View.attacker_ko_route と同一契約
can_ko(attacker, target, **kw) -> bool
ターン内 KO ルート探索¶
ko_plan(obs, *, base_damage_fn=..., include_bench_attackers=True,
include_boss_targets=True, ...) -> KoRoute | None
garchomp_ex_cynthia の _win_plan を一般化。優先順:
- front KO — 自分 Active → 相手 Active (追加カード不要)
- boss KO — 自分 Active → 相手ベンチ (手札にボスの指令 + supporter 枠が空き)
- bench 昇格 KO — ベンチのアタッカーが昇格すれば相手 Active を KO
各ステップ内で winning route (そのKOで勝ち) を優先。KoRoute は attack_id / damage /
attacker_zone / target_zone / needs_boss / is_winning を持つ。
被リーサル推定¶
opponent_can_ko_me(obs, my_pokemon=None, *, opp_extra_energy=1,
fallback_max_damage=None) -> bool
相手の場は完全可視 (エネ・HP・ポケモン) なので、相手 Active の実ワザ + 手張り 1 枚を想定して評価する。動的打点のワザは静的値しか見えないため過小評価になる
— agent 側の matchup 打点上限テーブル (raging_bolt の OPP_MAX_DAMAGE) を fallback_max_damage
に渡して上から抑えるのが前提。
プライズレース¶
estimate_prize_race(obs, *, i_can_ko_now=None, opp_can_ko_now=None, ...) -> PrizeRace
# PrizeRace: my_remaining / opp_remaining / my_turns / opp_turns / diff
残りサイド ÷
(相手盤面の平均 prize_value) で「あと何ターンで取り切るか」を両者分見積もる。KO 可能でない側は準備 1 ターンを加算。diff = opp_turns - my_turns
が正なら自分優勢。フェーズ判断 (押すか温存するか) 用の粗い見積もりであり、正確なリーサルカウントではない。
raging_bolt では View.prize_race (lazy) として公開し、params.yaml の
prize_race.behind_aggression_bonus / ahead_conserve_penalty (default 0
= 無効) が非ゼロのときだけ攻撃スコアに加算される。
同ターン文脈¶
parse_turn_context(obs, my_index=None) -> TurnContext
# TurnContext: played_card_ids / attached_card_ids / evolved_card_ids / attacked
obs.logs (累積ゲームログ) を末尾から自分の直近 TURN_START
まで遡って「今ターン自分が確定させた行動」を復元する。State フラグで足りるものは使わない:
supporter / stadium / エネ手張り / 逃げの済みフラグは obs.current.supporterPlayed
等が正であり、TurnContext は「どのカードを何枚使ったか」等フラグに無い情報専用 (二重管理禁止)。
pilot 利用 (raging_bolt): 同一ターンに使用済みの Glass
Trumpet の 2 枚目を generic スコアへ降格。stadium の 1 ターン 1 枚制約は state.stadiumPlayed
フラグで判定 (logs 不使用)。
既知の制約¶
- boss route はシーケンス保証しない
— 「ボスを撃てば KO できる」ことを示すだけで、同ターンの行動順 (ボス → 攻撃) の実行は各 SelectContext のスコアリングに委ねる (garchomp の
_win_planと同水準) - 相手の動的打点は過小評価 —
fallback_max_damageで緩和 (上述) - エネルギー判定はコスト長ベース — 色拘束は見ない (エンジンが違法ワザを弾くため KO 検出用途では十分)。ダブルターボ等の 1 枚 2 エネも数えない
- プライズレースは 1 ターン 1 KO 仮定 — スプレッド (dragapult) やベンチ狙撃は表現しない
テスト方針¶
tests/rule_agents/test_combat.py:
- パリティ: raging_bolt View の
attacker_ko_routeとbest_ko_attackの一致を合成盤面 128 ケースで検証 (View 委譲前の実装に対して green を確認してから委譲した) - KO 境界 / 弱点 ×2 / 抵抗 -30 / エネ不足 / boss 条件 / bench 昇格 / winning 優先 / TurnContext の logs parse edge case
tests/rule_agents/test_raging_bolt_scoring.py:
- attack tier のデフォルトが tier 化前のリテラルと一致すること
classify_attack_tierの境界パリティwinning_ko < supporter_tier.lethalの相対順序不変条件
変更履歴¶
- 2026-07-09: 初版。best_ko_attack / ko_plan / opponent_can_ko_me / estimate_prize_race / parse_turn_context。raging_bolt View を委譲化。