Hi !  Enhance the gui with built in audio of pygame. Debug, refine, enhance and optimize the following game. Generate the full code here to copy/paste/run. import pickle
import random
from datetime import datetime
import os
import tkinter as tk
from tkinter import messagebox, simpledialog, scrolledtext, filedialog

# ====================== Health Item Class ======================

class HealthItem:
    def __init__(self, name, description, heal_amount, quantity=1):
        self.name = name.lower()
        self.description = description
        self.heal_amount = heal_amount
        self.quantity = quantity

    def use(self, player, game):
        if self.quantity > 0:
            player.heal(self.heal_amount)
            self.quantity -= 1
            messagebox.showinfo("Item Used",
                                f"You used a {self.name.capitalize()}. Your health is now {player.health}/{player.max_health}.")
            if self.quantity <= 0:
                player.remove_from_inventory(self)
        else:
            messagebox.showwarning("Item Unavailable", f"You don't have any {self.name} left.")

    def __str__(self):
        return f"{self.name.capitalize()} (x{self.quantity}): {self.description}"

# ====================== Item Classes ======================

class Item:
    def __init__(self, name, description, quantity=1):
        self.name = name.lower()
        self.description = description
        self.quantity = quantity

    def use(self, player, game):
        messagebox.showinfo("Use Item", f"You can't use the {self.name}.")

    def __str__(self):
        return f"{self.name.capitalize()} (x{self.quantity}): {self.description}"


class Weapon(Item):
    def __init__(self, name, description, damage, upgrade_level=0, quantity=1):
        super().__init__(name, description, quantity)
        self.damage = damage
        self.upgrade_level = upgrade_level

    def use(self, player, game):
        player.equip_weapon(self)
        messagebox.showinfo("Weapon Equipped", f"You have equipped the {self.name.capitalize()}.")

    def upgrade(self):
        self.upgrade_level += 1
        self.damage += 5
        messagebox.showinfo("Weapon Upgraded",
                            f"{self.name.capitalize()} has been upgraded to level {self.upgrade_level}! Damage increased to {self.damage}.")


class Armor(Item):
    def __init__(self, name, description, defense, upgrade_level=0, quantity=1):
        super().__init__(name, description, quantity)
        self.defense = defense
        self.upgrade_level = upgrade_level

    def use(self, player, game):
        player.equip_armor(self)
        messagebox.showinfo("Armor Equipped", f"You have equipped the {self.name.capitalize()}.")

    def upgrade(self):
        self.upgrade_level += 1
        self.defense += 3
        messagebox.showinfo("Armor Upgraded",
                            f"{self.name.capitalize()} has been upgraded to level {self.upgrade_level}! Defense increased to {self.defense}.")


class Consumable(Item):
    def __init__(self, name, description, heal_amount, quantity=1):
        super().__init__(name, description, quantity)
        self.heal_amount = heal_amount

    def use(self, player, game):
        if self.quantity > 0:
            player.heal(self.heal_amount)
            self.quantity -= 1
            messagebox.showinfo("Item Used",
                                f"You used a {self.name.capitalize()}. Your health is now {player.health}/{player.max_health}.")
            if self.quantity <= 0:
                player.remove_from_inventory(self)
        else:
            messagebox.showwarning("Item Unavailable", f"You don't have any {self.name} left.")


class Craftable(Item):
    def __init__(self, name, description, recipe, quantity=1):
        super().__init__(name, description, quantity)
        self.recipe = recipe  # dict of item_name: required_quantity

    def can_craft(self, player):
        for item_name, qty in self.recipe.items():
            if not player.has_item_quantity(item_name, qty):
                return False
        return True

    def craft(self, player, game):
        if self.can_craft(player):
            for item_name, qty in self.recipe.items():
                player.remove_specific_item(item_name, qty)
            crafted_item = Item(self.name, self.description, self.quantity)
            player.add_to_inventory(crafted_item)
            messagebox.showinfo("Crafted", f"You have crafted a {self.name.capitalize()}.")
            game.update_quests("craft", self.name)
            return True
        else:
            messagebox.showwarning("Cannot Craft", "You don't have the required items to craft this.")
            return False


class LockedChest(Item):
    def __init__(self, name, description, locked=True, contents=None, quantity=1):
        super().__init__(name, description, quantity)
        self.locked = locked
        self.contents = contents or []

    def use(self, player, game):
        if self.locked:
            key = player.has_item('key')
            if key:
                self.locked = False
                player.remove_specific_item('key', 1)
                messagebox.showinfo("Chest Unlocked", "You used a key to unlock the chest.")
                if self.contents:
                    for item in self.contents:
                        player.add_to_inventory(item)
                    self.contents = []
                    game.update_quests("chest_opened", self.name)
                else:
                    messagebox.showinfo("Chest Empty", "The chest is empty.")
            else:
                messagebox.showwarning("Locked", "The chest is locked. You need a key to open it.")
        else:
            if self.contents:
                for item in self.contents:
                    player.add_to_inventory(item)
                self.contents = []
                game.update_quests("chest_opened", self.name)
                messagebox.showinfo("Chest Opened", "You have taken the items from the chest.")
            else:
                messagebox.showinfo("Chest Empty", "The chest is empty.")


class Gold(Item):
    def __init__(self, quantity=1):
        super().__init__("gold", "Shiny gold coins used for trading.", quantity)

    def __str__(self):
        return f"Gold (x{self.quantity}): Shiny gold coins used for trading."


# ====================== Monster Class ======================

class Monster:
    def __init__(self, name, health, attack_power, defense, attack_chance=0.8, critical_chance=0.1, xp_reward=50,
                 gold_reward=20):
        self.name = name
        self.health = health
        self.max_health = health
        self.attack_power = attack_power
        self.defense = defense
        self.attack_chance = attack_chance
        self.critical_chance = critical_chance
        self.xp_reward = xp_reward
        self.gold_reward = gold_reward

    def is_alive(self):
        return self.health > 0

    def attack(self, player):
        if random.random() <= self.attack_chance:
            if random.random() <= self.critical_chance:
                damage = max(0, int((self.attack_power * 2) - player.defense))
                attack_type = "CRITICAL HIT"
            else:
                damage = max(0, self.attack_power - player.defense)
                attack_type = "hit"
            player.health -= damage
            if player.health < 0:
                player.health = 0
            return f"The {self.name} {attack_type} dealing {damage} damage! Your health is now {player.health}/{player.max_health}."
        else:
            return f"The {self.name} attempts to attack but misses!"

    def __str__(self):
        return f"{self.name} (Health: {self.health}/{self.max_health}, Attack: {self.attack_power}, Defense: {self.defense})"


# ====================== Quest and Objective Classes ======================

class Objective:
    def __init__(self, objective_type, target, required_amount):
        self.objective_type = objective_type  # e.g., 'kill', 'collect', 'reach', 'chest_opened'
        self.target = target  # e.g., 'Goblin', 'Gold Coin', 'Mysterious Ruins', 'Locked Chest'
        self.required_amount = required_amount
        self.current_amount = 0

    def update_progress(self, amount=1):
        self.current_amount += amount
        if self.current_amount > self.required_amount:
            self.current_amount = self.required_amount

    def is_completed(self):
        return self.current_amount >= self.required_amount

    def __str__(self):
        return f"{self.objective_type.capitalize()} {self.target}: {self.current_amount}/{self.required_amount}"


class Quest:
    def __init__(self, name, description, reward, objectives):
        self.name = name
        self.description = description
        self.reward = [self.create_reward_item(item) for item in reward]  # list of Items
        self.objectives = [Objective(**obj) for obj in objectives]  # list of Objective objects
        self.completed = False

    def create_reward_item(self, item):
        if isinstance(item, dict):
            if item.get('type') == 'gold':
                return Gold(quantity=item.get('quantity', 0))
            # Add more item types if needed
        elif isinstance(item, Item):
            # Deep copy to prevent shared references
            if isinstance(item, Weapon):
                return Weapon(item.name, item.description, item.damage, item.upgrade_level, item.quantity)
            elif isinstance(item, Armor):
                return Armor(item.name, item.description, item.defense, item.upgrade_level, item.quantity)
            elif isinstance(item, Consumable):
                return Consumable(item.name, item.description, item.heal_amount, item.quantity)
            elif isinstance(item, Craftable):
                return Craftable(item.name, item.description, item.recipe, item.quantity)
            elif isinstance(item, LockedChest):
                return LockedChest(item.name, item.description, item.locked, list(item.contents), item.quantity)
            else:
                return Item(item.name, item.description, item.quantity)
        return Item("unknown", "Unknown reward item.", 1)

    def update_progress(self, objective_type, target=None):
        for obj in self.objectives:
            if obj.objective_type == objective_type:
                if target:
                    if obj.target.lower() == target.lower():
                        obj.update_progress()
                else:
                    obj.update_progress()
        self.check_completion()

    def check_completion(self):
        if all(obj.is_completed() for obj in self.objectives):
            self.complete()

    def complete(self):
        if not self.completed:
            self.completed = True
            rewards_str = ""
            for item in self.reward:
                rewards_str += f"- {item}\n"
            messagebox.showinfo(
                "Quest Completed",
                f"Congratulations! You have completed the quest: {self.name}\nReward Received:\n{rewards_str}")
            return self.reward
        return []

    def __str__(self):
        status = "Completed" if self.completed else "In Progress"
        objectives_str = '\n  '.join([str(obj) for obj in self.objectives])
        return (f"Quest: {self.name}\nDescription: {self.description}\nStatus: {status}\nObjectives:\n  {objectives_str}")


# ====================== Skill Classes ======================

class Skill:
    def __init__(self, name, description, cooldown=0):
        self.name = name
        self.description = description
        self.cooldown = cooldown  # in turns
        self.current_cooldown = 0

    def use(self, player, monster, game):
        messagebox.showinfo("Skill Used", f"You used {self.name}, but nothing happened.")
        return False

    def tick_cooldown(self):
        if self.current_cooldown > 0:
            self.current_cooldown -= 1

    def __str__(self):
        status = f"Cooldown: {self.current_cooldown}" if self.current_cooldown > 0 else "Ready"
        return f"{self.name} - {self.description} [{status}]"

    def upgrade(self):
        # Default skills cannot be upgraded; override in subclasses if applicable
        pass


class PowerStrike(Skill):
    def __init__(self):
        super().__init__("Power Strike", "A powerful attack that deals extra damage.", cooldown=2)

    def use(self, player, monster, game):
        if self.current_cooldown == 0:
            if player.equipped_weapon:
                damage = max(0, int((player.equipped_weapon.damage * 1.5) - monster.defense))
                monster.health -= damage
                self.current_cooldown = self.cooldown
                action_result = f"You used Power Strike dealing {damage} damage to the {monster.name}."
                if monster.health <= 0:
                    monster.health = 0
                    action_result += f"\nYou have defeated the {monster.name} using Power Strike!"
                    player.score += 200
                    player.gain_xp(monster.xp_reward)
                    player.gold += monster.gold_reward
                    game.award_rewards(monster, player)
                    game.update_quests("kill", monster.name)
                return action_result
            else:
                messagebox.showwarning("No Weapon", "You have no weapon equipped.")
        else:
            messagebox.showwarning("Skill Cooldown",
                                   f"The skill {self.name} is on cooldown for {self.current_cooldown} more turn(s).")
        return ""


class Fireball(Skill):
    def __init__(self):
        super().__init__("Fireball", "A fiery spell that burns the enemy.", cooldown=3)

    def use(self, player, monster, game):
        if self.current_cooldown == 0:
            damage = max(0, 15 - monster.defense)
            monster.health -= damage
            self.current_cooldown = self.cooldown
            action_result = f"You cast Fireball dealing {damage} fire damage to the {monster.name}."
            if monster.health <= 0:
                monster.health = 0
                action_result += f"\nYou have defeated the {monster.name} using Fireball!"
                player.score += 200
                player.gain_xp(monster.xp_reward)
                player.gold += monster.gold_reward
                game.award_rewards(monster, player)
                game.update_quests("kill", monster.name)
            return action_result
        else:
            messagebox.showwarning("Skill Cooldown",
                                   f"The skill {self.name} is on cooldown for {self.current_cooldown} more turn(s).")
        return ""


class ArrowShot(Skill):
    def __init__(self):
        super().__init__("Arrow Shot", "A precise arrow aimed at the enemy.", cooldown=1)

    def use(self, player, monster, game):
        if self.current_cooldown == 0:
            if player.equipped_weapon:
                damage = max(0, int((player.equipped_weapon.damage * 1.2) - monster.defense))
                monster.health -= damage
                self.current_cooldown = self.cooldown
                action_result = f"You used Arrow Shot dealing {damage} damage to the {monster.name}."
                if monster.health <= 0:
                    monster.health = 0
                    action_result += f"\nYou have defeated the {monster.name} using Arrow Shot!"
                    player.score += 200
                    player.gain_xp(monster.xp_reward)
                    player.gold += monster.gold_reward
                    game.award_rewards(monster, player)
                    game.update_quests("kill", monster.name)
                return action_result
            else:
                messagebox.showwarning("No Weapon", "You have no weapon equipped.")
        else:
            messagebox.showwarning("Skill Cooldown",
                                   f"The skill {self.name} is on cooldown for {self.current_cooldown} more turn(s).")
        return ""


class HealSkill(Skill):
    def __init__(self):
        super().__init__("Heal", "Restore a portion of your health.", cooldown=3)

    def use(self, player, monster, game):
        if self.current_cooldown == 0:
            heal_amount = 30
            player.heal(heal_amount)
            self.current_cooldown = self.cooldown
            messagebox.showinfo("Heal",
                                f"You used Heal and restored {heal_amount} health. Current health: {player.health}/{player.max_health}.")
            return True
        else:
            messagebox.showwarning("Skill Cooldown",
                                   f"The skill {self.name} is on cooldown for {self.current_cooldown} more turn(s).")
        return False

    def upgrade(self):
        # Example of upgrading HealSkill to increase heal amount or reduce cooldown
        self.cooldown = max(1, self.cooldown - 1)
        messagebox.showinfo("Skill Upgraded",
                            f"{self.name} cooldown reduced to {self.cooldown} turn(s).")


# ====================== NPC Classes ======================

class NPC:
    def __init__(self, name, description, dialogues):
        self.name = name
        self.description = description
        self.dialogues = dialogues
        self.current_dialogue = 0

    def talk(self, player, game, gui):
        if self.current_dialogue < len(self.dialogues):
            player_choices, npc_response, _ = self.dialogues[self.current_dialogue]
            gui.append_text(f"{self.name}: {npc_response}\n")
            if not player_choices:
                self.current_dialogue += 1
                return
            gui.display_npc_choices(player_choices, lambda choice: self.handle_choice(choice, player, game, gui))
        else:
            gui.append_text(f"{self.name} has nothing more to say.\n")

    def handle_choice(self, choice, player, game, gui):
        gui.append_text(f"You: {choice}\n")
        self.process_choice(choice, player, game, gui)
        self.current_dialogue += 1
        self.talk(player, game, gui)

    def process_choice(self, choice, player, game, gui):
        # To be overridden by subclasses or specific NPC implementations
        pass


class FriendlyNPC(NPC):
    def __init__(self, name, description, dialogues, quest=None, gift=None, shop_items=None):
        super().__init__(name, description, dialogues)
        self.quest = quest
        self.gift = gift
        self.shop_items = shop_items or []
        self.has_received_gift = False

    def process_choice(self, choice, player, game, gui):
        # Example: Giving a quest or a gift
        if self.quest and self.quest not in player.quests and not self.has_received_gift:
            player.add_quest(self.quest)
            gui.append_text(f"{self.name} has given you a quest: {self.quest.name}\n")
            if self.gift:
                if isinstance(self.gift, Skill):
                    player.add_skill(self.gift)
                    gui.append_text(f"{self.name} has taught you the skill: {self.gift.name}.\n")
                else:
                    player.add_to_inventory(self.gift)
                    gui.append_text(f"{self.name} has given you a {self.gift.name.capitalize()}.\n")
            self.has_received_gift = True
        # Implement shop interactions if applicable
        if self.shop_items:
            game.gui.open_shop(self.shop_items, gui)


class UnfriendlyNPC(NPC):
    def __init__(self, name, description, dialogues, monster):
        super().__init__(name, description, dialogues)
        self.monster = monster

    def process_choice(self, choice, player, game, gui):
        messagebox.showinfo("Hostile NPC", f"{self.name} is hostile and attacks you!")
        game.battle(player, self.monster, gui)


# ====================== Character Classes ======================

class CharacterClass:
    def __init__(self, name, description, base_health, base_defense, skills, xp_multiplier=1.0):
        self.name = name
        self.description = description
        self.base_health = base_health
        self.base_defense = base_defense
        self.skills = skills  # list of Skill instances
        self.xp_multiplier = xp_multiplier


# ====================== Player Class ======================

class Player:
    def __init__(self, name, character_class):
        self.name = name
        self.character_class = character_class
        self.health = character_class.base_health
        self.max_health = character_class.base_health
        self.defense = character_class.base_defense
        self.inventory = []
        self.equipped_weapon = None
        self.equipped_armor = None
        self.quests = []
        self.score = 0
        self.lives = 3
        self.skills = character_class.skills.copy()
        self.level = 1
        self.xp = 0
        self.xp_to_next_level = 100
        self.gold = 0
        self.inventory_capacity = 20  # Limit the number of items

    def is_alive(self):
        return self.health > 0

    def attack(self, monster):
        if self.equipped_weapon:
            damage = max(0, self.equipped_weapon.damage - monster.defense)
            monster.health -= damage
            action_result = f"You attack the {monster.name} with your {self.equipped_weapon.name.capitalize()} dealing {damage} damage."
        else:
            damage = max(0, 10 - monster.defense)
            monster.health -= damage
            action_result = f"You attack the {monster.name} with your bare hands dealing {damage} damage."
        self.score += damage
        return action_result

    def heal(self, amount):
        previous_health = self.health
        self.health += amount
        if self.health > self.max_health:
            self.health = self.max_health
        healed_amount = self.health - previous_health
        # heal feedback handled in Skill and Item use methods

    def add_to_inventory(self, item):
        if self.get_inventory_count() + item.quantity > self.inventory_capacity:
            messagebox.showwarning("Inventory Full", "You don't have enough inventory space to carry this item.")
            return False
        existing_item = next((i for i in self.inventory if i.name == item.name.lower() and type(i) == type(item)), None)
        if existing_item and not isinstance(item, (Weapon, Armor, LockedChest, Craftable)):
            existing_item.quantity += item.quantity
        else:
            self.inventory.append(item)
        return True

    def get_inventory_count(self):
        return sum(item.quantity for item in self.inventory)

    def remove_from_inventory(self, item):
        if item in self.inventory:
            self.inventory.remove(item)

    def remove_specific_item(self, item_name, quantity):
        for item in self.inventory:
            if item.name == item_name.lower():
                if item.quantity > quantity:
                    item.quantity -= quantity
                    return True
                elif item.quantity == quantity:
                    self.inventory.remove(item)
                    return True
                else:
                    messagebox.showwarning("Insufficient Quantity", f"You don't have enough {item_name}.")
                    return False
        messagebox.showwarning("Item Not Found", f"You don't have a {item_name}.")
        return False

    def has_item(self, item_name):
        for item in self.inventory:
            if item.name == item_name.lower():
                return item
        return None

    def has_item_quantity(self, item_name, quantity):
        item = self.has_item(item_name)
        if item and item.quantity >= quantity:
            return True
        return False

    def equip_weapon(self, weapon):
        self.equipped_weapon = weapon
        # GUI should update equipped weapon display

    def equip_armor(self, armor):
        if self.equipped_armor:
            self.defense -= self.equipped_armor.defense
        self.equipped_armor = armor
        self.defense += armor.defense
        # GUI should update equipped armor display

    def unequip_armor(self):
        if self.equipped_armor:
            self.defense -= self.equipped_armor.defense
            self.equipped_armor = None
            # GUI should update equipped armor display
            messagebox.showinfo("Armor Unequipped", "You have unequipped your armor.")
        else:
            messagebox.showinfo("No Armor", "You have no armor equipped.")

    def show_inventory(self, gui):
        inventory_text = ""
        if not self.inventory:
            inventory_text = "Your inventory is empty.\n"
        else:
            inventory_text += "=== Inventory ===\n"
            for item in self.inventory:
                if isinstance(item, Gold):
                    inventory_text += f"- Gold (x{item.quantity})\n"
                elif isinstance(item, Weapon):
                    inventory_text += f"- {item.name.capitalize()} (x{item.quantity}): Damage {item.damage}, " \
                                      f"Upgrade Level {item.upgrade_level}\n"
                elif isinstance(item, Armor):
                    inventory_text += f"- {item.name.capitalize()} (x{item.quantity}): Defense {item.defense}, " \
                                      f"Upgrade Level {item.upgrade_level}\n"
                elif isinstance(item, Craftable):
                    recipe_str = ', '.join([f"{k} (x{v})" for k, v in item.recipe.items()])
                    inventory_text += f"- {item.name.capitalize()} (x{item.quantity}): {item.description}. Recipe: {recipe_str}\n"
                else:
                    inventory_text += f"- {item.name.capitalize()} (x{item.quantity}): {item.description}\n"
            inventory_text += f"Gold: {self.gold}\n"
            inventory_text += f"Inventory Capacity: {self.get_inventory_count()}/{self.inventory_capacity}\n"
            inventory_text += "=================\n"
        gui.update_inventory(inventory_text)

    def add_quest(self, quest):
        if any(q.name == quest.name for q in self.quests):
            return  # Quest already added
        self.quests.append(quest)
        # GUI should update quests display

    def check_level_up(self):
        while self.xp >= self.xp_to_next_level:
            self.xp -= self.xp_to_next_level
            self.level += 1
            self.xp_to_next_level = int(self.xp_to_next_level * 1.5)
            self.max_health += 20
            self.health = self.max_health
            self.defense += 2
            messagebox.showinfo("Level Up",
                                f"*** Congratulations! You have reached Level {self.level}! ***\nHealth increased to {self.max_health}. Defense increased to {self.defense}.")

    def gain_xp(self, amount):
        self.xp += int(amount * self.character_class.xp_multiplier)
        messagebox.showinfo("XP Gained", f"You gained {int(amount * self.character_class.xp_multiplier)} XP.")
        self.check_level_up()

    def upgrade_skill(self, skill_name, game, gui):
        skill = next((s for s in self.skills if s.name.lower() == skill_name.lower()), None)
        if skill:
            if hasattr(skill, 'upgrade'):
                skill.upgrade()
                messagebox.showinfo("Skill Upgraded", f"{skill.name} has been upgraded.")
                gui.update_player_stats()
            else:
                messagebox.showwarning("Cannot Upgrade", "This skill cannot be upgraded.")
        else:
            messagebox.showwarning("Skill Not Found", "You don't have that skill.")

    def upgrade_weapon(self, game, gui):
        if self.equipped_weapon:
            if self.gold >= 50:
                self.gold -= 50
                self.equipped_weapon.upgrade()
                messagebox.showinfo("Weapon Upgraded", "Weapon upgraded successfully!")
                gui.update_player_stats()
                gui.update_inventory()
            else:
                messagebox.showwarning("Insufficient Gold", "You don't have enough gold to upgrade your weapon.")
        else:
            messagebox.showwarning("No Weapon", "You have no weapon equipped.")

    def upgrade_armor(self, game, gui):
        if self.equipped_armor:
            if self.gold >= 50:
                self.gold -= 50
                self.equipped_armor.upgrade()
                messagebox.showinfo("Armor Upgraded", "Armor upgraded successfully!")
                gui.update_player_stats()
                gui.update_inventory()
            else:
                messagebox.showwarning("Insufficient Gold", "You don't have enough gold to upgrade your armor.")
        else:
            messagebox.showwarning("No Armor", "You have no armor equipped.")

    def add_skill(self, skill):
        if any(s.name == skill.name for s in self.skills):
            messagebox.showinfo("Skill Exists", f"You already know the skill: {skill.name}.")
            return
        self.skills.append(skill)
        messagebox.showinfo("New Skill", f"You have learned a new skill: {skill.name}.")

    def use_skill(self, skill_name, monster, game):
        skill = next((s for s in self.skills if s.name.lower() == skill_name.lower()), None)
        if skill:
            result = skill.use(self, monster, game)
            if isinstance(skill, HealSkill):
                if result:
                    game.gui.append_text(f"You used {skill.name}.\n")
                    return True
            if result:
                game.gui.append_text(f"{result}\n")
                return True
            return False
        else:
            messagebox.showwarning("Skill Not Found", "You don't have that skill.")
            return False


# ====================== Room Class with Coordinates ======================

class Room:
    def __init__(self, name, description, x, y, items=None, monsters=None, npcs=None, quests=None):
        self.name = name
        self.description = description
        self.x = x  # Coordinate x
        self.y = y  # Coordinate y
        self.items = items or []
        self.monsters = monsters or []
        self.npcs = npcs or []
        self.quests = quests or []
        self.connections = {}  # direction: Room

    def connect_rooms(self, direction, room):
        self.connections[direction.lower()] = room

    def get_room_in_direction(self, direction):
        return self.connections.get(direction.lower(), None)

    def describe(self):
        description = f"=== {self.name} ===\n{self.description}\n"
        if self.items:
            description += "\nItems in the room:\n"
            for item in self.items:
                if isinstance(item, Gold):
                    description += f"- Gold (x{item.quantity})\n"
                elif isinstance(item, Weapon):
                    description += f"- {item.name.capitalize()} (x{item.quantity}): Damage {item.damage}, Upgrade Level {item.upgrade_level}\n"
                elif isinstance(item, Armor):
                    description += f"- {item.name.capitalize()} (x{item.quantity}): Defense {item.defense}, Upgrade Level {item.upgrade_level}\n"
                elif isinstance(item, Craftable):
                    recipe_str = ', '.join([f"{k} (x{v})" for k, v in item.recipe.items()])
                    description += f"- {item.name.capitalize()} (x{item.quantity}): {item.description}. Recipe: {recipe_str}\n"
                else:
                    description += f"- {item.name.capitalize()} (x{item.quantity}): {item.description}\n"
        if self.monsters:
            description += "\nMonsters in the room:\n"
            for monster in self.monsters:
                if monster.is_alive():
                    description += f"- {monster.name} (Health: {monster.health}/{monster.max_health})\n"
        if self.npcs:
            description += "\nNPCs in the room:\n"
            for npc in self.npcs:
                description += f"- {npc.name}: {npc.description}\n"
        if self.quests:
            description += "\nQuests available in this room:\n"
            for quest in self.quests:
                if not quest.completed:
                    description += f"- {quest.name}: {quest.description}\n"
        return description


# ====================== SelectionWindow Class ======================

class SelectionWindow:
    def __init__(self, parent, title, options, callback):
        self.top = tk.Toplevel(parent)
        self.top.title(title)
        self.callback = callback
        self.title_text = title
        self.create_widgets(options)

    def create_widgets(self, options):
        label = tk.Label(self.top, text=self.title_text, font=('Arial', 14))
        label.pack(pady=10)

        for option in options:
            btn = tk.Button(self.top, text=option, width=30, command=lambda opt=option: self.select(opt))
            btn.pack(pady=2)

    def select(self, choice):
        self.callback(choice)
        self.top.destroy()


# ====================== GameGUI Class ======================

class GameGUI:
    def __init__(self, root, game):
        self.root = root
        self.game = game
        self.root.title("Adventure Game")
        self.create_widgets()
        self.update_gui()

    def create_widgets(self):
        # Frames
        self.left_frame = tk.Frame(self.root, width=600, height=400)
        self.left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        self.right_frame = tk.Frame(self.root, width=300, height=400)
        self.right_frame.pack(side=tk.RIGHT, fill=tk.Y)

        # Room Description
        self.room_text = scrolledtext.ScrolledText(self.left_frame, wrap=tk.WORD, state='disabled')
        self.room_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # Player Stats
        self.stats_label = tk.Label(self.right_frame, text="=== Player Stats ===", justify=tk.LEFT, anchor='nw')
        self.stats_label.pack(anchor='nw', padx=10, pady=5)

        # Buttons for actions
        self.actions_label = tk.Label(self.right_frame, text="=== Actions ===", justify=tk.LEFT, anchor='nw')
        self.actions_label.pack(anchor='nw', padx=10, pady=5)

        self.buttons_frame = tk.Frame(self.right_frame)
        self.buttons_frame.pack(anchor='nw', padx=10, pady=5)

        self.look_button = tk.Button(self.buttons_frame, text="Look Around", width=20, command=self.look_around)
        self.look_button.pack(pady=2)

        self.inventory_button = tk.Button(self.buttons_frame, text="Inventory", width=20, command=self.show_inventory)
        self.inventory_button.pack(pady=2)

        self.take_button = tk.Button(self.buttons_frame, text="Take Item", width=20, command=self.take_item)
        self.take_button.pack(pady=2)

        self.use_button = tk.Button(self.buttons_frame, text="Use Item", width=20, command=self.use_item)
        self.use_button.pack(pady=2)

        self.craft_button = tk.Button(self.buttons_frame, text="Craft Item", width=20, command=self.craft_item)
        self.craft_button.pack(pady=2)

        self.quests_button = tk.Button(self.buttons_frame, text="Quests", width=20, command=self.show_quests)
        self.quests_button.pack(pady=2)

        self.fight_button = tk.Button(self.buttons_frame, text="Fight", width=20, command=self.fight)
        self.fight_button.pack(pady=2)

        self.interact_button = tk.Button(self.buttons_frame, text="Interact with NPC", width=20, command=self.interact_npc)
        self.interact_button.pack(pady=2)

        self.move_button = tk.Button(self.buttons_frame, text="Move", width=20, command=self.move)
        self.move_button.pack(pady=2)

        self.upgrade_button = tk.Button(self.buttons_frame, text="Upgrade", width=20, command=self.upgrade)
        self.upgrade_button.pack(pady=2)

        self.map_button = tk.Button(self.buttons_frame, text="View Map", width=20, command=self.view_map)
        self.map_button.pack(pady=2)

        self.save_button = tk.Button(self.buttons_frame, text="Save Game", width=20, command=self.save_game)
        self.save_button.pack(pady=2)

        self.load_button = tk.Button(self.buttons_frame, text="Load Game", width=20, command=self.load_game)
        self.load_button.pack(pady=2)

        self.quit_button = tk.Button(self.buttons_frame, text="Quit", width=20, command=self.quit_game)
        self.quit_button.pack(pady=2)

        # Bottom Text Area for messages
        self.message_text = scrolledtext.ScrolledText(self.root, wrap=tk.WORD, height=10, state='disabled')
        self.message_text.pack(fill=tk.BOTH, padx=10, pady=10)

    def update_gui(self):
        self.update_room_description()
        self.update_player_stats()

    def append_text(self, text):
        self.message_text.configure(state='normal')
        self.message_text.insert(tk.END, text)
        self.message_text.configure(state='disabled')
        self.message_text.see(tk.END)

    def update_room_description(self):
        self.room_text.configure(state='normal')
        self.room_text.delete(1.0, tk.END)
        self.room_text.insert(tk.END, self.game.current_room.describe())
        self.room_text.configure(state='disabled')

    def update_player_stats(self):
        skills = ', '.join([skill.name for skill in self.game.player.skills]) if self.game.player.skills else 'None'
        equipped_weapon = self.game.player.equipped_weapon.name.capitalize() if self.game.player.equipped_weapon else 'None'
        equipped_armor = self.game.player.equipped_armor.name.capitalize() if self.game.player.equipped_armor else 'None'

        stats = (
            f"Name: {self.game.player.name}\n"
            f"Class: {self.game.player.character_class.name}\n"
            f"Level: {self.game.player.level}\n"
            f"XP: {self.game.player.xp}/{self.game.player.xp_to_next_level}\n"
            f"Health: {self.game.player.health}/{self.game.player.max_health}\n"
            f"Defense: {self.game.player.defense}\n"
            f"Equipped Weapon: {equipped_weapon}\n"
            f"Equipped Armor: {equipped_armor}\n"
            f"Score: {self.game.player.score}\n"
            f"Lives: {self.game.player.lives}\n"
            f"Skills: {skills}\n"
            f"Gold: {self.game.player.gold}\n"
        )
        self.stats_label.configure(text=stats)

    def look_around(self):
        self.update_room_description()
        self.append_text("You look around and take in your surroundings.\n")

    def show_inventory(self):
        inventory_window = tk.Toplevel(self.root)
        inventory_window.title("Inventory")
        inventory_window.geometry("400x500")
        inventory_text = scrolledtext.ScrolledText(inventory_window, wrap=tk.WORD, state='normal')
        inventory_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        if not self.game.player.inventory:
            inventory_text.insert(tk.END, "Your inventory is empty.\n")
        else:
            inventory_text.insert(tk.END, "=== Inventory ===\n")
            for item in self.game.player.inventory:
                if isinstance(item, Gold):
                    inventory_text.insert(tk.END, f"- Gold (x{item.quantity})\n")
                elif isinstance(item, Weapon):
                    inventory_text.insert(tk.END, f"- {item.name.capitalize()} (x{item.quantity}): Damage {item.damage}, "
                                               f"Upgrade Level {item.upgrade_level}\n")
                elif isinstance(item, Armor):
                    inventory_text.insert(tk.END, f"- {item.name.capitalize()} (x{item.quantity}): Defense {item.defense}, "
                                               f"Upgrade Level {item.upgrade_level}\n")
                elif isinstance(item, Craftable):
                    recipe_str = ', '.join([f"{k} (x{v})" for k, v in item.recipe.items()])
                    inventory_text.insert(tk.END, f"- {item.name.capitalize()} (x{item.quantity}): {item.description}. Recipe: {recipe_str}\n")
                else:
                    inventory_text.insert(tk.END, f"- {item.name.capitalize()} (x{item.quantity}): {item.description}\n")
            inventory_text.insert(tk.END, f"\nGold: {self.game.player.gold}\n")
            inventory_text.insert(tk.END, f"Inventory Capacity: {self.game.player.get_inventory_count()}/{self.game.player.inventory_capacity}\n")
            inventory_text.insert(tk.END, "=================\n")
        inventory_text.configure(state='disabled')

    def take_item(self):
        if not self.game.current_room.items:
            messagebox.showinfo("No Items", "There are no items to take here.")
            return

        # Create a selection window
        item_names = [item.name.capitalize() if not isinstance(item, LockedChest) else f"Locked {item.name.capitalize()}" for item in
                      self.game.current_room.items]
        SelectionWindow(self.root, "Take Item", item_names,
                        lambda choice: self.process_take_choice(choice, self.game.current_room.items))

    def process_take_choice(self, choice, items):
        chosen_item = next((item for item in items if (item.name.capitalize() == choice or f"Locked {item.name.capitalize()}" == choice)), None)
        if chosen_item:
            if isinstance(chosen_item, LockedChest):
                chosen_item.use(self.game.player, self.game)
                if not chosen_item.locked and not chosen_item.contents:
                    items.remove(chosen_item)
            else:
                if chosen_item.quantity > 1:
                    quantity = simpledialog.askinteger("Take Item",
                                                       f"How many {chosen_item.name.capitalize()}s do you want to take? (1-{chosen_item.quantity})",
                                                       parent=self.root, minvalue=1, maxvalue=chosen_item.quantity)
                    if quantity is None:
                        return  # User cancelled
                else:
                    quantity = 1
                # Create a copy of the item to add
                if isinstance(chosen_item, Weapon):
                    picked_item = Weapon(chosen_item.name, chosen_item.description, chosen_item.damage,
                                         chosen_item.upgrade_level, quantity)
                elif isinstance(chosen_item, Armor):
                    picked_item = Armor(chosen_item.name, chosen_item.description, chosen_item.defense,
                                       chosen_item.upgrade_level, quantity)
                elif isinstance(chosen_item, Consumable):
                    picked_item = Consumable(chosen_item.name, chosen_item.description, chosen_item.heal_amount,
                                             quantity)
                elif isinstance(chosen_item, Craftable):
                    picked_item = Craftable(chosen_item.name, chosen_item.description, chosen_item.recipe,
                                           quantity)
                else:
                    picked_item = Item(chosen_item.name, chosen_item.description, quantity)
                if self.game.player.add_to_inventory(picked_item):
                    chosen_item.quantity -= quantity
                    if chosen_item.quantity <= 0:
                        items.remove(chosen_item)
                    self.append_text(f"You took {quantity} x {chosen_item.name.capitalize()}.\n")
                    self.game.update_quests("collect", chosen_item.name)
        self.update_gui()

    def use_item(self):
        if not self.game.player.inventory:
            messagebox.showinfo("No Items", "Your inventory is empty.")
            return

        # Filter usable items
        usable_items = [item for item in self.game.player.inventory if isinstance(item, (Consumable, HealthItem))]
        if not usable_items:
            messagebox.showinfo("No Usable Items", "You have no usable items.")
            return

        # Create a selection window
        item_names = [item.name.capitalize() for item in usable_items]
        SelectionWindow(self.root, "Use Item", item_names,
                        lambda choice: self.process_use_choice(choice, usable_items))

    def process_use_choice(self, choice, usable_items):
        chosen_item = next((item for item in usable_items if item.name.capitalize() == choice), None)
        if chosen_item:
            chosen_item.use(self.game.player, self.game)
            self.append_text(f"You used a {chosen_item.name.capitalize()}.\n")
            self.game.update_quests("use_item", chosen_item.name)
        self.update_gui()

    def craft_item(self):
        craftable_items = [item for item in self.game.player.inventory if isinstance(item, Craftable)]
        if not craftable_items:
            messagebox.showinfo("No Craftable Items", "You have no craftable items.")
            return

        # Check which craftable items the player can craft
        craftable_available = [item for item in craftable_items if item.can_craft(self.game.player)]
        if not craftable_available:
            messagebox.showinfo("Cannot Craft", "You don't have the necessary materials to craft any items.")
            return

        # Create a selection window
        craft_names = [item.name.capitalize() for item in craftable_available]
        SelectionWindow(self.root, "Craft Item", craft_names,
                        lambda choice: self.process_craft_choice(choice, craftable_available))

    def process_craft_choice(self, choice, craftable_available):
        chosen_item = next((item for item in craftable_available if item.name.capitalize() == choice), None)
        if chosen_item:
            crafted = chosen_item.craft(self.game.player, self.game)
            if crafted:
                self.append_text(f"You crafted a {chosen_item.name.capitalize()}.\n")
        self.update_gui()

    def show_quests(self):
        if not self.game.player.quests:
            messagebox.showinfo("No Quests", "You have no active quests.")
            return

        # Create a new window to display quests
        quests_window = tk.Toplevel(self.root)
        quests_window.title("Quests")
        quests_window.geometry("500x600")
        quests_text = scrolledtext.ScrolledText(quests_window, wrap=tk.WORD, state='normal')
        quests_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        for quest in self.game.player.quests:
            quests_text.insert(tk.END, f"{quest}\n\n")
        quests_text.configure(state='disabled')

    def fight(self):
        alive_monsters = [monster for monster in self.game.current_room.monsters if monster.is_alive()]
        if not alive_monsters:
            messagebox.showinfo("No Monsters", "There are no monsters to fight here.")
            return

        # If multiple monsters, allow player to choose which one to fight
        if len(alive_monsters) > 1:
            monster_names = [monster.name for monster in alive_monsters]
            SelectionWindow(self.root, "Choose Monster to Fight", monster_names,
                           lambda choice: self.process_fight_choice(choice, alive_monsters))
        else:
            # Single monster, proceed to battle
            self.initiate_battle(alive_monsters[0])

    def process_fight_choice(self, choice, alive_monsters):
        chosen_monster = next((monster for monster in alive_monsters if monster.name == choice), None)
        if chosen_monster:
            self.initiate_battle(chosen_monster)
        self.update_gui()

    def initiate_battle(self, monster):
        battle_window = tk.Toplevel(self.root)
        battle_window.title(f"Battle: {monster.name}")
        battle_window.geometry("600x500")

        battle_log = scrolledtext.ScrolledText(battle_window, wrap=tk.WORD, state='disabled', width=70, height=25)
        battle_log.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        action_frame = tk.Frame(battle_window)
        action_frame.pack(pady=10)

        def append_battle(text):
            battle_log.configure(state='normal')
            battle_log.insert(tk.END, text + "\n")
            battle_log.configure(state='disabled')
            battle_log.see(tk.END)

        def player_attack():
            if not self.game.player.is_alive():
                append_battle("You have no lives left. Game Over.")
                battle_window.destroy()
                return
            action_result = self.game.player.attack(monster)
            append_battle(action_result)
            if monster.is_alive():
                attack_result = monster.attack(self.game.player)
                append_battle(attack_result)
                if not self.game.player.is_alive():
                    append_battle("You have been defeated.")
                    battle_window.destroy()
            else:
                append_battle(f"You have defeated the {monster.name}!")
                self.game.player.score += 100
                self.game.player.gain_xp(monster.xp_reward)
                self.game.player.gold += monster.gold_reward
                self.game.update_quests("kill", monster.name)
                self.game.current_room.monsters.remove(monster)
                battle_window.destroy()
            self.update_gui()

        def player_use_item():
            if not self.game.player.inventory:
                append_battle("Your inventory is empty.")
                return
            # Filter consumables
            consumables = [item for item in self.game.player.inventory if isinstance(item, (Consumable, HealthItem))]
            if not consumables:
                append_battle("You have no usable items.")
                return

            def use_selected_item(choice):
                chosen_item = next((item for item in consumables if item.name.capitalize() == choice), None)
                if chosen_item:
                    chosen_item.use(self.game.player, self.game)
                    append_battle(f"You used a {chosen_item.name.capitalize()}.\n")
                    self.game.update_quests("use_item", chosen_item.name)
                    if not monster.is_alive():
                        append_battle(f"You have defeated the {monster.name}!\n")
                        self.game.player.score += 100
                        self.game.player.gain_xp(monster.xp_reward)
                        self.game.player.gold += monster.gold_reward
                        self.game.update_quests("kill", monster.name)
                        self.game.current_room.monsters.remove(monster)
                        battle_window.destroy()
                    else:
                        # Monster's turn to attack
                        attack_result = monster.attack(self.game.player)
                        append_battle(attack_result)
                        if not self.game.player.is_alive():
                            append_battle("You have been defeated.")
                            battle_window.destroy()

            SelectionWindow(battle_window, "Use Item", [item.name.capitalize() for item in consumables],
                           use_selected_item)

        def player_use_skill():
            if not self.game.player.skills:
                append_battle("You have no skills to use.")
                return

            # Filter skills that are ready
            skills_available = [skill for skill in self.game.player.skills if skill.current_cooldown == 0]
            if not skills_available:
                append_battle("All your skills are on cooldown.")
                return

            def use_selected_skill(choice):
                chosen_skill = next((skill for skill in skills_available if skill.name == choice), None)
                if chosen_skill:
                    result = self.game.player.use_skill(chosen_skill.name, monster, self.game)
                    if result:
                        append_battle(result)
                        if not monster.is_alive():
                            append_battle(f"You have defeated the {monster.name}!\n")
                            self.game.player.score += 100
                            self.game.player.gain_xp(monster.xp_reward)
                            self.game.player.gold += monster.gold_reward
                            self.game.update_quests("kill", monster.name)
                            self.game.current_room.monsters.remove(monster)
                            battle_window.destroy()
                        else:
                            attack_result = monster.attack(self.game.player)
                            append_battle(attack_result)
                            if not self.game.player.is_alive():
                                append_battle("You have been defeated.")
                                battle_window.destroy()

            SelectionWindow(battle_window, "Use Skill", [skill.name for skill in skills_available],
                           use_selected_skill)

        # Battle Action Buttons
        attack_btn = tk.Button(action_frame, text="Attack", width=15, command=player_attack)
        attack_btn.pack(side=tk.LEFT, padx=5)

        use_item_btn = tk.Button(action_frame, text="Use Item", width=15, command=player_use_item)
        use_item_btn.pack(side=tk.LEFT, padx=5)

        use_skill_btn = tk.Button(action_frame, text="Use Skill", width=15, command=player_use_skill)
        use_skill_btn.pack(side=tk.LEFT, padx=5)

        append_battle("=== Battle Start ===\n")
        append_battle(f"You have encountered a {monster.name}!\n")

    def interact_npc(self):
        if not self.game.current_room.npcs:
            messagebox.showinfo("No NPCs", "There are no NPCs to interact with here.")
            return

        # If multiple NPCs, allow player to choose
        if len(self.game.current_room.npcs) > 1:
            npc_names = [npc.name for npc in self.game.current_room.npcs]
            SelectionWindow(self.root, "Choose NPC to Interact", npc_names,
                           lambda choice: self.process_npc_choice(choice, self.game.current_room.npcs))
        else:
            # Single NPC, interact directly
            self.interact_with_npc(self.game.current_room.npcs[0])

    def process_npc_choice(self, choice, npcs):
        chosen_npc = next((npc for npc in npcs if npc.name == choice), None)
        if chosen_npc:
            self.interact_with_npc(chosen_npc)
        self.update_gui()

    def interact_with_npc(self, npc):
        npc_window = tk.Toplevel(self.root)
        npc_window.title(f"Interacting with {npc.name}")
        npc_window.geometry("500x500")

        dialog_text = scrolledtext.ScrolledText(npc_window, wrap=tk.WORD, state='disabled')
        dialog_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        action_frame = tk.Frame(npc_window)
        action_frame.pack(pady=10)

        def append_dialog(text):
            dialog_text.configure(state='normal')
            dialog_text.insert(tk.END, text + "\n")
            dialog_text.configure(state='disabled')
            dialog_text.see(tk.END)

        def display_dialogue():
            if npc.current_dialogue < len(npc.dialogues):
                player_choices, npc_response, _ = npc.dialogues[npc.current_dialogue]
                append_dialog(f"{npc.name}: {npc_response}")
                if player_choices:
                    for choice in player_choices:
                        button = tk.Button(action_frame, text=choice, width=30,
                                           command=lambda c=choice: select_choice(c))
                        button.pack(pady=2)
                else:
                    npc.current_dialogue += 1
                    display_dialogue()
            else:
                append_dialog(f"{npc.name}: I have nothing more to say.")
                close_btn = tk.Button(action_frame, text="Close", command=npc_window.destroy)
                close_btn.pack(pady=10)

        def select_choice(choice):
            append_dialog(f"You: {choice}")
            npc.handle_choice(choice, self.game.player, self.game, self)
            npc_window.destroy()
            self.update_gui()

        display_dialogue()

    def move(self):
        available_directions = list(self.game.current_room.connections.keys())
        if not available_directions:
            messagebox.showinfo("No Exits", "There are no exits from this room.")
            return

        # Create a selection window
        directions = [f"{direction.capitalize()} to {self.game.current_room.get_room_in_direction(direction).name}" for direction in
                      available_directions]
        SelectionWindow(self.root, "Choose Direction to Move", directions,
                       lambda choice: self.process_move_choice(choice, available_directions))

    def process_move_choice(self, choice, available_directions):
        direction = next((dir for dir in available_directions if f"{dir.capitalize()} to " in choice), None)
        if direction:
            next_room = self.game.current_room.get_room_in_direction(direction)
            self.game.current_room = next_room
            self.append_text(f"You moved {direction.capitalize()} to the {next_room.name}.\n")
            self.game.update_quests("reach", next_room.name)
            self.update_gui()

    def upgrade(self):
        upgrade_window = tk.Toplevel(self.root)
        upgrade_window.title("Upgrade Menu")
        upgrade_window.geometry("500x600")

        upgrade_log = scrolledtext.ScrolledText(upgrade_window, wrap=tk.WORD, state='disabled', width=60, height=20)
        upgrade_log.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        action_frame = tk.Frame(upgrade_window)
        action_frame.pack(pady=10)

        def append_upgrade(text):
            upgrade_log.configure(state='normal')
            upgrade_log.insert(tk.END, text + "\n")
            upgrade_log.configure(state='disabled')
            upgrade_log.see(tk.END)

        def upgrade_skills():
            skills_available = [skill for skill in self.game.player.skills if isinstance(skill, (PowerStrike, Fireball, ArrowShot, HealSkill))]
            if not skills_available:
                append_upgrade("You have no upgradeable skills.")
                return

            # Create a selection window
            SelectionWindow(upgrade_window, "Choose Skill to Upgrade", [skill.name for skill in skills_available],
                           lambda choice: self.process_skill_upgrade(choice, skills_available, upgrade_log))

        def process_skill_upgrade(choice, skills_available, log_widget):
            chosen_skill = next((skill for skill in skills_available if skill.name == choice), None)
            if chosen_skill:
                if self.game.player.gold >= 100:
                    self.game.player.gold -= 100
                    chosen_skill.upgrade()
                    append_upgrade(f"{chosen_skill.name} has been upgraded.")
                    self.game.update_gui()
                else:
                    append_upgrade("You don't have enough gold to upgrade this skill.")

        def upgrade_weapon():
            if self.game.player.equipped_weapon:
                if self.game.player.gold >= 50:
                    self.game.player.upgrade_weapon(self.game, self)
                    append_upgrade("Weapon upgraded successfully!")
                else:
                    append_upgrade("You don't have enough gold to upgrade your weapon.")
            else:
                append_upgrade("You have no weapon equipped.")

        def upgrade_armor():
            if self.game.player.equipped_armor:
                if self.game.player.gold >= 50:
                    self.game.player.upgrade_armor(self.game, self)
                    append_upgrade("Armor upgraded successfully!")
                else:
                    append_upgrade("You don't have enough gold to upgrade your armor.")
            else:
                append_upgrade("You have no armor equipped.")

        # Upgrade Buttons
        skill_btn = tk.Button(action_frame, text="Upgrade Skills", width=20, command=upgrade_skills)
        skill_btn.pack(pady=5)

        weapon_btn = tk.Button(action_frame, text="Upgrade Weapon", width=20, command=upgrade_weapon)
        weapon_btn.pack(pady=5)

        armor_btn = tk.Button(action_frame, text="Upgrade Armor", width=20, command=upgrade_armor)
        armor_btn.pack(pady=5)

        # Show current upgrades or other info
        append_upgrade("=== Upgrade Menu ===\n")

    def view_map(self):
        map_window = tk.Toplevel(self.root)
        map_window.title("Game Map")
        map_window.geometry("600x500")
        map_text = scrolledtext.ScrolledText(map_window, wrap=tk.WORD, state='normal')
        map_text.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        rooms = self.game.all_rooms
        min_x = min(room.x for room in rooms)
        max_x = max(room.x for room in rooms)
        min_y = min(room.y for room in rooms)
        max_y = max(room.y for room in rooms)

        grid = []
        for y in range(min_y, max_y + 1):
            row = []
            for x in range(min_x, max_x + 1):
                room = self.game.get_room_by_coordinates(x, y)
                if room:
                    if room == self.game.current_room:
                        row.append("[P]")
                    else:
                        row.append("[R]")
                else:
                    row.append("   ")
            grid.append(row)

        map_str = "=== Game Map ===\n"
        for y_index, row in enumerate(grid, start=min_y):
            row_str = f"Y={y_index} " + " ".join(row)
            map_str += row_str + "\n"
        map_str += "\nLegend: [P] - Your Position | [R] - Room"
        map_text.insert(tk.END, map_str)
        map_text.configure(state='disabled')

    def save_game(self):
        try:
            file_path = filedialog.asksaveasfilename(defaultextension=".pkl",
                                                     filetypes=[("Pickle Files", "*.pkl"), ("All Files", "*.*")],
                                                     title="Save Game")
            if file_path:
                with open(file_path, "wb") as f:
                    pickle.dump(self.game, f)
                messagebox.showinfo("Game Saved", "Game has been saved successfully!")
        except Exception as e:
            messagebox.showerror("Save Error", f"Failed to save the game: {e}")

    def load_game(self):
        try:
            file_path = filedialog.askopenfilename(defaultextension=".pkl",
                                                   filetypes=[("Pickle Files", "*.pkl"), ("All Files", "*.*")],
                                                   title="Load Game")
            if file_path and os.path.exists(file_path):
                with open(file_path, "rb") as f:
                    loaded_game = pickle.load(f)
                # Update the current game's state
                self.game = loaded_game
                self.game.gui = self
                self.update_gui()
                self.append_text("Game loaded successfully!\n")
            else:
                messagebox.showwarning("Load Error", "No valid save file selected.")
        except Exception as e:
            messagebox.showerror("Load Error", f"Failed to load the game: {e}")

    def quit_game(self):
        if messagebox.askyesno("Quit", "Are you sure you want to quit?"):
            self.save_game()
            self.root.destroy()

    def open_shop(self, shop_items, gui):
        shop_window = tk.Toplevel(self.root)
        shop_window.title("Shop")
        shop_window.geometry("600x600")

        shop_log = scrolledtext.ScrolledText(shop_window, wrap=tk.WORD, state='disabled', width=70, height=25)
        shop_log.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        action_frame = tk.Frame(shop_window)
        action_frame.pack(pady=10)

        def append_shop(text):
            shop_log.configure(state='normal')
            shop_log.insert(tk.END, text + "\n")
            shop_log.configure(state='disabled')
            shop_log.see(tk.END)

        append_shop("=== Shop ===\n")
        for idx, item in enumerate(shop_items, 1):
            price = self.get_item_price(item)
            if isinstance(item, Weapon):
                item_desc = f"Damage: {item.damage}, Upgrade Level: {item.upgrade_level}"
            elif isinstance(item, Armor):
                item_desc = f"Defense: {item.defense}, Upgrade Level: {item.upgrade_level}"
            elif isinstance(item, Craftable):
                recipe_str = ', '.join([f"{k} (x{v})" for k, v in item.recipe.items()])
                item_desc = f"{item.description}. Recipe: {recipe_str}"
            else:
                item_desc = item.description
            append_shop(f"{idx}. {item.name.capitalize()} - {item_desc} - Price: {price} Gold")

        def buy_item():
            choice = simpledialog.askinteger("Buy Item",
                                             f"Enter the number of the item you want to buy (1-{len(shop_items)}):",
                                             parent=shop_window, minvalue=1, maxvalue=len(shop_items))
            if choice is None:
                return  # User cancelled
            chosen_item = shop_items[choice - 1]
            price = self.get_item_price(chosen_item)
            if self.game.player.gold >= price:
                self.game.player.gold -= price
                if isinstance(chosen_item, Weapon):
                    bought_item = Weapon(chosen_item.name, chosen_item.description, chosen_item.damage,
                                        chosen_item.upgrade_level, 1)
                elif isinstance(chosen_item, Armor):
                    bought_item = Armor(chosen_item.name, chosen_item.description, chosen_item.defense,
                                       chosen_item.upgrade_level, 1)
                elif isinstance(chosen_item, Craftable):
                    bought_item = Craftable(chosen_item.name, chosen_item.description, chosen_item.recipe, 1)
                else:
                    bought_item = Item(chosen_item.name, chosen_item.description, 1)
                if self.game.player.add_to_inventory(bought_item):
                    append_shop(f"You bought a {chosen_item.name.capitalize()} for {price} Gold.")
                else:
                    # Refund gold if inventory is full
                    self.game.player.gold += price
                    append_shop("Inventory full! Couldn't buy the item.")
            else:
                append_shop("You don't have enough gold to buy this item.")

        buy_btn = tk.Button(action_frame, text="Buy Item", width=20, command=buy_item)
        buy_btn.pack(pady=5)

    def get_item_price(self, item):
        # Simple pricing logic
        if isinstance(item, Weapon):
            return 50 + item.damage * 2  # Example: price based on damage
        elif isinstance(item, Armor):
            return 40 + item.defense * 2  # Example: price based on defense
        elif isinstance(item, Skill):
            return 100
        elif isinstance(item, Craftable):
            return 30
        elif isinstance(item, Consumable):
            return 10
        elif isinstance(item, LockedChest):
            return 0  # Chests are not sold
        else:
            return 10  # Base price for other items


# ====================== Game Class ======================

class Game:
    def __init__(self):
        self.game_time = datetime.now().replace(hour=9, minute=0, second=0, microsecond=0)
        self.player = self.create_player()
        self.current_room = self.create_world()
        self.running = True
        self.all_rooms = self.get_all_rooms()
        self.gui = None

    def create_player(self):
        # Character Class Selection Dialog
        root = tk.Tk()
        root.withdraw()  # Hide the root window

        classes = self.get_character_classes()
        class_names = [cls.name for cls in classes]
        class_descriptions = "\n".join([f"{idx + 1}. {cls.name} - {cls.description}" for idx, cls in enumerate(classes)])
        selected_class = simpledialog.askinteger("Character Class Selection",
                                                 f"Select your character class:\n{class_descriptions}",
                                                 parent=root, minvalue=1, maxvalue=len(classes))
        if selected_class is None:
            root.destroy()
            exit()
        selected_class_obj = classes[selected_class - 1]

        name = ""
        while not name:
            name = simpledialog.askstring("Character Name", "Enter your character's name:", parent=root)
            if not name:
                messagebox.showwarning("Invalid Name", "Name cannot be empty.")
        root.destroy()
        return Player(name, selected_class_obj)

    def get_character_classes(self):
        warrior = CharacterClass(
            "Warrior",
            "A strong melee fighter with high health and defense.",
            base_health=150,
            base_defense=15,
            skills=[PowerStrike(), HealSkill()],
            xp_multiplier=1.0
        )
        mage = CharacterClass(
            "Mage",
            "A master of elemental magic with powerful spells.",
            base_health=100,
            base_defense=5,
            skills=[Fireball(), HealSkill()],
            xp_multiplier=1.2
        )
        archer = CharacterClass(
            "Archer",
            "A skilled marksman with ranged attacks and agility.",
            base_health=120,
            base_defense=10,
            skills=[ArrowShot(), HealSkill()],
            xp_multiplier=1.1
        )
        paladin = CharacterClass(
            "Paladin",
            "A holy warrior with healing and combat prowess.",
            base_health=140,
            base_defense=12,
            skills=[HealSkill(), PowerStrike()],
            xp_multiplier=1.05
        )
        rogue = CharacterClass(
            "Rogue",
            "A stealthy fighter with critical strikes and agility.",
            base_health=110,
            base_defense=8,
            skills=[ArrowShot(), PowerStrike()],
            xp_multiplier=1.15
        )
        return [warrior, mage, archer, paladin, rogue]

    def create_world(self):
        # Define Items
        sword = Weapon("Sword", "A sharp blade gleaming in the light.", damage=20)
        potion = Consumable("Potion", "Heals 25 health points.", heal_amount=25)
        fishing_rod = Craftable("Fishing Rod", "Assembled from a stick and string.", recipe={"stick": 1, "string": 1})
        key = Item("Key", "An old rusty key. It might open something.", quantity=1)
        gold = Gold(quantity=10)
        food = Consumable("Food", "Restores 20 health points.", heal_amount=20)
        water = Consumable("Water", "Quenches your thirst.", heal_amount=10)
        herbs = HealthItem("Herbs", "Medicinal herbs for healing.", heal_amount=15, quantity=3)
        stick = Item("Stick", "A sturdy wooden stick.", quantity=5)
        string = Item("String", "A piece of durable string.", quantity=5)
        bow = Weapon("Bow", "A ranged weapon for attacking from afar.", damage=15)
        dagger = Weapon("Dagger", "A small blade ideal for quick attacks.", damage=10)
        shield = Armor("Shield", "Provides additional defense.", defense=8)
        leather_armor = Armor("Leather Armor", "Light armor made of tough leather.", defense=5)
        chainmail = Armor("Chainmail", "Sturdy chainmail offering better protection.", defense=10)
        # Locked Chest
        locked_chest = LockedChest(
            name="Locked Chest",
            description="A sturdy chest locked tight. It might contain valuable items.",
            locked=True,
            contents=[potion, gold],
            quantity=1
        )

        # Define Monsters
        goblin = Monster("Goblin", health=30, attack_power=8, defense=5, xp_reward=50, gold_reward=20)
        orc = Monster("Orc", health=50, attack_power=12, defense=8, xp_reward=80, gold_reward=35)
        troll = Monster("Troll", health=80, attack_power=15, defense=10, xp_reward=120, gold_reward=50)
        skeleton = Monster("Skeleton", health=25, attack_power=7, defense=4, xp_reward=40, gold_reward=15)
        bandit = Monster("Bandit", health=40, attack_power=10, defense=6, xp_reward=60, gold_reward=25)
        hydra = Monster("Hydra", health=120, attack_power=18, defense=12, attack_chance=0.7,
                       critical_chance=0.15, xp_reward=200, gold_reward=80)
        dark_wizard = Monster("Dark Wizard", health=70, attack_power=20, defense=5,
                              attack_chance=0.8, critical_chance=0.25, xp_reward=250, gold_reward=100)

        # Define Quests
        quest1 = Quest(
            name="Goblin Menace",
            description="Eliminate 3 goblins causing trouble in the cave and collect 2 gold coins.",
            reward=[potion, {"type": "gold", "quantity": 50}],
            objectives=[
                {"objective_type": "kill", "target": "Goblin", "required_amount": 3},
                {"objective_type": "collect", "target": "gold", "required_amount": 2}
            ]
        )
        quest2 = Quest(
            name="Crafting Basics",
            description="Craft a fishing rod using a stick and string.",
            reward=[food],
            objectives=[
                {"objective_type": "craft", "target": "Fishing Rod", "required_amount": 1}
            ]
        )
        quest3 = Quest(
            name="Treasure Hunter",
            description="Find and open the locked chest in the mysterious ruins and reach the sanctuary.",
            reward=[water, herbs],
            objectives=[
                {"objective_type": "chest_opened", "target": "Locked Chest", "required_amount": 1},
                {"objective_type": "reach", "target": "Sanctuary", "required_amount": 1}
            ]
        )
        quest4 = Quest(
            name="Defeat the Dark Wizard",
            description="Defeat the Dark Wizard threatening the village.",
            reward=[chainmail],
            objectives=[
                {"objective_type": "kill", "target": "Dark Wizard", "required_amount": 1}
            ]
        )
        quest5 = Quest(
            name="Skeleton Challenge",
            description="Defeat 5 skeletons lurking in the dungeon.",
            reward=[food, {"type": "gold", "quantity": 30}],
            objectives=[
                {"objective_type": "kill", "target": "Skeleton", "required_amount": 5}
            ]
        )

        # Define NPCs
        elder_mira = FriendlyNPC(
            name="Elder Mira",
            description="A wise elder who knows much about the land.",
            dialogues=[
                (["Ask about the cave", "Ask about the river"], "Greetings, traveler. What would you like to know?", None),
                (["Ask about the cave"], "The cave to the north is home to many goblins. Be careful!", None),
                (["Ask about the river"], "The river flows east and is said to hold mystical properties.", None)
            ],
            quest=quest1,
            gift=sword
        )
        rokhar = UnfriendlyNPC(
            name="Rokhar",
            description="A nasty bandit looking for trouble.",
            dialogues=[
                (["Attack him"], "You'll regret challenging me!", None)
            ],
            monster=bandit
        )
        merchant = FriendlyNPC(
            name="Merchant Tom",
            description="A traveling merchant with various goods.",
            dialogues=[
                (["Buy Potion", "Buy Sword", "Buy Food"], "What would you like to buy?", None),
                (["Buy Potion"], "Here's your Potion for 10 gold.", None),
                (["Buy Sword"], "Here's your Sword for 50 gold.", None),
                (["Buy Food"], "Here's your Food for 5 gold.", None)
            ],
            shop_items=[potion, sword, food]
        )
        sorcerer = FriendlyNPC(
            name="Sage Elara",
            description="A mysterious sorcerer offering knowledge.",
            dialogues=[
                (["Learn Fireball", "Ask about the dark wizard"], "What knowledge do you seek?", None),
                (["Learn Fireball"], "Very well. I have bestowed upon you the Fireball spell.", None),
                (["Ask about the dark wizard"], "The Dark Wizard wields immense power and seeks to dominate us all.", None)
            ],
            quest=quest4,
            gift=Fireball()
        )
        healer = FriendlyNPC(
            name="Healer Lina",
            description="A compassionate healer offering assistance.",
            dialogues=[
                (["Ask for healing", "Ask about herbs"], "How can I assist you today?", None),
                (["Ask for healing"], "You have been healed to full health.", None),
                (["Ask about herbs"], "Herbs are essential for crafting potions and healing items.", None)
            ],
            gift=herbs
        )

        # Define Rooms with Coordinates
        village = Room(
            name="Village",
            description="A peaceful village with friendly inhabitants.",
            x=0,
            y=0,
            items=[leather_armor, potion, gold],
            npcs=[elder_mira, merchant, healer],
            quests=[quest4]
        )
        cave = Room(
            name="Cave",
            description="A dark cave where goblins lurk.",
            x=0,
            y=1,
            monsters=[goblin, goblin, goblin],
            quests=[]
        )
        clearing = Room(
            name="Clearing",
            description="A sunny clearing with a gentle stream.",
            x=1,
            y=1,
            items=[chainmail, bow],
            quests=[quest2]
        )
        river = Room(
            name="River",
            description="A flowing river. The sound of water is soothing.",
            x=1,
            y=2,
            items=[fishing_rod, stick, string, water],
            quests=[]
        )
        mysterious_ruins = Room(
            name="Mysterious Ruins",
            description="Ancient ruins filled with secrets.",
            x=2,
            y=2,
            items=[locked_chest],
            quests=[quest3],
            npcs=[sorcerer]
        )
        castle = Room(
            name="Abandoned Castle",
            description="An old castle with towering walls.",
            x=3,
            y=2,
            items=[key],
            npcs=[rokhar],
            quests=[]
        )
        forest = Room(
            name="Forest",
            description="A dense forest with towering trees and hidden paths.",
            x=-1,
            y=0,
            monsters=[orc],
            quests=[]
        )
        mountain = Room(
            name="Mountain",
            description="A steep mountain offering a challenging climb.",
            x=-1,
            y=1,
            monsters=[troll],
            quests=[]
        )
        dark_fortress = Room(
            name="Dark Fortress",
            description="A foreboding fortress where the Dark Wizard resides.",
            x=2,
            y=3,
            monsters=[dark_wizard],
            quests=[]
        )
        dungeon = Room(
            name="Dungeon",
            description="A dank dungeon filled with the undead.",
            x=3,
            y=1,
            monsters=[skeleton, skeleton, skeleton],
            quests=[quest5]
        )
        oasis = Room(
            name="Oasis",
            description="A refreshing oasis in the midst of the desert.",
            x=4,
            y=2,
            items=[water],
            quests=[]
        )
        sanctuary = Room(
            name="Sanctuary",
            description="A holy place offering peace and refuge.",
            x=2,
            y=4,
            items=[shield],
            quests=[]
        )

        # Connect Rooms
        village.connect_rooms("north", cave)
        cave.connect_rooms("south", village)
        cave.connect_rooms("east", clearing)
        clearing.connect_rooms("west", cave)
        clearing.connect_rooms("east", river)
        river.connect_rooms("west", clearing)
        river.connect_rooms("north", mysterious_ruins)
        mysterious_ruins.connect_rooms("south", river)
        mysterious_ruins.connect_rooms("east", castle)
        castle.connect_rooms("west", mysterious_ruins)
        castle.connect_rooms("east", dark_fortress)
        dark_fortress.connect_rooms("west", castle)
        village.connect_rooms("east", forest)
        forest.connect_rooms("west", village)
        forest.connect_rooms("north", mountain)
        mountain.connect_rooms("south", forest)
        mountain.connect_rooms("east", dungeon)
        dungeon.connect_rooms("west", mountain)
        dungeon.connect_rooms("east", oasis)
        oasis.connect_rooms("west", dungeon)
        oasis.connect_rooms("north", sanctuary)
        sanctuary.connect_rooms("south", oasis)

        return village

    def get_all_rooms(self):
        # Perform BFS to collect all rooms
        visited = set()
        queue = [self.current_room]
        all_rooms = []
        while queue:
            current = queue.pop(0)
            if current in visited:
                continue
            visited.add(current)
            all_rooms.append(current)
            for connected_room in current.connections.values():
                if connected_room not in visited:
                    queue.append(connected_room)
        return all_rooms

    def get_room_by_coordinates(self, x, y):
        for room in self.all_rooms:
            if room.x == x and room.y == y:
                return room
        return None

    def append_text(self, text):
        if self.gui:
            self.gui.append_text(text)

    def battle(self, player, monster, gui):
        if not player.is_alive():
            messagebox.showinfo("Game Over", "You have no lives left. Game Over.")
            gui.root.destroy()
            exit()

        if not monster.is_alive():
            messagebox.showinfo("Already Defeated", f"The {monster.name} has already been defeated.")
            return

        self.append_text(f"=== Battle Start ===\nYou have encountered a {monster.name}!\n")
        gui.initiate_battle(monster)

    def update_quests(self, objective_type, target=None):
        for quest in self.player.quests:
            if not quest.completed:
                quest.update_progress(objective_type, target)
                rewards = quest.complete()
                if rewards:
                    for item in rewards:
                        if isinstance(item, Gold):
                            self.player.gold += item.quantity
                            self.append_text(f"You received {item.quantity} gold.\n")
                        elif isinstance(item, Skill):
                            self.player.add_skill(item)
                        else:
                            self.player.add_to_inventory(item)
                    self.append_text("Your rewards have been added to your inventory.\n")
        # Update GUI if necessary


# ====================== Run the Game ======================

class GameStarter:
    @staticmethod
    def start():
        game = Game()
        root = tk.Tk()
        gui = GameGUI(root, game)
        game.gui = gui
        root.mainloop()


# ====================== Run the Game ======================

if __name__ == "__main__":
    GameStarter.start()