diff --git a/README.md b/README.md index 23a8368..427d250 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,8 @@ A fantasy console game that I made using Python. ## Requirements - Python 3.6 or higher - - Pygame - - - Colorama - + - Rich - Tkinter ## Usage @@ -17,15 +14,14 @@ A fantasy console game that I made using Python. > Clone the repository to your computer: ```sh -$ git clone https://github.com/Sid110307/ShadowDoom - +$ git clone https://github.com/Sid110307/ShadowDoom.git Cloning into 'ShadowDoom'... -. . . ``` -> Open the `Console` folder in your terminal and type: +> Enter the directory and run the game: ```sh +$ cd ShadowDoom $ python3 ShadowDoom.py # Or just ./ShadowDoom.py for Unix-based systems. ``` diff --git a/ShadowDoom.py b/ShadowDoom.py index 2e77d00..0e951cc 100644 --- a/ShadowDoom.py +++ b/ShadowDoom.py @@ -1,140 +1,95 @@ #!/usr/bin/env python3 -__version__ = "2.0.0" +__version__ = "2.1.0" import base64 import json -import os import random import sys import time -import tkinter from tkinter import filedialog -os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" - try: - import colorama - from colorama import Fore, Back, Style + from utils import tui + from utils import audio + import pygame except ImportError: import subprocess try: print("Error: Missing dependencies!\nInstalling...") - subprocess.call([sys.executable, "-m", "pip", "install", "colorama", "pygame"], - stdout = subprocess.DEVNULL, stderr = subprocess.STDOUT) + subprocess.call([sys.executable, "-m", "pip", "install", "rich", "pygame"], stdout = subprocess.DEVNULL, + stderr = subprocess.STDOUT) except ImportError: - print("Error: Failed to install dependencies!\nPlease install manually!") - print("Dependencies: colorama, pygame") + print("Error: Failed to install dependencies!\nPlease install manually!\nDependencies: rich, pygame") sys.exit() - import colorama - from colorama import Fore, Back, Style - import pygame - - -class TUI: - def __init__(self): - colorama.init() - self.clear() - - @staticmethod - def styled_print(message="", fore_color=Fore.RESET, back_color=Back.RESET, style=Style.NORMAL): - print(f"{style}{fore_color}{back_color}{message}{Style.RESET_ALL}") - - def decorative_header(self, text, width=60, fore_color=Fore.CYAN): - border = "+" + "-" * (width - 2) + "+" - - self.styled_print(border, fore_color) - self.styled_print(f"| {text.center(width - 4)} |", fore_color) - self.styled_print(border, fore_color) - - def section_title(self, text, width=60, fore_color=Fore.GREEN): - self.styled_print(f"\n {text.center(width)} ", fore_color, style = Style.BRIGHT) - self.styled_print("-" * width, fore_color) - - def render_menu(self, options, width=60, fore_color=Fore.YELLOW, border_char="|"): - for i, option in enumerate(options, 1): - option_text = f"{i}. {option}".center(width - 4) - self.styled_print(f"{border_char} {option_text} {border_char}", fore_color) - - self.styled_print("+" + "-" * (width - 2) + "+", fore_color) - - def key_value_display(self, items, width=60, fore_color=Fore.YELLOW, border_char="|"): - for key, value in items.items(): - key_text = f"{key}: {value}".ljust(width - 4) - self.styled_print(f"{border_char} {key_text} {border_char}", fore_color) - self.styled_print("+" + "-" * (width - 2) + "+", fore_color) + from utils import tui + from utils import audio - def list_display(self, items, center=False, width=60, fore_color=Fore.YELLOW, border_char="|"): - for item in items: - item_text = " | ".join([f"{key}: {value}" for key, value in item.items()]).strip() - item_text = item_text.center(width - 4) if center else item_text.ljust(width - 4) - - self.styled_print(f"{border_char} {item_text} {border_char}", fore_color) - self.styled_print("+" + "-" * (width - 2) + "+", fore_color) - - def table_display(self, items, width=60, fore_color=Fore.YELLOW, border_char="|"): - headers = [header for header in items[0] if isinstance(items[0], dict)] - column_widths = { - header: max(len(str(header)), *(len(str(row[header])) for row in items if isinstance(row, dict))) - for header in headers - } - - total_table_width = sum(column_widths.values()) + len(headers) * 3 + 1 - - if total_table_width > width: - available_width = width - (len(headers) * 3 + 1) - scaling_factor = available_width / sum(column_widths.values()) - column_widths = {header: max(5, int(width * scaling_factor)) for header, width in column_widths.items()} - else: - remaining_space = width - total_table_width - for header in headers: - column_widths[header] += remaining_space // len(headers) - - def format_row(row): - return f"{border_char} " + " | ".join( - f"{str(row[header])[:column_widths[header]].ljust(column_widths[header])}" for header in headers - ) + f" {border_char}" - - header_line = format_row({header: header for header in headers}) - border_line = "+" + "+".join("-" * (column_widths[header] + 2) for header in headers) + "+" + import pygame - self.styled_print(border_line, fore_color) - self.styled_print(header_line, fore_color) - self.styled_print(border_line, fore_color) - for item in items: - self.styled_print(border_line if item == "DIVIDER" else format_row(item), fore_color) - self.styled_print(border_line, fore_color) +class Game: + def __init__(self, _tui, _audio_manager): + self.tui = _tui + self.audio_manager = _audio_manager - @staticmethod - def clear(): - return os.system("cls" if os.name == "nt" else "clear") + self.journey_events = [self.pre_battle, self.treasure] + self.journey_events_chances = [0.7, 0.3] + self.main_menu_music = [1, 2, 3] + self.attack_phrases = ["Slash!", "Bang!", "Pow!", "Boom!", "Splat!", "Bish!", "Bash!", "Biff!", "Ouch!", "Ow!", + "Whoosh!", "Arghh!", "Paf!", "Slice!", "Wham!", "Bam!"] + self.monsters_list = ["Ogre", "Orc", "Beast", "Demon", "Giant", "Golem", "Mummy", "Zombie", "Skeleton", "Witch", + "Dragon", "Pirate", "Vampire"] + self.treasure_list = ["some Dust", "nothing", f"{random.randint(1, 200)} Gold", "An old rusty sword", + "a Mystery Liquid", "a healing potion", "a Colt Anaconda"] + self.treasure_weights = [0.25, 0.2, 0.2, 0.2, 0.05, 0.05, 0.05] + self.reset_player_state() -class Game: - @staticmethod - def play_music(file: str, loop: bool = True, stop_previous: bool = False, volume: int = 100): - if stop_previous: - pygame.mixer.music.stop() - pygame.mixer.music.load(f"{os.path.dirname(__file__)}/audio/{file}.wav") - pygame.mixer.music.set_volume(volume / 100) - pygame.mixer.music.play(loops = -1 if loop else 0) + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.leave() - while not pygame.mixer.music.get_busy(): - continue + self.audio_manager.play_audio(f"menu/{random.choice(self.main_menu_music)}", busy_check = True) - def play_attack_music(self): - self.play_music(f"attack/{random.choice([1, 2, 3, 4, 5, 6])}", False, False) + self.tui.clear() + art_lines = [ + " .:+sss+:` `-/ossss/` `:+++o/. `-:+oss: .:+osssssso/-. ````:+sso:` `-+sso:` .:oss+- `:+ss+.", + " `ohy+//shh+` `/yy///ydd/` `:ydo``/o:` -+o+:odmh: `-sy+//sdmy++shdh+. .+o/:sy++hmd+``///odmh+ .+//smmy- `:+/omds`", + " `+mmo` `--` :dd/ :hmy- :hmd- ` /ho.`.smmo` .yds` .ymd/` `-ommy. `/hy/`-s+``+mmd. ` -smmy` ` /dmm/` `/md+.", + " +mmdo-` `/-` `ymmo.``.omms` `/d+` /hmh/ ` .odms- -ymm/ `odd+` `` `+mmd. .ommy. `smmm/` `:yh+`", + " `.+hdmds:` :dmmhyyyhdmd: ``` `/dd+///ymms. .hmm/` .smm/ :dmy- `smmo` `ommy. .symmm/` `/yy:", + " ./++o+/sdmdo. `smms:---ommy` `:/+o+.`:ddsssshmmh/ +dmy- -ymh- odms. `/mmy. `ommy``:y+:dmm/`.oy+.", + " .sd: `` `+dms- -dmd:` -ymd+ .sh+````:hd: `smms. .hmh:` .odh: odmy- `/hmy. `omms-os- .dmd//so-", + " .ymy:..-/ydy:` `-/- omd/` /dmd+-:/ -ymy/.-+hy: .ymms-:/. .smh/-..-:oyy+. -ymms:.-/sdy/` .smmds/` .dmmho:`", + " ./shhhys+-` `:oso+s/. .+yhyo:` -oyhhyo:` :shhs+- .oyhhhhhhhys+-` `:oyhhys/- .oyo:. -ys+-`", + "\n `.:+osssssso/-. `` .:oss+-` `` -/oss/. `./osso:` `-/o++o+-`", + " :sy+//sdmy++ohdh+. `-+o:-yy/+dmd/` `:oo./do/sdmy: -o//ommds- .+ydmy.`:+/.", + " hdo` -smd/ `.ommy. .+hy:`:y/ `ymms. -ohs.`+s- -hmm+` ` `sddmm+` `/sydmm:", + " /:. `odmy- .hmm:` .odh/ ` smms. -ymy- `` -hmm+` :ds+hmy- .+y+/mmy`", + " -ymd+` `smm/` +dms- .hmm/` .omdo` /dmh: sd:.smd/-os:.smm/", + " +dmy- .hmh- `smmo. +mms. :ymd+ .ymd+` .dy. +dmyys-`:dmd.", + " -ymh/` .omh: `smms. `/mdo. -ymdo .sdh/` +m/` -hmdo. .omms", + " .sdh/-..-:oyh+. :ymdo-.-/ydy:` `/dmh+-.-ohho. .:/` .hy` `os/` -hmmo.:/`", + f" .oyhhhhhhhys+-` ./shhhyo/. .+shhhy+:` .+ss++/` `/yhhs/.", + f"\n Version: {__version__} \n\n", + ] + for line in art_lines: + self.tui.styled_print(line, fore_color = "cyan") + + self.input("Press Enter to start... ") + self.menu() def menu(self): self.tui.clear() - self.tui.decorative_header("Main Menu") + self.tui.decorative_header("Main Menu", width = 40) options = ["Begin a New Game", "Exit", "Credits"] - self.tui.render_menu(options) - c = input("CHOOSE>> ") + self.tui.render_menu(options, width = 40) + c = self.input("CHOOSE>> ") if c == "1": self.new() @@ -143,14 +98,15 @@ def menu(self): elif c == "3": self.game_credits() else: - self.tui.decorative_header("Invalid Choice!", fore_color = Fore.RED) + self.tui.decorative_header("Invalid Choice!", fore_color = "red", width = 40) + time.sleep(1) self.menu() def leave(self): self.tui.clear() - self.tui.decorative_header("Thanks for Playing!", fore_color = Fore.GREEN, width = 80) - self.tui.decorative_header("Keep ShadowDooming!", fore_color = Fore.GREEN, width = 80) + self.tui.decorative_header("Thanks for Playing!", fore_color = "green", width = 90) + self.tui.decorative_header("Keep ShadowDooming!", fore_color = "green", width = 90) ascii_art = [ " `o. ", @@ -186,10 +142,10 @@ def leave(self): " `-. ` ", ] for line in ascii_art: - self.tui.styled_print(line, Fore.YELLOW) + self.tui.styled_print(line, "yellow", style = "bold") - time.sleep(3) - self.tui.styled_print("\n\nBye!", Fore.LIGHTCYAN_EX) + time.sleep(2) + self.tui.decorative_header("Bye!", fore_color = "cyan", width = 90) sys.exit() def reset_player_state(self): @@ -216,15 +172,14 @@ def reset_player_state(self): def new(self): self.reset_player_state() - self.tui.decorative_header("Starting a New Game!", fore_color = Fore.GREEN) - self.tui.styled_print("Your journey begins...", Fore.CYAN) + self.tui.decorative_header("Starting a New Game!", fore_color = "green", width = 40) + self.tui.styled_print("Your journey begins...", fore_color = "cyan") time.sleep(2) self.home() def home(self): - if not pygame.mixer.music.get_busy(): - self.play_music(f"menu/{random.choice(self.main_menu_music)}") + self.audio_manager.play_audio(f"menu/{random.choice(self.main_menu_music)}", busy_check = True) self.tui.clear() self.health = min(self.health, self.max_health) @@ -240,7 +195,7 @@ def home(self): "Kills": self.killcount } self.tui.section_title("Statistics") - self.tui.key_value_display(stats) + self.tui.key_value_display(stats, align = True) options = [ "Go out of your Village and Fight!", @@ -253,7 +208,7 @@ def home(self): self.tui.section_title("What would you like to do?") self.tui.render_menu(options) - c = input("CHOOSE>> ") + c = self.input("CHOOSE>> ") if c == "1": self.pre_battle() elif c == "2": @@ -264,16 +219,22 @@ def home(self): self.save_game() elif c == "5": self.load_game() - elif c == "6": - self.menu() + elif c == options[5]: + c = self.input("Are you sure you want to exit? Any unsaved progress will be lost. (y/n) ").lower() + while c not in ["y", "n"]: + self.tui.decorative_header("Invalid Choice!", fore_color = "red") + c = self.input("Are you sure you want to exit? Any unsaved progress will be lost. (y/n) ").lower() + + if c == "y": + self.menu() else: - self.tui.decorative_header("Invalid Choice!", fore_color = Fore.RED) - time.sleep(2) + self.tui.decorative_header("Invalid Choice!", fore_color = "red") + time.sleep(1) self.home() def inventory(self): self.is_inventory = True - self.play_music("shop") + self.audio_manager.play_audio("shop") self.tui.clear() self.tui.decorative_header("Your Inventory") @@ -298,59 +259,58 @@ def inventory(self): possession = False for weapon, (owned, damage) in weapons.items(): if owned and self.currentweapon != weapon: - self.tui.styled_print(f"{weapon} ({damage} Damage)", Fore.YELLOW) + self.tui.styled_print(f"{weapon} - {damage} Damage", fore_color = "yellow") possession = True if not possession: - self.tui.styled_print("You don't have any Weapons. Buy them in the Shop!", Fore.RED) - time.sleep(3) - pygame.mixer.music.stop() + self.tui.decorative_header("You don't have any Weapons. Buy them in the Shop!", fore_color = "red") + time.sleep(2) + self.audio_manager.stop_audio() self.home() return self.tui.section_title("Equip a Weapon") - self.tui.styled_print("Press the corresponding key to equip a weapon or type 'Q' to quit:", Fore.CYAN) + self.tui.styled_print("Press the corresponding key to equip a weapon or type '0' to quit:", fore_color = "cyan") equip_keys = { - "S": "Stick", - "C": "Old Club", - "P": "Spiked Mace", - "F": "Fire Axe", - "R": "Spear", - "D": "Double Axe", - "K": "Katana", - "G": "Shotgun", - "M": "Magic Scythe", - "T": "Rusty Sword", - "A": "Colt Anaconda" + 1: "Stick", + 2: "Old Club", + 3: "Spiked Mace", + 4: "Fire Axe", + 5: "Spear", + 6: "Double Axe", + 7: "Katana", + 8: "Shotgun", + 9: "Magic Scythe", + 10: "Rusty Sword", + 11: "Colt Anaconda" } for key, weapon in equip_keys.items(): if weapons[weapon][0]: - self.tui.styled_print(f"{key} - {weapon}", Fore.YELLOW) - c = input("\nCHOOSE>> ").strip().upper() + self.tui.styled_print(f"{key} - {weapon}", fore_color = "yellow") + c = self.input("\nCHOOSE>> ") - if c in equip_keys and weapons[equip_keys[c]][0]: - selected_weapon = equip_keys[c] + if c.isdigit() and int(c) in equip_keys and weapons[equip_keys[int(c)]][0]: + selected_weapon = equip_keys[int(c)] self.currentweapon = selected_weapon self.playerdamage = weapons[selected_weapon][1] setattr(self, selected_weapon.lower().replace(" ", "_"), False) - self.tui.styled_print(f"\nYou have equipped the {selected_weapon}!", Fore.GREEN) + self.tui.styled_print(f"\nYou have equipped the {selected_weapon}!", fore_color = "green") time.sleep(1) self.inventory() - elif c == "Q": - pygame.mixer.music.stop() + elif c == "0": + self.audio_manager.stop_audio() self.home() else: - self.tui.decorative_header("Invalid Choice!", fore_color = Fore.RED) + self.tui.decorative_header("Invalid Choice!", fore_color = "red") time.sleep(1) self.inventory() def shop(self): - if not pygame.mixer.music.get_busy(): - self.play_music("shop") + self.audio_manager.play_audio("shop", busy_check = True) self.money = max(self.money, 0) self.tui.clear() @@ -363,12 +323,12 @@ def shop(self): }] self.tui.list_display(stats, center = True, width = 100) - # if self.money == 0: - # self.tui.styled_print("You have no money to spend. Go kill Monsters and earn more!", Fore.RED) - # time.sleep(3) - # self.home() - # - # return + if self.money == 0: + self.tui.styled_print("You have no money to spend. Go kill Monsters and earn more!", "red") + time.sleep(3) + self.home() + + return items = { 1: { @@ -468,18 +428,19 @@ def shop(self): }) self.tui.table_display(display_items, width = 100) - self.tui.styled_print("\nType the item number to buy, or 'quit' to go back home.", Fore.CYAN) - choice = input("CHOOSE>> ").strip() + self.tui.styled_print("\nType the item number to buy, or 'quit' to go back home.", fore_color = "cyan") + choice = self.input("CHOOSE>> ") if choice.lower() == "quit": - self.tui.styled_print("Going back home...", Fore.GREEN) + self.tui.styled_print("Going back home...", fore_color = "green") time.sleep(2) - pygame.mixer.music.stop() + self.audio_manager.stop_audio() self.home() + return if not choice.isdigit() or int(choice) not in items: - self.tui.styled_print("Please enter a valid number!", Fore.RED) + self.tui.decorative_header("Invalid Choice!", fore_color = "red") time.sleep(2) self.shop() return @@ -489,50 +450,38 @@ def shop(self): if item["attr"] in {"health_pill", "health_potion"}: if self.health >= self.max_health: - self.tui.styled_print("You already have full health!", Fore.RED) + self.tui.styled_print("You already have full health!", fore_color = "red") elif self.money >= item["price"]: self.money -= item["price"] self.health = min(self.health + (10 if item["attr"] == "health_pill" else 50), self.max_health) - self.tui.styled_print(f"You replenished your health by {item['effect']}!", Fore.GREEN) + self.tui.styled_print(f"You replenished your health by {item['effect']}!", fore_color = "green") else: - self.tui.styled_print("You don't have enough money!", Fore.RED) + self.tui.styled_print("You don't have enough money!", fore_color = "red") else: if getattr(self, item["attr"], False): - self.tui.styled_print("You already own this item!", Fore.RED) + self.tui.styled_print("You already own this item!", fore_color = "red") elif self.money >= item["price"]: self.money -= item["price"] self.playerdamage = int(item["effect"].split('+')[1].split()[0]) self.currentweapon = item["name"] setattr(self, item["attr"], True) - self.tui.styled_print(f"You bought the {item['name']}!", Fore.GREEN) + self.tui.styled_print(f"You bought the {item['name']}!", fore_color = "green") else: - self.tui.styled_print("You don't have enough money!", Fore.RED) + self.tui.styled_print("You don't have enough money!", fore_color = "red") time.sleep(2) self.shop() def die(self): - self.play_music("heartbeat", loop = True, stop_previous = False, volume = 200) - self.play_music("die", loop = False, stop_previous = False, volume = 50) + self.audio_manager.play_audio("heartbeat", loop = True, stop_previous = False, volume = 200) + self.audio_manager.play_audio("die", loop = False, stop_previous = False, volume = 50) self.money = max(self.money, 0) - death_phrase = random.choice(self.death_phrases) self.tui.clear() - print( - " = = = = = = = = = = = = = = = = = = = = = = = = = = = = = == ==") - print( - " = = = = = = = = = = = = == ==") - print( - " = = = = = = = = = = = = = == ==") - print( - " = = = = = = = = = = = ") - print( - " = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = == ==") - print() - - self.tui.styled_print(death_phrase, Fore.YELLOW, style = Style.BRIGHT) + self.tui.decorative_header("You Died!", fore_color = "red") + self.tui.section_title("Statistics") stats = { "Max Health": f"{self.max_health} HP", @@ -540,22 +489,22 @@ def die(self): "Current Weapon": self.currentweapon, "Kills": self.killcount } - self.tui.key_value_display(stats) + self.tui.key_value_display(stats, align = True) time.sleep(5) - self.tui.styled_print("But by some magic from a village wizard, you live again!\n", Fore.CYAN) - self.tui.styled_print("However, you lose all your weapons and money.", Fore.CYAN) + self.tui.styled_print("But by some magic from a village wizard, you live again!", fore_color = "cyan") + self.tui.styled_print("However, you lose all your weapons and money.", fore_color = "cyan") - input("\nPress Enter to continue...") + self.input("\nPress Enter to continue...") self.reset_player_state() - pygame.mixer.music.stop() + self.audio_manager.stop_audio() self.home() def pre_battle(self): self.tui.clear() - self.play_music("attack", loop = True, stop_previous = False) - self.tui.decorative_header("Get Ready to Fight!", fore_color = Fore.RED) + self.audio_manager.play_audio("attack", loop = True, stop_previous = False) + self.tui.decorative_header("Get Ready to Fight!", fore_color = "red") time.sleep(2) base_health = [30, 60, 60, 60, 60] @@ -595,7 +544,7 @@ def pre_battle(self): self.monster = random.choice(self.monsters_list) article = "an" if self.monster[0].lower() in "aeiou" else "a" - self.tui.styled_print(f"You encounter {article} {self.monster}!", Fore.YELLOW, style = Style.BRIGHT) + self.tui.styled_print(f"You encounter {article} {self.monster}!", "yellow", style = "bold") setattr(self, f"monsterhealth{self.difficulty}", base_health[self.difficulty]) setattr(self, f"monsterdamage{self.difficulty}", base_damage[self.difficulty]) @@ -604,7 +553,7 @@ def pre_battle(self): def treasure(self): self.tui.clear() - self.play_music("treasure") + self.audio_manager.play_audio("treasure") self.tui.decorative_header("You found a treasure chest!") self.tui.styled_print( """ @@ -629,90 +578,89 @@ def treasure(self): '-.||_/.-' '-.__. """, - Fore.YELLOW, + fore_color = "yellow", ) - self.tui.styled_print("You open the treasure chest and find...", Fore.CYAN) + self.tui.styled_print("You open the treasure chest and find... ", fore_color = "cyan", end = "") time.sleep(2) treasure = random.choice(random.choices(population = self.treasure_list, weights = self.treasure_weights)) - self.tui.styled_print(f"{treasure}!", Fore.GREEN, style = Style.BRIGHT) + self.tui.styled_print(f"{treasure}!", fore_color = "green", style = "bold") if "Gold" in treasure: amount = int(treasure.replace("Gold", "").strip()) self.money += amount self.money = max(self.money, 0) - self.tui.styled_print(f"Ka-Ching! You found {amount} Gold!", Fore.YELLOW) + self.tui.styled_print(f"Ka-Ching! You found {amount} Gold!", fore_color = "yellow") elif treasure == "An old rusty sword": - self.tui.styled_print("You equip the sword.", Fore.CYAN) + self.tui.styled_print("You equip the sword.", fore_color = "cyan") self.rusty_sword = True self.currentweapon = "Rusty Sword" elif treasure in {"nothing", "some Dust"}: - self.tui.styled_print("Damn, just your luck.", Fore.RED) + self.tui.styled_print("Damn, just your luck.", fore_color = "red") elif treasure == "a Mystery Liquid": - self.tui.styled_print("You found a Mystery Liquid. Drink it?", Fore.CYAN) - c = input("(y/n) ").lower() + c = self.input("You found a Mystery Liquid. Drink it? (y/n) ").lower() while c not in ["y", "n"]: - self.tui.styled_print("Please enter a valid option (y/n).", Fore.RED) - c = input("(y/n) ").lower() + self.tui.decorative_header("Invalid Choice!", fore_color = "red") + c = self.input("(y/n) ").lower() if c == "y": - self.tui.styled_print("You drink the Mystery Liquid...", Fore.YELLOW) + self.tui.styled_print("You drink the Mystery Liquid...", fore_color = "yellow") time.sleep(1) random.choice(random.choices( population = [self.mystery_liquid1, self.mystery_liquid2, self.mystery_liquid3, self.mystery_liquid4], weights = [0.3, 0.25, 0.2, 0.15]))() elif c == "n": - self.tui.styled_print("You decide not to drink the liquid.", Fore.CYAN) + self.tui.styled_print("You decide not to drink the liquid.", fore_color = "cyan") elif treasure == "a healing potion": - self.tui.styled_print("You drink the healing potion.", Fore.GREEN) + self.tui.styled_print("You drink the healing potion.", fore_color = "green") self.health += 50 self.health = min(self.health, self.max_health) - self.tui.styled_print(f"You now have {self.health} health.", Fore.YELLOW) + self.tui.styled_print(f"You now have {self.health} health.", fore_color = "yellow") elif treasure == "a Colt Anaconda": - self.tui.styled_print("Wow, that's rare!", Fore.YELLOW) - self.tui.styled_print("You equip the Colt Anaconda.", Fore.CYAN) + self.tui.styled_print("Wow, that's rare!", fore_color = "yellow") + self.tui.styled_print("You equip the Colt Anaconda.", fore_color = "cyan") self.colt_anaconda = True self.currentweapon = "Colt Anaconda" - pygame.mixer.music.stop() + self.audio_manager.stop_audio() time.sleep(2) self.home() def mystery_liquid1(self): - self.tui.styled_print("You feel very strong.", Fore.GREEN) + self.tui.styled_print("You feel very strong.", fore_color = "green") time.sleep(2) self.health += 50 self.health = min(self.health, self.max_health) - self.tui.styled_print(f"You now have {self.health} health.", Fore.GREEN) + self.tui.styled_print(f"You now have {self.health} health.", fore_color = "green") time.sleep(1) def mystery_liquid2(self): - self.tui.styled_print("You feel a bit dizzy.", Fore.GREEN) + self.tui.styled_print("You feel a bit dizzy.", fore_color = "green") time.sleep(2) self.health -= 10 if self.health <= 0: self.health = 5 - self.tui.styled_print(f"You now have {self.health} health.", Fore.GREEN) + self.tui.styled_print(f"You now have {self.health} health.", fore_color = "green") time.sleep(1) def mystery_liquid3(self): self.money = max(self.money, 0) - self.tui.styled_print("You suddenly vomit some paper.", Fore.GREEN) + self.tui.styled_print("You suddenly vomit some paper.", fore_color = "green") time.sleep(2) - self.tui.styled_print("Nope, it's cash!", Fore.GREEN) + self.tui.styled_print("Nope, it's cash!", fore_color = "green") self.money += 100 - self.tui.styled_print(f"You now have {self.money} money.", Fore.GREEN) + self.tui.styled_print(f"You now have {self.money} money.", fore_color = "green") time.sleep(1) def mystery_liquid4(self): - self.tui.styled_print("You feel nothing.", Fore.GREEN) + self.tui.styled_print("You feel nothing.", fore_color = "green") time.sleep(1) - self.tui.styled_print("You now feel a bit stronger, but nothing else.", Fore.GREEN) + self.tui.styled_print("You now feel a bit stronger, but nothing else.", fore_color = "green") self.playerdamage += 10 - self.tui.styled_print("You go back to your village.", Fore.GREEN) + self.tui.styled_print("You go back to your village.", fore_color = "green") time.sleep(2) self.home() @@ -723,9 +671,9 @@ def encounter(self, attacked=False): self.tui.clear() if attacked: - self.tui.styled_print(f"\n\n{random.choice(self.attack_phrases)}", Fore.CYAN) + self.tui.styled_print(random.choice(self.attack_phrases), fore_color = "cyan") - self.tui.decorative_header("Fight!", fore_color = Fore.RED) + self.tui.decorative_header("Fight!", fore_color = "red") stats = [ { "You (HP)": f"{self.health} HP", @@ -734,26 +682,26 @@ def encounter(self, attacked=False): }, { f"{self.monster} (HP)": f"{monster_health} HP", - "Enemy Damage": f"{monster_damage}", + "Monster Damage": f"{monster_damage}", } ] - self.tui.list_display(stats) + self.tui.list_display(stats, center = True) self.tui.section_title("What would you like to do?") options = ["Attack", "Retreat back to your village"] self.tui.render_menu(options) - c = input("\nCHOOSE>> ") + c = self.input("CHOOSE>> ") if c == "1": self.tui.clear() self.attack() elif c == "2": - self.tui.styled_print("\nYou retreat back to your village.", Fore.YELLOW) + self.tui.styled_print("You retreat back to your village.", fore_color = "yellow") time.sleep(1) self.home() else: - self.tui.styled_print("Please enter a valid option.", Fore.RED) - time.sleep(2) + self.tui.decorative_header("Invalid Choice!", fore_color = "red") + time.sleep(1) self.encounter() def attack(self): @@ -763,7 +711,7 @@ def attack(self): monster_health = getattr(self, monster_health_attr) monster_damage = getattr(self, monster_damage_attr) - self.play_attack_music() + self.audio_manager.play_audio(f"attack/{random.choice([1, 2, 3, 4, 5, 6])}", False, False) self.health -= monster_damage monster_health -= self.playerdamage @@ -772,12 +720,12 @@ def attack(self): if self.health <= 0: self.die() - self.tui.styled_print(f"\nYou killed the {self.monster}!", Fore.GREEN) + self.tui.styled_print(f"\nYou killed the {self.monster}!", fore_color = "green") self.killcount += 1 reward = int(random.uniform(self.difficulty, self.difficulty * 5)) * 2 self.money += reward - self.tui.styled_print(f"You earned ${reward}!", Fore.YELLOW) + self.tui.styled_print(f"You earned ${reward}!", fore_color = "yellow") time.sleep(2) self.continue_journey() @@ -789,31 +737,29 @@ def attack(self): def continue_journey(self): self.tui.clear() - self.tui.decorative_header("Continue Your Journey?") + self.tui.decorative_header("Continue Your Journey?", width = 40) options = ["Yes", "No"] - self.tui.render_menu(options) + self.tui.render_menu(options, width = 40) - c = input("\nCHOOSE>> ") + c = self.input("\nCHOOSE>> ") if c == "1": - self.tui.styled_print("\nYou continue on your journey.", Fore.GREEN) + self.tui.styled_print("\nYou continue on your journey.", fore_color = "green") time.sleep(1) random.choice(random.choices(population = self.journey_events, weights = self.journey_events_chances))() elif c == "2": - self.tui.styled_print("\nYou go back to your village.", Fore.YELLOW) + self.tui.styled_print("\nYou go back to your village.", fore_color = "yellow") time.sleep(1) self.home() else: - self.tui.styled_print("Please enter 1 or 2.", Fore.RED) + self.tui.decorative_header("Invalid Choice!", fore_color = "red", width = 40) time.sleep(1) self.continue_journey() def save_game(self): self.tui.clear() - self.tui.decorative_header("Saving Game...", fore_color = Fore.CYAN) + self.tui.decorative_header("Saving Game...", fore_color = "cyan") - root = tkinter.Tk() - root.withdraw() file_path = filedialog.asksaveasfilename( defaultextension = ".sdoom", title = "Save Game As...", @@ -821,9 +767,10 @@ def save_game(self): ) if not file_path: - self.tui.styled_print("\nGame not saved.", Fore.RED) + self.tui.decorative_header("Game not saved.", fore_color = "red") time.sleep(1) self.home() + return game_data = { @@ -844,27 +791,24 @@ def save_game(self): try: with open(file_path, "w") as f: f.write(encoded_data) - self.tui.styled_print("\nGame saved successfully!", Fore.GREEN) + self.tui.styled_print("\nGame saved successfully!", fore_color = "green") except Exception as e: - self.tui.styled_print(f"\nFailed to save the game. Error: {e}", Fore.RED) + self.tui.styled_print(f"\nFailed to save the game. Error: {e}", fore_color = "red") finally: - root.destroy() time.sleep(1) self.home() def load_game(self): self.tui.clear() - self.tui.decorative_header("Loading Game...", fore_color = Fore.CYAN) + self.tui.decorative_header("Loading Game...", fore_color = "cyan") - root = tkinter.Tk() - root.withdraw() file_path = filedialog.askopenfilename( title = "Select Save File", filetypes = [("ShadowDoom Save Files", "*.sdoom")], ) if not file_path: - self.tui.styled_print("\nNo file selected.", Fore.RED) + self.tui.decorative_header("No file selected.", fore_color = "red") time.sleep(1) self.home() @@ -873,9 +817,15 @@ def load_game(self): try: with open(file_path, "r") as f: encoded_data = f.read() - json_data = base64.b64decode(encoded_data.encode('utf-8')).decode('utf-8') - game_data = json.loads(json_data) + + try: + game_data = json.loads(json_data) + except json.JSONDecodeError: + raise Exception("Invalid save file.") + if not all(key in game_data for key in + ["difficulty", "health", "money", "killcount", "current_weapon", "monsters"]): + raise Exception("Invalid save file.") self.difficulty = game_data["difficulty"] self.health = game_data["health"] @@ -887,120 +837,65 @@ def load_game(self): setattr(self, f"monsterhealth{i}", monster["health"]) setattr(self, f"monsterdamage{i}", monster["damage"]) - self.tui.styled_print("\nGame loaded successfully!", Fore.GREEN) + self.tui.styled_print("\nGame loaded successfully!", fore_color = "green") except Exception as e: - self.tui.styled_print(f"\nFailed to load the game. Error: {e}", Fore.RED) + self.tui.styled_print(f"\nFailed to load the game. Error: {e}", fore_color = "red") finally: - root.destroy() time.sleep(1) self.home() def game_credits(self): - width = 50 - self.tui.clear() - self.tui.decorative_header("Credits", width = width, fore_color = Fore.CYAN) - - credits_text = [ - "Story: Siddharth", - "Programming: Siddharth", - "Playtesting: Krithik, Aditya", - "Written in: Python", - f"Version: {__version__}", - "This Game was made possible by you.", - "Keep ShadowDooming!!", - ] - - border = "#" * width - padding = "##" + " " * (width - 4) + "##" - - self.tui.styled_print(border, Fore.YELLOW) - self.tui.styled_print(padding, Fore.YELLOW) - for line in credits_text: - content = f"{line.center(width - 4)}" - self.tui.styled_print(f"##{content}##", Fore.YELLOW) - - self.tui.styled_print(padding, Fore.YELLOW) - self.tui.styled_print(border, Fore.YELLOW) + self.tui.decorative_header("Credits", width = 50, fore_color = "cyan") + + credits_text = { + "Programming": "Siddharth", + "Playtesting": "Krithik, Aditya", + "Written in": "Python", + "Version": __version__, + "DIVIDER": None, + "This Game was made possible by you.": None, + "Keep ShadowDooming!!": None, + } + self.tui.key_value_display(credits_text, align = True, width = 50, fore_color = "yellow") - input("\nPress Enter to continue...") + self.input("\nPress Enter to continue...") self.menu() - def __init__(self, _tui): - pygame.mixer.init() - self.tui = _tui - - self.journey_events = [self.pre_battle, self.treasure] - self.journey_events_chances = [0.7, 0.3] - - self.main_menu_music = [1, 2, 3] - self.attack_phrases = ["Slash!", "Bang!", "Pow!", "Boom!", "Splat!", "Bish!", "Bash!", "Biff!", "Ouch!", "Ow!", - "Whoosh!", "Arghh!", "Paf!", "Slice!", "Wham!", "Bam!"] - self.death_phrases = ["You failed!", "Game over!", "OOF!!", "Did you get that on camera?", "That was quick.", - "Well, you're dead. This place just doesn't seem very safe now, does it?", - "NOOOOO!!!!!!!", "You were killed.", "U NEED MORE PRACTICE!", "You're dead."] - self.monsters_list = ["Ogre", "Orc", "Beast", "Demon", "Giant", "Golem", "Mummy", "Zombie", "Skeleton", "Witch", - "Dragon", "Pirate", "Vampire"] - self.treasure_list = ["some Dust", "nothing", f"{random.randint(1, 200)} Gold", "An old rusty sword", - "a Mystery Liquid", "a healing potion", "a Colt Anaconda"] - self.treasure_weights = [0.25, 0.2, 0.2, 0.2, 0.05, 0.05, 0.05] - self.reset_player_state() - - if not pygame.mixer.music.get_busy(): - self.play_music(f"menu/{random.choice(self.main_menu_music)}") - - self.tui.clear() - art_lines = [ - " .:+sss+:` `-/ossss/` `:+++o/. `-:+oss: .:+osssssso/-. ````:+sso:` `-+sso:` .:oss+- `:+ss+.", - " `ohy+//shh+` `/yy///ydd/` `:ydo``/o:` -+o+:odmh: `-sy+//sdmy++shdh+. .+o/:sy++hmd+``///odmh+ .+//smmy- `:+/omds`", - " `+mmo` `--` :dd/ :hmy- :hmd- ` /ho.`.smmo` .yds` .ymd/` `-ommy. `/hy/`-s+``+mmd. ` -smmy` ` /dmm/` `/md+.", - " +mmdo-` `/-` `ymmo.``.omms` `/d+` /hmh/ ` .odms- -ymm/ `odd+` `` `+mmd. .ommy. `smmm/` `:yh+`", - " `.+hdmds:` :dmmhyyyhdmd: ``` `/dd+///ymms. .hmm/` .smm/ :dmy- `smmo` `ommy. .symmm/` `/yy:", - " ./++o+/sdmdo. `smms:---ommy` `:/+o+.`:ddsssshmmh/ +dmy- -ymh- odms. `/mmy. `ommy``:y+:dmm/`.oy+.", - " .sd: `` `+dms- -dmd:` -ymd+ .sh+````:hd: `smms. .hmh:` .odh: odmy- `/hmy. `omms-os- .dmd//so-", - " .ymy:..-/ydy:` `-/- omd/` /dmd+-:/ -ymy/.-+hy: .ymms-:/. .smh/-..-:oyy+. -ymms:.-/sdy/` .smmds/` .dmmho:`", - " ./shhhys+-` `:oso+s/. .+yhyo:` -oyhhyo:` :shhs+- .oyhhhhhhhys+-` `:oyhhys/- .oyo:. -ys+-`", - "\n `.:+osssssso/-. `` .:oss+-` `` -/oss/. `./osso:` `-/o++o+-`", - " :sy+//sdmy++ohdh+. `-+o:-yy/+dmd/` `:oo./do/sdmy: -o//ommds- .+ydmy.`:+/.", - " hdo` -smd/ `.ommy. .+hy:`:y/ `ymms. -ohs.`+s- -hmm+` ` `sddmm+` `/sydmm:", - " /:. `odmy- .hmm:` .odh/ ` smms. -ymy- `` -hmm+` :ds+hmy- .+y+/mmy`", - " -ymd+` `smm/` +dms- .hmm/` .omdo` /dmh: sd:.smd/-os:.smm/", - " +dmy- .hmh- `smmo. +mms. :ymd+ .ymd+` .dy. +dmyys-`:dmd.", - " -ymh/` .omh: `smms. `/mdo. -ymdo .sdh/` +m/` -hmdo. .omms", - " .sdh/-..-:oyh+. :ymdo-.-/ydy:` `/dmh+-.-ohho. .:/` .hy` `os/` -hmmo.:/`", - f" .oyhhhhhhhys+-` ./shhhyo/. .+shhhy+:` .+ss++/` `/yhhs/.", - f"\n Version: {__version__} \n\n", - ] - for line in art_lines: - self.tui.styled_print(line, Fore.CYAN) - - input("Press Enter to start... ") - self.menu() + def input(self, prompt=""): + while True: + user_input = input(prompt).strip() + if user_input.lower() == "m": + self.audio_manager.muted = not self.audio_manager.muted + if self.audio_manager.muted: + self.audio_manager.stop_audio() + else: + return user_input if __name__ == "__main__": - tui = TUI() + tui = tui.TUI() + audio_manager = audio.AudioManager() if sys.version_info < (3, 6): - tui.styled_print( + tui.decorative_header( "ShadowDoom requires Python 3.6 or higher. Please update your Python version.", - Fore.RED + fore_color = "red" ) - tui.styled_print("Exiting...", Fore.RED) + tui.styled_print("Exiting...", fore_color = "red") sys.exit(1) try: - Game(tui) + Game(tui, audio_manager) except KeyboardInterrupt: - tui.section_title("Game Interrupted!", fore_color = Fore.RED) - tui.styled_print("Interrupted by Keyboard!", Fore.YELLOW) - tui.styled_print("Exiting...", Fore.RED) + tui.section_title("Game Interrupted!", fore_color = "red") + tui.styled_print("Interrupted by Keyboard!", fore_color = "yellow") + tui.styled_print("Exiting...", fore_color = "red") try: - Game.play_music("bye", loop = False, stop_previous = True) + audio_manager.play_audio("bye", loop = False, stop_previous = True) time.sleep(5) except KeyboardInterrupt: - tui.styled_print("Okay, I'm exiting!! Jeez!", Fore.RED) + tui.styled_print("Okay, I'm exiting!! Jeez!", fore_color = "red") - print(f"{Fore.RESET}{Back.RESET}\n") sys.exit() diff --git a/audio/treasure.wav b/audio/treasure.wav index bfdc133..34318e4 100644 Binary files a/audio/treasure.wav and b/audio/treasure.wav differ diff --git a/utils/audio.py b/utils/audio.py new file mode 100644 index 0000000..8fbc800 --- /dev/null +++ b/utils/audio.py @@ -0,0 +1,39 @@ +import os + +os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" +import pygame + + +class AudioManager: + def __init__(self): + pygame.init() + pygame.mixer.init() + + self.enabled = pygame.mixer.get_num_channels() > 0 + self.is_muted = False + + @property + def muted(self): + return self.is_muted + + @muted.setter + def muted(self, value): + self.is_muted = value + + def play_audio(self, file, loop=True, stop_previous=False, volume=100, busy_check=False): + if (not self.enabled or self.is_muted) or (busy_check and pygame.mixer.music.get_busy()): + return + if stop_previous: + pygame.mixer.music.stop() + + pygame.mixer.music.load(f"audio/{file}.wav") + pygame.mixer.music.set_volume(volume / 100) + pygame.mixer.music.play(loops = -1 if loop else 0) + + while not pygame.mixer.music.get_busy(): + continue + + def stop_audio(self): + if not self.enabled: + return + pygame.mixer.music.stop() diff --git a/utils/tui.py b/utils/tui.py new file mode 100644 index 0000000..55513f0 --- /dev/null +++ b/utils/tui.py @@ -0,0 +1,76 @@ +from rich import box +from rich.console import Console +from rich.panel import Panel +from rich.style import Style +from rich.table import Table + + +class TUI: + def __init__(self): + self.console = Console() + self.clear() + + def styled_print(self, message="", fore_color="default", back_color="default", style="normal", end="\n"): + self.console.print(message, style = Style(color = fore_color, bgcolor = back_color, bold = (style == "bold")), + end = end) + + def decorative_header(self, text, width=60, fore_color="cyan"): + self.console.print(Panel(text.center(width), style = fore_color, width = width)) + + def section_title(self, text, width=60, fore_color="green"): + self.console.print(f"\n{text.center(width)}", style = f"bold {fore_color}") + self.console.print("─" * width, style = fore_color) + + def render_menu(self, options, width=60, fore_color="yellow"): + table = Table(box = box.ROUNDED, show_header = False, width = width, style = fore_color) + for i, option in enumerate(options, 1): + table.add_row(f"{i}. {option}".ljust(width - 4)) + self.console.print(table) + + def key_value_display(self, items, align=False, width=60, fore_color="yellow"): + table = Table(box = box.ROUNDED, show_header = False, width = width, style = fore_color) + + max_key_length = max(len(key) for key in items.keys() if key != "DIVIDER" and items[key] is not None) + max_value_length = max(len(str(value)) for value in items.values() if value is not None) + + for key, value in items.items(): + if key == "DIVIDER": + table.add_section() + else: + if align: + if value is None: + row_text = key.rjust(max_key_length) + else: + row_text = f"{key.rjust(max_key_length)}: {str(value).ljust(max_value_length)}" + row_text = row_text.center(width - 4) + else: + row_text = f"{key}" if value is None else f"{key}: {value}" + row_text = row_text.ljust(width - 4) + + table.add_row(row_text) + self.console.print(table) + + def list_display(self, items, center=False, width=60, fore_color="yellow"): + table = Table(box = box.ROUNDED, show_header = False, width = width, style = fore_color) + for item in items: + row_text = " | ".join([f"{key}: {value}" for key, value in item.items()]) + row_text = row_text.center(width - 4) if center else row_text.ljust(width - 4) + table.add_row(row_text) + self.console.print(table) + + def table_display(self, items, width=60, fore_color="yellow"): + headers = [header for header in items[0] if isinstance(items[0], dict)] + table = Table(box = box.ROUNDED, width = width, style = fore_color) + + for header in headers: + table.add_column(header) + + for item in items: + if item == "DIVIDER": + table.add_section() + else: + table.add_row(*[str(item.get(header, "")) for header in headers]) + self.console.print(table) + + def clear(self): + self.console.clear()