import argparse import enum import logging import signal import sys import time from collections.abc import Sequence from datetime import datetime, timedelta from typing import Any from pqcli import lingo from pqcli.config import PRIME_STATS from pqcli.game_data import GameData from pqcli.mechanic import Player, Simulation, StatsBuilder, create_player from pqcli.roster import Roster from pqcli.ui.base import BaseUserInterface LOGO = r""" ____ ___ _ | _ \ _ __ ___ __ _ _ __ ___ ___ ___ / _ \ _ _ ___ ___| |_ | |_) | '__/ _ \ / _` | '__/ _ \/ __/ __| | | | | | |/ _ \/ __| __| | __/| | | (_) | (_| | | | __/\__ \__ \ |_| | |_| | __/\__ \ |_ |_| |_| \___/ \__, |_| \___||___/___/\__\_\\__,_|\___||___/\__| |___/ """ class MainMenu(enum.IntEnum): create = 1 play = 2 info = 3 delete = 4 quit = 5 class BasicUserInterface(BaseUserInterface): def __init__( self, roster: Roster, player: Player | None, args: argparse.Namespace, data: GameData, ) -> None: super().__init__(roster, player, args, data) def signal_handler(sig: Any, frame: Any) -> None: logging.info("Quitting") if self.args.use_saves: self.roster.save() sys.exit(0) signal.signal(signal.SIGINT, signal_handler) def run(self) -> None: logging.basicConfig( stream=sys.stdout, level=logging.INFO, format="[%(asctime)s] %(message)s", ) self.logo() if self.player: self.play(self.player) else: self.main_menu() def logo(self) -> None: print(LOGO) def main_menu(self) -> None: while True: choice = self.menu( [ (MainMenu.create, "Create a new character"), (MainMenu.play, "Play as character"), (MainMenu.info, "Query character info"), (MainMenu.delete, "Delete a character"), (MainMenu.quit, "Quit"), ] ) if choice == MainMenu.create: player = self.create_player() elif choice == MainMenu.play: player = self.choose_player() if not player: continue self.play(player) elif choice == MainMenu.info: player = self.choose_player() if not player: continue self.print_player_info(player) elif choice == MainMenu.delete: player = self.choose_player() if not player: continue self.delete_player(player) elif choice == MainMenu.quit: self.quit() break def create_player(self, *, auto_play: bool = True) -> Player | None: name = input("Name your new character: ") if not name: print("Cancelled.") return None race = self.menu([(race, race.name) for race in self.data.races]) class_ = self.menu( [(class_, class_.name) for class_ in self.data.classes] ) stats_builder = StatsBuilder() while True: stats = stats_builder.roll() for stat in PRIME_STATS: print(f"{stat.value}: {stats[stat]}") print(f"Total: {sum(stats[stat] for stat in PRIME_STATS)}") if self.confirm("Is this okay?"): break player = create_player( name=name, race=race, class_=class_, stats=stats, seed=stats_builder.seed, ) self.roster.players.append(player) if auto_play and self.confirm( "Do you want to play as your new character?" ): self.play(player) return player def play(self, player: Player) -> None: print(f"Playing as {player.name}") simulation = Simulation(player, self.data) last_tick = datetime.now() last_level = 0 while True: elapsed = datetime.now() - last_tick simulation.tick(elapsed.total_seconds() * 1000) last_tick = datetime.now() wait = (timedelta(milliseconds=100) - elapsed).total_seconds() wait = max(0, wait) time.sleep(wait) if player.level != last_level: last_level = player.level self.print_player_info(player) if self.args.use_saves: self.roster.save_periodically() def print_player_info(self, player: Player) -> None: print("--- Character Sheet ---") print(f"Name: {player.name}") print(f"Race: {player.race.name}") print(f"Class: {player.class_.name}") print(f"Level: {player.level}") print() for stat, value in player.stats: print(f"{stat.value}: {value}") print() print("--- Spell Book ---") if not player.spell_book: print("No spells memorized yet.") else: for spell in player.spell_book: print(f"{spell.name} {lingo.to_roman(spell.level)}") print() print("--- Equipment ---") for equipment_type, name in player.equipment: print(f"{equipment_type.value}: {name}") print() print("--- Inventory ---") print(f"Gold: {player.inventory.gold}") for item in player.inventory: print(f"{item.name}: {item.quantity}") print() print("--- Plot ---") print(f"Current act: {lingo.to_roman(player.quest_book.act)}") print(f"Current quest: {player.quest_book.current_quest or '?'}") print( f"Current task: {player.task.description if player.task else '?'}" ) def delete_player(self, player: Player) -> None: if self.confirm(lingo.terminate_message(player.name)): del self.roster.players[self.roster.players.index(player)] def quit(self) -> None: if self.args.use_saves: self.roster.save() def choose_player(self) -> Player | None: if not self.roster.players: print("No characters to choose from!") return None return self.menu( [(player, player.name) for player in self.roster.players], title="Choose your character", ) def confirm(self, message: str) -> bool: while True: choice = input(f"{message} [y/n] ") if choice.lower() in {"y", "yes", "1"}: return True if choice.lower() in {"n", "no", "0"}: return False def menu( self, options: Sequence[tuple[Any, str]], title: str | None = None, ) -> Any: print() if title: print(f"{title}:") for i, item in enumerate(options, 1): _, description = item print(f"{i}) {description}") print() while True: try: num = int(input("Your choice: ")) except ValueError: print("Not a number") continue if num not in range(1, len(options) + 1): print(f"Expected a number between 1..{len(options)}") continue return options[num - 1][0]