From 595538cdcad8a8676977784ee8c3a5540600745f Mon Sep 17 00:00:00 2001 From: rr- Date: Thu, 13 Dec 2018 23:28:19 +0100 Subject: [PATCH] ui/basic: add basic user interface --- pqcli/__main__.py | 11 +- pqcli/lingo.py | 5 + pqcli/mechanic.py | 8 +- pqcli/ui/basic/__init__.py | 218 +++++++++++++++++++++++++++++++++++++ pqcli/ui/urwid/__init__.py | 4 +- 5 files changed, 242 insertions(+), 4 deletions(-) create mode 100644 pqcli/ui/basic/__init__.py diff --git a/pqcli/__main__.py b/pqcli/__main__.py index 6de76cb..dc966bc 100644 --- a/pqcli/__main__.py +++ b/pqcli/__main__.py @@ -4,6 +4,7 @@ from pathlib import Path import xdg from pqcli.roster import Roster +from pqcli.ui.basic import BasicUserInterface from pqcli.ui.urwid import UrwidUserInterface SAVE_PATH = Path(xdg.XDG_CONFIG_HOME) / "pqcli" / "save.dat" @@ -11,6 +12,9 @@ SAVE_PATH = Path(xdg.XDG_CONFIG_HOME) / "pqcli" / "save.dat" def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(prog="pqcli") + parser.add_argument( + "--basic", action="store_true", help="Use basic user interface" + ) parser.add_argument( "--no-config", dest="use_config", @@ -35,7 +39,12 @@ def main() -> None: roster = Roster.load(SAVE_PATH) try: - UrwidUserInterface(roster, args).run() + if args.basic: + ui = BasicUserInterface(roster, args) + else: + ui = UrwidUserInterface(roster, args) + + ui.run() finally: if args.use_saves: roster.save() diff --git a/pqcli/lingo.py b/pqcli/lingo.py index 3b00a08..25dae19 100644 --- a/pqcli/lingo.py +++ b/pqcli/lingo.py @@ -122,3 +122,8 @@ def special(m: int, subject: str) -> str: return prefix( ["Battle-", "cursed ", "Were-", "undead ", "demon "], m, subject, "" ) + + +def terminate_message(player_name: str) -> None: + adjective = random.choice(["faithful", "noble", "loyal", "brave"]) + return f"Terminate {adjective} {player_name}?" diff --git a/pqcli/mechanic.py b/pqcli/mechanic.py index 753de32..9a739c2 100644 --- a/pqcli/mechanic.py +++ b/pqcli/mechanic.py @@ -170,7 +170,7 @@ class Inventory(SignalMixin): def pop(self, index: int) -> None: item = self._items[index] - logger.info("Lost %s (qty=%d)", item.name, item.quantity) + logger.info("Lost %s", indefinite(item.name, item.quantity)) self._items.pop(index) self.sync_encumbrance() self.emit("item_del", item) @@ -237,6 +237,12 @@ class SpellBook(SignalMixin): def __iter__(self) -> T.Iterator[Spell]: return iter(self._spells) + def __bool__(self) -> bool: + return bool(self._spells) + + def __len__(self) -> int: + return len(self._spells) + def add(self, spell_name: str, level: int) -> None: for spell in self._spells: if spell.name == spell_name: diff --git a/pqcli/ui/basic/__init__.py b/pqcli/ui/basic/__init__.py new file mode 100644 index 0000000..899a6a1 --- /dev/null +++ b/pqcli/ui/basic/__init__.py @@ -0,0 +1,218 @@ +import argparse +import enum +import logging +import signal +import sys +import time +import typing as T +from datetime import datetime, timedelta + +from pqcli import lingo +from pqcli.config import CLASSES, PRIME_STATS, RACES +from pqcli.mechanic import Player, Simulation, StatsBuilder, create_player +from pqcli.roster import Roster + + +LOGO = """ + ____ ___ _ +| _ \ _ __ ___ __ _ _ __ ___ ___ ___ / _ \ _ _ ___ ___| |_ +| |_) | '__/ _ \ / _` | '__/ _ \/ __/ __| | | | | | |/ _ \/ __| __| +| __/| | | (_) | (_| | | | __/\__ \__ \ |_| | |_| | __/\__ \ |_ +|_| |_| \___/ \__, |_| \___||___/___/\__\_\\\\__,_|\___||___/\__| + |___/ +""" + + +class MainMenu(enum.IntEnum): + create = 1 + play = 2 + info = 3 + delete = 4 + quit = 5 + + +class BasicUserInterface: + def __init__(self, roster: Roster, args: argparse.Namespace) -> None: + self.roster = roster + self.args = args + + def signal_handler(sig, frame): + print("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() + self.main_menu() + + def logo(self) -> None: + print(LOGO) + + def main_menu(self): + 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) -> T.Optional[Player]: + name = input("Name your new character: ") + if not name: + print("Cancelled.") + return None + + race = self.menu([(race, race.name) for race in RACES]) + class_ = self.menu([(class_, class_.name) for class_ in 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 + ) + self.roster.players.append(player) + if 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) + 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) + + 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}") + + 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) -> T.Optional[Player]: + 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: T.Sequence[T.Tuple[T.Any, str]], + title: T.Optional[str] = None, + ) -> T.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 betwen 1..{len(options)}") + continue + return options[num - 1][0] diff --git a/pqcli/ui/urwid/__init__.py b/pqcli/ui/urwid/__init__.py index adcffec..e563845 100644 --- a/pqcli/ui/urwid/__init__.py +++ b/pqcli/ui/urwid/__init__.py @@ -7,6 +7,7 @@ import urwid import xdg from pqcli import random +from pqcli import lingo from pqcli.mechanic import Player from pqcli.roster import Roster from pqcli.ui.urwid.confirm_dialog import ConfirmDialog @@ -132,12 +133,11 @@ class UrwidUserInterface: self._connect("cancel", self.switch_to_roster_view) def switch_to_delete_player_view(self, player_idx: int) -> None: - adjective = random.choice(["faithful", "noble", "loyal", "brave"]) player_name = self.roster.players[player_idx].name self.old_views.append(self.loop.widget) self.loop.widget = ConfirmDialog( - f"Terminate {adjective} {player_name}?", self.old_views[-1] + lingo.terminate_message(player_name), self.old_views[-1] ) self._connect("confirm", lambda: self.delete_player(player_idx)) self._connect("cancel", self.cancel_dialog)