From 55522c289315633ef3ab9f026a764b918e1a4a52 Mon Sep 17 00:00:00 2001 From: marcel Date: Tue, 31 Dec 2024 15:18:59 +0100 Subject: [PATCH 1/9] add a skeleton for the FSM, the basic architecture. --- src/GUI/gameloop.py | 10 ++++++++-- src/states/base_state.py | 21 +++++++++++++++++++++ src/states/game_running.py | 15 +++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/states/base_state.py create mode 100644 src/states/game_running.py diff --git a/src/GUI/gameloop.py b/src/GUI/gameloop.py index 5d2efd4..6b1ac99 100644 --- a/src/GUI/gameloop.py +++ b/src/GUI/gameloop.py @@ -17,6 +17,8 @@ from src.GUI.inventory_gui import InventoryGUI from src.GUI.inventory import Inventory +# import states +from src.states.game_running import GameRunning @dataclass class GUI: @@ -53,6 +55,9 @@ def __post_init__(self): tmx_maps=self.tmx_map["map"], player_start_pos="top_left_island" ) # The start positions will be one of the 4 islands in the corners of the board + # instanciate the initial state + self.state = GameRunning() + def import_assets(self): """load the map""" # The map was made as a basic start for the game, it can be changes or altered if it is better for the overall flow of the game @@ -94,8 +99,9 @@ def setup(self, tmx_maps, player_start_pos): def run(self) -> None: """main loop of the game""" while self.running: - self.handle_events() - self.render() + self.state.handle_events() + self.state.update() + self.state.render() def handle_events(self) -> None: """get events like keypress or mouse clicks""" diff --git a/src/states/base_state.py b/src/states/base_state.py new file mode 100644 index 0000000..bace208 --- /dev/null +++ b/src/states/base_state.py @@ -0,0 +1,21 @@ +from abc import ABC, abstractmethod + + +class BaseState(ABC): + """ + using an abstract class to ensure each state has the right methods + """ + def __init__(self) -> None: + pass + + @abstractmethod + def handle_events(self) -> None: + pass + + @abstractmethod + def update(self): + pass + + @abstractmethod + def render(self) -> None: + pass diff --git a/src/states/game_running.py b/src/states/game_running.py new file mode 100644 index 0000000..db9c404 --- /dev/null +++ b/src/states/game_running.py @@ -0,0 +1,15 @@ +from src.states.base_state import BaseState + + +class GameRunning(BaseState): + def __init__(self) -> None: + super().__init__() + + def update(self): + pass + + def render(self) -> None: + pass + + def handle_events(self) -> None: + pass From ac715a72480d7c29b31c72cbe16bdcae22350e5e Mon Sep 17 00:00:00 2001 From: marcel Date: Sat, 4 Jan 2025 01:19:14 +0100 Subject: [PATCH 2/9] moved game logic to GameRunning and general cleanup --- main.py | 4 +- src/CLI/board.py | 521 --------------------------------- src/CLI/buying.py | 27 -- src/CLI/gameloop.py | 112 ------- src/CLI/inventory.py | 26 -- src/CLI/money.py | 37 --- src/CLI/player.py | 63 ---- src/CLI/selling.py | 19 -- src/GUI/__init__.py | 0 src/__init__.py | 0 src/{GUI => }/gameloop.py | 73 ++--- src/{GUI => }/inventory.py | 0 src/{GUI => }/inventory_gui.py | 0 src/sprites.py | 2 +- src/states/game_running.py | 63 +++- tests/__init__.py | 0 16 files changed, 87 insertions(+), 860 deletions(-) delete mode 100644 src/CLI/board.py delete mode 100644 src/CLI/buying.py delete mode 100644 src/CLI/gameloop.py delete mode 100644 src/CLI/inventory.py delete mode 100644 src/CLI/money.py delete mode 100644 src/CLI/player.py delete mode 100644 src/CLI/selling.py delete mode 100644 src/GUI/__init__.py delete mode 100644 src/__init__.py rename src/{GUI => }/gameloop.py (71%) rename src/{GUI => }/inventory.py (100%) rename src/{GUI => }/inventory_gui.py (100%) delete mode 100644 tests/__init__.py diff --git a/main.py b/main.py index 7a7524c..79c0bab 100644 --- a/main.py +++ b/main.py @@ -23,7 +23,7 @@ # import Pygame specific objects, functions and functionality -from src.GUI.gameloop import GUI +from src.gameloop import GameStateManager class Launcher: @@ -33,7 +33,7 @@ def __init__(self) -> None: if __name__ == "__main__": # version choice is disabled for debugging reasons - game = GUI() + game = GameStateManager() game.run() # print( diff --git a/src/CLI/board.py b/src/CLI/board.py deleted file mode 100644 index 753c628..0000000 --- a/src/CLI/board.py +++ /dev/null @@ -1,521 +0,0 @@ -from random import randrange, choice - -from .inventory import Inventory -from .money import Money -from .selling import ( - chest_wealth, - chest_ancient, - chest_volcanic, - chest_captain, - chest_cursed, - chest_greg, - chest_legend, - chest_mermaid, - chest_rage, - chest_strong, -) -from .buying import basic_quest, medium_quest, hard_quest, drunken_quest - - -player_inventory = Inventory() -player_wallet = Money(currency="", worth=0) - - -class Board: - def __init__(self, players): - - # all the sea of thieves inspired locations you can visit on the board - self.locations: list[str] = [ - "start", - "isle", - "event", - "isle", - "harbor", - "pirate_king", - "isle", - "captain_blazeheart", - "harbor", - "the_syndicate", - "harbor", - "change", - "isle", - "isle", - "pirate_king", - "isle", - "wreckage_isle", - "harbor", - "ghost_brig", - "harbor", - "event", - "isle", - "isle", - "pirate_king", - "harbor", - "captain_blazeheart", - "harbor", - "the_syndicate", - "isle", - "chance", - "harbor", - "harbor", - "pirate_king", - "wreckage_isle", - "dangerous_sea", - "dangerous_sea", - "dangerous_sea", - ] - - # created a list with an index and locations in a dict to handle the game logic of landing on a tile on the board to call a function - self.locations_with_index: list[dict[str, int | str]] = [] - for i, c in enumerate(self.locations): - self.locations_with_index.append({"index": i, "location": c}) - self.current_position = self.locations_with_index[0]["index"] - - # when a player rolls the dice he/she lands on one of these tiles and a function gets called - - # we had the idea to color all the tiles later on but due to time constrains we did not had the time to implement this - self.locations_actions = { - "start": self.visit_start, # yellow - "isle": self.visit_isle, # white - "event": self.visit_event, # green - "harbor": self.visit_harbor, # white - "pirate_king": self.visit_pirate_king, # grey - "captain_blazeheart": self.visit_captain_blazeheart, # grey - "the_syndicate": self.visit_the_syndicate, # red - "change": self.visit_change, # purple - "wreckage_isle": self.visit_wreckage_isle, # brown - "ghost_brig": self.visit_ghost_brig, # black - "dangerous_sea": self.visit_dangerous_sea, # white - } - self.players = {player.player_id: 0 for player in players} - - def visit_locations_by_index(self, index: int): - """ - This handles all the functions in self.locations_actions - When a player lands on a tile the action variable gets called and via that logic a function gets called when it is in the self.locations list - """ - location = self.locations[index] - action = self.locations_actions.get(location) - if action: - action() - - def update_player_position(self, player, total_roll): - """Logic of the game that updates the current player pos to a new player pos when the dices are throwed""" - # current position of the player - current_position = self.players[player.player_id] - # new position after roll - new_position = (current_position + total_roll) % len(self.locations_with_index) - # update current position of the player - self.players[player.player_id] = new_position - return new_position - - def test(self): - """ - This is an old function cause before I was still busy by implementing the logic when you throw the dice, land on a tile, a function gets called - """ - pass - - # print("Board is working!") - - def visit_start(self): - print("You are at the start.") - # self.test() - - # added a randomizer to call different isles you could visit as player - def visit_isle(self): - """The idea was that you could get a random item on the islands you visit, because of time constraints we had no time to implement this.""" - print("You arrived at an isle.") - # isle = "Island" - # isles = [can_cove, cres_isle, lone_cove, m_hide, s_bounty, smug_bay, wand_ref] - # rand_isle = random.randrange(len(isles)) - # print(f"You arrived at {isles[rand_isle]}") - # print("\nThere is a chest on the isle.") - - # we had events you could encounter like sea of thieves does - def visit_event(self): - """You get a little story event which gets you a random reward to your inventory.""" - # print("You encountered an event.\n") - loc_1 = "Red Tornado" - loc_2 = "Green Tornado" - - sea_events = [loc_1, loc_2] - event = choice(sea_events) - print(f"\nYou sail with your crew and on the horizon you see {event}") - - if event == loc_1: - player_inventory.extension(chest_rage.name) - print( - """You see a specific storming red tornado on a horizon and decide to sail and confront it! Uppon arrival you see the legendary Ghost Casper. - You emmidiatly engage in a battle with the Ashen Lord. Its a truely hellish battle. She summons her troops to help her fight you off. - It rains fire balls out of the red sky! Everywhere it shoots fire! You barely survive that fight but in the end you kill the ruthless Lord - and het your reward...""" - ) - print("\nYou get a dangerous Doom Chest!") - return - elif event == loc_2: - player_inventory.extension(chest_cursed.name) - print( - """You see a specific storming green tornado on a horizon and decide to sail and confront it! - You arrive and what you see is a frightening sight. Ghost ships come out of portals appearing on the sea! - You fight them off. You fight them all off! Heaps and heaps of waves of ghos ships appear and you and your - crew fight them until they're all sunk! - You see your price on the see!""" - ) - print("\nYou get a dangerous Cursed Chest!") - return event - - # just as sea of thieves you could visit the different sea posts in the game. - def visit_harbor(self): - """I came up with some star constellations to name these harbors""" - aquila = "The Aquila" - north_star = "The North Star Constellation" - great_har = "The Great Trade Harbor" - steph_spoils = "Stephan's Spoils" - great_bear = "The Great Bear" - phoenix_store = "The Phoenix Store" - orion = "Orion the Hunter" - mermaid = "Mermaid Twins" - - harbors = [ - aquila, - north_star, - great_har, - steph_spoils, - great_bear, - phoenix_store, - orion, - mermaid, - ] - rand_harbor = randrange(len(harbors)) - print(f"You arrived at {harbors[rand_harbor]}") - - # a randomizer of questions the pirate king could ask you if you landed on his tile - def visit_pirate_king(self): - print("You encountered the Pirate King.") - q1 = """What is the main objective in PySeas? - A. Collecting treasure - B. Battling mythical creatures - C. Exploring ancient ruins """ - pirate_king = [q1] - - question = choice(pirate_king) - print(f"Let me ask ye a question, matey! \n{question}") - is_valid_choice = False - while not is_valid_choice: - inp_choice: str = input("So what is it then? ").lower() - is_valid_choice = inp_choice in ['a', 'b', 'c'] - if not is_valid_choice: - print("Didn't they learn you to read, fool? Try again! ") - if question == q1: - if inp_choice == "c": - print( - "I've never heard that one in me life! Piss off! No gold for you..." - ) - elif inp_choice == "b": - print( - "Mate, you'll walk the plank next time if ya give such a pathetic answer again!" - ) - elif inp_choice == "a": - player_inventory.extension(chest_captain.name) - print("Well done, privateer! I'll reward you good for this one.") - # self.test() - - # just as the pirate lord tile, this logic was the same on here - def visit_captain_blazeheart(self): - print("You encountered Captain Blazeheart.") - q1 = """Which of the following is not a type of ship available in PySeas? - A) Brigantine - B) Galleon - C) Sloop """ - - cap_blaze = [q1] - - question = choice(cap_blaze) - print(f"Let me ask ye a question, matey! \n{question}") - is_valid_choice = False - while not is_valid_choice: - inp_choice = input("So what is it then? ").lower() - is_valid_choice = inp_choice in ['a', 'b', 'c'] - if not is_valid_choice: - print("Didn't they learn you to read, sea dog? Try again! ") - if question == q1: - if inp_choice.lower() == "c": - print( - "I've never heard that one in me life! Piss off! No gold for you..." - ) - elif inp_choice.lower() == "b": - print( - "Mate, you'll walk the plank next time if ya give such a pathetic answer again!" - ) - elif inp_choice.lower() == "a": - player_inventory.extension(chest_captain.name) - print("Well done, privateer! I'll reward you good for this one.") - - def visit_the_syndicate(self): - """This is the shop where you can buy quests, sell chests you found or got via events etc""" - - print("You visit the Syndicate.") - while True: - print( - """Welcome to the shop, privateer! There are a couple of actions you could do!: - 1. Choose and buy a quest for a chest! - 2. Check your inventory to see if you have enough gold! - 3. If you have something you'd like to sell, sell it here! - 4. Check how much gold you have! - 5. If you don't mean no bussiness, I'll welcome you another round! Farewell! - """ - ) - choice = input("So, what are ya wating for? What is it?: ") - - if choice == "1": - print( - """Available Quests: - 1. Quest for the burried treasure - 200 gold - 2. Quest for the Lost Chest - 1000 gold - 3. Quest for the Drunken Sailor - 1250 gold - 4. Quest for the The Vault - 1750 gold - """ - ) - quest_choice = input("Wich one would you like to buy, privateer? ") - if quest_choice == "1": - player_wallet.buy_quest(basic_quest) - player_inventory.extension(chest_captain.name) - print( - """You've got a map that leads to the burried treasure. - You make your way all the way to Cutlass Cay and dig out a wonderfull Captain's Chest - """ - ) - elif quest_choice == "2": - player_wallet.buy_quest(medium_quest) - player_inventory.extension(chest_strong.name) - print( - """You got a misterious compass. Although your facing north, the compass doesnt point that way. - It seems that the misterious compass shows you the way to the Lost Treasure! - You quickly gather your crew and sail to the location given by the misterious compass. - You arrive at a vulcanic isle and you see the treasure but its guarded by skeletons. - You and your crew quickly fight them of and take whats yours!""" - ) - elif quest_choice == "3": - player_wallet.buy_quest(hard_quest) - player_inventory.extension(chest_ancient.name) - print( - """The Pirate gives you a location of the Drunken Sailor. The Hoarder adds that he is in a possession of a great price! - You sail forth to the location and you find a port. You ask the locals of they know about the Drunken Sailor. - They tell you about the local tavern. There you find the drunken bastard sleeping with a barrel under his foot. - You slowly but quietly take the barrel out of his possession and you take it onto your ship. You notice that when - holding the treasure your view is distorted and you can’t walk straight. You’re drunk! - It seems to be the effect of holding the barrel. You also hear in your head the famous sea shanty “Drunken Sailor”... - Great! Now you have the chest of thousands Grogs and a shanty in your head...""" - ) - elif quest_choice == "4": - player_wallet.buy_quest(drunken_quest) - player_inventory.extension(chest_greg.name) - print( - """You raise your sails and sail forth to the location. - Uppon arrival it seems that there is no treasure but only another map that leads to a new locations. - You sail to a couple isles with your new maps you keep finding and you arrive at a fort where you find a burned compas. - It leeds you to a isle and the vault on it. You open the vault and see all the treasure it contains. - You take the first chest and suddenly the vault starts to close. You panic and run away with only one chest.""" - ) - else: - print("I don't have that quest in me shop! Try again!") - - elif choice == "2": - player_inventory.show_eq() - - elif choice == "3": - print("This is what you can sell: ") - player_inventory.show_eq() - print( - """Chest's overwiev: - 1. Chest of Wealth - 9500 gold - 2. Legendary Chest - 8600 gold - 3. Captain's Chest - 560 gold - 4. Chest of the Cursed One - 1160 gold - 5. Chest of a the drunken Greg - 2500 gold - 6. Mermaid's Chest - 910 gold - 7. Volcanic Chest - 520 gold - 8. Stronghold's Chest - 2000 gold - 9. Chest of Doom - 3500 gold - 10. Chest of the Ancient - 3000 gold - """ - ) - - while True: - chest_choice = input( - "What would you like to sell? (Press q to quit selling): " - ).lower() - if chest_choice == "q": - break - if chest_choice == "1": - player_wallet.sell_chest(chest_wealth) - player_inventory.remove_item(chest_wealth.name) - elif chest_choice == "2": - player_wallet.sell_chest(chest_legend) - player_inventory.remove_item(chest_legend.name) - elif chest_choice == "3": - player_wallet.sell_chest(chest_captain) - player_inventory.remove_item(chest_captain.name) - elif chest_choice == "4": - player_wallet.sell_chest(chest_cursed) - player_inventory.remove_item(chest_cursed.name) - elif chest_choice == "5": - player_wallet.sell_chest(chest_greg) - player_inventory.remove_item(chest_greg.name) - elif chest_choice == "6": - player_wallet.sell_chest(chest_mermaid) - player_inventory.remove_item(chest_mermaid.name) - elif chest_choice == "7": - player_wallet.sell_chest(chest_volcanic) - player_inventory.remove_item(chest_volcanic.name) - elif chest_choice == "8": - player_wallet.sell_chest(chest_strong) - player_inventory.remove_item(chest_strong.name) - elif chest_choice == "9": - player_wallet.sell_chest(chest_rage) - player_inventory.remove_item(chest_rage.name) - elif chest_choice == "10": - player_wallet.sell_chest(chest_ancient) - player_inventory.remove_item(chest_ancient.name) - print( - "Something else you'd like to sell? (Press q to quit selling) " - ) - - elif choice == "4": - player_wallet.show_money() - elif choice == "5": - print("Farewell, bloody Pirate!") - else: - print("There is no choice like that, try again!") - break - # self.test() - - # just like monopoly you could get a change card, because of time constraints couldn't we get this finished in time - def visit_change(self): - print("You landed on change.") - # self.test() - - # on the board you can land on shipwreck isles, these are also randomized - def visit_wreckage_isle(self): - # print(f"You found {wreck_land[rand_land]}") - wreck_1 = "La Dama Negra" - wreck_2 = "El Impulto" - wreck_3 = "The Black Pearl" - land = "Nassau" - - wreck_land = [wreck_1, wreck_2, wreck_3, land] - # rand_land = random.randrange(len(wreck_land)) - random_item = choice(wreck_land) - print("You dwell with your crew through the sea and find", random_item) - - if random_item == wreck_1: - player_inventory.extension(chest_mermaid.name) - print( - """A great man'o'war galleon that once was feared on the seas... - Now, its just a wreck. It seems a Pirate King has defeated the Legendary ship. - You dive into the water and swim into the shipwreck.!""" - ) - print("\nYou find a Coral Marauder's Chest!") - elif random_item == wreck_2: - player_inventory.extension(chest_strong.name) - print( - """Once the most powerfull ship that these seas have ever seen. Fast, a strong fire power and mighty ram attack. - Now it's just shipwreck with many others, but its legend goes on... - You dive into the water and swim into the shipwreck.""" - ) - print("\nYou find a Stronghold chest!") - elif random_item == wreck_3: - player_inventory.extension(chest_legend.name) - print( - """Every Pirate knows the legend of the Black Pearl! Once the fastest ship on the seas, a ghost ship, with black sails, - a damned crew and a Captain so evil that hell itself spat him back out... - It has been told that people entering the black pearl never came back, but then, where are the stories comming from? - You dive into the water and swim into the shipwreck.""" - ) - print("\n You find a Chest of the Damned!") - - # we wanted to have this as a jail, we could not get to this and hope we may get it later if we refactor the game to pygame for instance - def visit_ghost_brig(self): - print("You are visting the Ghost Brig.") - # self.test() - - # in sea of thieves you have boundaries, we made some tiles the red sea, maybe redundant but in practice it is okay for what you do with it - def visit_dangerous_sea(self): - """We need another name for red sea.""" - print("You are sailing in the dangerous sea.") - # self.test() - - # this is an higher order function that calls the other functions to print out the board - def print(self): - # After the function gets called it prints out this format of the board - # ROW C - # _________________________________________________ - # | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | - # | 17 | | 28 | - # R | 16 | | 29 | R - # O | 15 | | 30 | O - # W | 14 | | 31 | W - # | 13 | | 32 | - # B | 12 | | 33 | D - # | 11 | | 34 | - # | 10 |_______________________________________| 35 | - # | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | - - # ROW A - self.print_header_board() - self.print_left_right_board() - self.print_footer_board() - - def print_header_board(self) -> None: - """Prints out the header of the board""" - - row_c = self.locations_with_index[18:28] - print(" ._________________________________________________. ") - - # creates a new list with all the index numbers - new_row_c = [str(cell["index"]) for cell in row_c] - # print(new_row_c) - - # print a '|' between every index number - header = " | ".join(new_row_c) - - # print the header with a '|' on both ends - print(" | " + header + " | ") - - def print_left_right_board(self) -> None: - """Prints the left/right border of the board""" - - # haal de rijen met indexnummers op - row_b = self.locations_with_index[10:18] - row_d = self.locations_with_index[28:36] - - # for row_b we had to make it reverse, because of in the assignment we had to make the board clockwise, otherwise it could not work. - # reverse de list - row_b.reverse() - - # loops through all the index numbers - for i in range(len(row_b)): - cell_column_b = row_b[i]["index"] - cell_column_d = row_d[i]["index"] - - board_row = f" | {cell_column_b} | | {cell_column_d} |" - print(board_row) - - def print_footer_board(self) -> None: - """Prints the footer of the board""" - - # gets the first row of all index numbers - row_a = self.locations_with_index[0:10] - - row_a.reverse() - - # new list with index numbers only - new_row_a = [] - - # format the index numbers and append them to the list - for cell in row_a: - new_row_a.append(f"{cell['index']:>02}") - - # create a formatted str from the formatted index numbers - formatted_row = " | ".join(new_row_a) - print(f" | {formatted_row} | ") \ No newline at end of file diff --git a/src/CLI/buying.py b/src/CLI/buying.py deleted file mode 100644 index 70b88a5..0000000 --- a/src/CLI/buying.py +++ /dev/null @@ -1,27 +0,0 @@ -from dataclasses import dataclass - - -@dataclass -class Buying: - """There are some problems with how the game perceives buying items. it needs to be fixed or it needs to be reworked entirely.""" - - name: str - quest_type: str - quest_worth: int - - -basic_quest = Buying( - name="Quest for the burried treasure", quest_type="Basic Quest", quest_worth=200 -) - -medium_quest = Buying( - name="Quest to the Lost Treasure", quest_type="Medium Quest", quest_worth=1000 -) - -drunken_quest = Buying( - name="Quest for the Drunken Sailor", quest_type="Drunken Quest", quest_worth=1250 -) - -hard_quest = Buying( - name="Quest for the Secret Vault", quest_type="Hard Quest", quest_worth=1750 -) diff --git a/src/CLI/gameloop.py b/src/CLI/gameloop.py deleted file mode 100644 index 818ce42..0000000 --- a/src/CLI/gameloop.py +++ /dev/null @@ -1,112 +0,0 @@ -import os - -# import dataclasses and typchecking -from dataclasses import dataclass, field -from typing import List - -from src.CLI.board import Board -from src.CLI.player import Player - - -@dataclass -class CLI: - """Command Line Interface, only using print statements""" - - players: List[Player] = field(init=False) - board: Board = field(init=False) - current_player_index: int = 0 - running: bool = False - - def __post_init__(self): - self.clear_screen() - self.players = [ - Player(name_of_player="player 1", player_id=0), - Player(name_of_player="player 2", player_id=1), - ] - self.board = Board(self.players) - - # logic which players turn it is - def toggle_player_index(self): - if self.current_player_index == 0: - self.current_player_index = 1 - else: - self.current_player_index = 0 - - @staticmethod - def clear_screen(): - """A function made to clear the screen in the Python boardgame. - Could be used or altered in Pygame if needed.""" - - os.system("cls" if os.name == "nt" else "clear") - - def run(self): - self.initialize_game() - self.running = True - while self.running: - start_the_game = input( - "Would you like to start the game? Press enter to continue. " - ) - if start_the_game == "": - self.clear_screen() - break - self.board.print() - self.player_switch(self.players) - - def player_switch(self, players): - """This decides what the current player pos is and then switch to the other player, when the player gets asked to end the turn""" - - while True: - current_player = players[self.current_player_index] - print( - f"\nIt's {current_player.name_of_player.capitalize()}'s turn! Current position is {self.board.players[self.current_player_index]}" - ) - - # end_turn = False - - while True: - roll_dice = input("\nDo you want to roll the dice? (yes/y) \n") - if roll_dice.lower() in ["yes", "y"]: - # current_player.dice_roll() - current_player.move_player(self.board) - if current_player: - current_player.perform_action( - self.board, - new_position=self.board.players[self.current_player_index], - ) - break - else: - print( - "You made a mistake, you can only answer yes to roll the dice!" - ) - continue - - stop_turn = input("\nDo you want to end your turn? (yes/no) \n") - if stop_turn.lower() in ["yes", "y"]: - self.toggle_player_index() - Board(players).print() - elif stop_turn.lower() in ["no", "n"]: - print("\nYou must continue your turn.") - self.toggle_player_index() - Board(players).print() - else: - print("Input error. Please answer yes or no.") - - def initialize_game(self): - print("Ahoy Mateyy To PySeas!\n") - - for player in self.players: - while True: - name = input(f"Enter player {player.player_id+1} name: ").strip() - if 2 <= len(name) <= 10 and name.isalpha(): - player.name_of_player = name - break - else: - print( - "Input error. Player name should be between two characters and maximum ten characters containing only alphabets." - ) - continue - - # print(f"\nAhoy {self.players[0].name_of_player.capitalize()} & {self.players[1].name_of_player.capitalize()} to the start of the game!\n") - for player in self.players: - player.print_info() - return self.players diff --git a/src/CLI/inventory.py b/src/CLI/inventory.py deleted file mode 100644 index 4196700..0000000 --- a/src/CLI/inventory.py +++ /dev/null @@ -1,26 +0,0 @@ -from dataclasses import dataclass, field -from typing import List - - -@dataclass -class Inventory: - """The inventory has some problems with appending the right items, also because we use lists it can become a bit disorganised to store those items""" - - items: List[str] = field(default_factory=list) - - def extension(self, new_item: str) -> None: - self.items.append(new_item) - - def remove_item(self, sell_item: str) -> None: - if sell_item in self.items: - self.items.remove(sell_item) - else: - print("There is no such item in your inventory!") - - def show_eq(self) -> None: - if not self.items: - print("Your inventory is empty!") - else: - print("Your inventory:") - for item in self.items: - print(item) diff --git a/src/CLI/money.py b/src/CLI/money.py deleted file mode 100644 index b3a0fc8..0000000 --- a/src/CLI/money.py +++ /dev/null @@ -1,37 +0,0 @@ -from dataclasses import dataclass - - -@dataclass -class Money: - """The money class should be called something else perhaps, we should""" - - currency: str - worth: int - max_worth: int = 1500000 - # self.purchase = basic_quest - - def buy(self, purchase_amount) -> None: - self.worth -= purchase_amount - self.worth = max(self.worth, 0) - - def buy_quest(self, quest) -> None: - if self.worth >= quest.quest_worth: - print( - f"You have purchesed {quest.name} for {quest.quest_worth} {self.currency}" - ) - self.buy(quest.quest_worth) - else: - print("You don't have enough gold to buy a quest!") - - def sell(self, sell_amount) -> None: - self.worth += sell_amount - - def sell_chest(self, chest) -> None: - if self.worth <= chest.chest_worth and self.worth >= chest.chest_worth: - print(f"You sold {chest.name} for {chest.chest_worth} {self.currency}") - self.sell(chest.chest_worth) - else: - ("You don't have the items to sell!") - - def show_money(self) -> None: - print(f"You have now an impressive amount of {self.worth} {self.currency}") diff --git a/src/CLI/player.py b/src/CLI/player.py deleted file mode 100644 index 74a38eb..0000000 --- a/src/CLI/player.py +++ /dev/null @@ -1,63 +0,0 @@ -from random import randint -from dataclasses import dataclass, field -from typing import List - -from .money import Money -from .inventory import Inventory - - -@dataclass -class Player: - - name_of_player: str - player_id: int - inventory: List[str] = field(default_factory=list) - wallet: int = 0 - board_index: int = 0 - - def __post_init__(self): - self.inventory = Inventory().items - self.wallet = Money(currency="gold", worth=0).worth - - # self.roll_history_p1 = [] - # self.roll_history_p2 = [] - - def print_info(self): - print( - f"Arrr.. Mateyy {self.name_of_player.capitalize()} Down below you can see your stats:\n" - ) - print( - f"Player: {self.name_of_player.capitalize()}\nInventory: {self.inventory}\nWallet: {self.wallet}\nBoard index: {self.board_index}\n" - ) - - def dice_roll(self): - roll_results = [randint(1, 6) for _ in range(2)] - total_roll = sum(roll_results) - print( - f"\n{self.name_of_player.capitalize()} throws the dices {roll_results[0]} and {roll_results[1]}. You rolled a total of {sum(roll_results)}\n" - ) - # if self.player_id == 1: - # self.roll_history_p1.append(roll_results) - # elif self.player_id == 2: - # self.roll_history_p2.append(roll_results) - # print(f"Roll results p1: {self.roll_history_p1}\nRoll results p2: {self.roll_history_p2}") - return total_roll - - def move_player(self, board): - """ - This is the logic of the game, it moves the player when dices are throwed, - and updates the player pos to the new pos - """ - new_position = board.update_player_position( - player=self, total_roll=self.dice_roll() - ) - print( - f"{self.name_of_player.capitalize()} moves to {board.locations_with_index[new_position]}\n" - ) - return new_position - - def perform_action(self, board, new_position): - """Made this function to call the board everytime the player got on a tile to call the function""" - board.visit_locations_by_index(new_position) - if new_position in board.locations_actions: - board.locations_actions[new_position]() diff --git a/src/CLI/selling.py b/src/CLI/selling.py deleted file mode 100644 index 93387a1..0000000 --- a/src/CLI/selling.py +++ /dev/null @@ -1,19 +0,0 @@ -class Selling: - def __init__(self, name: str, chest_worth: str) -> None: - self.name = name - self.chest_worth = chest_worth - - def __str__(self): - return f"{self.name}" - - -chest_wealth = Selling(name="Chest of Wealth", chest_worth="9500") -chest_legend = Selling(name="Legendary Chest", chest_worth="8600") -chest_captain = Selling(name="Captain's Chest", chest_worth="560") -chest_cursed = Selling(name="Chest of the Cursed One", chest_worth="1160") -chest_greg = Selling(name="Chest of the drunken Greg", chest_worth="2500") -chest_mermaid = Selling(name="Mermaid's Chest", chest_worth="910") -chest_volcanic = Selling(name="Volcanic Chest", chest_worth="520") -chest_strong = Selling(name="Stronghold Chest", chest_worth="2000") -chest_rage = Selling(name="Chest of Doom", chest_worth="3500") -chest_ancient = Selling(name="Chest of the Ancients", chest_worth="3000") diff --git a/src/GUI/__init__.py b/src/GUI/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/GUI/gameloop.py b/src/gameloop.py similarity index 71% rename from src/GUI/gameloop.py rename to src/gameloop.py index 6b1ac99..dd48209 100644 --- a/src/GUI/gameloop.py +++ b/src/gameloop.py @@ -1,40 +1,23 @@ from os.path import join -import sys import json -# import dataclasses and typchecking -from dataclasses import dataclass, field - # import pygame related import pygame from pytmx.util_pygame import load_pygame # type: ignore -# import Pygame specific objects, functions and functionality from src.settings import SCREEN_WIDTH, SCREEN_HEIGHT, TILE_SIZE import src.sprites -# import inventory related classes -from src.GUI.inventory_gui import InventoryGUI -from src.GUI.inventory import Inventory - # import states from src.states.game_running import GameRunning -@dataclass class GUI: """Graphial User Interface vertion of the game, using pygame-ce""" - screen_size: tuple[int, int] = (SCREEN_WIDTH, SCREEN_HEIGHT) - screen: pygame.Surface = field(init=False) - - # groups - # all_sprites: pygame.sprite.Group = field( - # init=False, default_factory=pygame.sprite.Group - # ) def __post_init__(self): pygame.init() - self.screen = pygame.display.set_mode(self.screen_size) + self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("PySeas") self.clock = pygame.Clock() @@ -81,10 +64,13 @@ def import_assets(self): def setup(self, tmx_maps, player_start_pos): """create tiles""" + self.tmx_map = { + "map": load_pygame(join(".", "data", "maps", "100x100_map.tmx")) + } + # Islands islands = tmx_maps.get_layer_by_name("Islands") for x, y, surface in islands.tiles(): - # print(x * TILE_SIZE, y * TILE_SIZE, surface) src.sprites.Tile( self.all_sprites, pos=(x * TILE_SIZE, y * TILE_SIZE), @@ -96,24 +82,6 @@ def setup(self, tmx_maps, player_start_pos): if obj.name == "Player" and obj.properties["pos"] == player_start_pos: self.player = src.sprites.Player((obj.x, obj.y), self.all_sprites) - def run(self) -> None: - """main loop of the game""" - while self.running: - self.state.handle_events() - self.state.update() - self.state.render() - - def handle_events(self) -> None: - """get events like keypress or mouse clicks""" - for event in pygame.event.get(): - match event.type: - case pygame.QUIT: - pygame.quit() - sys.exit() - case pygame.KEYDOWN: - if event.key == pygame.K_i: # Toggle inventory with "I" key - self.toggle_inventory() - def toggle_inventory(self): """Toggle the inventory overlay.""" self.inventory_gui.running = not self.inventory_gui.running @@ -143,15 +111,26 @@ def load_inventory_from_json(self, file_path: str): except (FileNotFoundError, json.JSONDecodeError): print(f"Error: The file at {file_path} does not exist.") - def render(self) -> None: - """draw sprites to the canvas""" - self.screen.fill("#000000") - self.all_sprites.update() - self.all_sprites.draw(self.player.rect.center, self.player.player_preview, self.player.player_preview_rect) - '''No need to loop through the players because it is now in the sprite group AllSprites''' - # draw players on top of the other sprites - # for player in self.players: - # player.render(surface=self.screen) +class GameStateManager: + def __init__(self): + + # init pygame + pygame.init() + self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("PySeas") - pygame.display.update() + self.clock = pygame.Clock() + self.running = True + + # instanciate the initial state + self.state = GameRunning() + + + def run(self) -> None: + """main loop of the game""" + while self.running: + self.state.handle_events() + # state.update() return self or a new state + self.state = self.state.update() + self.state.render(self.screen) diff --git a/src/GUI/inventory.py b/src/inventory.py similarity index 100% rename from src/GUI/inventory.py rename to src/inventory.py diff --git a/src/GUI/inventory_gui.py b/src/inventory_gui.py similarity index 100% rename from src/GUI/inventory_gui.py rename to src/inventory_gui.py diff --git a/src/sprites.py b/src/sprites.py index 54827d9..b6feabd 100644 --- a/src/sprites.py +++ b/src/sprites.py @@ -3,7 +3,7 @@ import pygame from pygame import FRect from src.settings import TILE_SIZE, SCREEN_HEIGHT, SCREEN_WIDTH -from src.GUI.inventory import Inventory +from src.inventory import Inventory # class Entity(pygame.sprite.Sprite): diff --git a/src/states/game_running.py b/src/states/game_running.py index db9c404..980057c 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -1,15 +1,68 @@ +import sys +import os +import pygame +from pytmx.util_pygame import load_pygame # type: ignore from src.states.base_state import BaseState +from src.settings import SCREEN_WIDTH, SCREEN_HEIGHT, TILE_SIZE +import src.sprites class GameRunning(BaseState): def __init__(self) -> None: super().__init__() - def update(self): - pass + # Initialize player inventory + # self.player_inventory = Inventory() + # self.inventory_gui = InventoryGUI(self.screen, self.player_inventory) - def render(self) -> None: - pass + # Load initial inventory items from JSON file + # self.load_inventory_from_json("data/inventory.json") + + self.all_sprites = src.sprites.AllSprites() + + # The start positions will be one of the 4 islands in the corners of the board + self.setup(player_start_pos="top_left_island") + + def setup(self, player_start_pos): + + self.tmx_map = { + "map": load_pygame(os.path.join(".", "data", "maps", "100x100_map.tmx")) + } + + # Islands + islands = self.tmx_map['map'].get_layer_by_name("Islands") + for x, y, surface in islands.tiles(): + src.sprites.Tile( + self.all_sprites, + pos=(x * TILE_SIZE, y * TILE_SIZE), + surf=surface, + ) + + # Objects + for obj in self.tmx_map['map'].get_layer_by_name("Ships"): + if obj.name == "Player" and obj.properties["pos"] == player_start_pos: + self.player = src.sprites.Player((obj.x, obj.y), self.all_sprites) + + def update(self) -> BaseState: + return self + + def render(self, screen) -> None: + """draw sprites to the canvas""" + screen.fill("#000000") + self.all_sprites.update() + self.all_sprites.draw(self.player.rect.center, self.player.player_preview, self.player.player_preview_rect) + + pygame.display.update() def handle_events(self) -> None: - pass + """get events like keypress or mouse clicks""" + for event in pygame.event.get(): + match event.type: + case pygame.QUIT: + pygame.quit() + sys.exit() + case pygame.KEYDOWN: + if event.key == pygame.K_i: # Toggle inventory with "I" key + # TODO : use a paused state to access inventory + pass + # self.toggle_inventory() diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 From 5f52ba1414afcfe9f15bf226e9c40499c54280cd Mon Sep 17 00:00:00 2001 From: marcel Date: Sun, 5 Jan 2025 17:17:50 +0100 Subject: [PATCH 3/9] moved current logic into states --- main.py | 1 - src/gameloop.py | 156 ++++++++-------------------- src/inventory_gui.py | 2 +- src/states/base_state.py | 17 +-- src/states/game_running.py | 56 +++++----- src/states/paused.py | 205 +++++++++++++++++++++++++++++++++++++ 6 files changed, 290 insertions(+), 147 deletions(-) create mode 100644 src/states/paused.py diff --git a/main.py b/main.py index 79c0bab..b48a294 100644 --- a/main.py +++ b/main.py @@ -25,7 +25,6 @@ # import Pygame specific objects, functions and functionality from src.gameloop import GameStateManager - class Launcher: def __init__(self) -> None: pass diff --git a/src/gameloop.py b/src/gameloop.py index dd48209..2409afd 100644 --- a/src/gameloop.py +++ b/src/gameloop.py @@ -1,119 +1,19 @@ -from os.path import join -import json - -# import pygame related +import sys import pygame -from pytmx.util_pygame import load_pygame # type: ignore -from src.settings import SCREEN_WIDTH, SCREEN_HEIGHT, TILE_SIZE -import src.sprites +from src.settings import SCREEN_WIDTH, SCREEN_HEIGHT -# import states +# import state for typehint +from src.states.base_state import BaseState from src.states.game_running import GameRunning -class GUI: - """Graphial User Interface vertion of the game, using pygame-ce""" - - - def __post_init__(self): - pygame.init() - self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) - pygame.display.set_caption("PySeas") - self.clock = pygame.Clock() - - # Initialize player inventory - self.player_inventory = Inventory() - self.inventory_gui = InventoryGUI(self.screen, self.player_inventory) - - # Load initial inventory items from JSON file - self.load_inventory_from_json("data/inventory.json") - - # Players list currently commented out; keeping for potential future use - # self.players: list[src.sprites.Player] = [src.sprites.Player()] - - self.all_sprites = src.sprites.AllSprites() - self.running = True - self.import_assets() - self.setup( - tmx_maps=self.tmx_map["map"], player_start_pos="top_left_island" - ) # The start positions will be one of the 4 islands in the corners of the board - - # instanciate the initial state - self.state = GameRunning() - - def import_assets(self): - """load the map""" - # The map was made as a basic start for the game, it can be changes or altered if it is better for the overall flow of the game - self.tmx_map = { - "map": load_pygame(join(".", "data", "maps", "100x100_map.tmx")) - } - - # # Define the path to the TMX file - # tmx_path = os.path.join('data', 'maps', '100x100_map.tmx') - # sprite_group = pygame.sprite.Group() - - # # Check if the file exists - # if not os.path.exists(self.tmx_maps): - # print(f"Error: The file at {self.tmx_maps} does not exist.") - # return None - - # # Load the TMX file using load_pygame - # tmx_data = load_pygame(tmx_path) - # print(tmx_data.layers) - - def setup(self, tmx_maps, player_start_pos): - """create tiles""" - - self.tmx_map = { - "map": load_pygame(join(".", "data", "maps", "100x100_map.tmx")) - } - - # Islands - islands = tmx_maps.get_layer_by_name("Islands") - for x, y, surface in islands.tiles(): - src.sprites.Tile( - self.all_sprites, - pos=(x * TILE_SIZE, y * TILE_SIZE), - surf=surface, - ) - - # Objects - for obj in tmx_maps.get_layer_by_name("Ships"): - if obj.name == "Player" and obj.properties["pos"] == player_start_pos: - self.player = src.sprites.Player((obj.x, obj.y), self.all_sprites) - - def toggle_inventory(self): - """Toggle the inventory overlay.""" - self.inventory_gui.running = not self.inventory_gui.running - - while self.inventory_gui.running: - for event in pygame.event.get(): - if event.type == pygame.KEYDOWN and event.key == pygame.K_i: - self.inventory_gui.running = False # Close the inventory - elif ( - event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 - ): # Left click - self.inventory_gui.handle_mouse_click(event.pos) - elif event.type == pygame.MOUSEWHEEL: - self.inventory_gui.handle_events(event) - - self.inventory_gui.draw() - pygame.display.flip() # Update the display - - def load_inventory_from_json(self, file_path: str): - """Load initial inventory items from JSON file.""" - try: - with open(file_path, "r") as f: - items = json.load(f) - for item_name, properties in items.items(): - quantity = properties.get("quantity", 1) # Default to 1 if missing - self.player_inventory.add_item(item_name, quantity) - except (FileNotFoundError, json.JSONDecodeError): - print(f"Error: The file at {file_path} does not exist.") - class GameStateManager: - def __init__(self): + """ + Initialise pygame + and the first game state + """ + def __init__(self) -> None: # init pygame pygame.init() @@ -122,15 +22,43 @@ def __init__(self): self.clock = pygame.Clock() self.running = True + self.event: list[pygame.event.Event] = [] + self.states_stack: list[BaseState] = [] # instanciate the initial state - self.state = GameRunning() + self.states_stack.append(GameRunning(self)) + + + def enter_state(self, state: BaseState) -> None: + """ + append state to the stack + """ + self.states_stack.append(state) + def exit_state(self) -> BaseState: + """ + pop and return the last state + """ + return self.states_stack.pop() + + def _handle_events(self): + self.events = pygame.event.get() + for event in self.events: + match event.type: + case pygame.QUIT: + pygame.quit() + sys.exit() def run(self) -> None: """main loop of the game""" while self.running: - self.state.handle_events() - # state.update() return self or a new state - self.state = self.state.update() - self.state.render(self.screen) + self._handle_events() + + # give the pygame events to each states + # to ensure that pygame.event.get() is only called once per frame + self.states_stack[-1].update(self.events) + + self.states_stack[-1].render(self.screen) + + # magic value, use a FPS const in settings or delta time + self.clock.tick(60) diff --git a/src/inventory_gui.py b/src/inventory_gui.py index 2b39118..81f63c2 100644 --- a/src/inventory_gui.py +++ b/src/inventory_gui.py @@ -1,6 +1,6 @@ from typing import Dict, Tuple import pygame -from src.GUI.inventory import Inventory +from src.inventory import Inventory class InventoryGUI: diff --git a/src/states/base_state.py b/src/states/base_state.py index bace208..ef0e83e 100644 --- a/src/states/base_state.py +++ b/src/states/base_state.py @@ -1,3 +1,4 @@ +import pygame from abc import ABC, abstractmethod @@ -5,17 +6,17 @@ class BaseState(ABC): """ using an abstract class to ensure each state has the right methods """ - def __init__(self) -> None: - pass - - @abstractmethod - def handle_events(self) -> None: - pass + def __init__(self, GameStateManager) -> None: + self.GameStateManager = GameStateManager @abstractmethod - def update(self): + def update(self, events): # return self + # update current state + # handel events + # and return current state or another one pass @abstractmethod - def render(self) -> None: + def render(self, screen: pygame.Surface) -> None: + # render current state on a given surface pass diff --git a/src/states/game_running.py b/src/states/game_running.py index 980057c..e7096f5 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -1,22 +1,24 @@ import sys import os import pygame +import json from pytmx.util_pygame import load_pygame # type: ignore + from src.states.base_state import BaseState +from src.states.paused import Paused +from src.inventory import Inventory + from src.settings import SCREEN_WIDTH, SCREEN_HEIGHT, TILE_SIZE import src.sprites class GameRunning(BaseState): - def __init__(self) -> None: - super().__init__() + def __init__(self, GameStateManager) -> None: + super().__init__(GameStateManager) - # Initialize player inventory - # self.player_inventory = Inventory() - # self.inventory_gui = InventoryGUI(self.screen, self.player_inventory) - # Load initial inventory items from JSON file - # self.load_inventory_from_json("data/inventory.json") + # Initialize player inventory + self.player_inventory = Inventory() self.all_sprites = src.sprites.AllSprites() @@ -43,26 +45,34 @@ def setup(self, player_start_pos): if obj.name == "Player" and obj.properties["pos"] == player_start_pos: self.player = src.sprites.Player((obj.x, obj.y), self.all_sprites) - def update(self) -> BaseState: - return self + def load_inventory_from_json(self, file_path: str): + """Load initial inventory items from JSON file.""" + try: + with open(file_path, "r") as f: + items = json.load(f) + for item_name, properties in items.items(): + quantity = properties.get("quantity", 1) # Default to 1 if missing + self.player_inventory.add_item(item_name, quantity) + except (FileNotFoundError, json.JSONDecodeError): + print(f"Error: The file at {file_path} does not exist.") + + def update(self, events) -> BaseState: + """ + update each sprites and handle events + """ + + self.all_sprites.update() + + # get events like keypress or mouse clicks + for event in events: + match event.type: + case pygame.KEYDOWN: + if event.key == pygame.K_i: # Toggle inventory with "I" key + self.GameStateManager.enter_state(Paused(self.GameStateManager, Inventory())) def render(self, screen) -> None: """draw sprites to the canvas""" screen.fill("#000000") - self.all_sprites.update() self.all_sprites.draw(self.player.rect.center, self.player.player_preview, self.player.player_preview_rect) pygame.display.update() - - def handle_events(self) -> None: - """get events like keypress or mouse clicks""" - for event in pygame.event.get(): - match event.type: - case pygame.QUIT: - pygame.quit() - sys.exit() - case pygame.KEYDOWN: - if event.key == pygame.K_i: # Toggle inventory with "I" key - # TODO : use a paused state to access inventory - pass - # self.toggle_inventory() diff --git a/src/states/paused.py b/src/states/paused.py new file mode 100644 index 0000000..3ebb56e --- /dev/null +++ b/src/states/paused.py @@ -0,0 +1,205 @@ +from typing import Dict, Tuple + +import pygame +from src.states.base_state import BaseState +from src.inventory import Inventory # for typehints + +from src.settings import SCREEN_WIDTH, SCREEN_HEIGHT + + +class Paused(BaseState): + def __init__(self, GameStateManager, inventory: Inventory) -> None: + super().__init__(GameStateManager) + + self.inventory = inventory + self.font = pygame.font.Font(None, 36) + self.running = True + + # self.screen is a temp surface, blitted to the display after the rendering + self.screen = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) + + # Scrolling inventory + self.scroll_offset = 0 + self.max_visible_items = 10 + self.item_height = 60 + + # Load sprite sheet and extract the icons (Testing purposes) + # To be replaced when: + # 1) Spritesheet has been decide. 2) A 'Buy', 'Found' or 'Add' in-game feature has been implemented + self.sprite_sheet = pygame.image.load( + "images/tilesets/Treasure+.png" + ).convert_alpha() + self.icons = { + "Gold Coin": self.extract_icon(0, 0), + "Silver Coin": self.extract_icon(16, 0), + "Coin Stack (1)": self.extract_icon(32, 0), + "Coin Stack (2)": self.extract_icon(48, 0), + "Circular Gem": self.extract_icon(64, 0), + "Single Gold Bar": self.extract_icon(0, 16), + "Gold Bar Stack": self.extract_icon(16, 16), + "Treasure Block": self.extract_icon(32, 16), + "Golden Crown": self.extract_icon(0, 32), + "Ornate Cup": self.extract_icon(16, 32), + "Golden Figurine": self.extract_icon(32, 32), + "Simple Sword": self.extract_icon(0, 48), + "Ornate Sword": self.extract_icon(16, 48), + "Double-Bladed Axe": self.extract_icon(32, 48), + "Spear": self.extract_icon(48, 48), + "Circular Shield": self.extract_icon(64, 48), + "Golden Trophy": self.extract_icon(0, 64), + "Candelabra": self.extract_icon(16, 64), + "Potion (Red)": self.extract_icon(0, 80), + "Potion (Blue)": self.extract_icon(16, 80), + "Potion (Green)": self.extract_icon(32, 80), + "Square Jar": self.extract_icon(48, 80), + "Cake": self.extract_icon(0, 96), + "Donut": self.extract_icon(16, 96), + "Bread": self.extract_icon(32, 96), + "Rug Tile": self.extract_icon(0, 112), + "Geometric Pattern": self.extract_icon(16, 112), + "Glowing Orb (Blue)": self.extract_icon(0, 128), + "Glowing Orb (Red)": self.extract_icon(16, 128), + "Glowing Orb (Green)": self.extract_icon(32, 128), + "Golden Ring": self.extract_icon(48, 128), + "Amulet": self.extract_icon(64, 128), + "Scroll": self.extract_icon(0, 144), + "Key": self.extract_icon(16, 144), + "Tool": self.extract_icon(32, 144), + "Dragon (Red)": self.extract_icon(0, 160), + "Dragon (Green)": self.extract_icon(16, 160), + "Dragon (Black)": self.extract_icon(32, 160), + "Dragon (White)": self.extract_icon(48, 160), + "Gem Cluster": self.extract_icon(0, 176), + "Glowing Crystal": self.extract_icon(16, 176), + } + + # Button dimmentions + self.button_width = 100 + self.button_height = 50 + + # Initialize button actions + self.button_actions: Dict[str, Tuple[pygame.Rect, pygame.Rect]] = {} + + # Action messages + self.message = "" + self.message_end_time = 0 # Time to display the message + + def handle_mouse_click(self, mouse_pos) -> None: + """Handle mouse clicks on buttons.""" + for item, (use_button, discard_button) in self.button_actions.items(): + if use_button.collidepoint(mouse_pos): + self.message = self.inventory.use_item(item) # `self.message` stores strings + self.message_end_time = pygame.time.get_ticks() + 3000 # 3 seconds + elif discard_button.collidepoint(mouse_pos): + self.message = self.inventory.remove_item(item, 1) + self.message_end_time = pygame.time.get_ticks() + 4000 # 4 seconds + + def extract_icon(self, x, y, size=16): + """Extract a single icon from the sprite sheet.""" + return self.sprite_sheet.subsurface((x, y, size, size)) + + def draw_buttons(self, x: int, y: int, item: str) -> Tuple[pygame.Rect, pygame.Rect]: + """Draw Use and Discard buttons for a specific item.""" + use_button = pygame.Rect(x, y, self.button_width, self.button_height) + discard_button = pygame.Rect( + x + self.button_width + 10, y, self.button_width, self.button_height + ) + + pygame.draw.rect(self.screen, (0, 255, 0), use_button) # Green + pygame.draw.rect(self.screen, (150, 75, 0), discard_button) # Brown + + use_text = self.font.render("Use", True, (0, 0, 0)) # Black + discard_text = self.font.render("Discard", True, (0, 0, 0)) + + self.screen.blit(use_text, (x + 10, y + 10)) + self.screen.blit(discard_text, (x + self.button_width + 20, y + 10)) + + return use_button, discard_button + + def update(self, events): + for event in events: + match event.type: + case pygame.KEYDOWN: + if event.key == pygame.K_i: + self.GameStateManager.exit_state() + case pygame.MOUSEBUTTONDOWN: + if event.button == 1: + self.inventory_gui.handle_mouse_click(event.pos) + case pygame.MOUSEWHEEL: + self.scroll_offset = max(0, self.scroll_offset - event.y) + max_offset = max( + 0, len(self.inventory.get_items()) - self.max_visible_items + ) + self.scroll_offset = min(self.scroll_offset, max_offset) + + def render(self, screen: pygame.Surface) -> None: + """Draw the inventory overlay.""" + + self.screen.fill((0, 0, 0)) # Solid Black background + + # Reset button actions + self.button_actions = {} + + # Draw the inventory items + items = list(self.inventory.get_items().items()) + visible_items = items[ + self.scroll_offset : self.scroll_offset + self.max_visible_items + ] + y_offset = 50 # Start below the title + + for item, quantity in visible_items: + # Draw icon + if item in self.icons: + self.screen.blit(self.icons[item], (50, y_offset)) + + # Draw quantity next to the icon + quantity_text = self.font.render(f"x{quantity}", True, (255, 255, 255)) + self.screen.blit(quantity_text, (100, y_offset + 5)) + + # Draw item name (move text to the right) + text = self.font.render(item, True, (255, 255, 255)) + self.screen.blit(text, (150, y_offset)) + + # Draw buttons + use_button, discard_button = self.draw_buttons(400, y_offset, item) + + # Store button references for event handling + self.button_actions[item] = (use_button, discard_button) + y_offset += 60 # Move down for the next item + + # Draw hint + hint_text = self.font.render( + "Press 'I' to close inventory", True, (200, 200, 200) + ) # Light gray text + self.screen.blit(hint_text, (50, self.screen.get_height() - 60)) + + # Display action message above the hint + if self.message and pygame.time.get_ticks() < self.message_end_time: + # Render the message text + message_text = self.font.render(self.message, True, (255, 255, 0)) # Yellow + + # Measure the message text size + text_width, text_height = message_text.get_size() + + # Message background + message_bg_x = 40 + message_bg_y = self.screen.get_height() - 120 + message_bg_width = text_width + 20 # Add padding + message_bg_height = text_height + 10 # Add padding + + # Draw background rectangle for the message + pygame.draw.rect( + self.screen, + (0, 0, 0), # Black background + (message_bg_x, message_bg_y, message_bg_width, message_bg_height), + ) + + # Draw the message text on top of the background + self.screen.blit( + message_text, + (message_bg_x + 10, message_bg_y + 5), # Position text with padding + ) + + # blit tmp self.screen to the actual display (screen form the argument) + screen.blit(self.screen, dest=(0, 0)) + pygame.display.flip() # Update the display From f6b8aae51b29a1bcd9cd0ac53746d8830d1d5346 Mon Sep 17 00:00:00 2001 From: marcel Date: Sun, 5 Jan 2025 17:22:40 +0100 Subject: [PATCH 4/9] fix mypy and ruff --- src/states/game_running.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/states/game_running.py b/src/states/game_running.py index e7096f5..eadd506 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -1,4 +1,3 @@ -import sys import os import pygame import json @@ -8,7 +7,7 @@ from src.states.paused import Paused from src.inventory import Inventory -from src.settings import SCREEN_WIDTH, SCREEN_HEIGHT, TILE_SIZE +from src.settings import TILE_SIZE import src.sprites @@ -56,7 +55,7 @@ def load_inventory_from_json(self, file_path: str): except (FileNotFoundError, json.JSONDecodeError): print(f"Error: The file at {file_path} does not exist.") - def update(self, events) -> BaseState: + def update(self, events) -> None: """ update each sprites and handle events """ From edade4d25fe1e9ff3578834244c7512cecd50416 Mon Sep 17 00:00:00 2001 From: marcel Date: Sun, 5 Jan 2025 17:46:50 +0100 Subject: [PATCH 5/9] fix most of pylint --- main.py | 4 ---- setup.py | 5 ++++- src/gameloop.py | 6 ++++++ src/states/base_state.py | 25 ++++++++++++++++--------- src/states/game_running.py | 33 +++++++++++++++++++++++---------- src/states/paused.py | 22 +++++++++++++++++----- 6 files changed, 66 insertions(+), 29 deletions(-) diff --git a/main.py b/main.py index b48a294..867d9bd 100644 --- a/main.py +++ b/main.py @@ -25,10 +25,6 @@ # import Pygame specific objects, functions and functionality from src.gameloop import GameStateManager -class Launcher: - def __init__(self) -> None: - pass - if __name__ == "__main__": # version choice is disabled for debugging reasons diff --git a/setup.py b/setup.py index 284bb9f..49be575 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,7 @@ -"""Could be usefull if we ever make the decision to create a pip package for this project, as for now it is commented out.""" +""" +Could be usefull if we ever make the decision to create a pip package for this project, +as for now it is commented out. +""" # from setuptools import setup diff --git a/src/gameloop.py b/src/gameloop.py index 2409afd..d95c3be 100644 --- a/src/gameloop.py +++ b/src/gameloop.py @@ -1,3 +1,9 @@ +""" +main game loop +structure of the game, using a stack of states +""" + + import sys import pygame diff --git a/src/states/base_state.py b/src/states/base_state.py index ef0e83e..4e1816b 100644 --- a/src/states/base_state.py +++ b/src/states/base_state.py @@ -1,22 +1,29 @@ -import pygame +""" +Base exemple state +each state shall have an update (that loop throug events) +and and render method (who draw on the given surface). +""" from abc import ABC, abstractmethod +import pygame class BaseState(ABC): """ using an abstract class to ensure each state has the right methods """ - def __init__(self, GameStateManager) -> None: - self.GameStateManager = GameStateManager + def __init__(self, game_state_manager) -> None: + self.game_state_manager = game_state_manager @abstractmethod def update(self, events): # return self - # update current state - # handel events - # and return current state or another one - pass + """ + update current state + handel events + and return current state or another one + """ @abstractmethod def render(self, screen: pygame.Surface) -> None: - # render current state on a given surface - pass + """ + render current state on a given surface + """ diff --git a/src/states/game_running.py b/src/states/game_running.py index eadd506..ffed5e4 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -1,6 +1,9 @@ +""" +GameRunning state, where you move the ship +""" import os -import pygame import json +import pygame from pytmx.util_pygame import load_pygame # type: ignore from src.states.base_state import BaseState @@ -12,8 +15,11 @@ class GameRunning(BaseState): - def __init__(self, GameStateManager) -> None: - super().__init__(GameStateManager) + """ + GameRunning state, where you move the ship + """ + def __init__(self, game_state_manager) -> None: + super().__init__(game_state_manager) # Initialize player inventory @@ -25,7 +31,9 @@ def __init__(self, GameStateManager) -> None: self.setup(player_start_pos="top_left_island") def setup(self, player_start_pos): - + """ + setup the map and player from the tiled file + """ self.tmx_map = { "map": load_pygame(os.path.join(".", "data", "maps", "100x100_map.tmx")) } @@ -47,7 +55,7 @@ def setup(self, player_start_pos): def load_inventory_from_json(self, file_path: str): """Load initial inventory items from JSON file.""" try: - with open(file_path, "r") as f: + with open(file_path, "r", encoding="utf-8") as f: items = json.load(f) for item_name, properties in items.items(): quantity = properties.get("quantity", 1) # Default to 1 if missing @@ -64,14 +72,19 @@ def update(self, events) -> None: # get events like keypress or mouse clicks for event in events: - match event.type: - case pygame.KEYDOWN: - if event.key == pygame.K_i: # Toggle inventory with "I" key - self.GameStateManager.enter_state(Paused(self.GameStateManager, Inventory())) + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_i: # Toggle inventory with "I" key + self.game_state_manager.enter_state( + Paused(self.game_state_manager, Inventory()) + ) def render(self, screen) -> None: """draw sprites to the canvas""" screen.fill("#000000") - self.all_sprites.draw(self.player.rect.center, self.player.player_preview, self.player.player_preview_rect) + self.all_sprites.draw( + self.player.rect.center, + self.player.player_preview, + self.player.player_preview_rect + ) pygame.display.update() diff --git a/src/states/paused.py b/src/states/paused.py index 3ebb56e..8565ca1 100644 --- a/src/states/paused.py +++ b/src/states/paused.py @@ -1,3 +1,7 @@ +""" +paused state +holding the inventory +""" from typing import Dict, Tuple import pygame @@ -8,8 +12,12 @@ class Paused(BaseState): - def __init__(self, GameStateManager, inventory: Inventory) -> None: - super().__init__(GameStateManager) + """ + paused state + holding the inventory + """ + def __init__(self, game_state_manager, inventory: Inventory) -> None: + super().__init__(game_state_manager) self.inventory = inventory self.font = pygame.font.Font(None, 36) @@ -25,7 +33,8 @@ def __init__(self, GameStateManager, inventory: Inventory) -> None: # Load sprite sheet and extract the icons (Testing purposes) # To be replaced when: - # 1) Spritesheet has been decide. 2) A 'Buy', 'Found' or 'Add' in-game feature has been implemented + # 1) Spritesheet has been decide. + # 2) A 'Buy', 'Found' or 'Add' in-game feature has been implemented self.sprite_sheet = pygame.image.load( "images/tilesets/Treasure+.png" ).convert_alpha() @@ -117,14 +126,17 @@ def draw_buttons(self, x: int, y: int, item: str) -> Tuple[pygame.Rect, pygame.R return use_button, discard_button def update(self, events): + """ + handle key press and mouse scroll + """ for event in events: match event.type: case pygame.KEYDOWN: if event.key == pygame.K_i: - self.GameStateManager.exit_state() + self.game_state_manager.exit_state() case pygame.MOUSEBUTTONDOWN: if event.button == 1: - self.inventory_gui.handle_mouse_click(event.pos) + self.handle_mouse_click(event.pos) case pygame.MOUSEWHEEL: self.scroll_offset = max(0, self.scroll_offset - event.y) max_offset = max( From 13865502eb7016c772d8e78a7ac374e6a52fc3fe Mon Sep 17 00:00:00 2001 From: marcel Date: Sun, 5 Jan 2025 22:27:08 +0100 Subject: [PATCH 6/9] revert delete CLI --- src/CLI/board.py | 521 +++++++++++++++++++++++++++++++++++++++++++ src/CLI/buying.py | 27 +++ src/CLI/gameloop.py | 112 ++++++++++ src/CLI/inventory.py | 26 +++ src/CLI/money.py | 37 +++ src/CLI/player.py | 63 ++++++ src/CLI/selling.py | 19 ++ 7 files changed, 805 insertions(+) create mode 100644 src/CLI/board.py create mode 100644 src/CLI/buying.py create mode 100644 src/CLI/gameloop.py create mode 100644 src/CLI/inventory.py create mode 100644 src/CLI/money.py create mode 100644 src/CLI/player.py create mode 100644 src/CLI/selling.py diff --git a/src/CLI/board.py b/src/CLI/board.py new file mode 100644 index 0000000..753c628 --- /dev/null +++ b/src/CLI/board.py @@ -0,0 +1,521 @@ +from random import randrange, choice + +from .inventory import Inventory +from .money import Money +from .selling import ( + chest_wealth, + chest_ancient, + chest_volcanic, + chest_captain, + chest_cursed, + chest_greg, + chest_legend, + chest_mermaid, + chest_rage, + chest_strong, +) +from .buying import basic_quest, medium_quest, hard_quest, drunken_quest + + +player_inventory = Inventory() +player_wallet = Money(currency="", worth=0) + + +class Board: + def __init__(self, players): + + # all the sea of thieves inspired locations you can visit on the board + self.locations: list[str] = [ + "start", + "isle", + "event", + "isle", + "harbor", + "pirate_king", + "isle", + "captain_blazeheart", + "harbor", + "the_syndicate", + "harbor", + "change", + "isle", + "isle", + "pirate_king", + "isle", + "wreckage_isle", + "harbor", + "ghost_brig", + "harbor", + "event", + "isle", + "isle", + "pirate_king", + "harbor", + "captain_blazeheart", + "harbor", + "the_syndicate", + "isle", + "chance", + "harbor", + "harbor", + "pirate_king", + "wreckage_isle", + "dangerous_sea", + "dangerous_sea", + "dangerous_sea", + ] + + # created a list with an index and locations in a dict to handle the game logic of landing on a tile on the board to call a function + self.locations_with_index: list[dict[str, int | str]] = [] + for i, c in enumerate(self.locations): + self.locations_with_index.append({"index": i, "location": c}) + self.current_position = self.locations_with_index[0]["index"] + + # when a player rolls the dice he/she lands on one of these tiles and a function gets called + + # we had the idea to color all the tiles later on but due to time constrains we did not had the time to implement this + self.locations_actions = { + "start": self.visit_start, # yellow + "isle": self.visit_isle, # white + "event": self.visit_event, # green + "harbor": self.visit_harbor, # white + "pirate_king": self.visit_pirate_king, # grey + "captain_blazeheart": self.visit_captain_blazeheart, # grey + "the_syndicate": self.visit_the_syndicate, # red + "change": self.visit_change, # purple + "wreckage_isle": self.visit_wreckage_isle, # brown + "ghost_brig": self.visit_ghost_brig, # black + "dangerous_sea": self.visit_dangerous_sea, # white + } + self.players = {player.player_id: 0 for player in players} + + def visit_locations_by_index(self, index: int): + """ + This handles all the functions in self.locations_actions + When a player lands on a tile the action variable gets called and via that logic a function gets called when it is in the self.locations list + """ + location = self.locations[index] + action = self.locations_actions.get(location) + if action: + action() + + def update_player_position(self, player, total_roll): + """Logic of the game that updates the current player pos to a new player pos when the dices are throwed""" + # current position of the player + current_position = self.players[player.player_id] + # new position after roll + new_position = (current_position + total_roll) % len(self.locations_with_index) + # update current position of the player + self.players[player.player_id] = new_position + return new_position + + def test(self): + """ + This is an old function cause before I was still busy by implementing the logic when you throw the dice, land on a tile, a function gets called + """ + pass + + # print("Board is working!") + + def visit_start(self): + print("You are at the start.") + # self.test() + + # added a randomizer to call different isles you could visit as player + def visit_isle(self): + """The idea was that you could get a random item on the islands you visit, because of time constraints we had no time to implement this.""" + print("You arrived at an isle.") + # isle = "Island" + # isles = [can_cove, cres_isle, lone_cove, m_hide, s_bounty, smug_bay, wand_ref] + # rand_isle = random.randrange(len(isles)) + # print(f"You arrived at {isles[rand_isle]}") + # print("\nThere is a chest on the isle.") + + # we had events you could encounter like sea of thieves does + def visit_event(self): + """You get a little story event which gets you a random reward to your inventory.""" + # print("You encountered an event.\n") + loc_1 = "Red Tornado" + loc_2 = "Green Tornado" + + sea_events = [loc_1, loc_2] + event = choice(sea_events) + print(f"\nYou sail with your crew and on the horizon you see {event}") + + if event == loc_1: + player_inventory.extension(chest_rage.name) + print( + """You see a specific storming red tornado on a horizon and decide to sail and confront it! Uppon arrival you see the legendary Ghost Casper. + You emmidiatly engage in a battle with the Ashen Lord. Its a truely hellish battle. She summons her troops to help her fight you off. + It rains fire balls out of the red sky! Everywhere it shoots fire! You barely survive that fight but in the end you kill the ruthless Lord + and het your reward...""" + ) + print("\nYou get a dangerous Doom Chest!") + return + elif event == loc_2: + player_inventory.extension(chest_cursed.name) + print( + """You see a specific storming green tornado on a horizon and decide to sail and confront it! + You arrive and what you see is a frightening sight. Ghost ships come out of portals appearing on the sea! + You fight them off. You fight them all off! Heaps and heaps of waves of ghos ships appear and you and your + crew fight them until they're all sunk! + You see your price on the see!""" + ) + print("\nYou get a dangerous Cursed Chest!") + return event + + # just as sea of thieves you could visit the different sea posts in the game. + def visit_harbor(self): + """I came up with some star constellations to name these harbors""" + aquila = "The Aquila" + north_star = "The North Star Constellation" + great_har = "The Great Trade Harbor" + steph_spoils = "Stephan's Spoils" + great_bear = "The Great Bear" + phoenix_store = "The Phoenix Store" + orion = "Orion the Hunter" + mermaid = "Mermaid Twins" + + harbors = [ + aquila, + north_star, + great_har, + steph_spoils, + great_bear, + phoenix_store, + orion, + mermaid, + ] + rand_harbor = randrange(len(harbors)) + print(f"You arrived at {harbors[rand_harbor]}") + + # a randomizer of questions the pirate king could ask you if you landed on his tile + def visit_pirate_king(self): + print("You encountered the Pirate King.") + q1 = """What is the main objective in PySeas? + A. Collecting treasure + B. Battling mythical creatures + C. Exploring ancient ruins """ + pirate_king = [q1] + + question = choice(pirate_king) + print(f"Let me ask ye a question, matey! \n{question}") + is_valid_choice = False + while not is_valid_choice: + inp_choice: str = input("So what is it then? ").lower() + is_valid_choice = inp_choice in ['a', 'b', 'c'] + if not is_valid_choice: + print("Didn't they learn you to read, fool? Try again! ") + if question == q1: + if inp_choice == "c": + print( + "I've never heard that one in me life! Piss off! No gold for you..." + ) + elif inp_choice == "b": + print( + "Mate, you'll walk the plank next time if ya give such a pathetic answer again!" + ) + elif inp_choice == "a": + player_inventory.extension(chest_captain.name) + print("Well done, privateer! I'll reward you good for this one.") + # self.test() + + # just as the pirate lord tile, this logic was the same on here + def visit_captain_blazeheart(self): + print("You encountered Captain Blazeheart.") + q1 = """Which of the following is not a type of ship available in PySeas? + A) Brigantine + B) Galleon + C) Sloop """ + + cap_blaze = [q1] + + question = choice(cap_blaze) + print(f"Let me ask ye a question, matey! \n{question}") + is_valid_choice = False + while not is_valid_choice: + inp_choice = input("So what is it then? ").lower() + is_valid_choice = inp_choice in ['a', 'b', 'c'] + if not is_valid_choice: + print("Didn't they learn you to read, sea dog? Try again! ") + if question == q1: + if inp_choice.lower() == "c": + print( + "I've never heard that one in me life! Piss off! No gold for you..." + ) + elif inp_choice.lower() == "b": + print( + "Mate, you'll walk the plank next time if ya give such a pathetic answer again!" + ) + elif inp_choice.lower() == "a": + player_inventory.extension(chest_captain.name) + print("Well done, privateer! I'll reward you good for this one.") + + def visit_the_syndicate(self): + """This is the shop where you can buy quests, sell chests you found or got via events etc""" + + print("You visit the Syndicate.") + while True: + print( + """Welcome to the shop, privateer! There are a couple of actions you could do!: + 1. Choose and buy a quest for a chest! + 2. Check your inventory to see if you have enough gold! + 3. If you have something you'd like to sell, sell it here! + 4. Check how much gold you have! + 5. If you don't mean no bussiness, I'll welcome you another round! Farewell! + """ + ) + choice = input("So, what are ya wating for? What is it?: ") + + if choice == "1": + print( + """Available Quests: + 1. Quest for the burried treasure - 200 gold + 2. Quest for the Lost Chest - 1000 gold + 3. Quest for the Drunken Sailor - 1250 gold + 4. Quest for the The Vault - 1750 gold + """ + ) + quest_choice = input("Wich one would you like to buy, privateer? ") + if quest_choice == "1": + player_wallet.buy_quest(basic_quest) + player_inventory.extension(chest_captain.name) + print( + """You've got a map that leads to the burried treasure. + You make your way all the way to Cutlass Cay and dig out a wonderfull Captain's Chest + """ + ) + elif quest_choice == "2": + player_wallet.buy_quest(medium_quest) + player_inventory.extension(chest_strong.name) + print( + """You got a misterious compass. Although your facing north, the compass doesnt point that way. + It seems that the misterious compass shows you the way to the Lost Treasure! + You quickly gather your crew and sail to the location given by the misterious compass. + You arrive at a vulcanic isle and you see the treasure but its guarded by skeletons. + You and your crew quickly fight them of and take whats yours!""" + ) + elif quest_choice == "3": + player_wallet.buy_quest(hard_quest) + player_inventory.extension(chest_ancient.name) + print( + """The Pirate gives you a location of the Drunken Sailor. The Hoarder adds that he is in a possession of a great price! + You sail forth to the location and you find a port. You ask the locals of they know about the Drunken Sailor. + They tell you about the local tavern. There you find the drunken bastard sleeping with a barrel under his foot. + You slowly but quietly take the barrel out of his possession and you take it onto your ship. You notice that when + holding the treasure your view is distorted and you can’t walk straight. You’re drunk! + It seems to be the effect of holding the barrel. You also hear in your head the famous sea shanty “Drunken Sailor”... + Great! Now you have the chest of thousands Grogs and a shanty in your head...""" + ) + elif quest_choice == "4": + player_wallet.buy_quest(drunken_quest) + player_inventory.extension(chest_greg.name) + print( + """You raise your sails and sail forth to the location. + Uppon arrival it seems that there is no treasure but only another map that leads to a new locations. + You sail to a couple isles with your new maps you keep finding and you arrive at a fort where you find a burned compas. + It leeds you to a isle and the vault on it. You open the vault and see all the treasure it contains. + You take the first chest and suddenly the vault starts to close. You panic and run away with only one chest.""" + ) + else: + print("I don't have that quest in me shop! Try again!") + + elif choice == "2": + player_inventory.show_eq() + + elif choice == "3": + print("This is what you can sell: ") + player_inventory.show_eq() + print( + """Chest's overwiev: + 1. Chest of Wealth - 9500 gold + 2. Legendary Chest - 8600 gold + 3. Captain's Chest - 560 gold + 4. Chest of the Cursed One - 1160 gold + 5. Chest of a the drunken Greg - 2500 gold + 6. Mermaid's Chest - 910 gold + 7. Volcanic Chest - 520 gold + 8. Stronghold's Chest - 2000 gold + 9. Chest of Doom - 3500 gold + 10. Chest of the Ancient - 3000 gold + """ + ) + + while True: + chest_choice = input( + "What would you like to sell? (Press q to quit selling): " + ).lower() + if chest_choice == "q": + break + if chest_choice == "1": + player_wallet.sell_chest(chest_wealth) + player_inventory.remove_item(chest_wealth.name) + elif chest_choice == "2": + player_wallet.sell_chest(chest_legend) + player_inventory.remove_item(chest_legend.name) + elif chest_choice == "3": + player_wallet.sell_chest(chest_captain) + player_inventory.remove_item(chest_captain.name) + elif chest_choice == "4": + player_wallet.sell_chest(chest_cursed) + player_inventory.remove_item(chest_cursed.name) + elif chest_choice == "5": + player_wallet.sell_chest(chest_greg) + player_inventory.remove_item(chest_greg.name) + elif chest_choice == "6": + player_wallet.sell_chest(chest_mermaid) + player_inventory.remove_item(chest_mermaid.name) + elif chest_choice == "7": + player_wallet.sell_chest(chest_volcanic) + player_inventory.remove_item(chest_volcanic.name) + elif chest_choice == "8": + player_wallet.sell_chest(chest_strong) + player_inventory.remove_item(chest_strong.name) + elif chest_choice == "9": + player_wallet.sell_chest(chest_rage) + player_inventory.remove_item(chest_rage.name) + elif chest_choice == "10": + player_wallet.sell_chest(chest_ancient) + player_inventory.remove_item(chest_ancient.name) + print( + "Something else you'd like to sell? (Press q to quit selling) " + ) + + elif choice == "4": + player_wallet.show_money() + elif choice == "5": + print("Farewell, bloody Pirate!") + else: + print("There is no choice like that, try again!") + break + # self.test() + + # just like monopoly you could get a change card, because of time constraints couldn't we get this finished in time + def visit_change(self): + print("You landed on change.") + # self.test() + + # on the board you can land on shipwreck isles, these are also randomized + def visit_wreckage_isle(self): + # print(f"You found {wreck_land[rand_land]}") + wreck_1 = "La Dama Negra" + wreck_2 = "El Impulto" + wreck_3 = "The Black Pearl" + land = "Nassau" + + wreck_land = [wreck_1, wreck_2, wreck_3, land] + # rand_land = random.randrange(len(wreck_land)) + random_item = choice(wreck_land) + print("You dwell with your crew through the sea and find", random_item) + + if random_item == wreck_1: + player_inventory.extension(chest_mermaid.name) + print( + """A great man'o'war galleon that once was feared on the seas... + Now, its just a wreck. It seems a Pirate King has defeated the Legendary ship. + You dive into the water and swim into the shipwreck.!""" + ) + print("\nYou find a Coral Marauder's Chest!") + elif random_item == wreck_2: + player_inventory.extension(chest_strong.name) + print( + """Once the most powerfull ship that these seas have ever seen. Fast, a strong fire power and mighty ram attack. + Now it's just shipwreck with many others, but its legend goes on... + You dive into the water and swim into the shipwreck.""" + ) + print("\nYou find a Stronghold chest!") + elif random_item == wreck_3: + player_inventory.extension(chest_legend.name) + print( + """Every Pirate knows the legend of the Black Pearl! Once the fastest ship on the seas, a ghost ship, with black sails, + a damned crew and a Captain so evil that hell itself spat him back out... + It has been told that people entering the black pearl never came back, but then, where are the stories comming from? + You dive into the water and swim into the shipwreck.""" + ) + print("\n You find a Chest of the Damned!") + + # we wanted to have this as a jail, we could not get to this and hope we may get it later if we refactor the game to pygame for instance + def visit_ghost_brig(self): + print("You are visting the Ghost Brig.") + # self.test() + + # in sea of thieves you have boundaries, we made some tiles the red sea, maybe redundant but in practice it is okay for what you do with it + def visit_dangerous_sea(self): + """We need another name for red sea.""" + print("You are sailing in the dangerous sea.") + # self.test() + + # this is an higher order function that calls the other functions to print out the board + def print(self): + # After the function gets called it prints out this format of the board + # ROW C + # _________________________________________________ + # | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | + # | 17 | | 28 | + # R | 16 | | 29 | R + # O | 15 | | 30 | O + # W | 14 | | 31 | W + # | 13 | | 32 | + # B | 12 | | 33 | D + # | 11 | | 34 | + # | 10 |_______________________________________| 35 | + # | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | + + # ROW A + self.print_header_board() + self.print_left_right_board() + self.print_footer_board() + + def print_header_board(self) -> None: + """Prints out the header of the board""" + + row_c = self.locations_with_index[18:28] + print(" ._________________________________________________. ") + + # creates a new list with all the index numbers + new_row_c = [str(cell["index"]) for cell in row_c] + # print(new_row_c) + + # print a '|' between every index number + header = " | ".join(new_row_c) + + # print the header with a '|' on both ends + print(" | " + header + " | ") + + def print_left_right_board(self) -> None: + """Prints the left/right border of the board""" + + # haal de rijen met indexnummers op + row_b = self.locations_with_index[10:18] + row_d = self.locations_with_index[28:36] + + # for row_b we had to make it reverse, because of in the assignment we had to make the board clockwise, otherwise it could not work. + # reverse de list + row_b.reverse() + + # loops through all the index numbers + for i in range(len(row_b)): + cell_column_b = row_b[i]["index"] + cell_column_d = row_d[i]["index"] + + board_row = f" | {cell_column_b} | | {cell_column_d} |" + print(board_row) + + def print_footer_board(self) -> None: + """Prints the footer of the board""" + + # gets the first row of all index numbers + row_a = self.locations_with_index[0:10] + + row_a.reverse() + + # new list with index numbers only + new_row_a = [] + + # format the index numbers and append them to the list + for cell in row_a: + new_row_a.append(f"{cell['index']:>02}") + + # create a formatted str from the formatted index numbers + formatted_row = " | ".join(new_row_a) + print(f" | {formatted_row} | ") \ No newline at end of file diff --git a/src/CLI/buying.py b/src/CLI/buying.py new file mode 100644 index 0000000..70b88a5 --- /dev/null +++ b/src/CLI/buying.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass + + +@dataclass +class Buying: + """There are some problems with how the game perceives buying items. it needs to be fixed or it needs to be reworked entirely.""" + + name: str + quest_type: str + quest_worth: int + + +basic_quest = Buying( + name="Quest for the burried treasure", quest_type="Basic Quest", quest_worth=200 +) + +medium_quest = Buying( + name="Quest to the Lost Treasure", quest_type="Medium Quest", quest_worth=1000 +) + +drunken_quest = Buying( + name="Quest for the Drunken Sailor", quest_type="Drunken Quest", quest_worth=1250 +) + +hard_quest = Buying( + name="Quest for the Secret Vault", quest_type="Hard Quest", quest_worth=1750 +) diff --git a/src/CLI/gameloop.py b/src/CLI/gameloop.py new file mode 100644 index 0000000..818ce42 --- /dev/null +++ b/src/CLI/gameloop.py @@ -0,0 +1,112 @@ +import os + +# import dataclasses and typchecking +from dataclasses import dataclass, field +from typing import List + +from src.CLI.board import Board +from src.CLI.player import Player + + +@dataclass +class CLI: + """Command Line Interface, only using print statements""" + + players: List[Player] = field(init=False) + board: Board = field(init=False) + current_player_index: int = 0 + running: bool = False + + def __post_init__(self): + self.clear_screen() + self.players = [ + Player(name_of_player="player 1", player_id=0), + Player(name_of_player="player 2", player_id=1), + ] + self.board = Board(self.players) + + # logic which players turn it is + def toggle_player_index(self): + if self.current_player_index == 0: + self.current_player_index = 1 + else: + self.current_player_index = 0 + + @staticmethod + def clear_screen(): + """A function made to clear the screen in the Python boardgame. + Could be used or altered in Pygame if needed.""" + + os.system("cls" if os.name == "nt" else "clear") + + def run(self): + self.initialize_game() + self.running = True + while self.running: + start_the_game = input( + "Would you like to start the game? Press enter to continue. " + ) + if start_the_game == "": + self.clear_screen() + break + self.board.print() + self.player_switch(self.players) + + def player_switch(self, players): + """This decides what the current player pos is and then switch to the other player, when the player gets asked to end the turn""" + + while True: + current_player = players[self.current_player_index] + print( + f"\nIt's {current_player.name_of_player.capitalize()}'s turn! Current position is {self.board.players[self.current_player_index]}" + ) + + # end_turn = False + + while True: + roll_dice = input("\nDo you want to roll the dice? (yes/y) \n") + if roll_dice.lower() in ["yes", "y"]: + # current_player.dice_roll() + current_player.move_player(self.board) + if current_player: + current_player.perform_action( + self.board, + new_position=self.board.players[self.current_player_index], + ) + break + else: + print( + "You made a mistake, you can only answer yes to roll the dice!" + ) + continue + + stop_turn = input("\nDo you want to end your turn? (yes/no) \n") + if stop_turn.lower() in ["yes", "y"]: + self.toggle_player_index() + Board(players).print() + elif stop_turn.lower() in ["no", "n"]: + print("\nYou must continue your turn.") + self.toggle_player_index() + Board(players).print() + else: + print("Input error. Please answer yes or no.") + + def initialize_game(self): + print("Ahoy Mateyy To PySeas!\n") + + for player in self.players: + while True: + name = input(f"Enter player {player.player_id+1} name: ").strip() + if 2 <= len(name) <= 10 and name.isalpha(): + player.name_of_player = name + break + else: + print( + "Input error. Player name should be between two characters and maximum ten characters containing only alphabets." + ) + continue + + # print(f"\nAhoy {self.players[0].name_of_player.capitalize()} & {self.players[1].name_of_player.capitalize()} to the start of the game!\n") + for player in self.players: + player.print_info() + return self.players diff --git a/src/CLI/inventory.py b/src/CLI/inventory.py new file mode 100644 index 0000000..4196700 --- /dev/null +++ b/src/CLI/inventory.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass, field +from typing import List + + +@dataclass +class Inventory: + """The inventory has some problems with appending the right items, also because we use lists it can become a bit disorganised to store those items""" + + items: List[str] = field(default_factory=list) + + def extension(self, new_item: str) -> None: + self.items.append(new_item) + + def remove_item(self, sell_item: str) -> None: + if sell_item in self.items: + self.items.remove(sell_item) + else: + print("There is no such item in your inventory!") + + def show_eq(self) -> None: + if not self.items: + print("Your inventory is empty!") + else: + print("Your inventory:") + for item in self.items: + print(item) diff --git a/src/CLI/money.py b/src/CLI/money.py new file mode 100644 index 0000000..b3a0fc8 --- /dev/null +++ b/src/CLI/money.py @@ -0,0 +1,37 @@ +from dataclasses import dataclass + + +@dataclass +class Money: + """The money class should be called something else perhaps, we should""" + + currency: str + worth: int + max_worth: int = 1500000 + # self.purchase = basic_quest + + def buy(self, purchase_amount) -> None: + self.worth -= purchase_amount + self.worth = max(self.worth, 0) + + def buy_quest(self, quest) -> None: + if self.worth >= quest.quest_worth: + print( + f"You have purchesed {quest.name} for {quest.quest_worth} {self.currency}" + ) + self.buy(quest.quest_worth) + else: + print("You don't have enough gold to buy a quest!") + + def sell(self, sell_amount) -> None: + self.worth += sell_amount + + def sell_chest(self, chest) -> None: + if self.worth <= chest.chest_worth and self.worth >= chest.chest_worth: + print(f"You sold {chest.name} for {chest.chest_worth} {self.currency}") + self.sell(chest.chest_worth) + else: + ("You don't have the items to sell!") + + def show_money(self) -> None: + print(f"You have now an impressive amount of {self.worth} {self.currency}") diff --git a/src/CLI/player.py b/src/CLI/player.py new file mode 100644 index 0000000..74a38eb --- /dev/null +++ b/src/CLI/player.py @@ -0,0 +1,63 @@ +from random import randint +from dataclasses import dataclass, field +from typing import List + +from .money import Money +from .inventory import Inventory + + +@dataclass +class Player: + + name_of_player: str + player_id: int + inventory: List[str] = field(default_factory=list) + wallet: int = 0 + board_index: int = 0 + + def __post_init__(self): + self.inventory = Inventory().items + self.wallet = Money(currency="gold", worth=0).worth + + # self.roll_history_p1 = [] + # self.roll_history_p2 = [] + + def print_info(self): + print( + f"Arrr.. Mateyy {self.name_of_player.capitalize()} Down below you can see your stats:\n" + ) + print( + f"Player: {self.name_of_player.capitalize()}\nInventory: {self.inventory}\nWallet: {self.wallet}\nBoard index: {self.board_index}\n" + ) + + def dice_roll(self): + roll_results = [randint(1, 6) for _ in range(2)] + total_roll = sum(roll_results) + print( + f"\n{self.name_of_player.capitalize()} throws the dices {roll_results[0]} and {roll_results[1]}. You rolled a total of {sum(roll_results)}\n" + ) + # if self.player_id == 1: + # self.roll_history_p1.append(roll_results) + # elif self.player_id == 2: + # self.roll_history_p2.append(roll_results) + # print(f"Roll results p1: {self.roll_history_p1}\nRoll results p2: {self.roll_history_p2}") + return total_roll + + def move_player(self, board): + """ + This is the logic of the game, it moves the player when dices are throwed, + and updates the player pos to the new pos + """ + new_position = board.update_player_position( + player=self, total_roll=self.dice_roll() + ) + print( + f"{self.name_of_player.capitalize()} moves to {board.locations_with_index[new_position]}\n" + ) + return new_position + + def perform_action(self, board, new_position): + """Made this function to call the board everytime the player got on a tile to call the function""" + board.visit_locations_by_index(new_position) + if new_position in board.locations_actions: + board.locations_actions[new_position]() diff --git a/src/CLI/selling.py b/src/CLI/selling.py new file mode 100644 index 0000000..93387a1 --- /dev/null +++ b/src/CLI/selling.py @@ -0,0 +1,19 @@ +class Selling: + def __init__(self, name: str, chest_worth: str) -> None: + self.name = name + self.chest_worth = chest_worth + + def __str__(self): + return f"{self.name}" + + +chest_wealth = Selling(name="Chest of Wealth", chest_worth="9500") +chest_legend = Selling(name="Legendary Chest", chest_worth="8600") +chest_captain = Selling(name="Captain's Chest", chest_worth="560") +chest_cursed = Selling(name="Chest of the Cursed One", chest_worth="1160") +chest_greg = Selling(name="Chest of the drunken Greg", chest_worth="2500") +chest_mermaid = Selling(name="Mermaid's Chest", chest_worth="910") +chest_volcanic = Selling(name="Volcanic Chest", chest_worth="520") +chest_strong = Selling(name="Stronghold Chest", chest_worth="2000") +chest_rage = Selling(name="Chest of Doom", chest_worth="3500") +chest_ancient = Selling(name="Chest of the Ancients", chest_worth="3000") From 8f2f25c4d9ad1308d707eaa0a28c0e89e585bd13 Mon Sep 17 00:00:00 2001 From: marcel Date: Sun, 5 Jan 2025 22:57:55 +0100 Subject: [PATCH 7/9] fixed inventory --- src/states/game_running.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/states/game_running.py b/src/states/game_running.py index ffed5e4..6359b71 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -24,6 +24,7 @@ def __init__(self, game_state_manager) -> None: # Initialize player inventory self.player_inventory = Inventory() + self.load_inventory_from_json("data/inventory.json") self.all_sprites = src.sprites.AllSprites() @@ -75,7 +76,7 @@ def update(self, events) -> None: if event.type == pygame.KEYDOWN: if event.key == pygame.K_i: # Toggle inventory with "I" key self.game_state_manager.enter_state( - Paused(self.game_state_manager, Inventory()) + Paused(self.game_state_manager, self.player_inventory) ) def render(self, screen) -> None: From 8aa0ed058009662fea21270c4d8630dd408e52bf Mon Sep 17 00:00:00 2001 From: marcel Date: Wed, 8 Jan 2025 08:31:48 +0100 Subject: [PATCH 8/9] feat: add debug representation, and some stack exeption handling. --- src/gameloop.py | 15 +++++++++++++-- src/states/base_state.py | 3 +++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/gameloop.py b/src/gameloop.py index d95c3be..d27554c 100644 --- a/src/gameloop.py +++ b/src/gameloop.py @@ -9,7 +9,7 @@ from src.settings import SCREEN_WIDTH, SCREEN_HEIGHT -# import state for typehint +# import basestate for typehint from src.states.base_state import BaseState from src.states.game_running import GameRunning @@ -28,12 +28,21 @@ def __init__(self) -> None: self.clock = pygame.Clock() self.running = True - self.event: list[pygame.event.Event] = [] + self.events: list[pygame.event.Event] = [] self.states_stack: list[BaseState] = [] # instanciate the initial state self.states_stack.append(GameRunning(self)) + def __str__(self) -> str: + """ + return a string representing the stack + e.g. : >MainMenu>GameRunning>Paused + """ + stack_repr:str = "" + for state in self.states_stack: + stack_repr += '>' + str(state) + return stack_repr def enter_state(self, state: BaseState) -> None: """ @@ -45,6 +54,8 @@ def exit_state(self) -> BaseState: """ pop and return the last state """ + if len(self.states_stack) == 0: + raise ValueError("the stack is empty") return self.states_stack.pop() def _handle_events(self): diff --git a/src/states/base_state.py b/src/states/base_state.py index 4e1816b..fd50d7e 100644 --- a/src/states/base_state.py +++ b/src/states/base_state.py @@ -14,6 +14,9 @@ class BaseState(ABC): def __init__(self, game_state_manager) -> None: self.game_state_manager = game_state_manager + def __str__(self): + return self.__class__.__name__ + @abstractmethod def update(self, events): # return self """ From b09c79ac76a0371c3faa3648b18fa2d5f3de9527 Mon Sep 17 00:00:00 2001 From: marcel Date: Thu, 9 Jan 2025 16:13:40 +0100 Subject: [PATCH 9/9] fix: add docstrings and renamed gameloop to game_manager --- main.py | 2 +- src/{gameloop.py => game_manager.py} | 13 +++++++++---- src/settings.py | 1 + src/sprites.py | 2 +- src/states/base_state.py | 9 +++++---- src/states/game_running.py | 10 ++++++++-- 6 files changed, 25 insertions(+), 12 deletions(-) rename src/{gameloop.py => game_manager.py} (80%) diff --git a/main.py b/main.py index 867d9bd..cd9ac52 100644 --- a/main.py +++ b/main.py @@ -23,7 +23,7 @@ # import Pygame specific objects, functions and functionality -from src.gameloop import GameStateManager +from src.game_manager import GameStateManager if __name__ == "__main__": diff --git a/src/gameloop.py b/src/game_manager.py similarity index 80% rename from src/gameloop.py rename to src/game_manager.py index d27554c..2c82a9f 100644 --- a/src/gameloop.py +++ b/src/game_manager.py @@ -7,7 +7,7 @@ import sys import pygame -from src.settings import SCREEN_WIDTH, SCREEN_HEIGHT +from src.settings import SCREEN_WIDTH, SCREEN_HEIGHT, FPS # import basestate for typehint from src.states.base_state import BaseState @@ -16,8 +16,13 @@ class GameStateManager: """ - Initialise pygame - and the first game state + Manages the main game loop and the structure of the game using a stack of states. + + This class is responsible for: + - Initializing Pygame and setting up the main screen. + - Managing a stack of game states, allowing for seamless transitions (e.g., from gameplay to paused state). + - Handling Pygame events and delegating them to the active state. + - Running the main game loop with controlled frame rate. """ def __init__(self) -> None: @@ -78,4 +83,4 @@ def run(self) -> None: self.states_stack[-1].render(self.screen) # magic value, use a FPS const in settings or delta time - self.clock.tick(60) + self.clock.tick(FPS) diff --git a/src/settings.py b/src/settings.py index e157871..da849ba 100644 --- a/src/settings.py +++ b/src/settings.py @@ -5,6 +5,7 @@ SCREEN_WIDTH, SCREEN_HEIGHT = 1280, 720 TILE_SIZE = 16 +FPS = 60 if not getattr(pygame, "IS_CE", False): diff --git a/src/sprites.py b/src/sprites.py index b6feabd..9008089 100644 --- a/src/sprites.py +++ b/src/sprites.py @@ -152,7 +152,7 @@ def __init__( There is a known typing error related to missing type parameters for the generic type 'Group' in pygame. You may see warnings like: - src\\sprites.py:107: error: Missing type parameters for generic type 'Group' [type-arg] - - src\\GUI\\gameloop.py:45: error: Missing type parameters for generic type 'SpriteGroup' [type-arg] + - src\\GUI\\game_manager.py:45: error: Missing type parameters for generic type 'SpriteGroup' [type-arg] These errors can be ignored for now, as they are related to type annotations in pygame's codebase. """ diff --git a/src/states/base_state.py b/src/states/base_state.py index fd50d7e..f3db083 100644 --- a/src/states/base_state.py +++ b/src/states/base_state.py @@ -1,7 +1,8 @@ """ -Base exemple state -each state shall have an update (that loop throug events) -and and render method (who draw on the given surface). +Represents a base example state in the game. +Each state must implement: +- `update`: A method that loops through and handles events. +- `render`: A method responsible for drawing the state on the given surface. """ from abc import ABC, abstractmethod import pygame @@ -21,7 +22,7 @@ def __str__(self): def update(self, events): # return self """ update current state - handel events + handle events and return current state or another one """ diff --git a/src/states/game_running.py b/src/states/game_running.py index 6359b71..33cf9a4 100644 --- a/src/states/game_running.py +++ b/src/states/game_running.py @@ -1,5 +1,5 @@ """ -GameRunning state, where you move the ship +Represents the GameRunning state, where the player controls a ship and interacts with the game world. """ import os import json @@ -16,7 +16,13 @@ class GameRunning(BaseState): """ - GameRunning state, where you move the ship + Represents the GameRunning state, where the player controls a ship and interacts with the game world. + + Responsibilities: + - Loads the game map and player starting position. + - Manages player inventory. + - Updates game entities. + - Renders the game world on the screen. """ def __init__(self, game_state_manager) -> None: super().__init__(game_state_manager)