コンテンツにスキップ

ルールエージェントの戦闘評価とサイドレース

src/pca/rule_agents/combat.py の設計ドキュメント。ルールエージェントの「今ターン KO できるか」「相手に KO されるか」「サイドレースはどちらが速いか」「今ターン既に何をしたか」を deck 非依存の共有関数として提供する。

関連ドキュメント

なぜ新モジュールか (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 を一般化。優先順:

  1. front KO — 自分 Active → 相手 Active (追加カード不要)
  2. boss KO — 自分 Active → 相手ベンチ (手札にボスの指令 + supporter 枠が空き)
  3. 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 不使用)。

既知の制約

  1. boss route はシーケンス保証しない — 「ボスを撃てば KO できる」ことを示すだけで、同ターンの行動順 (ボス → 攻撃) の実行は各 SelectContext のスコアリングに委ねる (garchomp の _win_plan と同水準)
  2. 相手の動的打点は過小評価fallback_max_damage で緩和 (上述)
  3. エネルギー判定はコスト長ベース — 色拘束は見ない (エンジンが違法ワザを弾くため KO 検出用途では十分)。ダブルターボ等の 1 枚 2 エネも数えない
  4. プライズレースは 1 ターン 1 KO 仮定 — スプレッド (dragapult) やベンチ狙撃は表現しない

テスト方針

tests/rule_agents/test_combat.py:

  • パリティ: raging_bolt View の attacker_ko_routebest_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 を委譲化。