mechanic: port game logic

This commit is contained in:
rr-
2018-11-05 21:04:39 +01:00
parent c458938225
commit 35083e42e9
4 changed files with 1215 additions and 543 deletions
+428 -432
View File
@@ -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 = [
+124
View File
@@ -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, ""
)
+657 -109
View File
@@ -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
+6 -2
View File
@@ -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: