diff --git a/pqcli/config.py b/pqcli/config.py index ca8bee7..940b799 100644 --- a/pqcli/config.py +++ b/pqcli/config.py @@ -3,14 +3,7 @@ import typing as T from dataclasses import dataclass -class Traits(enum.Enum): - name = "Name" - race = "Race" - class_ = "Class" - level = "Level" - - -class Stat(enum.Enum): +class StatType(enum.Enum): strength = "STR" condition = "CON" dexterity = "DEX" @@ -21,7 +14,7 @@ class Stat(enum.Enum): mp_max = "MP Max" -class Equipment(enum.Enum): +class EquipmentType(enum.Enum): weapon = "Weapon" shield = "Shield" helm = "Helm" @@ -36,15 +29,15 @@ class Equipment(enum.Enum): PRIME_STATS = [ - Stat.strength, - Stat.condition, - Stat.dexterity, - Stat.intelligence, - Stat.wisdom, - Stat.charisma, + StatType.strength, + StatType.condition, + StatType.dexterity, + StatType.intelligence, + StatType.wisdom, + StatType.charisma, ] -ALL_STATS = PRIME_STATS + [Stat.hp_max, Stat.mp_max] +ALL_STATS = PRIME_STATS + [StatType.hp_max, StatType.mp_max] SPELLS = [ @@ -97,114 +90,157 @@ SPELLS = [ "Infinite Confusion", ] + +@dataclass +class Modifier: + name: str + quality: int + + OFFENSE_ATTRIB = [ - "Polished|+1", - "Serrated|+1", - "Heavy|+1", - "Pronged|+2", - "Steely|+2", - "Vicious|+3", - "Venomed|+4", - "Stabbity|+4", - "Dancing|+5", - "Invisible|+6", - "Vorpal|+7", + Modifier("Polished", 1), + Modifier("Serrated", 1), + Modifier("Heavy", 1), + Modifier("Pronged", 2), + Modifier("Steely", 2), + Modifier("Vicious", 3), + Modifier("Venomed", 4), + Modifier("Stabbity", 4), + Modifier("Dancing", 5), + Modifier("Invisible", 6), + Modifier("Vorpal", 7), ] DEFENSE_ATTRIB = [ - "Studded|+1", - "Banded|+2", - "Gilded|+2", - "Festooned|+3", - "Holy|+4", - "Cambric|+1", - "Fine|+4", - "Impressive|+5", - "Custom|+3", + Modifier("Studded", 1), + Modifier("Banded", 2), + Modifier("Gilded", 2), + Modifier("Festooned", 3), + Modifier("Holy", 4), + Modifier("Cambric", 1), + Modifier("Fine", 4), + Modifier("Impressive", 5), + Modifier("Custom", 3), ] +OFFENSE_BAD = [ + Modifier("Dull", -2), + Modifier("Tarnished", -1), + Modifier("Rusty", -3), + Modifier("Padded", -5), + Modifier("Bent", -4), + Modifier("Mini", -4), + Modifier("Rubber", -6), + Modifier("Nerf", -7), + Modifier("Unbalanced", -2), +] + +DEFENSE_BAD = [ + Modifier("Holey", -1), + Modifier("Patched", -1), + Modifier("Threadbare", -2), + Modifier("Faded", -1), + Modifier("Rusty", -3), + Modifier("Motheaten", -3), + Modifier("Mildewed", -2), + Modifier("Torn", -3), + Modifier("Dented", -3), + Modifier("Cursed", -5), + Modifier("Plastic", -4), + Modifier("Cracked", -4), + Modifier("Warped", -3), + Modifier("Corroded", -3), +] + + +@dataclass +class Equipment: + name: str + quality: int + + 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", + Equipment("Parasol", 0), + Equipment("Pie Plate", 1), + Equipment("Garbage Can Lid", 2), + Equipment("Buckler", 3), + Equipment("Plexiglass", 4), + Equipment("Fender", 4), + Equipment("Round Shield", 5), + Equipment("Carapace", 5), + Equipment("Scutum", 6), + Equipment("Propugner", 6), + Equipment("Kite Shield", 7), + Equipment("Pavise", 8), + Equipment("Tower Shield", 9), + Equipment("Baroque Shield", 11), + Equipment("Aegis", 12), + Equipment("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", + Equipment("Lace", 1), + Equipment("Macrame", 2), + Equipment("Burlap", 3), + Equipment("Canvas", 4), + Equipment("Flannel", 5), + Equipment("Chamois", 6), + Equipment("Pleathers", 7), + Equipment("Leathers", 8), + Equipment("Bearskin", 9), + Equipment("Ringmail", 10), + Equipment("Scale Mail", 12), + Equipment("Chainmail", 14), + Equipment("Splint Mail", 15), + Equipment("Platemail", 16), + Equipment("ABS", 17), + Equipment("Kevlar", 18), + Equipment("Titanium", 19), + Equipment("Mithril Mail", 20), + Equipment("Diamond Mail", 25), + Equipment("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", + Equipment("Stick", 0), + Equipment("Broken Bottle", 1), + Equipment("Shiv", 1), + Equipment("Sprig", 1), + Equipment("Oxgoad", 1), + Equipment("Eelspear", 2), + Equipment("Bowie Knife", 2), + Equipment("Claw Hammer", 2), + Equipment("Handpeen", 2), + Equipment("Andiron", 3), + Equipment("Hatchet", 3), + Equipment("Tomahawk", 3), + Equipment("Hackbarm", 3), + Equipment("Crowbar", 4), + Equipment("Mace", 4), + Equipment("Battleadze", 4), + Equipment("Leafmace", 5), + Equipment("Shortsword", 5), + Equipment("Longiron", 5), + Equipment("Poachard", 5), + Equipment("Baselard", 5), + Equipment("Whinyard", 6), + Equipment("Blunderbuss", 6), + Equipment("Longsword", 6), + Equipment("Crankbow", 6), + Equipment("Blibo", 7), + Equipment("Broadsword", 7), + Equipment("Kreen", 7), + Equipment("Warhammer", 7), + Equipment("Morning Star", 8), + Equipment("Pole-adze", 8), + Equipment("Spontoon", 8), + Equipment("Bastard Sword", 9), + Equipment("Peen-arm", 9), + Equipment("Culverin", 10), + Equipment("Lance", 10), + Equipment("Halberd", 11), + Equipment("Poleax", 12), + Equipment("Bandyclef", 15), ] SPECIALS = [ @@ -383,346 +419,306 @@ BORING_ITEMS = [ "writ", ] + +@dataclass +class Monster: + name: str + level: int + item: T.Optional[str] + + 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", + Monster("Anhkheg", 6, "chitin"), + Monster("Ant", 0, "antenna"), + Monster("Ape", 4, "ass"), + Monster("Baluchitherium", 14, "ear"), + Monster("Beholder", 10, "eyestalk"), + Monster("Black Pudding", 10, "saliva"), + Monster("Blink Dog", 4, "eyelid"), + Monster("Cub Scout", 1, "neckerchief"), + Monster("Girl Scout", 2, "cookie"), + Monster("Boy Scout", 3, "merit badge"), + Monster("Eagle Scout", 4, "merit badge"), + Monster("Bugbear", 3, "skin"), + Monster("Bugboar", 3, "tusk"), + Monster("Boogie", 3, "slime"), + Monster("Camel", 2, "hump"), + Monster("Carrion Crawler", 3, "egg"), + Monster("Catoblepas", 6, "neck"), + Monster("Centaur", 4, "rib"), + Monster("Centipede", 0, "leg"), + Monster("Cockatrice", 5, "wattle"), + Monster("Couatl", 9, "wing"), + Monster("Crayfish", 0, "antenna"), + Monster("Demogorgon", 53, "tentacle"), + Monster("Jubilex", 17, "gel"), + Monster("Manes", 1, "tooth"), + Monster("Orcus", 27, "wand"), + Monster("Succubus", 6, "bra"), + Monster("Vrock", 8, "neck"), + Monster("Hezrou", 9, "leg"), + Monster("Glabrezu", 10, "collar"), + Monster("Nalfeshnee", 11, "tusk"), + Monster("Marilith", 7, "arm"), + Monster("Balor", 8, "whip"), + Monster("Yeenoghu", 25, "flail"), + Monster("Asmodeus", 52, "leathers"), + Monster("Baalzebul", 43, "pants"), + Monster("Barbed Devil", 8, "flame"), + Monster("Bone Devil", 9, "hook"), + Monster("Dispater", 30, "matches"), + Monster("Erinyes", 6, "thong"), + Monster("Geryon", 30, "cornucopia"), + Monster("Malebranche", 5, "fork"), + Monster("Ice Devil", 11, "snow"), + Monster("Lemure", 3, "blob"), + Monster("Pit Fiend", 13, "seed"), + Monster("Ankylosaurus", 9, "tail"), + Monster("Brontosaurus", 30, "brain"), + Monster("Diplodocus", 24, "fin"), + Monster("Elasmosaurus", 15, "neck"), + Monster("Gorgosaurus", 13, "arm"), + Monster("Iguanadon", 6, "thumb"), + Monster("Megalosaurus", 12, "jaw"), + Monster("Monoclonius", 8, "horn"), + Monster("Pentasaurus", 12, "head"), + Monster("Stegosaurus", 18, "plate"), + Monster("Triceratops", 16, "horn"), + Monster("Tyranosauraus Rex", 18, "forearm"), + Monster("Djinn", 7, "lamp"), + Monster("Doppleganger", 4, "face"), + Monster("Black Dragon", 7, None), + Monster("Plaid Dragon", 7, "sporrin"), + Monster("Blue Dragon", 9, None), + Monster("Beige Dragon", 9, None), + Monster("Brass Dragon", 7, "pole"), + Monster("Tin Dragon", 8, None), + Monster("Bronze Dragon", 9, "medal"), + Monster("Chromatic Dragon", 16, "scale"), + Monster("Copper Dragon", 8, "loafer"), + Monster("Gold Dragon", 8, "filling"), + Monster("Green Dragon", 8, None), + Monster("Platinum Dragon", 21, None), + Monster("Red Dragon", 10, "cocktail"), + Monster("Silver Dragon", 10, None), + Monster("White Dragon", 6, "tooth"), + Monster("Dragon Turtle", 13, "shell"), + Monster("Dryad", 2, "acorn"), + Monster("Dwarf", 1, "drawers"), + Monster("Eel", 2, "sashimi"), + Monster("Efreet", 10, "cinder"), + Monster("Sand Elemental", 8, "glass"), + Monster("Bacon Elemental", 10, "bit"), + Monster("Porn Elemental", 12, "lube"), + Monster("Cheese Elemental", 14, "curd"), + Monster("Hair Elemental", 16, "follicle"), + Monster("Swamp Elf", 1, "lilypad"), + Monster("Brown Elf", 1, "tusk"), + Monster("Sea Elf", 1, "jerkin"), + Monster("Ettin", 10, "fur"), + Monster("Frog", 0, "leg"), + Monster("Violet Fungi", 3, "spore"), + Monster("Gargoyle", 4, "gravel"), + Monster("Gelatinous Cube", 4, "jam"), + Monster("Ghast", 4, "vomit"), + Monster("Ghost", 10, None), + Monster("Ghoul", 2, "muscle"), + Monster("Humidity Giant", 12, "drops"), + Monster("Beef Giant", 11, "steak"), + Monster("Quartz Giant", 10, "crystal"), + Monster("Porcelain Giant", 9, "fixture"), + Monster("Rice Giant", 8, "grain"), + Monster("Cloud Giant", 12, "condensation"), + Monster("Fire Giant", 11, "cigarettes"), + Monster("Frost Giant", 10, "snowman"), + Monster("Hill Giant", 8, "corpse"), + Monster("Stone Giant", 9, "hatchling"), + Monster("Storm Giant", 15, "barometer"), + Monster("Mini Giant", 4, "pompadour"), + Monster("Gnoll", 2, "collar"), + Monster("Gnome", 1, "hat"), + Monster("Goblin", 1, "ear"), + Monster("Grid Bug", 1, "carapace"), + Monster("Jellyrock", 9, "seedling"), + Monster("Beer Golem", 15, "foam"), + Monster("Oxygen Golem", 17, "platelet"), + Monster("Cardboard Golem", 14, "recycling"), + Monster("Rubber Golem", 16, "ball"), + Monster("Leather Golem", 15, "fob"), + Monster("Gorgon", 8, "testicle"), + Monster("Gray Ooze", 3, "gravy"), + Monster("Green Slime", 2, "sample"), + Monster("Griffon", 7, "nest"), + Monster("Banshee", 7, "larynx"), + Monster("Harpy", 3, "mascara"), + Monster("Hell Hound", 5, "tongue"), + Monster("Hippocampus", 4, "mane"), + Monster("Hippogriff", 3, "egg"), + Monster("Hobgoblin", 1, "patella"), + Monster("Homonculus", 2, "fluid"), + Monster("Hydra", 8, "gyrum"), + Monster("Imp", 2, "tail"), + Monster("Invisible Stalker", 8, None), + Monster("Iron Peasant", 3, "chaff"), + Monster("Jumpskin", 3, "shin"), + Monster("Kobold", 1, "penis"), + Monster("Leprechaun", 1, "wallet"), + Monster("Leucrotta", 6, "hoof"), + Monster("Lich", 11, "crown"), + Monster("Lizard Man", 2, "tail"), + Monster("Lurker", 10, "sac"), + Monster("Manticore", 6, "spike"), + Monster("Mastodon", 12, "tusk"), + Monster("Medusa", 6, "eye"), + Monster("Multicell", 2, "dendrite"), + Monster("Pirate", 1, "booty"), + Monster("Berserker", 1, "shirt"), + Monster("Caveman", 2, "club"), + Monster("Dervish", 1, "robe"), + Monster("Merman", 1, "trident"), + Monster("Mermaid", 1, "gills"), + Monster("Mimic", 9, "hinge"), + Monster("Mind Flayer", 8, "tentacle"), + Monster("Minotaur", 6, "map"), + Monster("Yellow Mold", 1, "spore"), + Monster("Morkoth", 7, "teeth"), + Monster("Mummy", 6, "gauze"), + Monster("Naga", 9, "rattle"), + Monster("Nebbish", 1, "belly"), + Monster("Neo-Otyugh", 11, "organ "), + Monster("Nixie", 1, "webbing"), + Monster("Nymph", 3, "hanky"), + Monster("Ochre Jelly", 6, "nucleus"), + Monster("Octopus", 2, "beak"), + Monster("Ogre", 4, "talon"), + Monster("Ogre Mage", 5, "apparel"), + Monster("Orc", 1, "snout"), + Monster("Otyugh", 7, "organ"), + Monster("Owlbear", 5, "feather"), + Monster("Pegasus", 4, "aileron"), + Monster("Peryton", 4, "antler"), + Monster("Piercer", 3, "tip"), + Monster("Pixie", 1, "dust"), + Monster("Man-o-war", 3, "tentacle"), + Monster("Purple Worm", 15, "dung"), + Monster("Quasit", 3, "tail"), + Monster("Rakshasa", 7, "pajamas"), + Monster("Rat", 0, "tail"), + Monster("Remorhaz", 11, "protrusion"), + Monster("Roc", 18, "wing"), + Monster("Roper", 11, "twine"), + Monster("Rot Grub", 1, "eggsac"), + Monster("Rust Monster", 5, "shavings"), + Monster("Satyr", 5, "hoof"), + Monster("Sea Hag", 3, "wart"), + Monster("Silkie", 3, "fur"), + Monster("Shadow", 3, "silhouette"), + Monster("Shambling Mound", 10, "mulch"), + Monster("Shedu", 9, "hoof"), + Monster("Shrieker", 3, "stalk"), + Monster("Skeleton", 1, "clavicle"), + Monster("Spectre", 7, "vestige"), + Monster("Sphinx", 10, "paw"), + Monster("Spider", 0, "web"), + Monster("Sprite", 1, "can"), + Monster("Stirge", 1, "proboscis"), + Monster("Stun Bear", 5, "tooth"), + Monster("Stun Worm", 2, "trode"), + Monster("Su-monster", 5, "tail"), + Monster("Sylph", 3, "thigh"), + Monster("Titan", 20, "sandal"), + Monster("Trapper", 12, "shag"), + Monster("Treant", 10, "acorn"), + Monster("Triton", 3, "scale"), + Monster("Troglodyte", 2, "tail"), + Monster("Troll", 6, "hide"), + Monster("Umber Hulk", 8, "claw"), + Monster("Unicorn", 4, "blood"), + Monster("Vampire", 8, "pancreas"), + Monster("Wight", 4, "lung"), + Monster("Will-o'-the-Wisp", 9, "wisp"), + Monster("Wraith", 5, "finger"), + Monster("Wyvern", 7, "wing"), + Monster("Xorn", 7, "jaw"), + Monster("Yeti", 4, "fur"), + Monster("Zombie", 2, "forehead"), + Monster("Wasp", 0, "stinger"), + Monster("Rat", 1, "tail"), + Monster("Bunny", 0, "ear"), + Monster("Moth", 0, "dust"), + Monster("Beagle", 0, "collar"), + Monster("Midge", 0, "corpse"), + Monster("Ostrich", 1, "beak"), + Monster("Billy Goat", 1, "beard"), + Monster("Bat", 1, "wing"), + Monster("Koala", 2, "heart"), + Monster("Wolf", 2, "paw"), + Monster("Whippet", 2, "collar"), + Monster("Uruk", 2, "boot"), + Monster("Poroid", 4, "node"), + Monster("Moakum", 8, "frenum"), + Monster("Fly", 0, None), + Monster("Hogbird", 3, "curl"), + Monster("Wolog", 4, "lemma"), ] @dataclass class Race: name: str - attr: T.List[Stat] + attr: T.List[StatType] 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]), + Race("Half Orc", [StatType.hp_max]), + Race("Half Man", [StatType.charisma]), + Race("Half Halfling", [StatType.dexterity]), + Race("Double Hobbit", [StatType.strength]), + Race("Hob-Hobbit", [StatType.dexterity, StatType.condition]), + Race("Low Elf", [StatType.condition]), + Race("Dung Elf", [StatType.wisdom]), + Race("Talking Pony", [StatType.mp_max, StatType.intelligence]), + Race("Gyrognome", [StatType.dexterity]), + Race("Lesser Dwarf", [StatType.condition]), + Race("Crested Dwarf", [StatType.charisma]), + Race("Eel Man", [StatType.dexterity]), + Race("Panda Man", [StatType.condition, StatType.strength]), + Race("Trans-Kobold", [StatType.wisdom]), + Race("Enchanted Motorcycle", [StatType.mp_max]), + Race("Will o' the Wisp", [StatType.wisdom]), + Race("Battle-Finch", [StatType.dexterity, StatType.intelligence]), + Race("Double Wookiee", [StatType.strength]), + Race("Skraeling", [StatType.wisdom]), + Race("Demicanadian", [StatType.condition]), + Race("Land Squid", [StatType.strength, StatType.hp_max]), ] @dataclass class Class: name: str - attr: T.List[Stat] + attr: T.List[StatType] 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]), + Class("Ur-Paladin", [StatType.wisdom, StatType.condition]), + Class("Voodoo Princess", [StatType.intelligence, StatType.charisma]), + Class("Robot Monk", [StatType.strength]), + Class("Mu-Fu Monk", [StatType.dexterity]), + Class("Mage Illusioner", [StatType.intelligence, StatType.mp_max]), + Class("Shiv-Knight", [StatType.dexterity]), + Class("Inner Mason", [StatType.condition]), + Class("Fighter/Organist", [StatType.charisma, StatType.strength]), + Class("Puma Burgular", [StatType.dexterity]), + Class("Runeloremaster", [StatType.wisdom]), + Class("Hunter Strangler", [StatType.dexterity, StatType.intelligence]), + Class("Battle-Felon", [StatType.strength]), + Class("Tickle-Mimic", [StatType.wisdom, StatType.intelligence]), + Class("Slow Poisoner", [StatType.condition]), + Class("Bastard Lunatic", [StatType.condition]), + Class("Lowling", [StatType.wisdom]), + Class("Birdrider", [StatType.wisdom]), + Class("Vermineer", [StatType.intelligence]), ] TITLES = [ diff --git a/pqcli/lingo.py b/pqcli/lingo.py new file mode 100644 index 0000000..3b00a08 --- /dev/null +++ b/pqcli/lingo.py @@ -0,0 +1,124 @@ +import typing as T + +from pqcli import random + + +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() + + +def to_roman(num: int) -> str: + if not num: + return "N" + + ret = "" + + def _rome(dn: int, ds: str) -> bool: + nonlocal num, ret + if num >= dn: + num -= dn + ret += ds + return True + return False + + if num < 0: + ret = "-" + num = -num + + while _rome(1000, "M"): + pass + _rome(900, "CM") + _rome(500, "D") + _rome(400, "CD") + while _rome(100, "C"): + pass + _rome(90, "XC") + _rome(50, "L") + _rome(40, "XL") + while _rome(10, "X"): + pass + _rome(9, "IX") + _rome(5, "V") + _rome(4, "IV") + while _rome(1, "I"): + pass + return ret + + +def act_name(act: int) -> str: + if act == 0: + return "Prologue" + return f"Act {to_roman(act)}" + + +def plural(subject: str) -> str: + if subject.endswith("y"): + return subject[:-1] + "ies" + if subject.endswith("us"): + return subject[:-2] + "i" + if subject.endswith(("ch", "x", "s", "sh")): + return subject + "es" + if subject.endswith("f"): + return subject[:-1] + "ves" + if subject.endswith(("man", "Man")): + return subject[:-2] + "en" + return subject + "s" + + +def indefinite(subject: str, qty: int) -> str: + if qty == 1: + if subject.startswith(tuple("AEIOU?aeiou?")): + return "an " + subject + return "a " + subject + return str(qty) + " " + plural(subject) + + +def definite(subject: str, qty: int) -> str: + if qty > 1: + subject = plural(subject) + return "the " + subject + + +def prefix(a: T.List[str], m: int, subject: str, sep: str = " ") -> str: + m = abs(m) + if m < 1 or m > len(a): + return subject + return a[m - 1] + sep + subject + + +def sick(m: int, subject: str) -> str: + m = 6 - abs(m) + return prefix( + ["dead", "comatose", "crippled", "sick", "undernourished"], m, subject + ) + + +def young(m: int, subject: str) -> str: + m = 6 - abs(m) + return prefix( + ["foetal", "baby", "preadolescent", "teenage", "underage"], m, subject + ) + + +def big(m: int, subject: str) -> str: + return prefix( + ["greater", "massive", "enormous", "giant", "titanic"], m, subject + ) + + +def special(m: int, subject: str) -> str: + if " " in subject: + return prefix( + ["veteran", "cursed", "warrior", "undead", "demon"], m, subject + ) + return prefix( + ["Battle-", "cursed ", "Were-", "undead ", "demon "], m, subject, "" + ) diff --git a/pqcli/mechanic.py b/pqcli/mechanic.py index 9b5bfc6..923e472 100644 --- a/pqcli/mechanic.py +++ b/pqcli/mechanic.py @@ -1,10 +1,14 @@ import datetime -import enum +import itertools +import logging import typing as T -from dataclasses import dataclass, field +from dataclasses import dataclass from pqcli import random from pqcli.config import * +from pqcli.lingo import * + +logger = logging.getLogger(__name__) def level_up_time(level: int) -> int: @@ -12,32 +16,58 @@ def level_up_time(level: int) -> int: return 20 * level * 60 -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() +class Bar: + def __init__(self, max_: int, position: float = 0.0) -> None: + self.position = position + self.max_ = max_ + + def reset(self, new_max: int, position: float = 0.0) -> None: + self.max_ = new_max + self.position = position + + def increment(self, inc: float) -> None: + self.reposition(self.position + inc) + + @property + def done(self) -> bool: + return self.position >= self.max_ + + def reposition(self, new_pos: float) -> None: + self.position = float(min(new_pos, self.max_)) -@dataclass class Stats: - def __init__(self) -> None: - self.values = {stat: 0 for stat in Stat} + def __init__(self, values: T.Dict[StatType, int]) -> None: + self.values = values - def __iter__(self) -> T.Iterable[T.Tuple[Stat, int]]: + def __iter__(self) -> T.Iterator[T.Tuple[StatType, int]]: return iter(self.values.items()) - def __setitem__(self, stat: Stat, value: int) -> None: - self.values[stat] = value - - def __getitem__(self, stat: Stat) -> int: + def __getitem__(self, stat: StatType) -> int: return self.values[stat] + def increment(self, stat: StatType, qty: int = 1) -> None: + self.values[stat] += qty + logger.info("Increased %s to %d", stat.value, self[stat]) + + +class QuestBook: + def __init__(self) -> None: + self._quests: T.List[str] = [] + self.quest_bar = Bar(max_=1) + self.monster: T.Optional[Monster] = None + + @property + def current(self) -> T.Optional[str]: + if self._quests: + return self._quests[-1] + return None + + def add(self, name: str) -> None: + logger.info("Commencing quest: %s", name) + self._quests = self._quests[-100:] + self._quests.append(name) + @dataclass class Item: @@ -45,58 +75,113 @@ class Item: quantity: int +class Inventory: + def __init__(self, capacity: int = 0) -> None: + self._gold = 0 + self._items: T.List[Item] = [] + self.encum_bar = Bar(max_=capacity) + + @property + def gold(self) -> int: + return self._gold + + def __len__(self) -> int: + return len(self._items) + + def __iter__(self) -> T.Iterator[Item]: + return iter(self._items) + + def add_gold(self, quantity: int) -> None: + logger.info( + "%s %s", + "Spent" if quantity < 0 else "Got paid", + indefinite("gold piece", abs(quantity)), + ) + self._gold += quantity + + def at(self, index: int) -> Item: + return self._items[index] + + def pop(self, index: int) -> None: + item = self._items[index] + logger.info("Lost %s (qty=%d)", item.name, item.quantity) + self._items.pop(index) + self.sync_encumbrance() + + def add(self, item_name: str, quantity: int) -> None: + logger.info("Gained %s", indefinite(item_name, quantity)) + for item in self._items: + if item.name == item_name: + item.quantity += quantity + break + else: + self._items.append(Item(name=item_name, quantity=quantity)) + self.sync_encumbrance() + + def sync_encumbrance(self) -> None: + self.encum_bar.reposition(sum(item.quantity for item in self._items)) + + def set_capacity(self, capacity: int) -> None: + self.encum_bar.reset(capacity, self.encum_bar.position) + + @dataclass class Spell: name: str level: int -class Inventory: - def __init__(self, items: T.Optional[T.List[Item]] = []) -> None: - self.items = [] if items is None else items - - def add(self, item_name: str, quantity: int) -> None: - for item in self.items: - if item.name == item_name: - item.quantity += quantity - break - else: - self.items.append(Item(name=item_name, quantity=quantity)) - - class SpellBook: - def __init__(self, spells: T.Optional[T.List[Item]] = None) -> None: - self.spells = [] if spells is None else spells + def __init__(self) -> None: + self.spells: T.List[Spell] = [] + + def __iter__(self) -> T.Iterator[Spell]: + return iter(self.spells) def add(self, spell_name: str, level: int) -> None: - for item in self.spells: - if item.name == spell_name: - item.level += level + for spell in self.spells: + if spell.name == spell_name: + spell.level += level + logger.info("Learned %s at level %d", spell_name, spell.level) break else: self.spells.append(Spell(name=spell_name, level=level)) - - -class Bar: - def __init__(self, max_: int, position: int = 0) -> None: - self.position = position - self.max_ = max_ - - def reset(self, new_max: int, position: int = 0) -> None: - self.max_ = new_max - self.position = position - - -class TaskType(enum.Enum): - task = "task" - plot = "plot" + logger.info("Learned %s at level %d", spell_name, level) @dataclass -class Task: - task_type: TaskType - length: int +class BaseTask: description: str + duration: int + + +@dataclass +class KillTask(BaseTask): + monster: T.Optional[Monster] = None + + +class BuyTask(BaseTask): + pass + + +class HeadingToKillingFieldsTask(BaseTask): + pass + + +class HeadingToMarketTask(BaseTask): + pass + + +class SellTask(BaseTask): + pass + + +class RegularTask(BaseTask): + pass + + +class PlotTask(RegularTask): + pass class Player: @@ -113,53 +198,66 @@ class Player: self.race: Race = race self.class_: Class = class_ self.stats: Stats = stats - self.level = 1 self.act = 0 - self.queue: T.List[Task] = [ - Task( - TaskType.task, - 10, - "Experiencing an enigmatic and foreboding night vision", + self.exp_bar = Bar(max_=level_up_time(1)) + self.level = 1 + + self.task_bar = Bar(max_=0) + self.task: T.Optional[BaseTask] = None + self.queue: T.List[BaseTask] = [ + RegularTask( + "Experiencing an enigmatic and foreboding night vision", 10_000 ), - Task( - TaskType.task, - 6, - "Much is revealed about that wise old bastard you'd underestimated", + RegularTask( + "Much is revealed about that wise old bastard " + "you'd underestimated", + 6_000, ), - Task( - TaskType.task, - 6, - "A shocking series of events leaves you alone and bewildered, but resolute", + RegularTask( + "A shocking series of events leaves you " + "alone and bewildered, but resolute", + 6_000, ), - Task( - TaskType.task, - 4, - "Drawing upon an unrealized reserve of determination, you set out on a long and dangerous journey", + RegularTask( + "Drawing upon an unrealized reserve of determination, " + "you set out on a long and dangerous journey", + 4_000, ), - Task(TaskType.plot, 2, "Loading"), + PlotTask(f"Loading {act_name(self.act + 1)}", 2_000), ] - self.exp_bar = Bar(max_=level_up_time(1)) - self.encum_bar = Bar(max_=stats[Stat.strength] + 10) - self.quest_bar = Bar(max_=1) - self.task_bar = Bar(max_=2000) - self.plot_bar = Bar(max_=sum(task.length for task in self.queue)) + self.plot_bar = Bar(max_=sum(task.duration for task in self.queue)) + self.quest_book = QuestBook() - self.inventory = Inventory([Item(name="Gold", quantity=0)]) - self.spell_book = SpellBook() - self.equipment: T.Dict[Equipment, str] = { - Equipment.weapon: "Sharp Rock", - Equipment.hauberk: "-3 Burlap", + self.inventory = Inventory(capacity=10 + self.stats[StatType.strength]) + self.equipment: T.Dict[EquipmentType, str] = { + EquipmentType.weapon: "Sharp Rock", + EquipmentType.hauberk: "-3 Burlap", } + self.spell_book = SpellBook() + + self.set_task(RegularTask("Loading", 2_000)) + + def set_task(self, task: BaseTask) -> None: + self.task = task + self.task_bar.reset(task.duration) + logger.info("%s...", task.description) + + def equip_price(self) -> int: + return 5 * (self.level ** 2) + 10 * self.level + 20 + def level_up(self) -> None: self.level += 1 - self.stats[Stat.hp_max] += ( - self.stats[Stat.condition] // 3 + 1 + random.below(4) + logger.info("Leveled up to level %d!", self.level) + self.stats.increment( + StatType.hp_max, + self.stats[StatType.condition] // 3 + 1 + random.below(4), ) - self.stats[Stat.mp_max] += ( - self.stats[Stat.intelligence] // 3 + 1 + random.below(4) + self.stats.increment( + StatType.mp_max, + self.stats[StatType.intelligence] // 3 + 1 + random.below(4), ) self.win_stat() self.win_stat() @@ -167,50 +265,496 @@ class Player: self.exp_bar.reset(level_up_time(self.level)) def win_stat(self) -> bool: + chosen_stat: T.Optional[StatType] = None + if random.odds(1, 2): - chosen_stat = random.choice(list(Stat)) + chosen_stat = random.choice(list(StatType)) else: - # favor the beststat so it will tend to clump + # favor the best stat so it will tend to clump t = sum(value ** 2 for _stat, value in self.stats) t = random.below(t) - chosen_stat: Stat = None + chosen_stat = None for stat, value in self.stats: chosen_stat = stat t -= value ** 2 if t < 0: - return False + break - self.stats[chosen_stat] += 1 - if chosen_stat == Stat.strength: - self.encum_bar.reset( - 10 + self.stats[Stat.strength], self.encum_bar.position - ) + assert chosen_stat is not None + self.stats.increment(chosen_stat) + if chosen_stat == StatType.strength: + self.inventory.set_capacity(10 + self.stats[StatType.strength]) return True def win_spell(self) -> None: self.spell_book.add( SPELLS[ random.below_low( - min(self.stats[Stat.wisdom] + self.level, len(SPELLS)) + min(self.stats[StatType.wisdom] + self.level, len(SPELLS)) ) ], 1, ) + def win_equipment(self) -> None: + choice = random.choice(list(EquipmentType)) + + stuff: T.List[Equipment] + better: T.List[Modifier] + worse: T.List[Modifier] + + if choice == EquipmentType.weapon: + stuff = WEAPONS + better = OFFENSE_ATTRIB + worse = OFFENSE_BAD + else: + stuff = SHIELDS if choice == EquipmentType.shield else ARMORS + better = DEFENSE_ATTRIB + worse = DEFENSE_BAD + + equipment = pick_equipment(stuff, self.level) + name = equipment.name + plus = self.level - equipment.quality + if plus < 0: + modifier_pool = worse + else: + modifier_pool = better + count = 0 + while count < 2 and plus: + modifier = random.choice(modifier_pool) + if modifier.name in name: + break # no repeats + if abs(plus) < abs(modifier.quality): + break # too much + name = modifier.name + " " + name + plus -= modifier.quality + count += 1 + + if plus < 0: + name = f"{plus} {name}" + if plus > 0: + name = f"+{plus} {name}" + + logger.info("Gained %s %s", name, choice.value) + self.equipment[choice] = name + def win_item(self) -> None: self.inventory.add(special_item(), 1) +class Simulation: + def __init__(self, player: Player) -> None: + self.player = player + self.last_tick = datetime.datetime.now() + self.elapsed = 0 + + def tick(self) -> None: + if not self.player.task_bar.done: + self.elapsed = 100 + self.player.task_bar.increment(100) + return + + # gain XP / level up + gain = self.player.task and isinstance(self.player.task, KillTask) + if gain: + if self.player.exp_bar.done: + self.player.level_up() + else: + self.player.exp_bar.increment(self.player.task_bar.max_ / 1000) + + # advance quest + if gain and self.player.act >= 1: + if ( + self.player.quest_book.quest_bar.done + or self.player.quest_book.current is None + ): + self.complete_quest() + else: + self.player.quest_book.quest_bar.increment( + self.player.task_bar.max_ / 1000 + ) + + # advance plot + if gain or not self.player.act: + if self.player.plot_bar.done: + self.interplot_cinematic() + else: + self.player.plot_bar.increment( + self.player.task_bar.max_ / 1000 + ) + + self.dequeue() + + def dequeue(self) -> None: + while self.player.task_bar.done: + if isinstance(self.player.task, KillTask): + if ( + self.player.task.monster is None + or self.player.task.monster.item is None + ): + # npc + self.player.win_item() + elif self.player.task.monster.item: + self.player.inventory.add( + ( + self.player.task.monster.name + + " " + + self.player.task.monster.item + ).lower(), + 1, + ) + + elif isinstance(self.player.task, BuyTask): + # buy some equipment + self.player.inventory.add_gold(-self.player.equip_price()) + self.player.win_equipment() + + elif isinstance(self.player.task, (HeadingToMarketTask, SellTask)): + if isinstance(self.player.task, SellTask): + item = self.player.inventory.at(0) + amount = item.quantity * self.player.level + if " of " in item.name: + amount *= (1 + random.below_low(10)) * ( + 1 + random.below_low(self.player.level) + ) + self.player.inventory.pop(0) + self.player.inventory.add_gold(amount) + if len(self.player.inventory): + item = self.player.inventory.at(0) + self.player.set_task( + SellTask( + "Selling " + indefinite(item.name, item.quantity), + 1_000, + ) + ) + break + + elif isinstance(self.player.task, PlotTask): + self.complete_act() + + old = self.player.task + if self.player.queue: + self.player.set_task(self.player.queue.pop(0)) + elif self.player.inventory.encum_bar.done: + self.player.set_task( + HeadingToMarketTask( + "Heading to market to sell loot", 4_000 + ) + ) + elif not isinstance(old, (KillTask, HeadingToKillingFieldsTask)): + if self.player.inventory.gold > self.player.equip_price(): + self.player.set_task( + BuyTask( + "Negotiating purchase of better equipment", 5_000 + ) + ) + else: + self.player.set_task( + HeadingToKillingFieldsTask( + "Heading to the killing fields", 4_000 + ) + ) + else: + self.player.set_task( + monster_task( + self.player.level, self.player.quest_book.monster + ) + ) + + def complete_act(self) -> None: + self.player.act += 1 + self.player.plot_bar.reset(60 * 60 * (1 + 5 * self.player.act)) + if self.player.act > 1: + self.player.win_item() + self.player.win_equipment() + + def complete_quest(self) -> None: + self.player.quest_book.quest_bar.reset(50 + random.below_low(1000)) + if self.player.quest_book.current: + logger.info("Quest completed: %s", self.player.quest_book.current) + random.choice( + [ + self.player.win_spell, + self.player.win_equipment, + self.player.win_stat, + self.player.win_item, + ] + )() + + self.player.quest_book.monster = None + caption = "" + choice = random.below(5) + if choice == 0: + self.player.quest_book.monster = unnamed_monster( + self.player.level, iterations=3 + ) + caption = "Exterminate " + definite( + self.player.quest_book.monster.name, 2 + ) + elif choice == 1: + caption = "Seek " + definite(interesting_item(), 1) + elif choice == 2: + caption = "Deliver this " + boring_item() + elif choice == 3: + caption = "Fetch me " + indefinite(boring_item(), 1) + elif choice == 4: + monster = unnamed_monster(self.player.level, iterations=1) + caption = "Placate " + definite(monster.name, 2) + else: + raise AssertionError + + self.player.quest_book.add(caption) + + def interplot_cinematic(self) -> None: + def enqueue(task: BaseTask) -> None: + self.player.queue.append(task) + self.dequeue() + + choice = random.below(3) + if choice == 0: + enqueue( + RegularTask( + "Exhausted, you arrive at a friendly oasis " + "in a hostile land", + 1_000, + ) + ) + enqueue( + RegularTask("You greet old friends and meet new allies", 2_000) + ) + enqueue( + RegularTask( + "You are privy to a council of powerful do-gooders", 2_000 + ) + ) + enqueue( + RegularTask("There is much to be done. You are chosen!", 1_000) + ) + + elif choice == 1: + enqueue( + RegularTask( + "Your quarry is in sight, " + "but a mighty enemy bars your path!", + 1_000, + ) + ) + + nemesis = named_monster(self.player.level + 3) + + enqueue( + RegularTask( + f"A desperate struggle commences with {nemesis}", 4_000 + ) + ) + + s = random.below(3) + for i in itertools.count(start=1): + if i > random.below(1 + self.player.act + 1): + break + s += 1 + random.below(2) + if s % 3 == 0: + enqueue( + RegularTask( + f"Locked in grim combat with {nemesis}", 2_000 + ) + ) + elif s % 3 == 1: + enqueue( + RegularTask( + f"{nemesis} seems to have the upper hand", 2_000 + ) + ) + elif s % 3 == 2: + enqueue( + RegularTask( + f"You seem to gain the advantage over {nemesis}", + 2_000, + ) + ) + else: + raise AssertionError() + + enqueue( + RegularTask( + f"Victory! {nemesis} is slain! " + "Exhausted, you lose conciousness", + 3_000, + ) + ) + enqueue( + RegularTask( + "You awake in a friendly place, but the road awaits", 2_000 + ) + ) + + elif choice == 2: + nemesis = impressive_guy() + enqueue( + RegularTask( + "Oh sweet relief! " + f"You've reached the protection of the good {nemesis}", + 2_000, + ) + ) + enqueue( + RegularTask( + "There is rejoicing, " + f"and an unnerving encouter with {nemesis} in private", + 3_000, + ) + ) + enqueue( + RegularTask( + f"You forget your {boring_item()} and go back to get it", + 2_000, + ) + ) + enqueue( + RegularTask( + "What's this!? You overhear something shocking!", 2_000 + ) + ) + enqueue( + RegularTask( + f"Could {nemesis} be a dirty double-dealer?", 2_000 + ) + ) + enqueue( + RegularTask( + "Who can possibly be trusted with this news!? ... " + "Oh yes, of course", + 3_000, + ) + ) + + else: + raise AssertionError + + enqueue(PlotTask(f"Loading {act_name(self.player.act + 1)}", 1_000)) + + def special_item() -> str: - return interesting_item() + " of " + random.choice(ITEM_OFS) + return interesting_item() + " of " + T.cast(str, random.choice(ITEM_OFS)) def interesting_item() -> str: - return random.choice(ITEM_ATTRIB) + " " + random.choice(SPECIALS) + return ( + T.cast(str, random.choice(ITEM_ATTRIB)) + + " " + + T.cast(str, random.choice(SPECIALS)) + ) def boring_item() -> str: - return random.choice(BORING_ITEMS) + return T.cast(str, random.choice(BORING_ITEMS)) + + +def impressive_guy() -> str: + return T.cast(str, random.choice(IMPRESSIVE_TITLES)) + ( + " of the " + T.cast(Race, random.choice(RACES)).name + if random.below(2) + else " of " + generate_name() + ) + + +def unnamed_monster(level: int, iterations: int) -> Monster: + result = T.cast(Monster, random.choice(MONSTERS)) + for _ in range(iterations): + alternative = T.cast(Monster, random.choice(MONSTERS)) + if abs(level - alternative.level) < abs(level - result.level): + result = alternative + return result + + +def named_monster(level: int) -> str: + monster = unnamed_monster(level, iterations=4) + return generate_name() + " the " + monster.name + + +def pick_equipment(source: T.List[Equipment], goal: int) -> Equipment: + result = T.cast(Equipment, random.choice(source)) + for _ in range(5): + alternative = T.cast(Equipment, random.choice(source)) + if abs(goal - alternative.quality) < abs(goal - result.quality): + result = alternative + return result + + +def monster_task( + player_level: int, quest_monster: T.Optional[Monster] +) -> KillTask: + level = player_level + for _ in range(level): + if random.odds(2, 5): + level += random.below(2) * 2 - 1 + if level < 1: + level = 1 + + definite = False + monster: T.Optional[Monster] = None + if random.odds(1, 25): + # use an NPC every once in a while + race = random.choice(RACES) + if random.odds(1, 2): + result = "passing " + race.name + " " + random.choice(CLASSES).name + else: + result = ( + random.choice_low(TITLES) + + " " + + generate_name() + + " the " + + race.name + ) + definite = True + lev = level + elif quest_monster and random.odds(1, 4): + # use the quest monster + monster = quest_monster + result = monster.name + lev = monster.level + else: + # pick the monster out of so many random ones closest to the level we want + monster = unnamed_monster(level, iterations=5) + result = monster.name + lev = monster.level + + qty = 1 + if level - lev > 10: + # lev is too low. multiply + qty = (level + random.below(max(lev, 1))) // max(lev, 1) + if qty < 1: + qty = 1 + level //= qty + + if level - lev <= -10: + result = "imaginary " + result + elif level - lev < -5: + i = 10 + level - lev + i = 5 - random.below(i + 1) + result = sick(i, young(lev - level - i, result)) + elif level - lev < 0 and random.below(2) == 1: + result = sick(level - lev, result) + elif level - lev < 0: + result = young(level - lev, result) + elif level - lev >= 10: + result = "messianic " + result + elif level - lev > 5: + i = 10 - (level - lev) + i = 5 - random.below(i + 1) + result = big(i, special(level - lev - i, result)) + elif level - lev > 0 and random.below(2) == 1: + result = big(level - lev, result) + elif level - lev > 0: + result = special(level - lev, result) + + lev = level + level = lev * qty + if not definite: + result = indefinite(result, qty) + + duration = (2 * 3 * level * 1000) // player_level + return KillTask(f"Executing {result}", duration, monster=monster) class Roster: @@ -226,13 +770,17 @@ class StatsBuilder: self.history: T.List[Stats] = [] def roll(self) -> Stats: - stats = Stats() - for stat in PRIME_STATS: - stats[stat] = ( - 3 + random.below(6) + random.below(6) + random.below(6) - ) - stats[Stat.hp_max] = random.below(8) + stats[Stat.condition] // 6 - stats[Stat.mp_max] = random.below(8) + stats[Stat.intelligence] // 6 + values: T.Dict[StatType, int] = { + stat: 3 + random.below(6) + random.below(6) + random.below(6) + for stat in PRIME_STATS + } + values[StatType.hp_max] = ( + random.below(8) + values[StatType.condition] // 6 + ) + values[StatType.mp_max] = ( + random.below(8) + values[StatType.intelligence] // 6 + ) + stats = Stats(values) self.history.append(stats) return stats diff --git a/pqcli/random.py b/pqcli/random.py index 57eb83e..61a88a0 100644 --- a/pqcli/random.py +++ b/pqcli/random.py @@ -6,8 +6,12 @@ def seed(source: T.Any) -> None: random.seed(source) -def choice(source: T.Iterable[T.Any]) -> T.Any: - return random.choice(source) +def choice(source: T.Sequence[T.Any]) -> T.Any: + return source[below(len(source))] + + +def choice_low(source: T.Sequence[T.Any]) -> T.Any: + return source[below_low(len(source))] def below(num: int) -> int: