Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions projects/004-zelda-game/python/main.py
Original file line number Diff line number Diff line change
@@ -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()
Empty file.
114 changes: 114 additions & 0 deletions projects/004-zelda-game/python/zelda_game/castle.py
Original file line number Diff line number Diff line change
@@ -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








31 changes: 31 additions & 0 deletions projects/004-zelda-game/python/zelda_game/file_manager.py
Original file line number Diff line number Diff line change
@@ -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

151 changes: 151 additions & 0 deletions projects/004-zelda-game/python/zelda_game/game.py
Original file line number Diff line number Diff line change
@@ -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 <direction>
PICK <item>
DROP <item>
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()
40 changes: 40 additions & 0 deletions projects/004-zelda-game/python/zelda_game/item.py
Original file line number Diff line number Diff line change
@@ -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)
14 changes: 14 additions & 0 deletions projects/004-zelda-game/python/zelda_game/monster.py
Original file line number Diff line number Diff line change
@@ -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
Loading