initial commit

This commit is contained in:
rr-
2018-11-05 21:04:39 +01:00
commit 7a1febe16e
8 changed files with 1182 additions and 0 deletions
View File
+27
View File
@@ -0,0 +1,27 @@
import pickle
from pathlib import Path
import xdg
from pqcli.game_state import Roster
from pqcli.ui import Ui
SAVE_PATH = Path(xdg.XDG_CONFIG_HOME) / "pqcli" / "save.dat"
def main() -> None:
if SAVE_PATH.exists():
roster = pickle.loads(SAVE_PATH.read_bytes())
else:
roster = Roster()
try:
Ui(roster).run()
finally:
SAVE_PATH.parent.mkdir(parents=True, exist_ok=True)
SAVE_PATH.write_bytes(pickle.dumps(roster))
if __name__ == "__main__":
main()
+757
View File
@@ -0,0 +1,757 @@
import enum
import typing as T
from dataclasses import dataclass
def level_up_time(level) -> int:
# seconds
return 20 * level * 60
class Traits(enum.Enum):
name = "Name"
race = "Race"
class_ = "Class"
level = "Level"
class Stat(enum.Enum):
strength = "STR"
condition = "CON"
dexterity = "DEX"
intelligence = "INT"
wisdom = "WIS"
charisma = "CHA"
hp_max = "HP Max"
mp_max = "MP Max"
PRIME_STATS = [
Stat.strength,
Stat.condition,
Stat.dexterity,
Stat.intelligence,
Stat.wisdom,
Stat.charisma,
]
ALL_STATS = PRIME_STATS + [Stat.hp_max, Stat.mp_max]
EQUIPS = [
"Weapon",
"Shield",
"Helm",
"Hauberk",
"Brassairts",
"Vambraces",
"Gauntlets",
"Gambeson",
"Cuisses",
"Greaves",
"Sollerets",
]
SPELLS = [
"Slime Finger",
"Rabbit Punch",
"Hastiness",
"Good Move",
"Sadness",
"Seasick",
"Shoelaces",
"Innoculate",
"Cone of Annoyance",
"Magnetic Orb",
"Invisible Hands",
"Revolting Cloud",
"Aqueous Humor",
"Spectral Miasma",
"Clever Fellow",
"Lockjaw",
"History Lesson",
"Hydrophobia",
"Big Sister",
"Cone of Paste",
"Mulligan",
"Nestor's Bright Idea",
"Holy Batpole",
"Tumor (Benign)",
"Braingate",
"Summon a Bitch",
"Nonplus",
"Animate Nightstand",
"Eye of the Troglodyte",
"Curse Name",
"Dropsy",
"Vitreous Humor",
"Roger's Grand Illusion",
"Covet",
"Black Idaho",
"Astral Miasma",
"Spectral Oyster",
"Acrid Hands",
"Angioplasty",
"Grognor's Big Day Off",
"Tumor (Malignant)",
"Animate Tunic",
"Ursine Armor",
"Holy Roller",
"Tonsilectomy",
"Curse Family",
"Infinite Confusion",
]
OFFENSE_ATTRIB = [
"Polished|+1",
"Serrated|+1",
"Heavy|+1",
"Pronged|+2",
"Steely|+2",
"Vicious|+3",
"Venomed|+4",
"Stabbity|+4",
"Dancing|+5",
"Invisible|+6",
"Vorpal|+7",
]
DEFENSE_ATTRIB = [
"Studded|+1",
"Banded|+2",
"Gilded|+2",
"Festooned|+3",
"Holy|+4",
"Cambric|+1",
"Fine|+4",
"Impressive|+5",
"Custom|+3",
]
SHIELDS = [
"Parasol|0",
"Pie Plate|1",
"Garbage Can Lid|2",
"Buckler|3",
"Plexiglass|4",
"Fender|4",
"Round Shield|5",
"Carapace|5",
"Scutum|6",
"Propugner|6",
"Kite Shield|7",
"Pavise|8",
"Tower Shield|9",
"Baroque Shield|11",
"Aegis|12",
"Magnetic Field|18",
]
ARMORS = [
"Lace|1",
"Macrame|2",
"Burlap|3",
"Canvas|4",
"Flannel|5",
"Chamois|6",
"Pleathers|7",
"Leathers|8",
"Bearskin|9",
"Ringmail|10",
"Scale Mail|12",
"Chainmail|14",
"Splint Mail|15",
"Platemail|16",
"ABS|17",
"Kevlar|18",
"Titanium|19",
"Mithril Mail|20",
"Diamond Mail|25",
"Plasma|30",
]
WEAPONS = [
"Stick|0",
"Broken Bottle|1",
"Shiv|1",
"Sprig|1",
"Oxgoad|1",
"Eelspear|2",
"Bowie Knife|2",
"Claw Hammer|2",
"Handpeen|2",
"Andiron|3",
"Hatchet|3",
"Tomahawk|3",
"Hackbarm|3",
"Crowbar|4",
"Mace|4",
"Battleadze|4",
"Leafmace|5",
"Shortsword|5",
"Longiron|5",
"Poachard|5",
"Baselard|5",
"Whinyard|6",
"Blunderbuss|6",
"Longsword|6",
"Crankbow|6",
"Blibo|7",
"Broadsword|7",
"Kreen|7",
"Warhammer|7",
"Morning Star|8",
"Pole-adze|8",
"Spontoon|8",
"Bastard Sword|9",
"Peen-arm|9",
"Culverin|10",
"Lance|10",
"Halberd|11",
"Poleax|12",
"Bandyclef|15",
]
SPECIALS = [
"Diadem",
"Festoon",
"Gemstone",
"Phial",
"Tiara",
"Scabbard",
"Arrow",
"Lens",
"Lamp",
"Hymnal",
"Fleece",
"Laurel",
"Brooch",
"Gimlet",
"Cobble",
"Albatross",
"Brazier",
"Bandolier",
"Tome",
"Garnet",
"Amethyst",
"Candelabra",
"Corset",
"Sphere",
"Sceptre",
"Ankh",
"Talisman",
"Orb",
"Gammel",
"Ornament",
"Brocade",
"Galoon",
"Bijou",
"Spangle",
"Gimcrack",
"Hood",
"Vulpeculum",
]
ITEM_ATTRIB = [
"Golden",
"Gilded",
"Spectral",
"Astral",
"Garlanded",
"Precious",
"Crafted",
"Dual",
"Filigreed",
"Cruciate",
"Arcane",
"Blessed",
"Reverential",
"Lucky",
"Enchanted",
"Gleaming",
"Grandiose",
"Sacred",
"Legendary",
"Mythic",
"Crystalline",
"Austere",
"Ostentatious",
"One True",
"Proverbial",
"Fearsome",
"Deadly",
"Benevolent",
"Unearthly",
"Magnificent",
"Iron",
"Ormolu",
"Puissant",
]
ITEM_OFS = [
"Foreboding",
"Foreshadowing",
"Nervousness",
"Happiness",
"Torpor",
"Danger",
"Craft",
"Silence",
"Invisibility",
"Rapidity",
"Pleasure",
"Practicality",
"Hurting",
"Joy",
"Petulance",
"Intrusion",
"Chaos",
"Suffering",
"Extroversion",
"Frenzy",
"Sisu",
"Solitude",
"Punctuality",
"Efficiency",
"Comfort",
"Patience",
"Internment",
"Incarceration",
"Misapprehension",
"Loyalty",
"Envy",
"Acrimony",
"Worry",
"Fear",
"Awe",
"Guile",
"Prurience",
"Fortune",
"Perspicacity",
"Domination",
"Submission",
"Fealty",
"Hunger",
"Despair",
"Cruelty",
"Grob",
"Dignard",
"Ra",
"the Bone",
"Diamonique",
"Electrum",
"Hydragyrum",
]
BORING_ITEMS = [
"nail",
"lunchpail",
"sock",
"I.O.U.",
"cookie",
"pint",
"toothpick",
"writ",
"newspaper",
"letter",
"plank",
"hat",
"egg",
"coin",
"needle",
"bucket",
"ladder",
"chicken",
"twig",
"dirtclod",
"counterpane",
"vest",
"teratoma",
"bunny",
"rock",
"pole",
"carrot",
"canoe",
"inkwell",
"hoe",
"bandage",
"trowel",
"towel",
"planter box",
"anvil",
"axle",
"tuppence",
"casket",
"nosegay",
"trinket",
"credenza",
"writ",
]
MONSTERS = [
"Anhkheg|6|chitin",
"Ant|0|antenna",
"Ape|4|ass",
"Baluchitherium|14|ear",
"Beholder|10|eyestalk",
"Black Pudding|10|saliva",
"Blink Dog|4|eyelid",
"Cub Scout|1|neckerchief",
"Girl Scout|2|cookie",
"Boy Scout|3|merit badge",
"Eagle Scout|4|merit badge",
"Bugbear|3|skin",
"Bugboar|3|tusk",
"Boogie|3|slime",
"Camel|2|hump",
"Carrion Crawler|3|egg",
"Catoblepas|6|neck",
"Centaur|4|rib",
"Centipede|0|leg",
"Cockatrice|5|wattle",
"Couatl|9|wing",
"Crayfish|0|antenna",
"Demogorgon|53|tentacle",
"Jubilex|17|gel",
"Manes|1|tooth",
"Orcus|27|wand",
"Succubus|6|bra",
"Vrock|8|neck",
"Hezrou|9|leg",
"Glabrezu|10|collar",
"Nalfeshnee|11|tusk",
"Marilith|7|arm",
"Balor|8|whip",
"Yeenoghu|25|flail",
"Asmodeus|52|leathers",
"Baalzebul|43|pants",
"Barbed Devil|8|flame",
"Bone Devil|9|hook",
"Dispater|30|matches",
"Erinyes|6|thong",
"Geryon|30|cornucopia",
"Malebranche|5|fork",
"Ice Devil|11|snow",
"Lemure|3|blob",
"Pit Fiend|13|seed",
"Ankylosaurus|9|tail",
"Brontosaurus|30|brain",
"Diplodocus|24|fin",
"Elasmosaurus|15|neck",
"Gorgosaurus|13|arm",
"Iguanadon|6|thumb",
"Megalosaurus|12|jaw",
"Monoclonius|8|horn",
"Pentasaurus|12|head",
"Stegosaurus|18|plate",
"Triceratops|16|horn",
"Tyranosauraus Rex|18|forearm",
"Djinn|7|lamp",
"Doppleganger|4|face",
"Black Dragon|7|*",
"Plaid Dragon|7|sporrin",
"Blue Dragon|9|*",
"Beige Dragon|9|*",
"Brass Dragon|7|pole",
"Tin Dragon|8|*",
"Bronze Dragon|9|medal",
"Chromatic Dragon|16|scale",
"Copper Dragon|8|loafer",
"Gold Dragon|8|filling",
"Green Dragon|8|*",
"Platinum Dragon|21|*",
"Red Dragon|10|cocktail",
"Silver Dragon|10|*",
"White Dragon|6|tooth",
"Dragon Turtle|13|shell",
"Dryad|2|acorn",
"Dwarf|1|drawers",
"Eel|2|sashimi",
"Efreet|10|cinder",
"Sand Elemental|8|glass",
"Bacon Elemental|10|bit",
"Porn Elemental|12|lube",
"Cheese Elemental|14|curd",
"Hair Elemental|16|follicle",
"Swamp Elf|1|lilypad",
"Brown Elf|1|tusk",
"Sea Elf|1|jerkin",
"Ettin|10|fur",
"Frog|0|leg",
"Violet Fungi|3|spore",
"Gargoyle|4|gravel",
"Gelatinous Cube|4|jam",
"Ghast|4|vomit",
"Ghost|10|*",
"Ghoul|2|muscle",
"Humidity Giant|12|drops",
"Beef Giant|11|steak",
"Quartz Giant|10|crystal",
"Porcelain Giant|9|fixture",
"Rice Giant|8|grain",
"Cloud Giant|12|condensation",
"Fire Giant|11|cigarettes",
"Frost Giant|10|snowman",
"Hill Giant|8|corpse",
"Stone Giant|9|hatchling",
"Storm Giant|15|barometer",
"Mini Giant|4|pompadour",
"Gnoll|2|collar",
"Gnome|1|hat",
"Goblin|1|ear",
"Grid Bug|1|carapace",
"Jellyrock|9|seedling",
"Beer Golem|15|foam",
"Oxygen Golem|17|platelet",
"Cardboard Golem|14|recycling",
"Rubber Golem|16|ball",
"Leather Golem|15|fob",
"Gorgon|8|testicle",
"Gray Ooze|3|gravy",
"Green Slime|2|sample",
"Griffon|7|nest",
"Banshee|7|larynx",
"Harpy|3|mascara",
"Hell Hound|5|tongue",
"Hippocampus|4|mane",
"Hippogriff|3|egg",
"Hobgoblin|1|patella",
"Homonculus|2|fluid",
"Hydra|8|gyrum",
"Imp|2|tail",
"Invisible Stalker|8|*",
"Iron Peasant|3|chaff",
"Jumpskin|3|shin",
"Kobold|1|penis",
"Leprechaun|1|wallet",
"Leucrotta|6|hoof",
"Lich|11|crown",
"Lizard Man|2|tail",
"Lurker|10|sac",
"Manticore|6|spike",
"Mastodon|12|tusk",
"Medusa|6|eye",
"Multicell|2|dendrite",
"Pirate|1|booty",
"Berserker|1|shirt",
"Caveman|2|club",
"Dervish|1|robe",
"Merman|1|trident",
"Mermaid|1|gills",
"Mimic|9|hinge",
"Mind Flayer|8|tentacle",
"Minotaur|6|map",
"Yellow Mold|1|spore",
"Morkoth|7|teeth",
"Mummy|6|gauze",
"Naga|9|rattle",
"Nebbish|1|belly",
"Neo-Otyugh|11|organ ",
"Nixie|1|webbing",
"Nymph|3|hanky",
"Ochre Jelly|6|nucleus",
"Octopus|2|beak",
"Ogre|4|talon",
"Ogre Mage|5|apparel",
"Orc|1|snout",
"Otyugh|7|organ",
"Owlbear|5|feather",
"Pegasus|4|aileron",
"Peryton|4|antler",
"Piercer|3|tip",
"Pixie|1|dust",
"Man-o-war|3|tentacle",
"Purple Worm|15|dung",
"Quasit|3|tail",
"Rakshasa|7|pajamas",
"Rat|0|tail",
"Remorhaz|11|protrusion",
"Roc|18|wing",
"Roper|11|twine",
"Rot Grub|1|eggsac",
"Rust Monster|5|shavings",
"Satyr|5|hoof",
"Sea Hag|3|wart",
"Silkie|3|fur",
"Shadow|3|silhouette",
"Shambling Mound|10|mulch",
"Shedu|9|hoof",
"Shrieker|3|stalk",
"Skeleton|1|clavicle",
"Spectre|7|vestige",
"Sphinx|10|paw",
"Spider|0|web",
"Sprite|1|can",
"Stirge|1|proboscis",
"Stun Bear|5|tooth",
"Stun Worm|2|trode",
"Su-monster|5|tail",
"Sylph|3|thigh",
"Titan|20|sandal",
"Trapper|12|shag",
"Treant|10|acorn",
"Triton|3|scale",
"Troglodyte|2|tail",
"Troll|6|hide",
"Umber Hulk|8|claw",
"Unicorn|4|blood",
"Vampire|8|pancreas",
"Wight|4|lung",
"Will-o'-the-Wisp|9|wisp",
"Wraith|5|finger",
"Wyvern|7|wing",
"Xorn|7|jaw",
"Yeti|4|fur",
"Zombie|2|forehead",
"Wasp|0|stinger",
"Rat|1|tail",
"Bunny|0|ear",
"Moth|0|dust",
"Beagle|0|collar",
"Midge|0|corpse",
"Ostrich|1|beak",
"Billy Goat|1|beard",
"Bat|1|wing",
"Koala|2|heart",
"Wolf|2|paw",
"Whippet|2|collar",
"Uruk|2|boot",
"Poroid|4|node",
"Moakum|8|frenum",
"Fly|0|*",
"Hogbird|3|curl",
"Wolog|4|lemma",
]
MON_MODS = [
"-4 fœtal *",
"-4 dying *",
"-3 crippled *",
"-3 baby *",
"-2 adolescent *",
"-2 very sick *",
"-1 lesser *",
"-1 undernourished *",
"+1 greater *",
"+1 * Elder",
"+2 war *",
"+2 Battle-*",
"+3 Were-*",
"+3 undead *",
"+4 giant *",
"+4 * Rex",
]
OFFENSE_BAD = [
"Dull|-2",
"Tarnished|-1",
"Rusty|-3",
"Padded|-5",
"Bent|-4",
"Mini|-4",
"Rubber|-6",
"Nerf|-7",
"Unbalanced|-2",
]
DEFENSE_BAD = [
"Holey|-1",
"Patched|-1",
"Threadbare|-2",
"Faded|-1",
"Rusty|-3",
"Motheaten|-3",
"Mildewed|-2",
"Torn|-3",
"Dented|-3",
"Cursed|-5",
"Plastic|-4",
"Cracked|-4",
"Warped|-3",
"Corroded|-3",
]
@dataclass
class Race:
name: str
attr: T.List[Stat]
RACES = [
Race("Half Orc", [Stat.hp_max]),
Race("Half Man", [Stat.charisma]),
Race("Half Halfling", [Stat.dexterity]),
Race("Double Hobbit", [Stat.strength]),
Race("Hob-Hobbit", [Stat.dexterity, Stat.condition]),
Race("Low Elf", [Stat.condition]),
Race("Dung Elf", [Stat.wisdom]),
Race("Talking Pony", [Stat.mp_max, Stat.intelligence]),
Race("Gyrognome", [Stat.dexterity]),
Race("Lesser Dwarf", [Stat.condition]),
Race("Crested Dwarf", [Stat.charisma]),
Race("Eel Man", [Stat.dexterity]),
Race("Panda Man", [Stat.condition, Stat.strength]),
Race("Trans-Kobold", [Stat.wisdom]),
Race("Enchanted Motorcycle", [Stat.mp_max]),
Race("Will o' the Wisp", [Stat.wisdom]),
Race("Battle-Finch", [Stat.dexterity, Stat.intelligence]),
Race("Double Wookiee", [Stat.strength]),
Race("Skraeling", [Stat.wisdom]),
Race("Demicanadian", [Stat.condition]),
Race("Land Squid", [Stat.strength, Stat.hp_max]),
]
@dataclass
class Class:
name: str
attr: T.List[Stat]
CLASSES = [
Class("Ur-Paladin", [Stat.wisdom, Stat.condition]),
Class("Voodoo Princess", [Stat.intelligence, Stat.charisma]),
Class("Robot Monk", [Stat.strength]),
Class("Mu-Fu Monk", [Stat.dexterity]),
Class("Mage Illusioner", [Stat.intelligence, Stat.mp_max]),
Class("Shiv-Knight", [Stat.dexterity]),
Class("Inner Mason", [Stat.condition]),
Class("Fighter/Organist", [Stat.charisma, Stat.strength]),
Class("Puma Burgular", [Stat.dexterity]),
Class("Runeloremaster", [Stat.wisdom]),
Class("Hunter Strangler", [Stat.dexterity, Stat.intelligence]),
Class("Battle-Felon", [Stat.strength]),
Class("Tickle-Mimic", [Stat.wisdom, Stat.intelligence]),
Class("Slow Poisoner", [Stat.condition]),
Class("Bastard Lunatic", [Stat.condition]),
Class("Lowling", [Stat.wisdom]),
Class("Birdrider", [Stat.wisdom]),
Class("Vermineer", [Stat.intelligence]),
]
TITLES = [
"Mr.",
"Mrs.",
"Sir",
"Sgt.",
"Ms.",
"Captain",
"Chief",
"Admiral",
"Saint",
]
IMPRESSIVE_TITLES = [
"King",
"Queen",
"Lord",
"Lady",
"Viceroy",
"Mayor",
"Prince",
"Princess",
"Chief",
"Boss",
"Archbishop",
]
+84
View File
@@ -0,0 +1,84 @@
import datetime
import random
import typing as T
from dataclasses import dataclass
from pqcli.config import Stat, PRIME_STATS, Race, Class, RACES, CLASSES
def generate_name() -> str:
parts = [
"br|cr|dr|fr|gr|j|kr|l|m|n|pr||||r|sh|tr|v|wh|x|y|z".split("|"),
"a|a|e|e|i|i|o|o|u|u|ae|ie|oo|ou".split("|"),
"b|ck|d|g|k|m|n|p|t|v|x|z".split("|"),
]
result = ""
for i in range(6):
result += random.choice(parts[i % 3])
return result.title()
@dataclass
class Stats:
def __init__(self):
self.values = {stat: 0 for stat in Stat}
def __iter__(self) -> T.Iterable[T.Tuple[Stat, int]]:
return iter(self.values.items())
def __setitem__(self, stat: Stat, value: int) -> None:
self.values[stat] = value
def __getitem__(self, stat: Stat) -> int:
return self.values[stat]
@dataclass
class Player:
name: str
stats: Stats
race: Race
class_: Class
birthday: datetime.datetime
class Roster:
def __init__(self) -> None:
self.players: T.List[Player] = []
class StatsBuilder:
def __init__(self):
self.history: T.List[Stats] = []
def roll(self):
stats = Stats()
for stat in PRIME_STATS:
stats[stat] = (
3
+ random.randint(0, 5)
+ random.randint(0, 5)
+ random.randint(0, 5)
)
stats[Stat.hp_max] = random.randint(0, 7) + stats[Stat.condition] // 6
stats[Stat.mp_max] = (
random.randint(0, 7) + stats[Stat.intelligence] // 6
)
self.history.append(stats)
return stats
def unroll(self):
self.history.pop()
return self.history[-1]
def create_player(stats_builder: StatsBuilder) -> Player:
now = datetime.datetime.now()
random.seed(now)
return Player(
birthday=now,
name=generate_name(),
stats=stats_builder.roll(),
race=random.choice(RACES),
class_=random.choice(CLASSES),
)
+74
View File
@@ -0,0 +1,74 @@
import typing as T
import urwid
from pqcli.game_state import Roster
from pqcli.ui.exit_view import ExitView
from pqcli.ui.new_game_view import NewGameView
from pqcli.ui.roster_view import RosterView
urwid.command_map["k"] = "cursor up"
urwid.command_map["K"] = "cursor up"
urwid.command_map["j"] = "cursor down"
urwid.command_map["J"] = "cursor down"
urwid.command_map["h"] = "cursor left"
urwid.command_map["H"] = "cursor left"
urwid.command_map["l"] = "cursor right"
urwid.command_map["L"] = "cursor right"
class Ui:
def __init__(self, roster: Roster) -> None:
self.roster = roster
self.loop = urwid.MainLoop(None, unhandled_input=self.unhandled_input)
self.old_view: T.Optional[urwid.Widget] = None
self.switch_to_roster_view()
def run(self) -> None:
self.loop.run()
def unhandled_input(self, key: str) -> bool:
callback = getattr(self.loop.widget, "unhandled_input", None)
if callback and callback(key):
return True
if key == "ctrl q":
self.old_view = self.loop.widget
self.switch_to_exit_view()
return True
return False
def switch_to_roster_view(self) -> None:
self.loop.widget = RosterView(
self.roster,
self.loop,
on_exit=self.switch_to_exit_view,
on_new_game=self.switch_to_new_game_view,
on_resume_game=self.switch_to_game_view,
)
def switch_to_exit_view(self) -> None:
self.old_view = self.loop.widget
self.loop.widget = ExitView(
self.old_view, on_exit=self.exit, on_cancel=self.cancel_exit
)
def switch_to_new_game_view(self) -> None:
self.loop.widget = NewGameView(
on_confirm=self.switch_to_game_view,
on_cancel=self.switch_to_roster_view,
)
def switch_to_game_view(self, player_name: str) -> None:
...
def exit(self) -> None:
raise urwid.ExitMainLoop()
def cancel_exit(self) -> None:
assert self.old_view is not None
self.loop.widget = self.old_view
self.old_view = None
+34
View File
@@ -0,0 +1,34 @@
import typing as T
import urwid
from pqcli.game_state import Roster
class ExitView(urwid.Overlay):
def __init__(
self, parent: urwid.Widget, on_exit: T.Callable, on_cancel: T.Callable
) -> None:
self.on_exit = on_exit
self.on_cancel = on_cancel
question = urwid.Text(("bold", "Really quit?"), "center")
yes_btn = urwid.Button("Yes", lambda _user_data: self.on_exit())
no_btn = urwid.Button("No", lambda _user_data: self.on_cancel())
line_box = urwid.LineBox(
urwid.ListBox(
urwid.SimpleFocusListWalker([question, no_btn, yes_btn])
)
)
super().__init__(line_box, parent, "center", 20, "middle", 5)
def keypress(self, size: T.Any, key: str) -> bool:
if key in {"y", "Y"}:
self.on_exit()
return True
if key in {"n", "N"}:
self.on_cancel()
return True
return super().keypress(size, key)
+141
View File
@@ -0,0 +1,141 @@
import random
import typing as T
import urwid
import urwid_readline
from pqcli.game_state import Player, StatsBuilder, generate_name, create_player
from pqcli.config import RACES, CLASSES, PRIME_STATS
class StatsGroupBox(urwid.Pile):
def __init__(self, player: Player, stats_builder: StatsBuilder) -> None:
self.player = player
self.stats_builder = stats_builder
self.stat_labels = {stat: urwid.Text("0") for stat in PRIME_STATS}
self.total_label = urwid.Text("0")
self.update_values()
roll_button = urwid.Button("Roll", on_press=self.on_roll_press)
unroll_button = urwid.Button("Unroll", on_press=self.on_unroll_press)
value_texts = list(self.stat_labels.values()) + [self.total_label]
label_texts = [
urwid.Text(f"{stat.value}: ") for stat in PRIME_STATS
] + [urwid.Text("Total: ")]
super().__init__(
[
urwid.Columns(
[
(8, urwid.ListBox(label_texts)),
urwid.ListBox(value_texts),
]
),
urwid.Filler(roll_button),
urwid.Filler(unroll_button),
]
)
def on_roll_press(self, _user_data: T.Any) -> None:
self.player.stats = self.stats_builder.roll()
self.update_values()
def on_unroll_press(self, _user_data: T.Any) -> None:
try:
self.player.stats = self.stats_builder.unroll()
except IndexError:
return
self.update_values()
def update_values(self) -> None:
for stat, edit in self.stat_labels.items():
edit.set_text(str(self.player.stats[stat]))
self.total_label.set_text(
str(
sum(
self.player.stats[stat] for stat in self.stat_labels.keys()
)
)
)
class NewGameView(urwid.Pile):
def __init__(self, on_confirm: T.Callable, on_cancel: T.Callable) -> None:
self.on_confirm = on_confirm
self.on_cancel = on_cancel
stats_builder = StatsBuilder()
self.player = create_player(stats_builder)
race_checkboxes = []
for race in RACES:
urwid.RadioButton(
group=race_checkboxes,
label=race.name,
state=race.name == self.player.race.name,
)
class_checkboxes = []
for class_ in CLASSES:
urwid.RadioButton(
group=class_checkboxes,
label=class_.name,
state=class_.name == self.player.class_.name,
)
self.char_name_edit = urwid_readline.ReadlineEdit(
"Name: ", self.player.name
)
generate_char_name_btn = urwid.Button(
"?", on_press=self.on_generate_char_name_press
)
urwid.connect_signal(
self.char_name_edit, "postchange", self.on_char_name_change
)
super().__init__(
[
(
1,
urwid.Columns(
[
urwid.Filler(self.char_name_edit),
urwid.Filler(generate_char_name_btn),
]
),
),
urwid.Columns(
[
urwid.LineBox(
urwid.ListBox(race_checkboxes), title="Race"
),
urwid.LineBox(
urwid.ListBox(class_checkboxes), title="Class"
),
urwid.LineBox(
StatsGroupBox(self.player, stats_builder),
title="Stats",
),
]
),
]
)
def generate_random_char_name(self) -> None:
self.char_name_edit.edit_text = generate_name()
self.char_name_edit.edit_pos = len(self.char_name_edit.edit_text)
def on_char_name_change(self, widget: urwid.Text, old_text: str) -> None:
self.player.name = widget.edit_text
def on_generate_char_name_press(self, _user_data: T.Any) -> None:
self.generate_random_char_name()
def unhandled_input(self, key: str) -> bool:
if key == "esc":
self.on_cancel()
return True
return False
+65
View File
@@ -0,0 +1,65 @@
import typing as T
import urwid
from pqcli.game_state import Roster
class RosterView(urwid.ListBox):
def __init__(
self,
roster: Roster,
loop: urwid.MainLoop,
on_new_game: T.Callable,
on_resume_game: T.Callable,
on_exit: T.Callable,
) -> None:
self.roster = roster
self.on_new_game = on_new_game
self.on_resume_game = on_resume_game
self.on_exit = on_exit
logo = urwid.BigText("ProgressQuest", urwid.HalfBlock5x4Font())
buttons = []
for player in self.roster.players:
buttons.append(
urwid.Button(
label=player.name,
on_press=self.on_resume_game_press,
user_data=player.name,
)
)
buttons.append(
urwid.Button(
label="Create a new character", on_press=self.on_new_game_press
)
)
buttons.append(urwid.Button(label="Exit", on_press=self.on_exit_press))
super().__init__(
[
urwid.Padding(logo, width="clip"),
urwid.Divider(),
urwid.Pile(buttons),
]
)
def on_resume_game_press(self, user_data: T.Any) -> None:
player_name = T.cast(str, user_data)
self.on_resume_game(player_name)
def on_new_game_press(self, _user_data: T.Any) -> None:
self.on_new_game()
def on_exit_press(self, _user_data: T.Any) -> None:
self.on_exit()
def unhandled_input(self, key: str) -> bool:
if key == "esc":
self.on_exit()
return True
return False