ボス戦闘インタラクションモジュールの実装設計

モジュール概要と動作環境

本作の特定フロアクリア後に発生する対心魔戦シークエンスを制御する中枢コンポーネントを構築します。本ファイル(src/game_logic/boss_fight.py)は、敵対AIの意思決定エンジンおよび事前生成された台詞キャッシュと連携し、プレイヤー入力の検証、ターン遷移、フィールド状態の更新、勝敗判定を一元管理します。加えて、メインゲームループに対してhintstatusなどのシステムコマンドを中継するインターフェースを公開し、LLM補助ヒントの生成に必要な盤面情報抽出機能を提供します。

依存関係と配置

  • 配置パス: src/game_logic/boss_fight.py
  • 必須依存: boss_ai(意思決定関数・型定義)、boss_dialogues(台詞キャッシュ関数)、typingenumcopyloggingrandom
  • 外部連携: llm_hintは直接インポートせず、状態クエリメソッド経由でメインループからパラメータを受け取る設計とします。

基盤クラスのアーキテクチャ

戦闘セッションの状態をカプセル化するBossEncounterクラスを定義します。初期化フェーズでカードプール、モラルパラメータ、敵側デッキ分布を解決し、台詞辞典をロードします。

class BossEncounter:
    def __init__(
        self,
        held_cards: List[Dict[str, Any]],
        karma_metrics: Dict[str, float],
        enemy_pool: Optional[Dict[str, int]] = None
    ):
        # プレイヤー手札の独立コピーを保持
        self.player_deck = copy.deepcopy(held_cards)
        self.karma_profile = karma_metrics.copy()
        
        # 敵側カードプールが未指定の場合は自動配分アルゴリズムを実行
        if enemy_pool is None:
            enemy_pool = self._allocate_enemy_deck(len(self.player_deck))
        self.boss_pool = enemy_pool

        # 盤面・状態変数の初期化
        self.field_stack: List[Dict[str, Any]] = []
        self.current_actor = 'boss'  # AIが先攻
        self.previous_declaration: Optional[str] = None
        self.graveyard_total = 0
        self.is_terminated = False
        self.victor: Optional[str] = None

        # 台詞システムと再生フラグ
        self.script_cache = prefetch_dialogues(self.karma_profile)
        self.entrance_spoken = False

状態属性の定義

プロパティ名データ型役割
player_deckList[Dict]プレイヤーの現在手札(更新可能)
boss_poolDict[str, int]敵AIのタイプ別カード保有数
field_stackList[Dict]場に出されているカードリスト。{'card': Dict, 'declared_type': str}形式で記録
graveyard_totalint除去済みカードの累計(UI表示用)
current_actorstr現在の手番主体('player' または 'boss')
previous_declarationOptional[str]直近のタイプ宣言内容
is_terminatedbool戦闘終了フラグ
victorOptional[str]勝者識別子(終了時に設定)
script_cacheDictフェーズ別台辞典(突入/嘲弄/苦慮/勝利/敗北)
entrance_spokenbool導入台詞再生可否フラグ

公開インターフェースと制御連携

UIおよび外部コントローラから呼び出されるメソッド群です。入力検証と状態遷移を分離し、返却辞書で統一した通信規約を採用します。

def fetch_game_snapshot(self) -> Dict[str, Any]:
    """UI描画用の盤面情報を抽出"""
    return {
        "player_count": len(self.player_deck),
        "player_type_dist": self._aggregate_deck_types(self.player_deck),
        "enemy_count": sum(self.boss_pool.values()),
        "field_cards": len(self.field_stack),
        "last_call": self.previous_declaration,
        "active_turn": self.current_actor
    }

def is_user_active(self) -> bool:
    return self.current_actor == 'player'

def process_user_input(self, command: str, **payload) -> Dict[str, Any]:
    """プレイヤーの行動を処理し、結果辞書を返却"""
    pass

def resolve_enemy_action(self) -> Dict[str, Any]:
    """AIターンを実行し、動作結果・台詞・状態変化を返却"""
    pass

def query_hint_context(self) -> Dict[str, Any]:
    """LLMヒント生成用パラメータを整形"""
    return {
        "hand_dist": self._aggregate_deck_types(self.player_deck),
        "enemy_remaining": sum(self.boss_pool.values()),
        "table_size": len(self.field_stack),
        "opponent_last_call": str(self.previous_declaration) if self.previous_declaration else None,
        "is_player_turn": self.is_user_active()
    }

def retrieve_line(self, category: str) -> str:
    """カテゴリ指定で台詞を取得"""
    if category == 'entrance' and not self.entrance_spoken:
        self.entrance_spoken = True
        return self.script_cache['entrance']
    return self.script_cache.get(category, "")

内部ロジックと判定規則

行動処理フロー

プレイヤー入力はplay(宣言)、call(異議申し立て)、fold(棄却)の3系統に分類されます。

  • 宣言: 手札枚数・宣言タイプの整合性検証後、場札スタックに追加。ターンをAIへ移行。手札枯渡時に即座に勝敗判定。
  • 異議申し立て: 直前の宣言から現在までの場札を対象に検証。ワイルドカードは如何なる宣言でも「正当」と判定。正当な場合:申し立て側が全回収&追加ペナルティカード1枚を相手に付与。虚偽の場合:被申し立て側が全回収&追加ペナルティカード1枚を申し立て側に付与。回収後に次の手番を決定。
  • 棄却: 全場札を墓地へ移動。相手ターンへ移行。

カード操作の抽象化

手札データ構造の差異(リスト vs 辞書カウント)を吸収するための汎用抽出ロジックです。

def _extract_cards(self, deck: Union[List, Dict], target_type: str, amount: int) -> Optional[List]:
    """指定タイプのカードを指定枚数取得し、残りを更新(不足時はNone)"""
    if isinstance(deck, dict):
        normal = min(deck.get(target_type, 0), amount)
        remaining = amount - normal
        wild = min(deck.get('WILD', 0), remaining)
        if normal + wild < amount:
            return None
        
        extracted = []
        if normal > 0:
            deck[target_type] -= normal
            extracted.extend([target_type] * normal)
        if wild > 0:
            deck['WILD'] -= wild
            extracted.extend(['WILD'] * wild)
        return extracted
    else:
        pool_normal = [c for c in deck if c.get('type') == target_type]
        pool_wild = [c for c in deck if c.get('type') == 'WILD']
        if len(pool_normal) + len(pool_wild) < amount:
            return None
        
        result = []
        while amount > 0 and pool_normal:
            result.append(pool_normal.pop(0))
            deck.remove(result[-1])
            amount -= 1
        while amount > 0 and pool_wild:
            result.append(pool_wild.pop(0))
            deck.remove(result[-1])
            amount -= 1
        return result

敵AIターン連携

メインループからresolve_enemy_actionが呼び出された際、現状の盤面情報をGameStateに注入してAIの意思決定を促します。AIの返却アクションに基づき、内部状態を更新した後、_evaluate_taunt_condition(優位時嘲弄)および_evaluate_hesitation_condition(劣勢時苦慮)の発火閾値を評価し、必要に応じて返却辞書のdialogueフィールドへ台詞を挿入します。

メインゲームループとの統合例

制御フローは状態マシンとして動作し、システムコマンドと戦闘アクションを厳密に分離します。

session = BossEncounter(initial_deck, moral_ratios)
output_line(session.retrieve_line('entrance'))

while not session.is_terminated:
    snapshot = session.fetch_game_snapshot()
    render_ui(snapshot)
    
    if session.is_user_active():
        input_cmd = await get_input()
        if input_cmd in SYSTEM_COMMANDS:
            handle_system_cmd(input_cmd, session.query_hint_context())
            continue
            
        outcome = session.process_user_input(input_cmd)
        render_message(outcome['message'])
        if outcome.get('dialogue'):
            render_dialogue(outcome['dialogue'])
        if outcome['is_terminated']:
            break
    else:
        enemy_result = session.resolve_enemy_action()
        render_message(enemy_result['message'])
        if enemy_result.get('dialogue'):
            render_dialogue(enemy_result['dialogue'])
        if enemy_result['is_terminated']:
            break

final_msg = session.retrieve_line('defeat') if session.victor == 'player' else session.retrieve_line('victory')
output_line(final_msg)

検証シナリオと提出基準

テストスイート(tests/test_boss_fight.py)では、以下のエッジケースを網羅的に検証してください。

  • 初期化パラメータのバリデーションと台詞キャッシュの整合性。
  • 入力不正(枚数超過・無効なタイプ・非手番時の実行)に対する防御的処理。
  • 異議申し立てロジック:虚偽/正当判定の分岐、手札移動、先手権の譲渡。
  • ワイルドカードの混在パターンにおける判定正確性。
  • AIターンにおけるモック利用と状態遷移の追跡。
  • 終局条件(手札0枚到達)のトリガーと勝者フラグ設定。
  • 台詞発火閾値(_should_taunt/_should_hesitate)の境界値テスト。

最終成果物として、src/game_logic/boss_fight.pyおよび対応するテストモジュールを提供してください。型ヒントの厳密な適用、ロジック分離の徹底、およびCI環境でのフルパス確認を必須とします。

タグ: Python game-logic state-management unit-testing type-hinting

5月14日 09:29 投稿