ui/basic: add basic user interface

This commit is contained in:
rr-
2018-12-14 00:32:21 +01:00
parent e27f3470a4
commit 595538cdca
5 changed files with 242 additions and 4 deletions
+10 -1
View File
@@ -4,6 +4,7 @@ from pathlib import Path
import xdg import xdg
from pqcli.roster import Roster from pqcli.roster import Roster
from pqcli.ui.basic import BasicUserInterface
from pqcli.ui.urwid import UrwidUserInterface from pqcli.ui.urwid import UrwidUserInterface
SAVE_PATH = Path(xdg.XDG_CONFIG_HOME) / "pqcli" / "save.dat" 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: def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(prog="pqcli") parser = argparse.ArgumentParser(prog="pqcli")
parser.add_argument(
"--basic", action="store_true", help="Use basic user interface"
)
parser.add_argument( parser.add_argument(
"--no-config", "--no-config",
dest="use_config", dest="use_config",
@@ -35,7 +39,12 @@ def main() -> None:
roster = Roster.load(SAVE_PATH) roster = Roster.load(SAVE_PATH)
try: try:
UrwidUserInterface(roster, args).run() if args.basic:
ui = BasicUserInterface(roster, args)
else:
ui = UrwidUserInterface(roster, args)
ui.run()
finally: finally:
if args.use_saves: if args.use_saves:
roster.save() roster.save()
+5
View File
@@ -122,3 +122,8 @@ def special(m: int, subject: str) -> str:
return prefix( return prefix(
["Battle-", "cursed ", "Were-", "undead ", "demon "], m, subject, "" ["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}?"
+7 -1
View File
@@ -170,7 +170,7 @@ class Inventory(SignalMixin):
def pop(self, index: int) -> None: def pop(self, index: int) -> None:
item = self._items[index] 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._items.pop(index)
self.sync_encumbrance() self.sync_encumbrance()
self.emit("item_del", item) self.emit("item_del", item)
@@ -237,6 +237,12 @@ class SpellBook(SignalMixin):
def __iter__(self) -> T.Iterator[Spell]: def __iter__(self) -> T.Iterator[Spell]:
return iter(self._spells) 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: def add(self, spell_name: str, level: int) -> None:
for spell in self._spells: for spell in self._spells:
if spell.name == spell_name: if spell.name == spell_name:
+218
View File
@@ -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]
+2 -2
View File
@@ -7,6 +7,7 @@ import urwid
import xdg import xdg
from pqcli import random from pqcli import random
from pqcli import lingo
from pqcli.mechanic import Player from pqcli.mechanic import Player
from pqcli.roster import Roster from pqcli.roster import Roster
from pqcli.ui.urwid.confirm_dialog import ConfirmDialog from pqcli.ui.urwid.confirm_dialog import ConfirmDialog
@@ -132,12 +133,11 @@ class UrwidUserInterface:
self._connect("cancel", self.switch_to_roster_view) self._connect("cancel", self.switch_to_roster_view)
def switch_to_delete_player_view(self, player_idx: int) -> None: 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 player_name = self.roster.players[player_idx].name
self.old_views.append(self.loop.widget) self.old_views.append(self.loop.widget)
self.loop.widget = ConfirmDialog( 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("confirm", lambda: self.delete_player(player_idx))
self._connect("cancel", self.cancel_dialog) self._connect("cancel", self.cancel_dialog)