ゲームメインループの実装

概要

「奇妙な幻想郷」の主要モジュールがすべて完成したため、これらを統合してプレイ可能なテキストベースのゲームとして動作させる必要があります。このタスクでは src/main.py を作成し、ゲーム全体の初期化・実行・終了処理を行うメインループを構築します。

ファイルと依存関係

  • パス: src/main.py
  • 必要なモジュール(すべて src/game_logic/ 配下):
    • config: ゲーム設定値(例:TOTAL_LAYERS, ROUNDS_PER_LAYERなど)
    • morality: 善悪値管理および判定機能
    • card_acquisition: カード取得条件のチェック
    • scene_generator: 各層のシナリオ生成
    • layer_event_generator: 層間イベント生成
    • option_outcome_generator: 選択肢による結果生成
    • boss_fight_loop: ボス戦の実施
    • commands: システムコマンドのハンドリング
  • 標準ライブラリ: os, sys, logging, random, json
  • 静的テキスト: texts/welcome.txt および texts/endings/ 内の各種エンドファイル

関数設計

1. メイン関数 main()

def main():
    """
    ゲームの開始点。状態の初期化、メインループの実行、終了時のエンド表示を行う。
    """
    pass  # 実装は以下参照

2. 補助関数

  • read_text(path): テキストファイルを読み込む。失敗時はデフォルトメッセージを使用。
  • display_end(type_, stats): 指定されたタイプに対応するエンドテキストを表示。
  • execute_choice(option, state, data): プレイヤーの選択肢に応じた処理を行い、更新された状態や終了フラグを返す。

メインループフロー

1. 初期設定

  1. config から全設定情報を取得
  2. ゲーム状態辞書を初期化:
    status = {
        'level': 1,
        'turn': 1,
        'virtue': 0,
        'balance': 0,
        'vice': 0,
        'streak': 0,
        'prev_action': None,
        'normal_cards': [],
        'universal_cards': [],
        'in_battle': False,
        'finished': False,
        'result': None
    }
    
  3. ウェルカムメッセージを表示:welcome_msg = read_text('texts/welcome.txt')
  4. 必要であればヘルプ表示を促す

2. レベルごとのループ

while status['level'] <= config.TOTAL_LAYERS and not status['finished']:
    current_level = status['level']
    
    # シナリオ生成
    scenario = generate_layer_scenes(
        layer=current_level,
        rounds_per_layer=config.ROUNDS_PER_LAYER,
        morality_ratios=get_morality_ratios(status['virtue'], status['balance'], status['vice'])
    )

    if not scenario:
        print("異常発生によりゲームを中断します")
        break

    print(scenario['opening'])

    collected_cards = 0
    status['normal_cards'] = []

    for turn_info in scenario['events']:
        current_turn = turn_info['turn']
        status['turn'] = current_turn

        print(f"\n【第{current_level}層 第{current_turn}ラウンド】")
        print(turn_info['description'])

        for key in ['A','B','C','D']:
            print(f"  {key}. {turn_info['choices'][key]['label']}")

        while True:
            user_input = input("> ").strip()

            if not user_input:
                continue

            command_response = handle_system_command(user_input, status, None)
            if command_response['exit']:
                status['finished'] = True
                status['result'] = 'quit'
                break
            if command_response['message']:
                print(command_response['message'])
                if '不明なコマンド' in command_response['message']:
                    continue
                else:
                    continue

            choice_key = user_input.upper()
            if choice_key in ['A', 'B', 'C', 'D']:
                result = execute_choice(choice_key, status, turn_info, collected_cards)
                status.update(result['updated_status'])
                collected_cards = result['collected_count']
                if result['ended']:
                    status['finished'] = True
                    status['result'] = result['outcome']
                break
            else:
                print("A/B/C/Dまたはシステムコマンドを入力してください。HELPで詳細確認")

        if status['finished']:
            break

    if status['finished']:
        break

    # ボス層かどうかの判定
    if current_level in config.BOSS_LAYERS:
        battle_instance = BossFight(
            player_deck=status['normal_cards'] + status['universal_cards'],
            morality_ratios=get_morality_ratios(status['virtue'], status['balance'], status['vice'])
        )
        status['in_battle'] = True
        outcome = run_boss_fight(battle_instance, status)
        status['in_battle'] = False

        if outcome == 'player':
            status['normal_cards'] = []
            status['universal_cards'] = []
            status['level'] += 1
            status['turn'] = 1
        elif outcome == 'boss':
            status['finished'] = True
            status['result'] = 'boss_defeat'
        else:
            status['finished'] = True
            status['result'] = 'quit'

    else:
        intermission_event = generate_layer_event(
            layer=current_level,
            morality_ratios=get_morality_ratios(status['virtue'], status['balance'], status['vice'])
        )

        if not intermission_event:
            intermission_event = DEFAULT_EVENT

        print("\n【層間変動】")
        print(intermission_event['summary'])
        for k in ['A','B','C','D']:
            trap_indicator = " [罠]" if intermission_event['actions'][k]['trap'] else ""
            print(f"  {k}. {intermission_event['actions'][k]['label']}{trap_indicator}")

        while True:
            action = input("> ").strip().upper()
            if action in ['A','B','C','D']:
                selected = intermission_event['actions'][action]
                print(selected['effect'])
                if not selected['trap'] and selected['reward']:
                    reward_card = {
                        'kind': 'universal',
                        'title': selected['reward'],
                        'detail': selected['desc'],
                        'origin': current_level
                    }
                    status['universal_cards'].append(reward_card)
                    print(f"ユニバーサルカード入手:{reward_card['title']} - {reward_card['detail']}")
                else:
                    print("カードは獲得できませんでした。")
                break
            else:
                cmd_res = handle_system_command(action, status, None)
                if cmd_res['exit']:
                    status['finished'] = True
                    status['result'] = 'quit'
                    break
                if cmd_res['message']:
                    print(cmd_res['message'])

        if status['finished']:
            break

        status['level'] += 1
        status['turn'] = 1

3. 選択肢処理関数 execute_choice

def execute_choice(choice, status, data, count):
    should_receive = should_get_card(
        total_rounds=config.ROUNDS_PER_LAYER,
        current_round=status['turn'],
        cards_obtained=count,
        max_cards=config.MAX_CARDS_PER_LAYER
    )

    card_kind = None
    if should_receive:
        mapping = {'A': 'virtue', 'B': 'balance', 'C': 'vice', 'D': random.choice(['virtue', 'vice'])}
        card_kind = mapping.get(choice)

    effect = generate_option_outcome(
        scene_description=data['description'],
        option_text=data['choices'][choice]['label'],
        morality_ratios=get_morality_ratios(status['virtue'], status['balance'], status['vice']),
        card_type=card_kind
    )
    print(effect['response'])

    if card_kind and effect['item']:
        item = {
            'category': card_kind,
            'name': effect['item'],
            'info': effect['details'],
            'stage': status['level'],
            'phase': status['turn']
        }
        status['normal_cards'].append(item)
        print(f"カード取得:{item['name']} - {item['info']}")
        count += 1

    updated_virtue, updated_balance, updated_vice, updated_streak, final_result = update_morality(
        current_round=(status['level']-1)*config.ROUNDS_PER_LAYER + status['turn'],
        last_choice=status['prev_action'],
        current_choice=choice,
        good=status['virtue'],
        neutral=status['balance'],
        evil=status['vice'],
        consecutive=status['streak']
    )

    status['virtue'] = updated_virtue
    status['balance'] = updated_balance
    status['vice'] = updated_vice
    status['streak'] = updated_streak
    status['prev_action'] = choice

    if final_result:
        display_end(final_result, get_morality_ratios(updated_virtue, updated_balance, updated_vice))
        return {
            'updated_status': status,
            'collected_count': count,
            'ended': True,
            'outcome': final_result
        }

    return {
        'updated_status': status,
        'collected_count': count,
        'ended': False,
        'outcome': None
    }

4. 結末表示関数

def display_end(end_type, ratios=None):
    endings_folder = os.path.join(os.path.dirname(__file__), '..', 'texts', 'endings')
    file_map = {
        'virtue': 'ending_extreme_good.txt',
        'vice': 'ending_extreme_evil.txt',
        'balance': 'ending_extreme_neutral.txt',
        'clever': 'ending_trick.txt',
        'normal_virtue': 'ending_good.txt',
        'normal_vice': 'ending_evil.txt',
        'normal_balance': 'ending_neutral.txt',
        'boss_defeat': 'ending_boss_loss.txt',
        'quit': 'ending_quit.txt'
    }

5. ファイル読み込み関数

def read_text(path):
    try:
        with open(path, 'r', encoding='utf-8') as file:
            return file.read()
    except Exception as error:
        logging.error(f"ファイル読み込みエラー {path}: {error}")
        return ""

ログ出力設定

import logging
logging.basicConfig(level=config.LOG_LEVEL, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

注意事項

  • モジュールのインポートはプロジェクト構造に基づき適切に行うこと(相対パスまたは絶対パス)
  • LLM呼び出しによる None 応答にはデフォルト値での代替処理が必要
  • ボス戦中は既存のコマンドハンドラを再利用可能

テスト要件

  • ゲーム全体の実行確認(エラーなし)
  • 各コマンドと選択肢の動作検証
  • 特殊エンド条件のトリガー確認
  • ボス戦勝利・敗北ケースの検証

提出物

  • src/main.py の完成版コード
  • texts/ ディレクトリ以下の必要なファイル群
  • 明確なコメントと説明付きのコード
  • python src/main.py で正常起動すること

タグ: Python text-based-game game-loop morality-system card-collection

7月15日 02:04 投稿