diff --git a/projects/004-zelda-game/python/main.py b/projects/004-zelda-game/python/main.py index e69de29..c855351 100644 --- a/projects/004-zelda-game/python/main.py +++ b/projects/004-zelda-game/python/main.py @@ -0,0 +1,11 @@ +from zelda_game.game import Game + + +def main() -> None: + """Entry point of the Zelda game application.""" + game = Game() + game.run() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/projects/004-zelda-game/python/zelda_game/__init__.py b/projects/004-zelda-game/python/zelda_game/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/projects/004-zelda-game/python/zelda_game/castle.py b/projects/004-zelda-game/python/zelda_game/castle.py new file mode 100644 index 0000000..e47f417 --- /dev/null +++ b/projects/004-zelda-game/python/zelda_game/castle.py @@ -0,0 +1,114 @@ +from zelda_game.room import Room +from zelda_game.item import Weapon, Treasure, Item +from zelda_game.monster import Monster +from zelda_game.file_manager import read_text_file, load_game_data + + +class Castle: + """Represents the game world composed of interconnected rooms. + + The Castle is responsible for: + - Building rooms from raw data + - Connecting rooms via directional exits + - Placing items inside rooms + - Placing monsters inside rooms + - Unlocking hidden passages when conditions are met + """ + + def __init__(self, rooms_data: dict) -> None: + """Initializes the Castle by building its structure from data. + + Args: + rooms_data (dict): Raw data describing rooms, connections, + items, and monsters. + """ + self.rooms = {} + self._build_rooms(rooms_data) + self._connect_rooms(rooms_data) + self._place_items(rooms_data) + self._place_monster(rooms_data) + + def _build_rooms(self, rooms_data: dict) -> None: + """Creates Room objects from raw data and stores them in the castle. + + Args: + rooms_data (dict): Dictionary containing room definitions. + """ + for room_number, room_data in rooms_data.items(): + self.rooms[int(room_number)] = Room( + room_number, + room_data["description"] + ) + + def _connect_rooms(self, rooms_data: dict) -> None: + """Links rooms together based on directional connections. + + Args: + rooms_data (dict): Dictionary containing room connection data. + """ + for room_number, room_data in rooms_data.items(): + for direction in ["north", "south", "east", "west"]: + if room_data[direction] is not None and room_data[direction] != "exit": + neighbor_number = int(room_data[direction]) + self.rooms[int(room_number)].exits[direction] = self.rooms[neighbor_number] + + def unlock_passage(self, monster_name: str) -> None: + """Unlocks hidden passages depending on defeated monsters. + + Args: + monster_name (str): Name of the defeated monster. + + Note: + This method modifies the map dynamically by creating new connections. + """ + if monster_name == "Medusa": + self.rooms[5].exits["south"] = self.rooms[8] + self.rooms[8].exits["north"] = self.rooms[5] + + elif monster_name == "Dracula": + self.rooms[6].exits["south"] = self.rooms[9] + self.rooms[9].exits["north"] = self.rooms[6] + + def _place_items(self, rooms_data: dict) -> None: + """Places items in their respective rooms. + + Args: + rooms_data (dict): Dictionary containing item placement data. + """ + for room_number, room_data in rooms_data.items(): + for item in room_data['items']: + + if item['type'] == 'weapon': + weapon = Weapon(item['name'], item['value'], item['target']) + self.rooms[int(room_number)].items.append(weapon) + + elif item['type'] == 'treasure': + treasure = Treasure(item["name"], item['value']) + self.rooms[int(room_number)].items.append(treasure) + + elif item['type'] == 'special': + special_item = Item(item['name'], item['value']) + self.rooms[int(room_number)].items.append(special_item) + + def _place_monster(self, rooms_data: dict) -> None: + """Places monsters in the appropriate rooms. + + Args: + rooms_data (dict): Dictionary containing monster data. + """ + for room_number, room_data in rooms_data.items(): + if room_data["monster"] is not None: + monster_data = room_data["monster"] + monster = Monster( + monster_data["name"], + monster_data['weapon_needed'] + ) + self.rooms[int(room_number)].monster = monster + + + + + + + + diff --git a/projects/004-zelda-game/python/zelda_game/file_manager.py b/projects/004-zelda-game/python/zelda_game/file_manager.py new file mode 100644 index 0000000..2943e96 --- /dev/null +++ b/projects/004-zelda-game/python/zelda_game/file_manager.py @@ -0,0 +1,31 @@ +import json + + + +def read_text_file(filename: str) -> str | None: + """Reads and returns the content of a text file.""" + + try: + with open(filename, "r", encoding="utf-8") as file: + return file.read() + + except FileNotFoundError: + print(f"File not found: {filename}") + return None + + +def load_game_data(filename: str) -> dict | None: + """Loads and returns JSON data from a file.""" + + try: + with open(filename, "r", encoding="utf-8") as file: + return json.load(file) + + except FileNotFoundError: + print(f"File not found: {filename}") + return None + + except json.JSONDecodeError: + print(f"Invalid JSON format in file: {filename}") + return None + diff --git a/projects/004-zelda-game/python/zelda_game/game.py b/projects/004-zelda-game/python/zelda_game/game.py new file mode 100644 index 0000000..c831628 --- /dev/null +++ b/projects/004-zelda-game/python/zelda_game/game.py @@ -0,0 +1,151 @@ +from zelda_game.file_manager import read_text_file, load_game_data +from zelda_game.player import Player +from zelda_game.castle import Castle +from zelda_game.user_interface import UserInterface + + +class Game: + """Main game controller. + + This class is responsible for: + - Loading game assets (texts and map data) + - Initializing the game world (Castle) + - Creating the player + - Managing the main game loop and user commands + """ + + def __init__(self) -> None: + """Initializes the game state and loads all required resources.""" + self.text = self._load_text() + self.castle = self._initialize_castle() + self.player = self._create_player() + + def _load_text(self) -> dict: + """Loads all game narrative text from files. + + Returns: + dict: A dictionary containing start, win, and lose messages. + """ + start = read_text_file("zelda_game/text/start.txt") + end_win = read_text_file("zelda_game/text/end_win.txt") + end_lose = read_text_file("zelda_game/text/end_lose.txt") + + return { + "start": start, + "end_win": end_win, + "end_lose": end_lose + } + + def _initialize_castle(self) -> Castle: + """Loads the map data and builds the Castle world. + + Returns: + Castle: The initialized game world. + """ + data_room = load_game_data("zelda_game/text/rooms.json") + return Castle(data_room["rooms"]) + + def _create_player(self) -> Player: + """Creates the player character and places it in the starting room. + + Returns: + Player: The initialized player instance. + """ + player_name = UserInterface.get_knight_name() + return Player(player_name, self.castle.rooms[1]) + + def _get_position(self): + """Returns the current room where the player is located. + + Returns: + Room: The player's current room. + """ + return self.player.current_room + + def _handle_command(self) -> None: + """Main game loop that processes user input commands. + + Supported commands: + MOVE + PICK + DROP + ATTACK + LOOK + EXIT + + The loop continues until the game ends (win/lose/exit condition). + """ + while True: + command = input( + f"{UserInterface.YELLOW}What do you want to do? " + ).strip().split() + + action = command[0].upper() + next_action = " ".join(command[1:]).lower() if len(command) > 1 else None + + match action: + + case "MOVE": + if self.player.move(next_action): + UserInterface.show_room(self.player.current_room) + UserInterface.show_inventory(self.player.inventory) + UserInterface.show_money(self.player.coin) + + case "PICK": + if self.player.pick(next_action) == "princess": + UserInterface.print_message( + self.text["end_win"], + UserInterface.PURPLE + ) + break + + case "DROP": + self.player.drop(next_action) + + case "ATTACK": + result = self.player.attack() + + if result == "killed": + self.castle.unlock_passage( + self.player.current_room.monster.name + ) + elif result == "dead": + UserInterface.print_message( + self.text["end_lose"], + UserInterface.PURPLE + ) + break + else: + print("There is no monster here!") + + case "LOOK": + self.player.look() + + case "EXIT": + if self.player.exit(): + UserInterface.print_message( + self.text["end_win"], + UserInterface.PURPLE + ) + else: + UserInterface.print_message( + self.text["end_lose"], + UserInterface.PURPLE + ) + break + + case _: + UserInterface.print_message( + "Invalid command, try again", + UserInterface.PURPLE + ) + + def run(self) -> None: + """Starts the game loop and displays the initial state.""" + UserInterface.print_message(self.text["start"], UserInterface.PURPLE) + + UserInterface.show_room(self.player.current_room) + UserInterface.show_inventory(self.player.inventory) + UserInterface.show_money(self.player.coin) + + self._handle_command() \ No newline at end of file diff --git a/projects/004-zelda-game/python/zelda_game/item.py b/projects/004-zelda-game/python/zelda_game/item.py new file mode 100644 index 0000000..c48dbab --- /dev/null +++ b/projects/004-zelda-game/python/zelda_game/item.py @@ -0,0 +1,40 @@ +class Item: + """Represents a generic item in the game.""" + + def __init__(self, name: str, value: int) -> None: + """Initializes a generic item. + + Args: + name (str): The name of the item. + value (int): The value or score associated with the item. + """ + self.name = name + self.value = value + + +class Weapon(Item): + """Represents a weapon item that can be used against a specific target.""" + + def __init__(self, name: str, value: int, target: str) -> None: + """Initializes a weapon item. + + Args: + name (str): The name of the weapon. + value (int): The value or power of the weapon. + target (str): The type or name of the monster this weapon is effective against. + """ + super().__init__(name, value) + self.target = target + + +class Treasure(Item): + """Represents a treasure item that can be collected by the player.""" + + def __init__(self, name: str, value: int) -> None: + """Initializes a treasure item. + + Args: + name (str): The name of the treasure. + value (int): The value of the treasure. + """ + super().__init__(name, value) \ No newline at end of file diff --git a/projects/004-zelda-game/python/zelda_game/monster.py b/projects/004-zelda-game/python/zelda_game/monster.py new file mode 100644 index 0000000..a029b5b --- /dev/null +++ b/projects/004-zelda-game/python/zelda_game/monster.py @@ -0,0 +1,14 @@ +class Monster: + """Represents a monster in the game world.""" + + def __init__(self, name: str, weapon_needed: str, is_alive: bool = True) -> None: + """Initializes a monster. + + Args: + name (str): The name of the monster. + weapon_needed (str): The weapon required to defeat the monster. + is_alive (bool, optional): Whether the monster is alive. Defaults to True. + """ + self.name = name + self.weapon_needed = weapon_needed + self.is_alive = is_alive \ No newline at end of file diff --git a/projects/004-zelda-game/python/zelda_game/player.py b/projects/004-zelda-game/python/zelda_game/player.py new file mode 100644 index 0000000..3405b3b --- /dev/null +++ b/projects/004-zelda-game/python/zelda_game/player.py @@ -0,0 +1,136 @@ +from zelda_game.item import Item +from zelda_game.room import Room +from zelda_game.user_interface import UserInterface + +class Player: + """Represents the player character in the game.""" + def __init__(self, name:str, starting_room: Room): + """Initializes the player. + + Args: + + name (str): Player name. + + starting_room (Room): The room where the player starts. + + """ + self.name = name + self.current_room = starting_room + self.has_princess = False + self.inventory:list[Item] = [] + self.coin: int = 0 + + def look(self): + """Displays the current room using the UI.""" + UserInterface.show_room(self.current_room) + + + def move(self, direction:str) -> bool: + """Moves the player to another room if possible. + + Args: + + direction (str): Direction to move (north, south, east, west). + + Returns: + + bool: True if movement was successful, otherwise False. + + """ + if direction not in ["north", "south", "east", "west"]: + print("Invalid direction, try again, use 'north', 'south', 'east', 'west'") + + elif direction in self.current_room.exits and self.current_room.exits[direction] is not None: + self.current_room = self.current_room.exits[direction] + print(f"You have successfully been moved to Room: {self.current_room.name}\n") + return True + else: + print("You can't move in this direction") + + def pick(self, item_name: str): + """Picks up an item from the current room. + + Args: + + item_name (str): Name of the item to pick. + + Returns: + + str | None: "princess" if the princess is rescued, otherwise None. + + """ + for current_item in self.current_room.items: + if current_item.name.lower() == item_name.lower(): + if current_item.name.lower() == "princess": + self.has_princess = True + print("You freed the Princess!") + return "princess" + elif len(self.inventory) < 10: + self.inventory.append(current_item) + self.current_room.items.remove(current_item) + print(f"You picked up {current_item.name}") + return + else: + print("Your bag is full!") + return + print("That item is not in this room") + + def drop(self, item_name: str) -> None: + """Drops an item from inventory into the current room. + + Args: + + item_name (str): Name of the item to drop. + + """ + for item in self.inventory: + if item.name == item_name: + if len(self.current_room.items) < 5: + self.inventory.remove(item) + self.current_room.items.append(item) + print(f"You dropped {item.name}") + + else: + print("You can't drop in this room, no more available space") + return + + print(f"You dont have {item.name} in your bag") + + def attack(self) -> str: + """Attacks the monster in the current room. + + Returns: + + str: Outcome of the attack: + + - "killed" if monster is defeated + + - "dead" if player dies + + - "no_monster" if no monster is present + + """ + if self.current_room.monster and self.current_room.monster.is_alive: + for item in self.inventory: + if item.name == self.current_room.monster.weapon_needed: + print(f"You killed {self.current_room.monster.name}") + self.current_room.monster.is_alive = False + return "killed" + print(f"You cant kill {self.current_room.monster.name} without the {self.current_room.monster.weapon_needed}.") + print(f"{self.current_room.monster.name} attack, you die.") + return "dead" + return "no_monster" + + def exit(self) -> bool: + """Checks if the player can exit the game. + + Returns: + + bool: True if the princess has been rescued, otherwise False. + + """ + return self.has_princess + + + + diff --git a/projects/004-zelda-game/python/zelda_game/room.py b/projects/004-zelda-game/python/zelda_game/room.py new file mode 100644 index 0000000..39ea3a3 --- /dev/null +++ b/projects/004-zelda-game/python/zelda_game/room.py @@ -0,0 +1,17 @@ +class Room: + """Represents a room in the game world.""" + def __init__(self, name:str, description:str) -> None: + """Initializes a room. + + Args: + + name (str): The name of the room. + + description (str): A textual description of the room. + + """ + self.name = name + self.description = description + self.monster = None + self.exits = {"north": None, "south": None, "east": None, "west": None} + self.items = [] \ No newline at end of file diff --git a/projects/004-zelda-game/python/zelda_game/text/end_lose.txt b/projects/004-zelda-game/python/zelda_game/text/end_lose.txt new file mode 100644 index 0000000..55f3997 --- /dev/null +++ b/projects/004-zelda-game/python/zelda_game/text/end_lose.txt @@ -0,0 +1,8 @@ +The Knight fell in the castle's final hall, defeated after a long and arduous journey. +His strength deserted him just a step away from salvation. + +In the highest tower, the Princess waited in vain for someone who would never come. +Silence enveloped the castle, as darkness returned to dominate the kingdom. + +Hope slowly faded... +and the hero's legend remained unfinished. \ No newline at end of file diff --git a/projects/004-zelda-game/python/zelda_game/text/end_win.txt b/projects/004-zelda-game/python/zelda_game/text/end_win.txt new file mode 100644 index 0000000..c6fe6c1 --- /dev/null +++ b/projects/004-zelda-game/python/zelda_game/text/end_win.txt @@ -0,0 +1,8 @@ +The Knight, covered in battle scars, advanced into the castle's final chamber. +After defeating the terrible Medusa in the dark depths and the cruel Dracula in the crimson tower, nothing could stop him. + +In the center of the hall, illuminated by the moonlight, the Princess waited, free from her chains. +With a smile filled with hope, she ran to meet her savior. + +The kingdom was finally saved. +The shadows had vanished, and a new era of peace had begun. \ No newline at end of file diff --git a/projects/004-zelda-game/python/zelda_game/text/rooms.json b/projects/004-zelda-game/python/zelda_game/text/rooms.json new file mode 100644 index 0000000..88f51c7 --- /dev/null +++ b/projects/004-zelda-game/python/zelda_game/text/rooms.json @@ -0,0 +1,97 @@ +{ + "rooms": { + "1": { + "description": "A cold shiver runs down your spine. You feel nervous, surrounded by dark paintings and dust; hanging torches mark this gloomy scene. The castle exit is to the WEST.", + "north": null, + "south": 4, + "east": 2, + "west": "exit", + "items": [], + "monster": null + }, + "2": { + "description": "A large hall with large statues depicting deities unknown to you.", + "north": null, + "south": 5, + "east": 3, + "west": 1, + "items": [{"name": "golden egg", "type": "treasure", "value": 500000, "target": null}], + "monster": null + }, + "3": { + "description": "A long array of gleaming armor gleams in the moonlight streaming in from a window.", + "north": null, + "south": null, + "east": null, + "west": 2, + "items": [ + { + "name": "shield", "type": "weapon", "value": 0, "target": "Medusa" + } + ], + "monster": null + }, + "4": { + "description": "A pile of treasures that would distract anyone from their every thought.", + "north": 1, + "south": null, + "east": null, + "west": null, + "items": [ + { + "name": "golden chalice", "type": "treasure", "value": 500000, "target": null + } + ], + "monster":null + }, + "5": { + "description": "Several stone statues of all ages and sexes fill this gloomy chamber. I wonder who they depict.", + "north": 2, + "south": null, + "east": 6, + "west": null, + "items": [], + "monster":{ + "name": "Medusa", "weapon_needed": "shield" + } + }, + "6": { + "description": "An open coffin stands in the center of the room, flanked by enormous windows.", + "north": null, + "south": null, + "east": null, + "west": 5, + "items": [], + "monster":{ + "name": "Dracula", "weapon_needed": "silver dagger" + } + }, + "7": { + "description": "At first glance, it looks like the room of a sword and dagger collector.", + "north": null, + "south": null, + "east": 8, + "west": null, + "items": [{"name": "silver dagger","type": "weapon","value": 0,"target": "Dracula"}], + "monster":null + }, + "8": { + "description": "A library crammed with books and manuscripts.", + "north": null, + "south": null, + "east": null, + "west": 7, + "items": [{"name": "demonstration problem p=np", "type": "treasure","value": 1000000, "target": null}], + "monster":null + }, + "9": { + "description": "After endless stairs, the castle tower, a room that looks like a bedroom, chained in a corner you can glimpse a silhouette...", + "north": null, + "south": null, + "east": null, + "west": null, + "items": [{"name": "princess", "type": "special", "value": 0}], + "monster": null + } + } +} \ No newline at end of file diff --git a/projects/004-zelda-game/python/zelda_game/text/start.txt b/projects/004-zelda-game/python/zelda_game/text/start.txt new file mode 100644 index 0000000..82d123d --- /dev/null +++ b/projects/004-zelda-game/python/zelda_game/text/start.txt @@ -0,0 +1,16 @@ + + + ______ _ _ _____ + |___ / | | | | / ____| + / / ___ | | __| | __ _ | | __ __ _ _ __ ___ ___ + / / / _ \| | / _` | / _` | | | |_ | / _` || '_ ` _ \ / _ \ + / /__| __/| || (_| || (_| | | |__| || (_| || | | | | || __/ + /_____|\___||_| \__,_| \__,_| \_____| \__,_||_| |_| |_| \___| + + +Princess Zelda of the kingdom of Cperia has been captured by an evil wizard and held prisoner in a castle guarded by monsters. +You have been chosen to free her and bring her home safely. +The castle has nine rooms and only one exit. Within each room, you will find treasures and weapons that may aid you in your noble task. +So, do you have what it takes to slay the monsters and save Princess Zelda? +Are you ready? +Let's do it! \ No newline at end of file diff --git a/projects/004-zelda-game/python/zelda_game/user_interface.py b/projects/004-zelda-game/python/zelda_game/user_interface.py new file mode 100644 index 0000000..471212d --- /dev/null +++ b/projects/004-zelda-game/python/zelda_game/user_interface.py @@ -0,0 +1,93 @@ + +class UserInterface: + """Handles all user interaction and console output formatting.""" + + RED = "\033[91m" + GREEN = "\033[92m" + YELLOW = "\033[93m" + CYAN = "\033[96m" + WHITE = "\033[97m" + PURPLE = "\033[95m" + BLUE = "\033[94m" + RESET = "\033[0m" + + @staticmethod + def print_message(message:str, color: str = ""): + """Prints a colored message to the console. + + Args: + + message (str): The message to display. + + color (str, optional): ANSI color code. Defaults to no color. + + """ + print(f"{color}{message}{UserInterface.RESET}") + + + @staticmethod + def show_room(room) -> None: + """Displays the current room state including exits, items, and monsters. + + Args: + + room: The current Room object. + + """ + UserInterface.print_message(f"Currently you are in Room{room.name}", UserInterface.RED) + + UserInterface.print_message(room.description, UserInterface.RED) + for direction, linked_room in room.exits.items(): + if linked_room is not None: + UserInterface.print_message(f"There is a room to your {direction}", UserInterface.CYAN) + if room.items: + for item in room.items: + UserInterface.print_message(f"The {item.name.upper()} is lying on the floor.", UserInterface.WHITE) + if room.monster: + if room.monster.is_alive: + UserInterface.print_message(f"{room.monster.name} is lurking in the shadows...", UserInterface.RED) + else: + UserInterface.print_message(f"{room.monster.name} lifeless body lies on the ground.", UserInterface.RED ) + + @staticmethod + def get_knight_name() -> str: + """Prompts the user to enter a valid knight name. + + Returns: + + str: The validated player name. + + """ + UserInterface.print_message("What is your name?", UserInterface.YELLOW) + while not (name := input(f"{UserInterface.YELLOW}> {UserInterface.RESET}").strip()): + UserInterface.print_message("A knight without a name is not really a knight!", UserInterface.YELLOW) + return name + + @staticmethod + def show_inventory(inventory: list) -> None: + """Displays the player's inventory. + + Args: + + inventory (list): List of Item objects. + + """ + if inventory: + UserInterface.print_message(f"Your bag contains the following items: " + ", ".join([item.name for item in inventory]), UserInterface.GREEN) + else: + UserInterface.print_message("Currently your bag is empty!", UserInterface.GREEN) + + @staticmethod + def show_money(coins: int) -> None: + + """Displays the player's current coin amount. + + Args: + + coins (int): Number of coins the player has. + + """ + UserInterface.print_message(f"Current cash is: {coins}\n", UserInterface.BLUE) + + + diff --git a/projects/__init__.py b/projects/__init__.py new file mode 100644 index 0000000..e69de29