【アプリ開発】pythonで簡単RPGの作成

# はじめに
昔から某RPGが大好きで、いつか勇者になれたらと思った過去もありました。
今回は学習しているpythonを通して、RPGの作成をしながら学んでいきたいと思います。
勉強の復習にもつながるように、内容はある程度詳細として表記をしてみました。
# 要件定義
**サーバーサイド**：python
**フロントエンド**：tkinter

**ゲーム構成**
はじまりのまち
↓ドロップアイテムの選択
中間町
↓ドロップアイテムの選択
魔王を倒す

選択したドロップアイテムにより、主人公に補正がかかるので、最終戦での戦いやすさが変化する。

# 作成過程
```python:gui_game.py 
import tkinter as tk
from tkinter import messagebox
import time
import random
from PIL import Image, ImageTk # Pillowライブラリをインポート

from main import create_player, items_data, enemies, locations, calculate_damage

class RPGGameGUI:
    def __init__(self, master):
        self.master = master
        master.title("勇者みならいの冒険")
        master.geometry("800x600")
        master.resizable(False, False)

        self.player = None
        self.current_location_key = None
        self.current_enemy = None
        self.current_enemy_key = None

        # キャラクター画像のパスを定義
        self.enemy_images = {
            "ゴブリン": "img/ゴブリン.png",
            "ゴブリンキング": "img/ゴブリン.png", # ゴブリンキングの画像がないため、一旦ゴブリンを代用
            "オーク": "img/オーク.png",
            "魔導士": "img/魔法使い.png", # ファイル名が「魔法使い.png」のため修正
            "デビル": "img/デビル.png",
            "魔王": "img/maou.png"
        }
        self.loaded_images = {} # 画像オブジェクトを保持するための辞書

        self.create_widgets()
        self.create_battle_widgets()
        
        self.start_game_setup()

    def create_widgets(self):
        self.master.configure(bg="#2c3e50")

        # 背景画像表示用のキャンバス
        self.background_canvas = tk.Canvas(self.master, bg="#2c3e50", highlightthickness=0)
        self.background_canvas.pack(fill="both", expand=True)

        # 全てのUIフレームをキャンバスのウィンドウとして配置
        self.message_frame = tk.Frame(self.background_canvas, bd=2, relief="solid", bg="#34495e", padx=10, pady=10)
        self.message_frame_window = self.background_canvas.create_window(400, 50, window=self.message_frame, width=760, anchor="n") # 中央上
        self.message_label = tk.Label(self.message_frame, text="ゲームを開始します...", wraplength=720, 
                                      justify="left", font=("Meiryo UI", 14), fg="#ecf0f1", bg="#34495e")
        self.message_label.pack(pady=5, fill="both", expand=True)

        self.status_frame = tk.Frame(self.background_canvas, bd=2, relief="solid", bg="#34495e", padx=10, pady=5)
        self.status_frame_window = self.background_canvas.create_window(400, 150, window=self.status_frame, width=760, anchor="n") # 少し下
        self.status_label = tk.Label(self.status_frame, 
                                     text="プレイヤーHP: 100/100 攻撃力: 10 防御力: 5", 
                                     font=("Meiryo UI", 12, "bold"), fg="#ecf0f1", bg="#34495e")
        self.status_label.pack(pady=3)

        self.choices_frame = tk.Frame(self.background_canvas, bd=2, relief="solid", bg="#34495e", padx=10, pady=10)
        self.choices_frame_window = self.background_canvas.create_window(400, 250, window=self.choices_frame, width=760, height=300, anchor="n") # さらに下、高さ指定
        
        self.choice_buttons = []
        self.item_drop_buttons = []

    def create_battle_widgets(self):
        # 敵のステータス表示はキャンバスの下部に配置
        self.enemy_status_label = tk.Label(self.background_canvas, text="", 
                                           font=("Meiryo UI", 12, "bold"), fg="#e74c3c", bg="#2c3e50")
        self.enemy_status_label_window = self.background_canvas.create_window(400, 200, window=self.enemy_status_label, anchor="n") # 敵のステータス位置調整

        self.battle_commands_frame = tk.Frame(self.background_canvas, bd=2, relief="solid", bg="#34495e", padx=10, pady=10)
        self.battle_commands_frame_window = self.background_canvas.create_window(400, 500, window=self.battle_commands_frame, width=760, anchor="s") # コマンドを下部に

        button_font = ("Meiryo UI", 12, "bold")
        button_bg = "#2980b9"
        button_fg = "#ecf0f1"

        self.attack_button = tk.Button(self.battle_commands_frame, text="⚔️ たたかう", 
                                       command=self.player_attack, font=button_font, bg=button_bg, fg=button_fg,
                                       activebackground="#3498db", activeforeground="#ecf0f1")
        self.item_button = tk.Button(self.battle_commands_frame, text="🩹 どうぐ", 
                                     command=self.show_item_menu, font=button_font, bg=button_bg, fg=button_fg,
                                     activebackground="#3498db", activeforeground="#ecf0f1")
        self.escape_button = tk.Button(self.battle_commands_frame, text="🏃 にげる", 
                                      command=self.try_escape, font=button_font, bg=button_bg, fg=button_fg,
                                      activebackground="#3498db", activeforeground="#ecf0f1")

        # 初期状態では非表示
        self.background_canvas.itemconfigure(self.enemy_status_label_window, state="hidden")
        self.background_canvas.itemconfigure(self.battle_commands_frame_window, state="hidden")


    def update_display(self):
        """現在のゲーム状態に基づいて表示を更新する (マップ画面用)"""
        # 戦闘用ウィジェットを非表示に
        self.background_canvas.itemconfigure(self.enemy_status_label_window, state="hidden")
        self.background_canvas.itemconfigure(self.battle_commands_frame_window, state="hidden")
        self.attack_button.pack_forget()
        self.item_button.pack_forget()
        self.escape_button.pack_forget()
        self._clear_item_drop_buttons()
        self._clear_enemy_image() # 戦闘終了後、敵画像をクリア

        # 通常のUIフレームを表示
        self.background_canvas.itemconfigure(self.message_frame_window, state="normal")
        self.background_canvas.itemconfigure(self.status_frame_window, state="normal")
        self.background_canvas.itemconfigure(self.choices_frame_window, state="normal")


        if not self.player:
            return

        current_loc = locations[self.current_location_key]
        self.message_label.config(text=f"--- 現在地: {current_loc['name']} ---\n{current_loc['description']}")
        self.status_label.config(text=f"👨‍🦰 勇者{self.player['name']} HP: {self.player['hp']}/{self.player['max_hp']} 攻撃力: {self.player['attack']} 防御力: {self.player['defense']}")
        
        # 回復スポットの場合の処理
        if current_loc.get("healing_spot"):
            if self.player["hp"] < self.player["max_hp"]:
                self.player["hp"] = self.player["max_hp"]
                self.message_label.config(text=f"--- 現在地: {current_loc['name']} ---\n{current_loc['description']}\n\n✨ 泉の力でHPが満タンになった！")
                self.update_battle_display() # HP表示を即座に更新
            else:
                self.message_label.config(text=f"--- 現在地: {current_loc['name']} ---\n{current_loc['description']}\n\nすでにHPは満タンだ。")


        for button in self.choice_buttons:
            button.destroy()
        self.choice_buttons.clear()

        # 動的に選択肢を生成
        current_choices = dict(current_loc["choices"]) # 元のchoicesをコピーして変更を加える

        # 回復の泉への選択肢を動的に追加
        if "東の森_戦闘" in self.player["cleared_enemies"] and \
           "西の洞窟_戦闘" in self.player["cleared_enemies"] and \
           self.current_location_key == "始まりの町":
            current_choices["回復の泉へ向かう"] = "回復の泉"


        if current_choices:
            for i, (choice_text, next_location_key) in enumerate(current_choices.items()):
                button = tk.Button(self.choices_frame, text=choice_text, 
                                   command=lambda key=next_location_key: self.move_to_location(key),
                                   font=("Meiryo UI", 12), bg="#2980b9", fg="#ecf0f1",
                                   activebackground="#3498db", activeforeground="#ecf0f1")
                button.pack(pady=5, fill="x", padx=10)
                self.choice_buttons.append(button)
            
            status_button = tk.Button(self.choices_frame, text="📊 ステータスを見る", command=self.show_status,
                                       font=("Meiryo UI", 12), bg="#7f8c8d", fg="#ecf0f1",
                                       activebackground="#95a5a6", activeforeground="#ecf0f1")
            status_button.pack(pady=5, fill="x", padx=10)
            self.choice_buttons.append(status_button)
            
            exit_button = tk.Button(self.choices_frame, text="🚪 ゲーム終了", command=self.master.quit,
                                    font=("Meiryo UI", 12), bg="#c0392b", fg="#ecf0f1",
                                    activebackground="#e74c3c", activeforeground="#ecf0f1")
            exit_button.pack(pady=5, fill="x", padx=10)
            self.choice_buttons.append(exit_button)
        else:
            if current_loc["enemy"]:
                battle_start_button = tk.Button(self.choices_frame, text=f"⚔️ {current_loc['enemy']}と戦う！", 
                                                command=lambda: self.start_battle(current_loc['enemy']),
                                                font=("Meiryo UI", 14, "bold"), bg="#e67e22", fg="#ecf0f1",
                                                activebackground="#f39c12", activeforeground="#ecf0f1")
                battle_start_button.pack(pady=20, fill="x", padx=10)
                self.choice_buttons.append(battle_start_button)
            else:
                pass # Game clear / game over message if applicable

    def start_game_setup(self):
        self.message_label.config(text="勇者の名前を入力してください：")
        
        self.name_entry = tk.Entry(self.choices_frame, font=("Meiryo UI", 12), bg="#ecf0f1", fg="#2c3e50", insertbackground="#2c3e50")
        self.name_entry.pack(pady=10)
        
        name_submit_button = tk.Button(self.choices_frame, text="名前を決定", command=self.process_name_input,
                                       font=("Meiryo UI", 12, "bold"), bg="#27ae60", fg="#ecf0f1",
                                       activebackground="#2ecc71", activeforeground="#ecf0f1")
        name_submit_button.pack(pady=5)
        self.choice_buttons.append(self.name_entry)
        self.choice_buttons.append(name_submit_button)


    def process_name_input(self):
        player_name = self.name_entry.get().strip()
        if not player_name:
            messagebox.showwarning("入力エラー", "名前を入力してください！", parent=self.master)
            return

        self.player = create_player(player_name)
        
        for widget in self.choice_buttons:
            widget.destroy()
        self.choice_buttons.clear()

        self.message_label.config(text="冒険を始めるにあたり、どちらかのオーブを選んでください。\n✨ 金のオーブ (攻撃力アップ)\n🛡️ 銀のオーブ (防御力アップ)")
        
        orb_button_1 = tk.Button(self.choices_frame, text="✨ 金のオーブ (攻撃力アップ)", command=lambda: self.choose_orb("1"),
                                 font=("Meiryo UI", 12), bg="#f1c40f", fg="#2c3e50",
                                 activebackground="#f39c12", activeforeground="#2c3e50")
        orb_button_1.pack(pady=5, fill="x", padx=10)
        self.choice_buttons.append(orb_button_1)

        orb_button_2 = tk.Button(self.choices_frame, text="🛡️ 銀のオーブ (防御力アップ)", command=lambda: self.choose_orb("2"),
                                 font=("Meiryo UI", 12), bg="#bdc3c7", fg="#2c3e50",
                                 activebackground="#95a5a6", activeforeground="#2c3e50")
        orb_button_2.pack(pady=5, fill="x", padx=10)
        self.choice_buttons.append(orb_button_2)

    def choose_orb(self, choice):
        if choice == "1":
            self.player["items"].append(items_data["金のオーブ"])
            # ここは修正: main.pyのitems_dataに合わせてアクセスする
            self.player["attack"] += items_data["金のオーブ"]["attack_boost"]
            self.message_label.config(text="金のオーブを手に入れた！攻撃力がアップした！")
        elif choice == "2":
            self.player["items"].append(items_data["銀のオーブ"])
            # ここは修正: main.pyのitems_dataに合わせてアクセスする
            self.player["defense"] += items_data["銀のオーブ"]["defense_boost"]
            self.message_label.config(text="銀のオーブを手に入れた！防御力がアップした！")
        else:
            self.message_label.config(text="無効な選択です。オーブは選べませんでした。")

        for button in self.choice_buttons:
            button.destroy()
        self.choice_buttons.clear()

        self.current_location_key = "始まりの町"
        self.master.after(1000, self.update_display)

    def move_to_location(self, next_location_key):
        self.current_location_key = next_location_key
        self.update_display()

    def start_battle(self, enemy_key):
        self.current_enemy_key = enemy_key
        self.current_enemy = dict(enemies[enemy_key])
        
        # 通常のUIフレームを非表示に
        self.background_canvas.itemconfigure(self.message_frame_window, state="hidden")
        self.background_canvas.itemconfigure(self.status_frame_window, state="hidden")
        self.background_canvas.itemconfigure(self.choices_frame_window, state="hidden")

        # 戦闘用ウィジェットを表示
        self.background_canvas.itemconfigure(self.enemy_status_label_window, state="normal")
        self.background_canvas.itemconfigure(self.battle_commands_frame_window, state="normal")
        self.message_frame_window_battle = self.background_canvas.create_window(400, 50, window=self.message_frame, width=760, anchor="n") # メッセージフレームは戦闘中も表示
        self.status_frame_window_battle = self.background_canvas.create_window(400, 150, window=self.status_frame, width=760, anchor="n") # ステータスフレームも戦闘中表示

        self.attack_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)
        self.item_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)
        self.escape_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)

        # 敵の画像を背景に表示
        self._load_and_display_enemy_image(enemy_key)

        self.update_battle_display()
        self.display_battle_message(f"--- ⚔️ {self.current_enemy['name']}が現れた！ ⚔️ ---")
        
        self.master.after(1000, self.player_turn)

    def _load_and_display_enemy_image(self, enemy_key):
        image_path = self.enemy_images.get(enemy_key)
        if image_path:
            try:
                # 画像を読み込み、サイズ調整
                original_image = Image.open(image_path)
                
                # 画像が大きすぎる場合、適切なサイズにリサイズ（例: 300x300）
                # アスペクト比を維持しつつリサイズ
                width, height = original_image.size
                max_size = 300
                if width > max_size or height > max_size:
                    if width > height:
                        new_width = max_size
                        new_height = int(max_size * height / width)
                    else:
                        new_height = max_size
                        new_width = int(max_size * width / height)
                    resized_image = original_image.resize((new_width, new_height), Image.Resampling.LANCZOS)
                else:
                    resized_image = original_image

                tk_image = ImageTk.PhotoImage(resized_image)
                self.loaded_images[enemy_key] = tk_image # 参照を保持

                # 古い画像を削除
                if hasattr(self, 'current_enemy_image_id'):
                    self.background_canvas.delete(self.current_enemy_image_id)
                
                # キャンバスの中央に配置
                self.current_enemy_image_id = self.background_canvas.create_image(400, 300, image=tk_image, anchor="center") # 中央に表示
                self.background_canvas.tag_lower(self.current_enemy_image_id) # 他のウィジェットの下に配置

            except FileNotFoundError:
                print(f"Error: Image file not found for {enemy_key} at {image_path}")
            except Exception as e:
                print(f"Error loading image for {enemy_key}: {e}")
        else:
            self._clear_enemy_image() # 画像がない場合はクリア

    def _clear_enemy_image(self):
        if hasattr(self, 'current_enemy_image_id'):
            self.background_canvas.delete(self.current_enemy_image_id)
            del self.current_enemy_image_id
        self.loaded_images.clear() # 保持していた画像参照もクリア

    def update_battle_display(self):
        self.status_label.config(text=f"👨‍🦰 勇者{self.player['name']} HP: {self.player['hp']}/{self.player['max_hp']} 攻撃力: {self.player['attack']} 防御力: {self.player['defense']}")
        
        enemy_icon = "👹" # デフォルトアイコン
        if self.current_enemy_key == "ゴブリン": enemy_icon = "🧌"
        elif self.current_enemy_key == "ゴブリンキング": enemy_icon = "👑"
        elif self.current_enemy_key == "オーク": enemy_icon = "🐷"
        elif self.current_enemy_key == "魔導士": enemy_icon = "🧙"
        elif self.current_enemy_key == "デビル": enemy_icon = "👿"
        elif self.current_enemy_key == "魔王": enemy_icon = "😈"

        self.enemy_status_label.config(text=f"{enemy_icon} 敵: {self.current_enemy['name']} HP: {self.current_enemy['hp']}/{enemies[self.current_enemy_key]['hp']}")

    def display_battle_message(self, message):
        current_message = self.message_label.cget("text")
        lines = (current_message + "\n" + message).split('\n')
        display_lines = lines[-5:]
        self.message_label.config(text="\n".join(display_lines))
        self.master.update_idletasks()

    def player_turn(self):
        self.update_battle_display()
        self.attack_button.config(state=tk.NORMAL)
        self.item_button.config(state=tk.NORMAL)
        self.escape_button.config(state=tk.NORMAL)
        self.display_battle_message("あなたのターンです。コマンドを選択してください:")

    def player_attack(self):
        self.attack_button.config(state=tk.DISABLED)
        self.item_button.config(state=tk.DISABLED)
        self.escape_button.config(state=tk.DISABLED)

        self.display_battle_message(f"👨‍🦰 {self.player['name']}の攻撃！")
        
        player_attack_power = self.player['attack']
        damage = calculate_damage(player_attack_power, self.current_enemy['defense'])
        self.current_enemy['hp'] -= damage
        
        self.master.after(1000, lambda: self._display_player_damage(damage))

    def _display_player_damage(self, damage):
        self.display_battle_message(f"{self.current_enemy['name']}に{damage}のダメージを与えた！")
        self.update_battle_display()
        if self.current_enemy['hp'] <= 0:
            self.check_battle_end()
        else:
            self.master.after(1000, self.enemy_attack)

    def enemy_attack(self):
        if self.current_enemy['hp'] <= 0:
            self.check_battle_end()
            return

        self.display_battle_message(f"\n--- ⚡️ {self.current_enemy['name']}の攻撃！ ⚡️ ---")
        
        enemy_attack_message = ""
        if "attack_patterns" in self.current_enemy and self.current_enemy["attack_patterns"]:
            enemy_attack_message = random.choice(self.current_enemy['attack_patterns'])
            self.display_battle_message(f"{self.current_enemy['name']}: {enemy_attack_message}")
        else:
            self.display_battle_message(f"{self.current_enemy['name']}は攻撃してきた！")
        
        self.master.after(1500, self._perform_enemy_damage)

    def _perform_enemy_damage(self):
        damage = calculate_damage(self.current_enemy['attack'], self.player['defense'])
        self.player['hp'] -= damage
        self.display_battle_message(f"👨‍🦰 {self.player['name']}は{damage}のダメージを受けた！")
        self.update_battle_display()

        if self.player['hp'] <= 0:
            self.check_battle_end()
        else:
            self.master.after(1000, self.player_turn)

    def check_battle_end(self):
        if self.player['hp'] <= 0:
            self.display_battle_message(f"👨‍🦰 {self.player['name']}は力尽きた...")
            messagebox.showerror("ゲームオーバー", "力尽きてしまった...", parent=self.master)
            self.master.quit()
            return
        
        if self.current_enemy['hp'] <= 0:
            self.display_battle_message(f"🎉 {self.current_enemy['name']}を倒した！ 🎉")
            
            current_loc_data = locations.get(self.current_location_key)
            if current_loc_data and "clear_condition" in current_loc_data:
                condition_key = current_loc_data["clear_condition"][0]
                if condition_key not in self.player["cleared_enemies"]:
                    self.player["cleared_enemies"].append(condition_key)
                    print(f"DEBUG: cleared_enemies: {self.player['cleared_enemies']}")


            self.master.after(1000, self._handle_item_drop)
            return
        
    def _handle_item_drop(self):
        # main.py の enemies データ構造に合わせて 'drop_items' に変更
        dropped_item_keys = enemies[self.current_enemy_key].get("drop_items", [])

        if not dropped_item_keys:
            self.display_battle_message("何もドロップしなかった...")
            self.master.after(1000, lambda: self.end_battle("win"))
            return

        self.display_battle_message("ドロップアイテムがあります！どれを選びますか？")
        
        self.attack_button.pack_forget()
        self.item_button.pack_forget()
        self.escape_button.pack_forget()

        for item_key in dropped_item_keys:
            item = items_data[item_key]
            icon = "🎁"
            if item.get("type") == "consumable":
                icon = "🩹"
            elif item.get("type") == "weapon":
                icon = "⚔️"
            elif item.get("type") == "armor":
                icon = "🛡️"
            elif item.get("type") == "permanent_boost":
                icon = "✨"
            elif item.get("type") == "special":
                icon = "🏆"

            item_description = item.get("description", "")
            
            if item.get("type") == "weapon" and "attack_boost" in item:
                item_description = f"攻撃力+{item['attack_boost']}"
            elif item.get("type") == "armor" and "defense_boost" in item:
                item_description = f"防御力+{item['defense_boost']}"
            elif item.get("type") == "permanent_boost":
                boost_info = []
                if "attack_boost" in item:
                    boost_info.append(f"攻撃力+{item['attack_boost']}")
                if "defense_boost" in item:
                    boost_info.append(f"防御力+{item['defense_boost']}")
                item_description = ", ".join(boost_info)

            item_button = tk.Button(self.battle_commands_frame,
                                    text=f"{icon} {item['name']} ({item_description})",
                                    command=lambda it=item: self._select_dropped_item(it),
                                    font=("Meiryo UI", 12), bg="#e7b416", fg="#2c3e50",
                                    activebackground="#f0d56b", activeforeground="#2c3e50")
            item_button.pack(pady=5, fill="x", expand=True)
            self.item_drop_buttons.append(item_button)
        
        self.battle_commands_frame.pack(pady=10, padx=20, fill="x")

    def _select_dropped_item(self, selected_item):
        self.player['items'].append(selected_item)
        self.display_battle_message(f"✨ {selected_item['name']}を手に入れた！ ✨")
        
        status_change_message = ""

        item_type = selected_item.get('type')
        if item_type == 'permanent_boost':
            if 'attack_boost' in selected_item:
                self.player['attack'] += selected_item['attack_boost']
                status_change_message += f"攻撃力が{selected_item['attack_boost']}アップした！\n"
            if 'defense_boost' in selected_item:
                self.player['defense'] += selected_item['defense_boost']
                status_change_message += f"防御力が{selected_item['defense_boost']}アップした！\n"
        elif item_type == 'weapon':
            if self.player['equipped_weapon']:
                self.player['attack'] -= self.player['equipped_weapon'].get('attack_boost', 0)
                status_change_message += f"{self.player['equipped_weapon']['name']}の装備を解除した。\n"
            self.player['equipped_weapon'] = selected_item
            self.player['attack'] += selected_item.get('attack_boost', 0)
            status_change_message += f"{selected_item['name']}を装備した！攻撃力が{selected_item.get('attack_boost', 0)}アップした！\n"
        elif item_type == 'armor':
            if self.player['equipped_armor']:
                self.player['defense'] -= self.player['equipped_armor'].get('defense_boost', 0)
                status_change_message += f"{self.player['equipped_armor']['name']}の装備を解除した。\n"
            self.player['equipped_armor'] = selected_item
            self.player['defense'] += selected_item.get('defense_boost', 0)
            status_change_message += f"{selected_item['name']}を装備した！防御力が{selected_item.get('defense_boost', 0)}アップした！\n"
        
        if status_change_message:
            self.display_battle_message(status_change_message.strip())

        messagebox.showinfo("アイテム獲得", f"{selected_item['name']}を手に入れた！\n{status_change_message.strip()}", parent=self.master)
        
        self._clear_item_drop_buttons()

        self.master.after(500, lambda: self.end_battle("win"))

    def _clear_item_drop_buttons(self):
        for button in self.item_drop_buttons:
            button.destroy()
        self.item_drop_buttons.clear()


    def end_battle(self, result):
        # キャンバスに配置されたウィジェットを非表示に
        self.background_canvas.itemconfigure(self.enemy_status_label_window, state="hidden")
        self.background_canvas.itemconfigure(self.battle_commands_frame_window, state="hidden")
        
        # パックされたボタンも非表示に
        self.attack_button.pack_forget()
        self.item_button.pack_forget()
        self.escape_button.pack_forget()
        self._clear_item_drop_buttons()
        self._clear_enemy_image() # 戦闘終了時に敵画像をクリア

        # 通常のUIフレームを表示に戻す
        self.background_canvas.itemconfigure(self.message_frame_window, state="normal")
        self.background_canvas.itemconfigure(self.status_frame_window, state="normal")
        self.background_canvas.itemconfigure(self.choices_frame_window, state="normal")


        if result == "win":
            if self.current_enemy_key == "ゴブリン":
                self.current_location_key = "北の砦"
            elif self.current_enemy_key == "ゴブリンキング":
                self.current_location_key = "古びた塔"
            elif self.current_enemy_key == "オーク":
                self.current_location_key = "奈落の淵"
            elif self.current_enemy_key == "魔導士":
                self.current_location_key = "奈落の淵"
            elif self.current_enemy_key == "デビル":
                self.current_location_key = "魔王城"
            elif self.current_enemy_key == "魔王":
                messagebox.showinfo("ゲームクリア！", "魔王を倒し、世界に平和が訪れた！\n勇者の伝説が今、始まった！", parent=self.master)
                self.master.quit()
                return
            
            self.update_display()
            self.display_battle_message("探索を続けよう！")
            

        elif result == "lose":
            pass
        elif result == "escaped":
            self.current_location_key = "始まりの町"
            self.update_display()
            self.display_battle_message("戦いを避け、始まりの町へ逃げ帰った。")

    def show_item_menu(self):
        self.attack_button.config(state=tk.DISABLED)
        self.item_button.config(state=tk.DISABLED)
        self.escape_button.config(state=tk.DISABLED)

        self.display_battle_message("どのどうぐを使いますか？")
        
        recoverable_items = [item for item in self.player['items'] if item.get('type') == 'consumable' and 'hp_recovery' in item]
        
        if not recoverable_items:
            self.display_battle_message("回復アイテムを持っていません！")
            self.master.after(1000, self.player_turn)
            return

        self.attack_button.pack_forget()
        self.item_button.pack_forget()
        self.escape_button.pack_forget()

        self.item_choice_buttons = []
        for i, item in enumerate(recoverable_items):
            item_button = tk.Button(self.battle_commands_frame, 
                                    text=f"🩹 {item['name']} (HP+{item['hp_recovery']})",
                                    command=lambda it=item: self._use_item(it),
                                    font=("Meiryo UI", 11), bg="#8e44ad", fg="#ecf0f1",
                                    activebackground="#9b59b6", activeforeground="#ecf0f1")
            item_button.pack(pady=3, fill="x", expand=True)
            self.item_choice_buttons.append(item_button)

        cancel_button = tk.Button(self.battle_commands_frame, text="キャンセル", 
                                  command=self._cancel_item_menu,
                                  font=("Meiryo UI", 11), bg="#34495e", fg="#ecf0f1",
                                  activebackground="#7f8c8d", activeforeground="#ecf0f1")
        cancel_button.pack(pady=3, fill="x", expand=True)
        self.item_choice_buttons.append(cancel_button)

    def _use_item(self, item):
        for btn in self.item_choice_buttons:
            btn.destroy()
        self.item_choice_buttons.clear()
        
        self.attack_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)
        self.item_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)
        self.escape_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)

        if item['type'] == 'consumable' and 'hp_recovery' in item:
            self.player['hp'] = min(self.player['max_hp'], self.player['hp'] + item['hp_recovery'])
            self.player['items'].remove(item)
            self.display_battle_message(f"🩹 {item['name']}を使った！HPが{item['hp_recovery']}回復した！")
            self.update_battle_display()
        else:
            self.display_battle_message("そのアイテムは使えません。")

        self.master.after(1000, self.enemy_attack)

    def _cancel_item_menu(self):
        for btn in self.item_choice_buttons:
            btn.destroy()
        self.item_choice_buttons.clear()
        
        self.attack_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)
        self.item_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)
        self.escape_button.pack(side="left", expand=True, fill="x", padx=5, pady=5)

        self.display_battle_message("アイテムの使用をキャンセルしました。")
        self.master.after(500, self.player_turn)

    def try_escape(self):
        self.attack_button.config(state=tk.DISABLED)
        self.item_button.config(state=tk.DISABLED)
        self.escape_button.config(state=tk.DISABLED)

        self.display_battle_message("🏃 逃走を試みる...")
        self.master.update_idletasks()
        
        if random.random() < 0.5:
            self.master.after(1000, lambda: self.display_battle_message("💨 うまく逃げ切れた！"))
            self.master.after(1500, lambda: messagebox.showinfo("逃走成功", "うまく逃げ切れた！", parent=self.master))
            self.master.after(2000, lambda: self.end_battle("escaped"))
        else:
            self.master.after(1000, lambda: self.display_battle_message("❌ 逃げられなかった！"))
            self.master.after(1500, self.enemy_attack)

    def show_status(self):
        status_text = (
            f"名前: {self.player['name']}\n"
            f"HP: {self.player['hp']}/{self.player['max_hp']}\n"
            f"攻撃力: {self.player['attack']}\n"
            f"防御力: {self.player['defense']}\n"
        )
        equipped_weapon_name = self.player["equipped_weapon"]["name"] if self.player["equipped_weapon"] else "なし"
        equipped_armor_name = self.player["equipped_armor"]["name"] if self.player["equipped_armor"] else "なし"
        status_text += f"装備: 武器 - {equipped_weapon_name}, 防具 - {equipped_armor_name}\n"
        
        items_list = [item['name'] for item in self.player['items']]
        status_text += f"所持アイテム: {', '.join(items_list) if items_list else 'なし'}"

        messagebox.showinfo("📊 プレイヤー情報", status_text, parent=self.master)

# メイン処理
if __name__ == "__main__":
    root = tk.Tk()
    game_gui = RPGGameGUI(root)
    root.mainloop()

```

<details><summary>Tkinter</summary>
pythonのライブラリの一つで、install不要のもの。
学習コストが少なく、簡易的なアプリのフロントであればおすすめ。
できること
ウィンドウの生成
ボタン、メニュー、テキストボックス、ラベル、チェックボックス、ラジオボタンなどのウィジェットの配置や制御


</details>




```python:main.py
import random
import time

# --- ゲームデータ定義 ---

# プレイヤーの初期データ
def create_player(name):
    return {
        "name": f"勇者{name}",
        "hp": 100,
        "max_hp": 100,
        "attack": 10,
        "defense": 5,
        "items": [],
        "equipped_weapon": None,
        "equipped_armor": None,
        "cleared_enemies": [], # 新しく追加: 倒した敵のキーを記録するリスト
    }

# アイテムデータ (変更なし)
items_data = {
    "金のオーブ": {"name": "金のオーブ", "type": "orb", "attack_boost": 10, "description": "攻撃力が上がる不思議なオーブ。"},
    "銀のオーブ": {"name": "銀のオーブ", "type": "orb", "defense_boost": 10, "description": "防御力が上がる不思議なオーブ。"},
    "薬草": {"name": "薬草", "type": "consumable", "hp_recovery": 30, "description": "HPを30回復する。"},
    "こんぼう": {"name": "こんぼう", "type": "weapon", "attack_boost": 5, "description": "ごく一般的なこんぼう。"},
    "革の盾": {"name": "革の盾", "type": "armor", "defense_boost": 5, "description": "初心者向けの革の盾。"},
    "勇者の証": {"name": "勇者の証", "type": "special", "description": "魔王を倒した勇者の証。"},
    "おまもり": {"name": "おまもり", "type": "permanent_boost", "attack_boost": 10, "defense_boost": 30, "description": "持っているだけで守りが強くなるおまもり。"},
    "戦いの護符": {"name": "戦いの護符", "type": "permanent_boost", "attack_boost": 50, "description": "持っているだけで攻撃力が高まる護符。"},
    "秘薬": {"name": "秘薬", "type": "consumable", "hp_recovery": 80, "description": "HPを大きく回復する秘薬。"},
    "勇者の剣": {"name": "勇者の剣", "type": "weapon", "attack_boost": 60, "description": "伝説の勇者が使ったとされる剣。"},
    "鉄の鎧": {"name": "鉄の鎧", "type": "armor", "defense_boost": 50, "description": "頑丈な鉄でできた鎧。"},
    "はねのブーツ": {"name": "はねのブーツ", "type": "armor", "defense_boost": 20, "description": "軽い素材でできたブーツ。"}
}

# 敵データ (変更なし)
enemies = {
    "ゴブリン": {
        "name": "ゴブリン", "hp": 50, "attack": 15, "defense": 3,
        "attack_patterns": ["「キーキー！」と叫びながら攻撃！", "素早い動きで切りかかってきた！"],
        "drop_items": ["薬草", "こんぼう"]
    },
    "ゴブリンキング": {
        "name": "ゴブリンキング", "hp": 100, "attack": 25, "defense": 8,
        "attack_patterns": ["「グガアアア！」と雄叫びを上げて殴りかかってきた！", "巨大な棍棒を振り下ろす！"],
        "drop_items": ["鉄の鎧", "秘薬"]
    },
    "オーク": {
        "name": "オーク", "hp": 80, "attack": 20, "defense": 6,
        "attack_patterns": ["鈍重な一撃！", "「ブヒー！」と突進してきた！"],
        "drop_items": ["革の盾", "薬草"]
    },
    "魔導士": {
        "name": "魔導士", "hp": 70, "attack": 22, "defense": 4,
        "attack_patterns": ["炎の魔法を唱えた！", "氷の魔法があなたを凍らせる！"],
        "drop_items": ["はねのブーツ", "秘薬"]
    },
    "デビル": {
        "name": "デビル", "hp": 120, "attack": 30, "defense": 10,
        "attack_patterns": ["闇の力で襲いかかってきた！", "邪悪な波動を放つ！"],
        "drop_items": ["おまもり", "戦いの護符"]
    },
    "魔王": {
        "name": "魔王", "hp": 200, "attack": 40, "defense": 15,
        "attack_patterns": ["絶望の淵に突き落とす一撃！", "世界の終わりを告げる闇の波動！"],
        "drop_items": ["勇者の証", "勇者の剣"]
    },
}

# 場所データ
locations = {
    "始まりの町": {
        "name": "始まりの町",
        "description": "ここから冒険が始まる。人々の活気に満ちている。",
        "choices": {
            "東の森へ向かう": "東の森",
            "西の洞窟へ向かう": "西の洞窟",
            "北の砦へ向かう": "北の砦",
            "古びた塔へ向かう": "古びた塔",
            "奈落の淵へ向かう": "奈落の淵",
        },
        "enemy": None
    },
    "東の森": {
        "name": "東の森",
        "description": "薄暗い森の奥から、何かの気配がする...",
        "choices": {
            "奥へ進む": "東の森_戦闘",
            "町へ戻る": "始まりの町"
        },
        "enemy": None
    },
    "東の森_戦闘": {
        "name": "東の森の奥",
        "description": "ゴブリンが待ち構えている！",
        "choices": {},
        "enemy": "ゴブリン",
        "clear_condition": ["東の森_戦闘"] # この戦闘をクリアしたことを示すキー
    },
    "北の砦": {
        "name": "北の砦",
        "description": "かつては堅牢な砦だったが、今はオークが徘徊しているようだ。",
        "choices": {
            "奥へ進む": "北の砦_戦闘",
            "町へ戻る": "始まりの町"
        },
        "enemy": None
    },
    "北の砦_戦闘": {
        "name": "北の砦の奥",
        "description": "オークが立ちふさがる！",
        "choices": {},
        "enemy": "オーク",
        "clear_condition": ["北の砦_戦闘"]
    },
    "西の洞窟": {
        "name": "西の洞窟",
        "description": "冷たい風が吹き荒れる洞窟。奥には何かいるようだ。",
        "choices": {
            "奥へ進む": "西の洞窟_戦闘",
            "町へ戻る": "始まりの町"
        },
        "enemy": None
    },
    "西の洞窟_戦闘": {
        "name": "西の洞窟の奥",
        "description": "ゴブリンキングが襲いかかる！",
        "choices": {},
        "enemy": "ゴブリンキング",
        "clear_condition": ["西の洞窟_戦闘"]
    },
    "古びた塔": {
        "name": "古びた塔",
        "description": "今にも崩れそうな古い塔。魔導士の気配がする。",
        "choices": {
            "塔の奥へ": "古びた塔_戦闘",
            "町へ戻る": "始まりの町"
        },
        "enemy": None
    },
    "古びた塔_戦闘": {
        "name": "古びた塔の頂",
        "description": "魔導士が詠唱を始めた！",
        "choices": {},
        "enemy": "魔導士",
        "clear_condition": ["古びた塔_戦闘"]
    },
    "奈落の淵": {
        "name": "奈落の淵",
        "description": "暗く深い奈落の底。ここから魔王城へと続く道がある。",
        "choices": {
            "魔王城へ": "奈落の淵_戦闘",
            "町へ戻る": "始まりの町"
        },
        "enemy": None
    },
    "奈落の淵_戦闘": {
        "name": "奈落の淵の奥",
        "description": "デビルが門番のように立ちはだかる！",
        "choices": {},
        "enemy": "デビル",
        "clear_condition": ["奈落の淵_戦闘"]
    },
    "魔王城": {
        "name": "魔王城",
        "description": "ついに魔王城に辿り着いた。最深部には魔王が待ち受けている。",
        "choices": {
            "最深部へ": "魔王城_戦闘",
            "奈落の淵へ戻る": "奈落の淵"
        },
        "enemy": None
    },
    "魔王城_戦闘": {
        "name": "魔王の間",
        "description": "魔王があなたを待ち構えていた！",
        "choices": {},
        "enemy": "魔王",
        "clear_condition": ["魔王城_戦闘"]
    },
    # 回復の泉を追加
    "回復の泉": {
        "name": "回復の泉",
        "description": "神秘的な光を放つ泉がある。全身の傷が癒されるのを感じる。",
        "choices": {
            "町へ戻る": "始まりの町"
        },
        "enemy": None,
        "healing_spot": True # 回復スポットであることを示すフラグ
    }
}


# --- ゲームシステム関数 ---

def display_player_status(player):
    """プレイヤーの現在のステータスを表示する関数 (コマンドライン版用)"""
    print("----------------------------------------")
    print(f"名前: {player['name']}")
    print(f"HP: {player['hp']}/{player['max_hp']}")
    print(f"攻撃力: {player['attack']}")
    print(f"防御力: {player['defense']}")
    equipped_weapon_name = player["equipped_weapon"]["name"] if player["equipped_weapon"] else "なし"
    equipped_armor_name = player["equipped_armor"]["name"] if player["equipped_armor"] else "なし"
    print(f"装備: 武器 - {equipped_weapon_name}, 防具 - {equipped_armor_name}")
    print(f"所持アイテム: {[item['name'] for item in player['items']] if player['items'] else 'なし'}")
    print("----------------------------------------")

def calculate_damage(attacker_attack, defender_defense):
    """ダメージを計算する関数"""
    damage = max(1, attacker_attack - defender_defense + random.randint(-2, 2))
    return damage


# この if ブロックの中に、これまでのゲーム開始処理とメインループを移動させる
if __name__ == "__main__":
    # --- ゲーム開始処理 ---

    print("----------------------------------------")
    print("          勇者みならいの冒険          ")
    print("----------------------------------------")

    player_name = input("勇者の名前を入力してください：")
    player = create_player(player_name)

    print("冒険を始めるにあたり、どちらかのオーブを選んでください。")
    print("1: 金のオーブ (攻撃力アップ)")
    print("2: 銀のオーブ (防御力アップ)")

    orb_choice = input("選択してください (1/2): ")

    if orb_choice == "1":
        player["items"].append(items_data["金のオーブ"])
        player["attack"] += items_data["金のオーブ"]["attack_boost"]
        print("金のオーブを手に入れた！攻撃力がアップした！")
    elif orb_choice == "2":
        player["items"].append(items_data["銀のオーブ"])
        player["defense"] += items_data["銀のオーブ"]["defense_boost"]
        print("銀のオーブを手に入れた！防御力がアップした！")
    else:
        print("無効な選択です。オーブは選べませんでした。")

    display_player_status(player)
    current_location_key = "始まりの町" # 常に始まりの町から開始

    print("\n--- 冒険が今、始まる！ ---")

    # --- ゲームメインループ ---

    game_over = False
    game_clear = False

    while not game_over and not game_clear:
        current_location = locations[current_location_key]

        print(f"\n--- 現在地: {current_location['name']} ---")
        print(current_location["description"])

        # 回復スポットの場合の処理 (コマンドライン版)
        if current_location.get("healing_spot"):
            if player["hp"] < player["max_hp"]:
                player["hp"] = player["max_hp"]
                print("泉の力でHPが満タンになった！")
            else:
                print("すでにHPは満タンだ。")


        # 場所に敵がいる場合、戦闘開始
        if current_location["enemy"]:
            enemy_key_in_location = current_location["enemy"] # これを保持
            enemy = dict(enemies[enemy_key_in_location]) # 敵データをコピーして使う
            print(f"\n--- {enemy['name']}が現れた！ ---")
            
            # コマンドライン版の戦闘ループ
            while player['hp'] > 0 and enemy['hp'] > 0:
                print(f"\n{player['name']} HP: {player['hp']} vs {enemy['name']} HP: {enemy['hp']}")
                print("どうする？ (1: たたかう, 2: どうぐ, 3: にげる)")
                
                choice = input(">> ")

                if choice == "1": # たたかう
                    print(f"{player['name']}の攻撃！")
                    time.sleep(1)
                    damage = calculate_damage(player['attack'], enemy['defense'])
                    enemy['hp'] -= damage
                    print(f"{enemy['name']}に{damage}のダメージを与えた！")
                    if enemy['hp'] <= 0:
                        break # 敵を倒したらループを抜ける
                    time.sleep(1)

                    print(f"\n{enemy['name']}の攻撃！")
                    time.sleep(1)
                    enemy_attack_pattern = random.choice(enemy['attack_patterns'])
                    print(f"{enemy['name']}: {enemy_attack_pattern}")
                    damage = calculate_damage(enemy['attack'], player['defense'])
                    player['hp'] -= damage
                    print(f"{player['name']}は{damage}のダメージを受けた！")
                    time.sleep(1)

                elif choice == "2": # どうぐ
                    if not player["items"]:
                        print("アイテムを持っていません。")
                        continue
                    print("どのアイテムを使いますか？")
                    consumable_items = [item for item in player["items"] if item.get("type") == "consumable"]
                    
                    if not consumable_items:
                        print("回復アイテムを持っていません。")
                        continue

                    for i, item in enumerate(consumable_items):
                         print(f"{i+1}: {item['name']} ({item['description']})")
                    
                    item_choice = input("アイテム番号を入力 (0でキャンセル): ")
                    if item_choice.isdigit() and 1 <= int(item_choice) <= len(consumable_items):
                        chosen_item = consumable_items[int(item_choice)-1]
                        if "hp_recovery" in chosen_item:
                            player["hp"] = min(player["max_hp"], player["hp"] + chosen_item["hp_recovery"])
                            player["items"].remove(chosen_item)
                            print(f"{chosen_item['name']}を使った！HPが{chosen_item['hp_recovery']}回復した！")
                        else:
                            print("そのアイテムは使えません。")
                    else:
                        print("キャンセルしました。")
                    time.sleep(1)
                    
                    # アイテム使用後も敵は攻撃
                    if enemy['hp'] > 0:
                        print(f"\n{enemy['name']}の攻撃！")
                        time.sleep(1)
                        enemy_attack_pattern = random.choice(enemy['attack_patterns'])
                        print(f"{enemy['name']}: {enemy_attack_pattern}")
                        damage = calculate_damage(enemy['attack'], player['defense'])
                        player['hp'] -= damage
                        print(f"{player['name']}は{damage}のダメージを受けた！")
                        time.sleep(1)

                elif choice == "3": # にげる
                    print("逃げ出した！")
                    time.sleep(1)
                    if random.random() < 0.5: # 50%の確率で逃走成功
                        print("うまく逃げ切れた！")
                        battle_result = "escaped"
                        break # 戦闘ループを抜ける
                    else:
                        print("逃げられなかった！")
                        time.sleep(1)
                        # 逃げられなかった場合も敵は攻撃
                        if enemy['hp'] > 0:
                            print(f"\n{enemy['name']}の攻撃！")
                            time.sleep(1)
                            enemy_attack_pattern = random.choice(enemy['attack_patterns'])
                            print(f"{enemy['name']}: {enemy_attack_pattern}")
                            damage = calculate_damage(enemy['attack'], player['defense'])
                            player['hp'] -= damage
                            print(f"{player['name']}は{damage}のダメージを受けた！")
                            time.sleep(1)

                else:
                    print("無効なコマンドです。")
                    continue

                if player['hp'] <= 0:
                    battle_result = "lose"
                    break # プレイヤーが倒れたらループを抜ける
            
            # コマンドライン版の戦闘結果処理
            if player['hp'] <= 0:
                print(f"{player['name']}は力尽きた...")
                game_over = True
                break
            elif enemy['hp'] <= 0:
                print(f"{enemy['name']}を倒した！")
                
                # 倒した敵の場所キーを記録 (回復の泉開放条件のため)
                if "clear_condition" in current_location and current_location["clear_condition"][0] not in player["cleared_enemies"]:
                    player["cleared_enemies"].append(current_location["clear_condition"][0])


                # ドロップアイテム処理
                if "drop_items" in enemies[enemy_key_in_location] and enemies[enemy_key_in_location]["drop_items"]:
                    print("\nドロップアイテムがあります！どれを選びますか？")
                    for i, item_key in enumerate(enemies[enemy_key_in_location]["drop_items"]):
                        item_info = items_data[item_key]
                        print(f"{i+1}: {item_info['name']} ({item_info['description']})")
                    
                    while True:
                        drop_choice = input("選択してください (1/2): ")
                        if drop_choice.isdigit() and 1 <= int(drop_choice) <= len(enemies[enemy_key_in_location]["drop_items"]):
                            chosen_item_key = enemies[enemy_key_in_location]["drop_items"][int(drop_choice)-1]
                            chosen_item = items_data[chosen_item_key]
                            
                            print(f"✨ {chosen_item['name']}を手に入れた！ ✨")
                            player['items'].append(chosen_item)

                            # permanent_boost / weapon / armor タイプのアイテムの効果を適用
                            if chosen_item.get('type') == 'permanent_boost':
                                if 'attack_boost' in chosen_item:
                                    player['attack'] += chosen_item['attack_boost']
                                    print(f"攻撃力が{chosen_item['attack_boost']}アップした！")
                                if 'defense_boost' in chosen_item:
                                    player['defense'] += chosen_item['defense_boost']
                                    print(f"防御力が{chosen_item['defense_boost']}アップした！")
                            elif chosen_item.get('type') == 'weapon':
                                if player['equipped_weapon']:
                                    player['attack'] -= player['equipped_weapon'].get('attack_boost', 0)
                                    print(f"{player['equipped_weapon']['name']}の装備を解除した。")
                                player['equipped_weapon'] = chosen_item
                                player['attack'] += chosen_item.get('attack_boost', 0)
                                print(f"{chosen_item['name']}を装備した！攻撃力が{chosen_item.get('attack_boost', 0)}アップした！")
                            elif chosen_item.get('type') == 'armor':
                                if player['equipped_armor']:
                                    player['defense'] -= player['equipped_armor'].get('defense_boost', 0)
                                    print(f"{player['equipped_armor']['name']}の装備を解除した。")
                                player['equipped_armor'] = chosen_item
                                player['defense'] += chosen_item.get('defense_boost', 0)
                                print(f"{chosen_item['name']}を装備した！防御力が{chosen_item.get('defense_boost', 0)}アップした！")
                            break
                        else:
                            print("無効な選択です。もう一度入力してください。")


                # 勝利後の場所遷移ロジック（コマンドライン版）
                if current_location_key == "東の森_戦闘":
                    current_location_key = "北の砦"
                elif current_location_key == "西の洞窟_戦闘":
                    current_location_key = "古びた塔"
                elif current_location_key == "北の砦_戦闘":
                    current_location_key = "奈落の淵"
                elif current_location_key == "古びた塔_戦闘":
                    current_location_key = "奈落の淵"
                elif current_location_key == "奈落の淵_戦闘":
                    current_location_key = "魔王城"
                
                if current_location_key == "魔王城_戦闘":
                    print("\n----------------------------------------")
                    print("          魔王を倒した！世界に平和が訪れた！")
                    print(f"          勇者{player_name}の伝説が始まった！")
                    print("----------------------------------------")
                    game_clear = True
                    break
            
            if "battle_result" in locals() and battle_result == "escaped":
                 pass
            else:
                 continue

        # 通常の場所移動選択肢の表示
        print("\nどこへ移動しますか？ (status: ステータス表示, exit: 終了)")
        available_choices_nums = []
        choice_mapping = {}

        # 通常の選択肢を追加
        for i, (choice_text, next_location_key) in enumerate(current_location["choices"].items()):
            choice_num = str(i + 1)
            print(f"{choice_num}: {choice_text}")
            available_choices_nums.append(choice_num)
            choice_mapping[choice_num] = next_location_key

        # 回復の泉への選択肢を動的に追加
        # 「東の森_戦闘」と「西の洞窟_戦闘」をクリアしているかチェック
        if "東の森_戦闘" in player["cleared_enemies"] and "西の洞窟_戦闘" in player["cleared_enemies"] and current_location_key == "始まりの町":
            # 既に選択肢になければ追加
            if "回復の泉へ向かう" not in current_location["choices"]:
                choice_num = str(len(available_choices_nums) + 1)
                print(f"{choice_num}: 回復の泉へ向かう")
                available_choices_nums.append(choice_num)
                choice_mapping[choice_num] = "回復の泉"


        user_input = input(">> ").strip()

        if user_input.lower() == 'exit':
            print("ゲームを終了します。また遊んでね！")
            break
        elif user_input.lower() == 'status':
            display_player_status(player)
            continue
        
        if user_input in available_choices_nums:
            current_location_key = choice_mapping[user_input]
        else:
            print("無効な選択です。")
            continue

    if game_over:
        print("\n----------------------------------------")
        print("               GAME OVER                ")
        print("----------------------------------------")
```
基本的に敵やアイテムなどの効果、ステータスについては辞書型で盛り込む形になっています。

# 課題
現状、正常に機能はするものの、名前が勇者勇者になっていたり、画像の大きさがあっていなかったりとまだGUI的にも改良余地があるので、いくつかに分けて投稿を進めようと思います。

今後改良したいところ：
金のオーブ、銀のオーブ→アイコン一のずれ
表示ログの左寄りを真ん中にする
名前の修正
画像の調整
戦闘コマンドの調整

# 参考
https://tonokokko.com/tkinter-messagebox/