mechanic: move word lists to data files

The word lists (monsters, spells, equipment, races, classes, titles and
so on) now live in pqcli/data/*.json instead of pqcli/config.py. A
GameData object loaded at startup is passed to the simulation and UIs.

Users can replace any list by putting NAME.json in
$XDG_CONFIG_HOME/pqcli/data/ or a directory given with --data-dir.
--dump-data DIR copies the built-in files out for editing. Malformed
files are reported and pqcli exits before starting the UI.

Combat task descriptions come from the new combat_verbs list, with
{target} standing for the enemy. The built-in list holds only "Executing
{target}", and a single verb consumes no random roll, so stock gameplay
is unchanged. Recorded traces in tests/fixtures pin this down.

Monster, Race and Class stay in pqcli.config so existing saves still
load.

Resolves #3
This commit is contained in:
Dash committed 2026-10-17 15:55:41 +02:00
1 parent 5337d94f82
commit c0c6c8aeba
38 files changed
+7143 -778

No files matched your search

+43
View File
@@ -78,6 +78,49 @@ For server usage, run first-time setup without a forced slot (`pqcli-init`),
then run the long-lived detached service (`pqcli`) that loads slot `1` by
default.
## Customizing the adventure
All word lists (monsters, spells, equipment, races, classes, titles and so on)
live in JSON files. To change them, copy the built-in files somewhere and edit
them:
```console
$ pqcli --dump-data ~/.config/pqcli/data
```
On startup, pqcli reads `$XDG_CONFIG_HOME/pqcli/data/` (or the directory
given with `--data-dir DIR`). Each `NAME.json` there replaces the built-in
list of the same name. Files you leave out fall back to the built-in lists,
so you can delete the copies you don't change. If a file is malformed, pqcli
prints what is wrong and refuses to start.
The available files are `spells`, `offense_attrib`, `defense_attrib`,
`offense_bad`, `defense_bad`, `shields`, `armors`, `weapons`, `specials`,
`item_attrib`, `item_ofs`, `boring_items`, `monsters`, `races`, `classes`,
`titles`, `impressive_titles` and `combat_verbs`.
For example, to vary the combat descriptions, create `combat_verbs.json`.
`{target}` is replaced with the enemy:
```json
[
"Executing {target}",
"In a fierce battle to thwart {target}",
"Fighting bravely against {target}",
"Kicking the butt of {target}"
]
```
A few things to keep in mind:
- Order matters in `spells.json`: spells are learned roughly from the top
down, so keep weaker spells first.
- Titles near the top of `titles.json` show up more often.
- Stats in `races.json` and `classes.json` are written as `STR`, `CON`,
`DEX`, `INT`, `WIS`, `CHA`, `HP Max` or `MP Max`.
- Existing characters keep their race and class even if you remove them from
the lists.
## Contributing
```sh
+45 -4
View File
@@ -5,12 +5,19 @@ from pathlib import Path
from xdg_base_dirs import xdg_config_home
from pqcli.game_data import (
DataError,
GameData,
dump_builtin_data,
load_game_data,
)
from pqcli.mechanic import Player
from pqcli.roster import Roster
from pqcli.ui.basic import BasicUserInterface
from pqcli.ui.curses import CursesUserInterface
SAVE_PATH = Path(xdg_config_home()) / "pqcli" / "save.dat"
DATA_PATH = Path(xdg_config_home()) / "pqcli" / "data"
def parse_args() -> argparse.Namespace:
@@ -58,6 +65,22 @@ def parse_args() -> argparse.Namespace:
metavar="NUM",
help="play chosen character",
)
parser.add_argument(
"--data-dir",
type=Path,
default=DATA_PATH,
metavar="DIR",
help=(
"directory with custom word lists; each NAME.json replaces "
f"the built-in list of that name (default: {DATA_PATH})"
),
)
parser.add_argument(
"--dump-data",
type=Path,
metavar="DIR",
help="copy the built-in word lists to DIR for editing and exit",
)
return parser.parse_args()
@@ -66,12 +89,14 @@ def list_players(roster: Roster, file: T.Optional[T.Any] = None) -> None:
print(f"{i}. {player.name}", file=file)
def bootstrap_first_run(roster: Roster, args: argparse.Namespace) -> None:
def bootstrap_first_run(
roster: Roster, args: argparse.Namespace, data: GameData
) -> None:
if roster.players or args.list_saves or args.load_save:
return
print("No saved characters found. Starting first-run character creation.")
ui = BasicUserInterface(roster, None, args)
ui = BasicUserInterface(roster, None, args, data)
while not roster.players:
player = ui.create_player(auto_play=False)
if player:
@@ -83,6 +108,22 @@ def bootstrap_first_run(roster: Roster, args: argparse.Namespace) -> None:
def main() -> None:
args = parse_args()
if args.dump_data:
try:
paths = dump_builtin_data(args.dump_data)
except DataError as ex:
print(f"pqcli: {ex}", file=sys.stderr)
exit(1)
print(f"Wrote {len(paths)} files to {args.dump_data}")
exit(0)
try:
data = load_game_data(args.data_dir)
except DataError as ex:
print(f"pqcli: {ex}", file=sys.stderr)
exit(1)
roster = Roster.load(SAVE_PATH)
if args.list_saves:
@@ -98,10 +139,10 @@ def main() -> None:
list_players(roster, file=sys.stderr)
exit(1)
bootstrap_first_run(roster, args)
bootstrap_first_run(roster, args, data)
try:
ui = args.ui(roster, player, args)
ui = args.ui(roster, player, args, data)
ui.run()
finally:
if args.use_saves:
+3 -676
View File
@@ -39,56 +39,9 @@ PRIME_STATS = [
ALL_STATS = PRIME_STATS + [StatType.hp_max, StatType.mp_max]
SPELLS = [
"Slime Finger",
"Rabbit Punch",
"Hastiness",
"Good Move",
"Sadness",
"Seasick",
"Shoelaces",
"Inoculate",
"Cone of Annoyance",
"Magnetic Orb",
"Invisible Hands",
"Revolting Cloud",
"Aqueous Humor",
"Spectral Miasma",
"Clever Fellow",
"Lockjaw",
"History Lesson",
"Hydrophobia",
"Big Sister",
"Cone of Paste",
"Mulligan",
"Nestor's Bright Idea",
"Holy Batpole",
"Tumor (Benign)",
"Braingate",
"Summon a Bitch",
"Nonplus",
"Animate Nightstand",
"Eye of the Troglodyte",
"Curse Name",
"Dropsy",
"Vitreous Humor",
"Roger's Grand Illusion",
"Covet",
"Black Idaho",
"Astral Miasma",
"Spectral Oyster",
"Acrid Hands",
"Angioplasty",
"Grognor's Big Day Off",
"Tumor (Malignant)",
"Animate Tunic",
"Ursine Armor",
"Holy Roller",
"Tonsillectomy",
"Curse Family",
"Infinite Confusion",
]
# The word lists live in pqcli/data/*.json (see pqcli.game_data).
# Saves pickle Monster, Race and Class instances by import path, so these
# classes must stay in this module.
@dataclass
@@ -97,329 +50,12 @@ class Modifier:
quality: int
OFFENSE_ATTRIB = [
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 = [
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 EquipmentPreset:
name: str
quality: int
SHIELDS = [
EquipmentPreset("Parasol", 0),
EquipmentPreset("Pie Plate", 1),
EquipmentPreset("Garbage Can Lid", 2),
EquipmentPreset("Buckler", 3),
EquipmentPreset("Plexiglass", 4),
EquipmentPreset("Fender", 4),
EquipmentPreset("Round Shield", 5),
EquipmentPreset("Carapace", 5),
EquipmentPreset("Scutum", 6),
EquipmentPreset("Propugner", 6),
EquipmentPreset("Kite Shield", 7),
EquipmentPreset("Pavise", 8),
EquipmentPreset("Tower Shield", 9),
EquipmentPreset("Baroque Shield", 11),
EquipmentPreset("Aegis", 12),
EquipmentPreset("Magnetic Field", 18),
]
ARMORS = [
EquipmentPreset("Lace", 1),
EquipmentPreset("Macrame", 2),
EquipmentPreset("Burlap", 3),
EquipmentPreset("Canvas", 4),
EquipmentPreset("Flannel", 5),
EquipmentPreset("Chamois", 6),
EquipmentPreset("Pleathers", 7),
EquipmentPreset("Leathers", 8),
EquipmentPreset("Bearskin", 9),
EquipmentPreset("Ringmail", 10),
EquipmentPreset("Scale Mail", 12),
EquipmentPreset("Chainmail", 14),
EquipmentPreset("Splint Mail", 15),
EquipmentPreset("Platemail", 16),
EquipmentPreset("ABS", 17),
EquipmentPreset("Kevlar", 18),
EquipmentPreset("Titanium", 19),
EquipmentPreset("Mithril Mail", 20),
EquipmentPreset("Diamond Mail", 25),
EquipmentPreset("Plasma", 30),
]
WEAPONS = [
EquipmentPreset("Stick", 0),
EquipmentPreset("Broken Bottle", 1),
EquipmentPreset("Shiv", 1),
EquipmentPreset("Sprig", 1),
EquipmentPreset("Oxgoad", 1),
EquipmentPreset("Eelspear", 2),
EquipmentPreset("Bowie Knife", 2),
EquipmentPreset("Claw Hammer", 2),
EquipmentPreset("Handpeen", 2),
EquipmentPreset("Andiron", 3),
EquipmentPreset("Hatchet", 3),
EquipmentPreset("Tomahawk", 3),
EquipmentPreset("Hackbarm", 3),
EquipmentPreset("Crowbar", 4),
EquipmentPreset("Mace", 4),
EquipmentPreset("Battleadze", 4),
EquipmentPreset("Leafmace", 5),
EquipmentPreset("Shortsword", 5),
EquipmentPreset("Longiron", 5),
EquipmentPreset("Poachard", 5),
EquipmentPreset("Baselard", 5),
EquipmentPreset("Whinyard", 6),
EquipmentPreset("Blunderbuss", 6),
EquipmentPreset("Longsword", 6),
EquipmentPreset("Crankbow", 6),
EquipmentPreset("Blibo", 7),
EquipmentPreset("Broadsword", 7),
EquipmentPreset("Kreen", 7),
EquipmentPreset("Warhammer", 7),
EquipmentPreset("Morning Star", 8),
EquipmentPreset("Pole-adze", 8),
EquipmentPreset("Spontoon", 8),
EquipmentPreset("Bastard Sword", 9),
EquipmentPreset("Peen-arm", 9),
EquipmentPreset("Culverin", 10),
EquipmentPreset("Lance", 10),
EquipmentPreset("Halberd", 11),
EquipmentPreset("Poleax", 12),
EquipmentPreset("Bandyclef", 15),
]
SPECIALS = [
"Diadem",
"Festoon",
"Gemstone",
"Phial",
"Tiara",
"Scabbard",
"Arrow",
"Lens",
"Lamp",
"Hymnal",
"Fleece",
"Laurel",
"Brooch",
"Gimlet",
"Cobble",
"Albatross",
"Brazier",
"Bandolier",
"Tome",
"Garnet",
"Amethyst",
"Candelabra",
"Corset",
"Sphere",
"Sceptre",
"Ankh",
"Talisman",
"Orb",
"Gammel",
"Ornament",
"Brocade",
"Galoon",
"Bijou",
"Spangle",
"Gimcrack",
"Hood",
"Vulpeculum",
]
ITEM_ATTRIB = [
"Golden",
"Gilded",
"Spectral",
"Astral",
"Garlanded",
"Precious",
"Crafted",
"Dual",
"Filigreed",
"Cruciate",
"Arcane",
"Blessed",
"Reverential",
"Lucky",
"Enchanted",
"Gleaming",
"Grandiose",
"Sacred",
"Legendary",
"Mythic",
"Crystalline",
"Austere",
"Ostentatious",
"One True",
"Proverbial",
"Fearsome",
"Deadly",
"Benevolent",
"Unearthly",
"Magnificent",
"Iron",
"Ormolu",
"Puissant",
]
ITEM_OFS = [
"Foreboding",
"Foreshadowing",
"Nervousness",
"Happiness",
"Torpor",
"Danger",
"Craft",
"Silence",
"Invisibility",
"Rapidity",
"Pleasure",
"Practicality",
"Hurting",
"Joy",
"Petulance",
"Intrusion",
"Chaos",
"Suffering",
"Extroversion",
"Frenzy",
"Sisu",
"Solitude",
"Punctuality",
"Efficiency",
"Comfort",
"Patience",
"Internment",
"Incarceration",
"Misapprehension",
"Loyalty",
"Envy",
"Acrimony",
"Worry",
"Fear",
"Awe",
"Guile",
"Prurience",
"Fortune",
"Perspicacity",
"Domination",
"Submission",
"Fealty",
"Hunger",
"Despair",
"Cruelty",
"Grob",
"Dignard",
"Ra",
"the Bone",
"Diamonique",
"Electrum",
"Hydragyrum",
]
BORING_ITEMS = [
"nail",
"lunchpail",
"sock",
"I.O.U.",
"cookie",
"pint",
"toothpick",
"writ",
"newspaper",
"letter",
"plank",
"hat",
"egg",
"coin",
"needle",
"bucket",
"ladder",
"chicken",
"twig",
"dirtclod",
"counterpane",
"vest",
"teratoma",
"bunny",
"rock",
"pole",
"carrot",
"canoe",
"inkwell",
"hoe",
"bandage",
"trowel",
"towel",
"planter box",
"anvil",
"axle",
"tuppence",
"casket",
"nosegay",
"trinket",
"credenza",
"writ",
]
@dataclass
class Monster:
name: str
@@ -427,322 +63,13 @@ class Monster:
item: T.Optional[str]
MONSTERS = [
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("Tyrannosaurus Rex", 18, "forearm"),
Monster("Djinn", 7, "lamp"),
Monster("Doppelganger", 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("Homunculus", 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[StatType]
RACES = [
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[StatType]
CLASSES = [
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 = [
"Mr.",
"Mrs.",
"Sir",
"Sgt.",
"Ms.",
"Captain",
"Chief",
"Admiral",
"Saint",
]
IMPRESSIVE_TITLES = [
"King",
"Queen",
"Lord",
"Lady",
"Viceroy",
"Mayor",
"Prince",
"Princess",
"Chief",
"Boss",
"Archbishop",
]
+22
View File
@@ -0,0 +1,22 @@
[
{"name": "Lace", "quality": 1},
{"name": "Macrame", "quality": 2},
{"name": "Burlap", "quality": 3},
{"name": "Canvas", "quality": 4},
{"name": "Flannel", "quality": 5},
{"name": "Chamois", "quality": 6},
{"name": "Pleathers", "quality": 7},
{"name": "Leathers", "quality": 8},
{"name": "Bearskin", "quality": 9},
{"name": "Ringmail", "quality": 10},
{"name": "Scale Mail", "quality": 12},
{"name": "Chainmail", "quality": 14},
{"name": "Splint Mail", "quality": 15},
{"name": "Platemail", "quality": 16},
{"name": "ABS", "quality": 17},
{"name": "Kevlar", "quality": 18},
{"name": "Titanium", "quality": 19},
{"name": "Mithril Mail", "quality": 20},
{"name": "Diamond Mail", "quality": 25},
{"name": "Plasma", "quality": 30}
]
+44
View File
@@ -0,0 +1,44 @@
[
"nail",
"lunchpail",
"sock",
"I.O.U.",
"cookie",
"pint",
"toothpick",
"writ",
"newspaper",
"letter",
"plank",
"hat",
"egg",
"coin",
"needle",
"bucket",
"ladder",
"chicken",
"twig",
"dirtclod",
"counterpane",
"vest",
"teratoma",
"bunny",
"rock",
"pole",
"carrot",
"canoe",
"inkwell",
"hoe",
"bandage",
"trowel",
"towel",
"planter box",
"anvil",
"axle",
"tuppence",
"casket",
"nosegay",
"trinket",
"credenza",
"writ"
]
+20
View File
@@ -0,0 +1,20 @@
[
{"name": "Ur-Paladin", "attr": ["WIS", "CON"]},
{"name": "Voodoo Princess", "attr": ["INT", "CHA"]},
{"name": "Robot Monk", "attr": ["STR"]},
{"name": "Mu-Fu Monk", "attr": ["DEX"]},
{"name": "Mage Illusioner", "attr": ["INT", "MP Max"]},
{"name": "Shiv-Knight", "attr": ["DEX"]},
{"name": "Inner Mason", "attr": ["CON"]},
{"name": "Fighter/Organist", "attr": ["CHA", "STR"]},
{"name": "Puma Burgular", "attr": ["DEX"]},
{"name": "Runeloremaster", "attr": ["WIS"]},
{"name": "Hunter Strangler", "attr": ["DEX", "INT"]},
{"name": "Battle-Felon", "attr": ["STR"]},
{"name": "Tickle-Mimic", "attr": ["WIS", "INT"]},
{"name": "Slow Poisoner", "attr": ["CON"]},
{"name": "Bastard Lunatic", "attr": ["CON"]},
{"name": "Lowling", "attr": ["WIS"]},
{"name": "Birdrider", "attr": ["WIS"]},
{"name": "Vermineer", "attr": ["INT"]}
]
+3
View File
@@ -0,0 +1,3 @@
[
"Executing {target}"
]
+11
View File
@@ -0,0 +1,11 @@
[
{"name": "Studded", "quality": 1},
{"name": "Banded", "quality": 2},
{"name": "Gilded", "quality": 2},
{"name": "Festooned", "quality": 3},
{"name": "Holy", "quality": 4},
{"name": "Cambric", "quality": 1},
{"name": "Fine", "quality": 4},
{"name": "Impressive", "quality": 5},
{"name": "Custom", "quality": 3}
]
+16
View File
@@ -0,0 +1,16 @@
[
{"name": "Holey", "quality": -1},
{"name": "Patched", "quality": -1},
{"name": "Threadbare", "quality": -2},
{"name": "Faded", "quality": -1},
{"name": "Rusty", "quality": -3},
{"name": "Motheaten", "quality": -3},
{"name": "Mildewed", "quality": -2},
{"name": "Torn", "quality": -3},
{"name": "Dented", "quality": -3},
{"name": "Cursed", "quality": -5},
{"name": "Plastic", "quality": -4},
{"name": "Cracked", "quality": -4},
{"name": "Warped", "quality": -3},
{"name": "Corroded", "quality": -3}
]
+13
View File
@@ -0,0 +1,13 @@
[
"King",
"Queen",
"Lord",
"Lady",
"Viceroy",
"Mayor",
"Prince",
"Princess",
"Chief",
"Boss",
"Archbishop"
]
+35
View File
@@ -0,0 +1,35 @@
[
"Golden",
"Gilded",
"Spectral",
"Astral",
"Garlanded",
"Precious",
"Crafted",
"Dual",
"Filigreed",
"Cruciate",
"Arcane",
"Blessed",
"Reverential",
"Lucky",
"Enchanted",
"Gleaming",
"Grandiose",
"Sacred",
"Legendary",
"Mythic",
"Crystalline",
"Austere",
"Ostentatious",
"One True",
"Proverbial",
"Fearsome",
"Deadly",
"Benevolent",
"Unearthly",
"Magnificent",
"Iron",
"Ormolu",
"Puissant"
]
+54
View File
@@ -0,0 +1,54 @@
[
"Foreboding",
"Foreshadowing",
"Nervousness",
"Happiness",
"Torpor",
"Danger",
"Craft",
"Silence",
"Invisibility",
"Rapidity",
"Pleasure",
"Practicality",
"Hurting",
"Joy",
"Petulance",
"Intrusion",
"Chaos",
"Suffering",
"Extroversion",
"Frenzy",
"Sisu",
"Solitude",
"Punctuality",
"Efficiency",
"Comfort",
"Patience",
"Internment",
"Incarceration",
"Misapprehension",
"Loyalty",
"Envy",
"Acrimony",
"Worry",
"Fear",
"Awe",
"Guile",
"Prurience",
"Fortune",
"Perspicacity",
"Domination",
"Submission",
"Fealty",
"Hunger",
"Despair",
"Cruelty",
"Grob",
"Dignard",
"Ra",
"the Bone",
"Diamonique",
"Electrum",
"Hydragyrum"
]
+234
View File
@@ -0,0 +1,234 @@
[
{"name": "Anhkheg", "level": 6, "item": "chitin"},
{"name": "Ant", "level": 0, "item": "antenna"},
{"name": "Ape", "level": 4, "item": "ass"},
{"name": "Baluchitherium", "level": 14, "item": "ear"},
{"name": "Beholder", "level": 10, "item": "eyestalk"},
{"name": "Black Pudding", "level": 10, "item": "saliva"},
{"name": "Blink Dog", "level": 4, "item": "eyelid"},
{"name": "Cub Scout", "level": 1, "item": "neckerchief"},
{"name": "Girl Scout", "level": 2, "item": "cookie"},
{"name": "Boy Scout", "level": 3, "item": "merit badge"},
{"name": "Eagle Scout", "level": 4, "item": "merit badge"},
{"name": "Bugbear", "level": 3, "item": "skin"},
{"name": "Bugboar", "level": 3, "item": "tusk"},
{"name": "Boogie", "level": 3, "item": "slime"},
{"name": "Camel", "level": 2, "item": "hump"},
{"name": "Carrion Crawler", "level": 3, "item": "egg"},
{"name": "Catoblepas", "level": 6, "item": "neck"},
{"name": "Centaur", "level": 4, "item": "rib"},
{"name": "Centipede", "level": 0, "item": "leg"},
{"name": "Cockatrice", "level": 5, "item": "wattle"},
{"name": "Couatl", "level": 9, "item": "wing"},
{"name": "Crayfish", "level": 0, "item": "antenna"},
{"name": "Demogorgon", "level": 53, "item": "tentacle"},
{"name": "Jubilex", "level": 17, "item": "gel"},
{"name": "Manes", "level": 1, "item": "tooth"},
{"name": "Orcus", "level": 27, "item": "wand"},
{"name": "Succubus", "level": 6, "item": "bra"},
{"name": "Vrock", "level": 8, "item": "neck"},
{"name": "Hezrou", "level": 9, "item": "leg"},
{"name": "Glabrezu", "level": 10, "item": "collar"},
{"name": "Nalfeshnee", "level": 11, "item": "tusk"},
{"name": "Marilith", "level": 7, "item": "arm"},
{"name": "Balor", "level": 8, "item": "whip"},
{"name": "Yeenoghu", "level": 25, "item": "flail"},
{"name": "Asmodeus", "level": 52, "item": "leathers"},
{"name": "Baalzebul", "level": 43, "item": "pants"},
{"name": "Barbed Devil", "level": 8, "item": "flame"},
{"name": "Bone Devil", "level": 9, "item": "hook"},
{"name": "Dispater", "level": 30, "item": "matches"},
{"name": "Erinyes", "level": 6, "item": "thong"},
{"name": "Geryon", "level": 30, "item": "cornucopia"},
{"name": "Malebranche", "level": 5, "item": "fork"},
{"name": "Ice Devil", "level": 11, "item": "snow"},
{"name": "Lemure", "level": 3, "item": "blob"},
{"name": "Pit Fiend", "level": 13, "item": "seed"},
{"name": "Ankylosaurus", "level": 9, "item": "tail"},
{"name": "Brontosaurus", "level": 30, "item": "brain"},
{"name": "Diplodocus", "level": 24, "item": "fin"},
{"name": "Elasmosaurus", "level": 15, "item": "neck"},
{"name": "Gorgosaurus", "level": 13, "item": "arm"},
{"name": "Iguanadon", "level": 6, "item": "thumb"},
{"name": "Megalosaurus", "level": 12, "item": "jaw"},
{"name": "Monoclonius", "level": 8, "item": "horn"},
{"name": "Pentasaurus", "level": 12, "item": "head"},
{"name": "Stegosaurus", "level": 18, "item": "plate"},
{"name": "Triceratops", "level": 16, "item": "horn"},
{"name": "Tyrannosaurus Rex", "level": 18, "item": "forearm"},
{"name": "Djinn", "level": 7, "item": "lamp"},
{"name": "Doppelganger", "level": 4, "item": "face"},
{"name": "Black Dragon", "level": 7, "item": null},
{"name": "Plaid Dragon", "level": 7, "item": "sporrin"},
{"name": "Blue Dragon", "level": 9, "item": null},
{"name": "Beige Dragon", "level": 9, "item": null},
{"name": "Brass Dragon", "level": 7, "item": "pole"},
{"name": "Tin Dragon", "level": 8, "item": null},
{"name": "Bronze Dragon", "level": 9, "item": "medal"},
{"name": "Chromatic Dragon", "level": 16, "item": "scale"},
{"name": "Copper Dragon", "level": 8, "item": "loafer"},
{"name": "Gold Dragon", "level": 8, "item": "filling"},
{"name": "Green Dragon", "level": 8, "item": null},
{"name": "Platinum Dragon", "level": 21, "item": null},
{"name": "Red Dragon", "level": 10, "item": "cocktail"},
{"name": "Silver Dragon", "level": 10, "item": null},
{"name": "White Dragon", "level": 6, "item": "tooth"},
{"name": "Dragon Turtle", "level": 13, "item": "shell"},
{"name": "Dryad", "level": 2, "item": "acorn"},
{"name": "Dwarf", "level": 1, "item": "drawers"},
{"name": "Eel", "level": 2, "item": "sashimi"},
{"name": "Efreet", "level": 10, "item": "cinder"},
{"name": "Sand Elemental", "level": 8, "item": "glass"},
{"name": "Bacon Elemental", "level": 10, "item": "bit"},
{"name": "Porn Elemental", "level": 12, "item": "lube"},
{"name": "Cheese Elemental", "level": 14, "item": "curd"},
{"name": "Hair Elemental", "level": 16, "item": "follicle"},
{"name": "Swamp Elf", "level": 1, "item": "lilypad"},
{"name": "Brown Elf", "level": 1, "item": "tusk"},
{"name": "Sea Elf", "level": 1, "item": "jerkin"},
{"name": "Ettin", "level": 10, "item": "fur"},
{"name": "Frog", "level": 0, "item": "leg"},
{"name": "Violet Fungi", "level": 3, "item": "spore"},
{"name": "Gargoyle", "level": 4, "item": "gravel"},
{"name": "Gelatinous Cube", "level": 4, "item": "jam"},
{"name": "Ghast", "level": 4, "item": "vomit"},
{"name": "Ghost", "level": 10, "item": null},
{"name": "Ghoul", "level": 2, "item": "muscle"},
{"name": "Humidity Giant", "level": 12, "item": "drops"},
{"name": "Beef Giant", "level": 11, "item": "steak"},
{"name": "Quartz Giant", "level": 10, "item": "crystal"},
{"name": "Porcelain Giant", "level": 9, "item": "fixture"},
{"name": "Rice Giant", "level": 8, "item": "grain"},
{"name": "Cloud Giant", "level": 12, "item": "condensation"},
{"name": "Fire Giant", "level": 11, "item": "cigarettes"},
{"name": "Frost Giant", "level": 10, "item": "snowman"},
{"name": "Hill Giant", "level": 8, "item": "corpse"},
{"name": "Stone Giant", "level": 9, "item": "hatchling"},
{"name": "Storm Giant", "level": 15, "item": "barometer"},
{"name": "Mini Giant", "level": 4, "item": "pompadour"},
{"name": "Gnoll", "level": 2, "item": "collar"},
{"name": "Gnome", "level": 1, "item": "hat"},
{"name": "Goblin", "level": 1, "item": "ear"},
{"name": "Grid Bug", "level": 1, "item": "carapace"},
{"name": "Jellyrock", "level": 9, "item": "seedling"},
{"name": "Beer Golem", "level": 15, "item": "foam"},
{"name": "Oxygen Golem", "level": 17, "item": "platelet"},
{"name": "Cardboard Golem", "level": 14, "item": "recycling"},
{"name": "Rubber Golem", "level": 16, "item": "ball"},
{"name": "Leather Golem", "level": 15, "item": "fob"},
{"name": "Gorgon", "level": 8, "item": "testicle"},
{"name": "Gray Ooze", "level": 3, "item": "gravy"},
{"name": "Green Slime", "level": 2, "item": "sample"},
{"name": "Griffon", "level": 7, "item": "nest"},
{"name": "Banshee", "level": 7, "item": "larynx"},
{"name": "Harpy", "level": 3, "item": "mascara"},
{"name": "Hell Hound", "level": 5, "item": "tongue"},
{"name": "Hippocampus", "level": 4, "item": "mane"},
{"name": "Hippogriff", "level": 3, "item": "egg"},
{"name": "Hobgoblin", "level": 1, "item": "patella"},
{"name": "Homunculus", "level": 2, "item": "fluid"},
{"name": "Hydra", "level": 8, "item": "gyrum"},
{"name": "Imp", "level": 2, "item": "tail"},
{"name": "Invisible Stalker", "level": 8, "item": null},
{"name": "Iron Peasant", "level": 3, "item": "chaff"},
{"name": "Jumpskin", "level": 3, "item": "shin"},
{"name": "Kobold", "level": 1, "item": "penis"},
{"name": "Leprechaun", "level": 1, "item": "wallet"},
{"name": "Leucrotta", "level": 6, "item": "hoof"},
{"name": "Lich", "level": 11, "item": "crown"},
{"name": "Lizard Man", "level": 2, "item": "tail"},
{"name": "Lurker", "level": 10, "item": "sac"},
{"name": "Manticore", "level": 6, "item": "spike"},
{"name": "Mastodon", "level": 12, "item": "tusk"},
{"name": "Medusa", "level": 6, "item": "eye"},
{"name": "Multicell", "level": 2, "item": "dendrite"},
{"name": "Pirate", "level": 1, "item": "booty"},
{"name": "Berserker", "level": 1, "item": "shirt"},
{"name": "Caveman", "level": 2, "item": "club"},
{"name": "Dervish", "level": 1, "item": "robe"},
{"name": "Merman", "level": 1, "item": "trident"},
{"name": "Mermaid", "level": 1, "item": "gills"},
{"name": "Mimic", "level": 9, "item": "hinge"},
{"name": "Mind Flayer", "level": 8, "item": "tentacle"},
{"name": "Minotaur", "level": 6, "item": "map"},
{"name": "Yellow Mold", "level": 1, "item": "spore"},
{"name": "Morkoth", "level": 7, "item": "teeth"},
{"name": "Mummy", "level": 6, "item": "gauze"},
{"name": "Naga", "level": 9, "item": "rattle"},
{"name": "Nebbish", "level": 1, "item": "belly"},
{"name": "Neo-Otyugh", "level": 11, "item": "organ "},
{"name": "Nixie", "level": 1, "item": "webbing"},
{"name": "Nymph", "level": 3, "item": "hanky"},
{"name": "Ochre Jelly", "level": 6, "item": "nucleus"},
{"name": "Octopus", "level": 2, "item": "beak"},
{"name": "Ogre", "level": 4, "item": "talon"},
{"name": "Ogre Mage", "level": 5, "item": "apparel"},
{"name": "Orc", "level": 1, "item": "snout"},
{"name": "Otyugh", "level": 7, "item": "organ"},
{"name": "Owlbear", "level": 5, "item": "feather"},
{"name": "Pegasus", "level": 4, "item": "aileron"},
{"name": "Peryton", "level": 4, "item": "antler"},
{"name": "Piercer", "level": 3, "item": "tip"},
{"name": "Pixie", "level": 1, "item": "dust"},
{"name": "Man-o-war", "level": 3, "item": "tentacle"},
{"name": "Purple Worm", "level": 15, "item": "dung"},
{"name": "Quasit", "level": 3, "item": "tail"},
{"name": "Rakshasa", "level": 7, "item": "pajamas"},
{"name": "Rat", "level": 0, "item": "tail"},
{"name": "Remorhaz", "level": 11, "item": "protrusion"},
{"name": "Roc", "level": 18, "item": "wing"},
{"name": "Roper", "level": 11, "item": "twine"},
{"name": "Rot Grub", "level": 1, "item": "eggsac"},
{"name": "Rust Monster", "level": 5, "item": "shavings"},
{"name": "Satyr", "level": 5, "item": "hoof"},
{"name": "Sea Hag", "level": 3, "item": "wart"},
{"name": "Silkie", "level": 3, "item": "fur"},
{"name": "Shadow", "level": 3, "item": "silhouette"},
{"name": "Shambling Mound", "level": 10, "item": "mulch"},
{"name": "Shedu", "level": 9, "item": "hoof"},
{"name": "Shrieker", "level": 3, "item": "stalk"},
{"name": "Skeleton", "level": 1, "item": "clavicle"},
{"name": "Spectre", "level": 7, "item": "vestige"},
{"name": "Sphinx", "level": 10, "item": "paw"},
{"name": "Spider", "level": 0, "item": "web"},
{"name": "Sprite", "level": 1, "item": "can"},
{"name": "Stirge", "level": 1, "item": "proboscis"},
{"name": "Stun Bear", "level": 5, "item": "tooth"},
{"name": "Stun Worm", "level": 2, "item": "trode"},
{"name": "Su-monster", "level": 5, "item": "tail"},
{"name": "Sylph", "level": 3, "item": "thigh"},
{"name": "Titan", "level": 20, "item": "sandal"},
{"name": "Trapper", "level": 12, "item": "shag"},
{"name": "Treant", "level": 10, "item": "acorn"},
{"name": "Triton", "level": 3, "item": "scale"},
{"name": "Troglodyte", "level": 2, "item": "tail"},
{"name": "Troll", "level": 6, "item": "hide"},
{"name": "Umber Hulk", "level": 8, "item": "claw"},
{"name": "Unicorn", "level": 4, "item": "blood"},
{"name": "Vampire", "level": 8, "item": "pancreas"},
{"name": "Wight", "level": 4, "item": "lung"},
{"name": "Will-o'-the-Wisp", "level": 9, "item": "wisp"},
{"name": "Wraith", "level": 5, "item": "finger"},
{"name": "Wyvern", "level": 7, "item": "wing"},
{"name": "Xorn", "level": 7, "item": "jaw"},
{"name": "Yeti", "level": 4, "item": "fur"},
{"name": "Zombie", "level": 2, "item": "forehead"},
{"name": "Wasp", "level": 0, "item": "stinger"},
{"name": "Rat", "level": 1, "item": "tail"},
{"name": "Bunny", "level": 0, "item": "ear"},
{"name": "Moth", "level": 0, "item": "dust"},
{"name": "Beagle", "level": 0, "item": "collar"},
{"name": "Midge", "level": 0, "item": "corpse"},
{"name": "Ostrich", "level": 1, "item": "beak"},
{"name": "Billy Goat", "level": 1, "item": "beard"},
{"name": "Bat", "level": 1, "item": "wing"},
{"name": "Koala", "level": 2, "item": "heart"},
{"name": "Wolf", "level": 2, "item": "paw"},
{"name": "Whippet", "level": 2, "item": "collar"},
{"name": "Uruk", "level": 2, "item": "boot"},
{"name": "Poroid", "level": 4, "item": "node"},
{"name": "Moakum", "level": 8, "item": "frenum"},
{"name": "Fly", "level": 0, "item": null},
{"name": "Hogbird", "level": 3, "item": "curl"},
{"name": "Wolog", "level": 4, "item": "lemma"}
]
+13
View File
@@ -0,0 +1,13 @@
[
{"name": "Polished", "quality": 1},
{"name": "Serrated", "quality": 1},
{"name": "Heavy", "quality": 1},
{"name": "Pronged", "quality": 2},
{"name": "Steely", "quality": 2},
{"name": "Vicious", "quality": 3},
{"name": "Venomed", "quality": 4},
{"name": "Stabbity", "quality": 4},
{"name": "Dancing", "quality": 5},
{"name": "Invisible", "quality": 6},
{"name": "Vorpal", "quality": 7}
]
+11
View File
@@ -0,0 +1,11 @@
[
{"name": "Dull", "quality": -2},
{"name": "Tarnished", "quality": -1},
{"name": "Rusty", "quality": -3},
{"name": "Padded", "quality": -5},
{"name": "Bent", "quality": -4},
{"name": "Mini", "quality": -4},
{"name": "Rubber", "quality": -6},
{"name": "Nerf", "quality": -7},
{"name": "Unbalanced", "quality": -2}
]
+23
View File
@@ -0,0 +1,23 @@
[
{"name": "Half Orc", "attr": ["HP Max"]},
{"name": "Half Man", "attr": ["CHA"]},
{"name": "Half Halfling", "attr": ["DEX"]},
{"name": "Double Hobbit", "attr": ["STR"]},
{"name": "Hob-Hobbit", "attr": ["DEX", "CON"]},
{"name": "Low Elf", "attr": ["CON"]},
{"name": "Dung Elf", "attr": ["WIS"]},
{"name": "Talking Pony", "attr": ["MP Max", "INT"]},
{"name": "Gyrognome", "attr": ["DEX"]},
{"name": "Lesser Dwarf", "attr": ["CON"]},
{"name": "Crested Dwarf", "attr": ["CHA"]},
{"name": "Eel Man", "attr": ["DEX"]},
{"name": "Panda Man", "attr": ["CON", "STR"]},
{"name": "Trans-Kobold", "attr": ["WIS"]},
{"name": "Enchanted Motorcycle", "attr": ["MP Max"]},
{"name": "Will o' the Wisp", "attr": ["WIS"]},
{"name": "Battle-Finch", "attr": ["DEX", "INT"]},
{"name": "Double Wookiee", "attr": ["STR"]},
{"name": "Skraeling", "attr": ["WIS"]},
{"name": "Demicanadian", "attr": ["CON"]},
{"name": "Land Squid", "attr": ["STR", "HP Max"]}
]
+18
View File
@@ -0,0 +1,18 @@
[
{"name": "Parasol", "quality": 0},
{"name": "Pie Plate", "quality": 1},
{"name": "Garbage Can Lid", "quality": 2},
{"name": "Buckler", "quality": 3},
{"name": "Plexiglass", "quality": 4},
{"name": "Fender", "quality": 4},
{"name": "Round Shield", "quality": 5},
{"name": "Carapace", "quality": 5},
{"name": "Scutum", "quality": 6},
{"name": "Propugner", "quality": 6},
{"name": "Kite Shield", "quality": 7},
{"name": "Pavise", "quality": 8},
{"name": "Tower Shield", "quality": 9},
{"name": "Baroque Shield", "quality": 11},
{"name": "Aegis", "quality": 12},
{"name": "Magnetic Field", "quality": 18}
]
+39
View File
@@ -0,0 +1,39 @@
[
"Diadem",
"Festoon",
"Gemstone",
"Phial",
"Tiara",
"Scabbard",
"Arrow",
"Lens",
"Lamp",
"Hymnal",
"Fleece",
"Laurel",
"Brooch",
"Gimlet",
"Cobble",
"Albatross",
"Brazier",
"Bandolier",
"Tome",
"Garnet",
"Amethyst",
"Candelabra",
"Corset",
"Sphere",
"Sceptre",
"Ankh",
"Talisman",
"Orb",
"Gammel",
"Ornament",
"Brocade",
"Galoon",
"Bijou",
"Spangle",
"Gimcrack",
"Hood",
"Vulpeculum"
]
+49
View File
@@ -0,0 +1,49 @@
[
"Slime Finger",
"Rabbit Punch",
"Hastiness",
"Good Move",
"Sadness",
"Seasick",
"Shoelaces",
"Inoculate",
"Cone of Annoyance",
"Magnetic Orb",
"Invisible Hands",
"Revolting Cloud",
"Aqueous Humor",
"Spectral Miasma",
"Clever Fellow",
"Lockjaw",
"History Lesson",
"Hydrophobia",
"Big Sister",
"Cone of Paste",
"Mulligan",
"Nestor's Bright Idea",
"Holy Batpole",
"Tumor (Benign)",
"Braingate",
"Summon a Bitch",
"Nonplus",
"Animate Nightstand",
"Eye of the Troglodyte",
"Curse Name",
"Dropsy",
"Vitreous Humor",
"Roger's Grand Illusion",
"Covet",
"Black Idaho",
"Astral Miasma",
"Spectral Oyster",
"Acrid Hands",
"Angioplasty",
"Grognor's Big Day Off",
"Tumor (Malignant)",
"Animate Tunic",
"Ursine Armor",
"Holy Roller",
"Tonsillectomy",
"Curse Family",
"Infinite Confusion"
]
+11
View File
@@ -0,0 +1,11 @@
[
"Mr.",
"Mrs.",
"Sir",
"Sgt.",
"Ms.",
"Captain",
"Chief",
"Admiral",
"Saint"
]
+41
View File
@@ -0,0 +1,41 @@
[
{"name": "Stick", "quality": 0},
{"name": "Broken Bottle", "quality": 1},
{"name": "Shiv", "quality": 1},
{"name": "Sprig", "quality": 1},
{"name": "Oxgoad", "quality": 1},
{"name": "Eelspear", "quality": 2},
{"name": "Bowie Knife", "quality": 2},
{"name": "Claw Hammer", "quality": 2},
{"name": "Handpeen", "quality": 2},
{"name": "Andiron", "quality": 3},
{"name": "Hatchet", "quality": 3},
{"name": "Tomahawk", "quality": 3},
{"name": "Hackbarm", "quality": 3},
{"name": "Crowbar", "quality": 4},
{"name": "Mace", "quality": 4},
{"name": "Battleadze", "quality": 4},
{"name": "Leafmace", "quality": 5},
{"name": "Shortsword", "quality": 5},
{"name": "Longiron", "quality": 5},
{"name": "Poachard", "quality": 5},
{"name": "Baselard", "quality": 5},
{"name": "Whinyard", "quality": 6},
{"name": "Blunderbuss", "quality": 6},
{"name": "Longsword", "quality": 6},
{"name": "Crankbow", "quality": 6},
{"name": "Blibo", "quality": 7},
{"name": "Broadsword", "quality": 7},
{"name": "Kreen", "quality": 7},
{"name": "Warhammer", "quality": 7},
{"name": "Morning Star", "quality": 8},
{"name": "Pole-adze", "quality": 8},
{"name": "Spontoon", "quality": 8},
{"name": "Bastard Sword", "quality": 9},
{"name": "Peen-arm", "quality": 9},
{"name": "Culverin", "quality": 10},
{"name": "Lance", "quality": 10},
{"name": "Halberd", "quality": 11},
{"name": "Poleax", "quality": 12},
{"name": "Bandyclef", "quality": 15}
]
+249
View File
@@ -0,0 +1,249 @@
import json
import sys
import typing as T
from dataclasses import dataclass, fields
from importlib import resources
from pathlib import Path
from pqcli.config import (
Class,
EquipmentPreset,
Modifier,
Monster,
Race,
StatType,
)
TARGET_PLACEHOLDER = "{target}"
class DataError(Exception):
pass
@dataclass(frozen=True)
class GameData:
spells: T.Tuple[str, ...]
offense_attrib: T.Tuple[Modifier, ...]
defense_attrib: T.Tuple[Modifier, ...]
offense_bad: T.Tuple[Modifier, ...]
defense_bad: T.Tuple[Modifier, ...]
shields: T.Tuple[EquipmentPreset, ...]
armors: T.Tuple[EquipmentPreset, ...]
weapons: T.Tuple[EquipmentPreset, ...]
specials: T.Tuple[str, ...]
item_attrib: T.Tuple[str, ...]
item_ofs: T.Tuple[str, ...]
boring_items: T.Tuple[str, ...]
monsters: T.Tuple[Monster, ...]
races: T.Tuple[Race, ...]
classes: T.Tuple[Class, ...]
titles: T.Tuple[str, ...]
impressive_titles: T.Tuple[str, ...]
combat_verbs: T.Tuple[str, ...]
DATA_FILES = tuple(field.name for field in fields(GameData))
class _Context:
def __init__(self, path: str) -> None:
self.path = path
def error(self, message: str) -> DataError:
return DataError(f"{self.path}: {message}")
def _check_keys(
ctx: _Context, where: str, obj: T.Any, keys: T.Sequence[str]
) -> T.Dict[str, T.Any]:
if not isinstance(obj, dict):
raise ctx.error(f"{where} must be an object")
missing = [key for key in keys if key not in obj]
if missing:
raise ctx.error(f"{where} is missing {', '.join(map(repr, missing))}")
extra = [key for key in obj if key not in keys]
if extra:
raise ctx.error(f"{where} has unknown {', '.join(map(repr, extra))}")
return obj
def _str(ctx: _Context, where: str, value: T.Any) -> str:
if not isinstance(value, str):
raise ctx.error(f"{where} must be a string")
return value
def _int(ctx: _Context, where: str, value: T.Any) -> int:
if isinstance(value, bool) or not isinstance(value, int):
raise ctx.error(f"{where} must be an integer")
return value
def _stat(ctx: _Context, where: str, value: T.Any) -> StatType:
_str(ctx, where, value)
for stat in StatType:
if value.lower() in (stat.value.lower(), stat.name):
return stat
allowed = ", ".join(stat.value for stat in StatType)
raise ctx.error(f"{where}: unknown stat {value!r} (use one of {allowed})")
def _parse_str(ctx: _Context, where: str, item: T.Any) -> str:
return _str(ctx, where, item)
def _parse_verb(ctx: _Context, where: str, item: T.Any) -> str:
verb = _str(ctx, where, item)
if TARGET_PLACEHOLDER not in verb:
raise ctx.error(f"{where} must contain {TARGET_PLACEHOLDER}")
return verb
def _parse_modifier(ctx: _Context, where: str, item: T.Any) -> Modifier:
obj = _check_keys(ctx, where, item, ["name", "quality"])
return Modifier(
_str(ctx, f"{where}.name", obj["name"]),
_int(ctx, f"{where}.quality", obj["quality"]),
)
def _parse_preset(ctx: _Context, where: str, item: T.Any) -> EquipmentPreset:
obj = _check_keys(ctx, where, item, ["name", "quality"])
return EquipmentPreset(
_str(ctx, f"{where}.name", obj["name"]),
_int(ctx, f"{where}.quality", obj["quality"]),
)
def _parse_monster(ctx: _Context, where: str, item: T.Any) -> Monster:
obj = _check_keys(ctx, where, item, ["name", "level", "item"])
loot = obj["item"]
return Monster(
_str(ctx, f"{where}.name", obj["name"]),
_int(ctx, f"{where}.level", obj["level"]),
None if loot is None else _str(ctx, f"{where}.item", loot),
)
def _parse_attrs(ctx: _Context, where: str, value: T.Any) -> T.List[StatType]:
if not isinstance(value, list):
raise ctx.error(f"{where} must be a list of stats")
return [_stat(ctx, f"{where}[{i}]", stat) for i, stat in enumerate(value)]
def _parse_race(ctx: _Context, where: str, item: T.Any) -> Race:
obj = _check_keys(ctx, where, item, ["name", "attr"])
return Race(
_str(ctx, f"{where}.name", obj["name"]),
_parse_attrs(ctx, f"{where}.attr", obj["attr"]),
)
def _parse_class(ctx: _Context, where: str, item: T.Any) -> Class:
obj = _check_keys(ctx, where, item, ["name", "attr"])
return Class(
_str(ctx, f"{where}.name", obj["name"]),
_parse_attrs(ctx, f"{where}.attr", obj["attr"]),
)
_PARSERS: T.Dict[str, T.Callable[[_Context, str, T.Any], T.Any]] = {
"spells": _parse_str,
"offense_attrib": _parse_modifier,
"defense_attrib": _parse_modifier,
"offense_bad": _parse_modifier,
"defense_bad": _parse_modifier,
"shields": _parse_preset,
"armors": _parse_preset,
"weapons": _parse_preset,
"specials": _parse_str,
"item_attrib": _parse_str,
"item_ofs": _parse_str,
"boring_items": _parse_str,
"monsters": _parse_monster,
"races": _parse_race,
"classes": _parse_class,
"titles": _parse_str,
"impressive_titles": _parse_str,
"combat_verbs": _parse_verb,
}
assert set(_PARSERS) == set(DATA_FILES)
def _parse(name: str, path: str, text: str) -> T.Tuple[T.Any, ...]:
ctx = _Context(path)
try:
value = json.loads(text)
except json.JSONDecodeError as ex:
raise ctx.error(f"invalid JSON: {ex}") from ex
if not isinstance(value, list):
raise ctx.error("must be a list")
if not value:
raise ctx.error("must not be empty")
parser = _PARSERS[name]
return tuple(parser(ctx, f"[{i}]", item) for i, item in enumerate(value))
def _builtin_dir() -> T.Any:
return resources.files("pqcli") / "data"
def _read_text(path: T.Any, display: str) -> str:
try:
return T.cast(str, path.read_text(encoding="utf-8"))
except UnicodeDecodeError as ex:
raise DataError(f"{display}: not valid UTF-8: {ex}") from ex
except OSError as ex:
raise DataError(f"{display}: cannot read: {ex}") from ex
def load_game_data(override_dir: T.Optional[Path] = None) -> GameData:
"""Load the built-in word lists, replacing any that override_dir has.
A missing override_dir is fine. Each <name>.json in it replaces the
whole built-in list of that name.
"""
overrides: T.Set[str] = set()
if override_dir is not None and override_dir.exists():
if not override_dir.is_dir():
raise DataError(f"{override_dir}: not a directory")
for path in sorted(override_dir.glob("*.json")):
if path.stem in DATA_FILES:
overrides.add(path.stem)
else:
print(
f"pqcli: ignoring {path}: not a known data file "
f"(known: {', '.join(DATA_FILES)})",
file=sys.stderr,
)
values: T.Dict[str, T.Tuple[T.Any, ...]] = {}
for name in DATA_FILES:
if name in overrides:
assert override_dir is not None
source: T.Any = override_dir / f"{name}.json"
display = str(source)
else:
source = _builtin_dir() / f"{name}.json"
display = f"<built-in>/{name}.json"
values[name] = _parse(name, display, _read_text(source, display))
return GameData(**values)
def dump_builtin_data(target_dir: Path) -> T.List[Path]:
"""Copy the built-in data files to target_dir, never overwriting."""
targets = [target_dir / f"{name}.json" for name in DATA_FILES]
existing = [path for path in targets if path.exists()]
if existing:
raise DataError(
"refusing to overwrite "
+ ", ".join(str(path) for path in existing)
)
target_dir.mkdir(parents=True, exist_ok=True)
for name, path in zip(DATA_FILES, targets):
source = _builtin_dir() / f"{name}.json"
path.write_text(
_read_text(source, f"<built-in>/{name}.json"), encoding="utf-8"
)
return targets
+84 -77
View File
@@ -1,4 +1,5 @@
import datetime
import functools
import itertools
import logging
import math
@@ -8,24 +9,7 @@ from dataclasses import dataclass
from pqcli import random
from pqcli.config import (
ARMORS,
BORING_ITEMS,
CLASSES,
DEFENSE_ATTRIB,
DEFENSE_BAD,
IMPRESSIVE_TITLES,
ITEM_ATTRIB,
ITEM_OFS,
MONSTERS,
OFFENSE_ATTRIB,
OFFENSE_BAD,
PRIME_STATS,
RACES,
SHIELDS,
SPECIALS,
SPELLS,
TITLES,
WEAPONS,
Class,
EquipmentPreset,
EquipmentType,
@@ -34,6 +18,7 @@ from pqcli.config import (
Race,
StatType,
)
from pqcli.game_data import TARGET_PLACEHOLDER, GameData
from pqcli.lingo import (
act_name,
big,
@@ -388,7 +373,7 @@ class Player(SignalMixin):
def equip_price(self) -> int:
return 5 * (self.level**2) + 10 * self.level + 20
def level_up(self) -> None:
def level_up(self, data: GameData) -> None:
self.level += 1
logger.info("Leveled up to level %d!", self.level)
self.stats.increment(
@@ -401,7 +386,7 @@ class Player(SignalMixin):
)
self.win_stat()
self.win_stat()
self.win_spell()
self.win_spell(data)
self.exp_bar.reset(level_up_time(self.level))
self.emit("level_up")
@@ -427,31 +412,34 @@ class Player(SignalMixin):
self.inventory.set_capacity(10 + self.stats[StatType.strength])
return True
def win_spell(self) -> None:
def win_spell(self, data: GameData) -> None:
spells = data.spells
self.spell_book.add(
SPELLS[
spells[
random.below_low(
min(self.stats[StatType.wisdom] + self.level, len(SPELLS))
min(self.stats[StatType.wisdom] + self.level, len(spells))
)
],
1,
)
def win_equipment(self) -> None:
def win_equipment(self, data: GameData) -> None:
choice = random.choice(list(EquipmentType))
stuff: T.List[EquipmentPreset]
better: T.List[Modifier]
worse: T.List[Modifier]
stuff: T.Sequence[EquipmentPreset]
better: T.Sequence[Modifier]
worse: T.Sequence[Modifier]
if choice == EquipmentType.weapon:
stuff = WEAPONS
better = OFFENSE_ATTRIB
worse = OFFENSE_BAD
stuff = data.weapons
better = data.offense_attrib
worse = data.offense_bad
else:
stuff = SHIELDS if choice == EquipmentType.shield else ARMORS
better = DEFENSE_ATTRIB
worse = DEFENSE_BAD
stuff = (
data.shields if choice == EquipmentType.shield else data.armors
)
better = data.defense_attrib
worse = data.defense_bad
equipment = pick_equipment(stuff, self.level)
name = equipment.name
@@ -479,13 +467,14 @@ class Player(SignalMixin):
logger.info("Gained %s %s", name, choice.value)
self.equipment.put(choice, name)
def win_item(self) -> None:
self.inventory.add(special_item(), 1)
def win_item(self, data: GameData) -> None:
self.inventory.add(special_item(data), 1)
class Simulation:
def __init__(self, player: Player) -> None:
def __init__(self, player: Player, data: GameData) -> None:
self.player = player
self.data = data
self.last_tick = datetime.datetime.now()
def tick(self, elapsed: float = 100.0) -> None:
@@ -526,7 +515,7 @@ class Simulation:
gain = self.player.task and isinstance(self.player.task, KillTask)
if gain:
if self.player.exp_bar.done:
self.player.level_up()
self.player.level_up(self.data)
else:
self.player.exp_bar.increment(self.player.task_bar.max_ / 1000)
@@ -561,7 +550,7 @@ class Simulation:
or self.player.task.monster.item is None
):
# npc
self.player.win_item()
self.player.win_item(self.data)
elif self.player.task.monster.item:
self.player.inventory.add(
(
@@ -575,7 +564,7 @@ class Simulation:
elif isinstance(self.player.task, BuyTask):
# buy some equipment
self.player.inventory.add_gold(-self.player.equip_price())
self.player.win_equipment()
self.player.win_equipment(self.data)
elif isinstance(self.player.task, (HeadingToMarketTask, SellTask)):
if isinstance(self.player.task, SellTask):
@@ -623,7 +612,9 @@ class Simulation:
else:
self.player.set_task(
monster_task(
self.player.level, self.player.quest_book.monster
self.data,
self.player.level,
self.player.quest_book.monster,
)
)
@@ -633,8 +624,8 @@ class Simulation:
60 * 60 * (1 + 5 * self.player.quest_book.act)
)
if self.player.quest_book.act > 1:
self.player.win_item()
self.player.win_equipment()
self.player.win_item(self.data)
self.player.win_equipment(self.data)
def complete_quest(self) -> None:
self.player.quest_book.quest_bar.reset(50 + random.below_low(1000))
@@ -644,10 +635,10 @@ class Simulation:
)
random.choice(
[
self.player.win_spell,
self.player.win_equipment,
functools.partial(self.player.win_spell, self.data),
functools.partial(self.player.win_equipment, self.data),
self.player.win_stat,
self.player.win_item,
functools.partial(self.player.win_item, self.data),
]
)()
@@ -656,19 +647,21 @@ class Simulation:
choice = random.below(5)
if choice == 0:
self.player.quest_book.monster = unnamed_monster(
self.player.level, iterations=3
self.data, self.player.level, iterations=3
)
caption = "Exterminate " + definite(
self.player.quest_book.monster.name, 2
)
elif choice == 1:
caption = "Seek " + definite(interesting_item(), 1)
caption = "Seek " + definite(interesting_item(self.data), 1)
elif choice == 2:
caption = "Deliver this " + boring_item()
caption = "Deliver this " + boring_item(self.data)
elif choice == 3:
caption = "Fetch me " + indefinite(boring_item(), 1)
caption = "Fetch me " + indefinite(boring_item(self.data), 1)
elif choice == 4:
monster = unnamed_monster(self.player.level, iterations=1)
monster = unnamed_monster(
self.data, self.player.level, iterations=1
)
caption = "Placate " + definite(monster.name, 2)
else:
raise AssertionError
@@ -711,7 +704,7 @@ class Simulation:
)
)
nemesis = named_monster(self.player.level + 3)
nemesis = named_monster(self.data, self.player.level + 3)
enqueue(
RegularTask(
@@ -760,7 +753,7 @@ class Simulation:
)
elif choice == 2:
nemesis = impressive_guy()
nemesis = impressive_guy(self.data)
enqueue(
RegularTask(
"Oh sweet relief! "
@@ -777,7 +770,8 @@ class Simulation:
)
enqueue(
RegularTask(
f"You forget your {boring_item()} and go back to get it",
f"You forget your {boring_item(self.data)} "
"and go back to get it",
2000,
)
)
@@ -807,46 +801,50 @@ class Simulation:
)
def special_item() -> str:
return interesting_item() + " of " + T.cast(str, random.choice(ITEM_OFS))
def interesting_item() -> str:
def special_item(data: GameData) -> str:
return (
T.cast(str, random.choice(ITEM_ATTRIB))
+ " "
+ T.cast(str, random.choice(SPECIALS))
interesting_item(data)
+ " of "
+ T.cast(str, random.choice(data.item_ofs))
)
def boring_item() -> str:
return T.cast(str, random.choice(BORING_ITEMS))
def interesting_item(data: GameData) -> str:
return (
T.cast(str, random.choice(data.item_attrib))
+ " "
+ T.cast(str, random.choice(data.specials))
)
def impressive_guy() -> str:
return T.cast(str, random.choice(IMPRESSIVE_TITLES)) + (
" of the " + T.cast(Race, random.choice(RACES)).name
def boring_item(data: GameData) -> str:
return T.cast(str, random.choice(data.boring_items))
def impressive_guy(data: GameData) -> str:
return T.cast(str, random.choice(data.impressive_titles)) + (
" of the " + T.cast(Race, random.choice(data.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))
def unnamed_monster(data: GameData, level: int, iterations: int) -> Monster:
result = T.cast(Monster, random.choice(data.monsters))
for _ in range(iterations):
alternative = T.cast(Monster, random.choice(MONSTERS))
alternative = T.cast(Monster, random.choice(data.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)
def named_monster(data: GameData, level: int) -> str:
monster = unnamed_monster(data, level, iterations=4)
return generate_name() + " the " + monster.name
def pick_equipment(
source: T.List[EquipmentPreset], goal: int
source: T.Sequence[EquipmentPreset], goal: int
) -> EquipmentPreset:
result = T.cast(EquipmentPreset, random.choice(source))
for _ in range(5):
@@ -857,7 +855,7 @@ def pick_equipment(
def monster_task(
player_level: int, quest_monster: T.Optional[Monster]
data: GameData, player_level: int, quest_monster: T.Optional[Monster]
) -> KillTask:
level = player_level
for _ in range(level):
@@ -870,12 +868,14 @@ def monster_task(
monster: T.Optional[Monster] = None
if random.odds(1, 25):
# use an NPC every once in a while
race = random.choice(RACES)
race = random.choice(data.races)
if random.odds(1, 2):
result = "passing " + race.name + " " + random.choice(CLASSES).name
result = (
"passing " + race.name + " " + random.choice(data.classes).name
)
else:
result = (
random.choice_low(TITLES)
random.choice_low(data.titles)
+ " "
+ generate_name()
+ " the "
@@ -890,7 +890,7 @@ def monster_task(
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)
monster = unnamed_monster(data, level, iterations=5)
result = monster.name
lev = monster.level
@@ -929,7 +929,14 @@ def monster_task(
result = indefinite(result, qty)
duration = (2 * 3 * level * 1000) // player_level
return KillTask(f"Executing {result}", duration, monster=monster)
# with a single verb, skip the roll so the stock RNG stream is unchanged
verb = (
data.combat_verbs[0]
if len(data.combat_verbs) == 1
else T.cast(str, random.choice(data.combat_verbs))
)
description = verb.replace(TARGET_PLACEHOLDER, result)
return KillTask(description, duration, monster=monster)
class StatsBuilder:
+3
View File
@@ -2,6 +2,7 @@ import abc
import argparse
import typing as T
from pqcli.game_data import GameData
from pqcli.mechanic import Player
from pqcli.roster import Roster
@@ -12,8 +13,10 @@ class BaseUserInterface(abc.ABC):
roster: Roster,
player: T.Optional[Player],
args: argparse.Namespace,
data: GameData,
) -> None:
self.roster = roster
self.data = data
self.args = args
self.player = player
+9 -5
View File
@@ -8,7 +8,8 @@ import typing as T
from datetime import datetime, timedelta
from pqcli import lingo
from pqcli.config import CLASSES, PRIME_STATS, RACES
from pqcli.config import PRIME_STATS
from pqcli.game_data import GameData
from pqcli.mechanic import Player, Simulation, StatsBuilder, create_player
from pqcli.roster import Roster
from pqcli.ui.base import BaseUserInterface
@@ -37,8 +38,9 @@ class BasicUserInterface(BaseUserInterface):
roster: Roster,
player: T.Optional[Player],
args: argparse.Namespace,
data: GameData,
) -> None:
super().__init__(roster, player, args)
super().__init__(roster, player, args, data)
def signal_handler(sig: T.Any, frame: T.Any) -> None:
logging.info("Quitting")
@@ -107,8 +109,10 @@ class BasicUserInterface(BaseUserInterface):
print("Cancelled.")
return None
race = self.menu([(race, race.name) for race in RACES])
class_ = self.menu([(class_, class_.name) for class_ in CLASSES])
race = self.menu([(race, race.name) for race in self.data.races])
class_ = self.menu(
[(class_, class_.name) for class_ in self.data.classes]
)
stats_builder = StatsBuilder()
while True:
@@ -131,7 +135,7 @@ class BasicUserInterface(BaseUserInterface):
def play(self, player: Player) -> None:
print(f"Playing as {player.name}")
simulation = Simulation(player)
simulation = Simulation(player, self.data)
last_tick = datetime.now()
last_level = 0
while True:
+14 -5
View File
@@ -7,6 +7,7 @@ import typing as T
from pqcli import lingo
from pqcli.config import Class, Race
from pqcli.game_data import GameData
from pqcli.mechanic import Player, Stats, create_player
from pqcli.roster import Roster
from pqcli.ui.base import BaseUserInterface
@@ -40,9 +41,13 @@ class StopMainLoop(Exception):
class CursesUserInterface(BaseUserInterface):
def __init__(
self, roster: Roster, player: Player, args: argparse.Namespace
self,
roster: Roster,
player: Player,
args: argparse.Namespace,
data: GameData,
) -> None:
super().__init__(roster, player, args)
super().__init__(roster, player, args, data)
os.environ.setdefault("ESCDELAY", "25")
self._screen = curses.initscr()
self._screen.nodelay(True)
@@ -119,7 +124,7 @@ class CursesUserInterface(BaseUserInterface):
def _switch_to_create_char_race_view(
self, player_name: str, race: T.Optional[Race] = None
) -> None:
view = ChooseCharacterRaceView(self._screen, race)
view = ChooseCharacterRaceView(self._screen, self.data.races, race)
view.on_cancel += lambda: self._switch_to_create_char_name_view(
player_name
)
@@ -131,7 +136,9 @@ class CursesUserInterface(BaseUserInterface):
def _switch_to_create_char_class_view(
self, player_name: str, race: Race, class_: T.Optional[Class] = None
) -> None:
view = ChooseCharacterClassView(self._screen, class_)
view = ChooseCharacterClassView(
self._screen, self.data.classes, class_
)
view.on_cancel += lambda: self._switch_to_create_char_race_view(
player_name, race
)
@@ -177,7 +184,9 @@ class CursesUserInterface(BaseUserInterface):
self._switch_view(view)
def _switch_to_game_view(self, player: Player) -> None:
view = GameView(self._screen, self.roster, player, self.args)
view = GameView(
self._screen, self.roster, player, self.args, self.data
)
view.on_exit += self._switch_to_roster_view
self._switch_view(view)
@@ -1,22 +1,25 @@
import functools
import typing as T
from pqcli.config import CLASSES, Class
from pqcli.config import Class
from pqcli.ui.curses.util import KEYS_CANCEL, Choice
from pqcli.ui.curses.views.menu_view import MenuView
class ChooseCharacterClassView(MenuView):
def __init__(
self, screen: T.Any, class_: T.Optional[Class] = None
self,
screen: T.Any,
classes: T.Sequence[Class],
class_: T.Optional[Class] = None,
) -> None:
super().__init__(
screen,
"Choose character class",
CLASSES.index(class_) if class_ is not None else 0,
classes.index(class_) if class_ is not None else 0,
)
for y, class_ in enumerate(CLASSES, 1):
for y, class_ in enumerate(classes, 1):
key: T.Optional[str] = str(y % 10) if y <= 10 else None
self._choices.append(
Choice(
@@ -1,20 +1,25 @@
import functools
import typing as T
from pqcli.config import RACES, Race
from pqcli.config import Race
from pqcli.ui.curses.util import KEYS_CANCEL, Choice
from pqcli.ui.curses.views.menu_view import MenuView
class ChooseCharacterRaceView(MenuView):
def __init__(self, screen: T.Any, race: T.Optional[Race] = None) -> None:
def __init__(
self,
screen: T.Any,
races: T.Sequence[Race],
race: T.Optional[Race] = None,
) -> None:
super().__init__(
screen,
"Choose character race",
RACES.index(race) if race is not None else 0,
races.index(race) if race is not None else 0,
)
for y, race in enumerate(RACES, 1):
for y, race in enumerate(races, 1):
key: T.Optional[str] = str(y % 10) if y <= 10 else None
self._choices.append(
Choice(
+3 -1
View File
@@ -3,6 +3,7 @@ import curses
import datetime
import typing as T
from pqcli.game_data import GameData
from pqcli.mechanic import Player, SignalMixin, Simulation
from pqcli.roster import Roster
from pqcli.ui.curses.event_handler import EventHandler
@@ -33,6 +34,7 @@ class GameView(BaseView):
roster: Roster,
player: Player,
args: argparse.Namespace,
data: GameData,
) -> None:
super().__init__(screen)
self.on_exit = EventHandler()
@@ -41,7 +43,7 @@ class GameView(BaseView):
self._player = player
self._args = args
self._simulation = Simulation(player)
self._simulation = Simulation(player, data)
self._last_tick = datetime.datetime.now()
def keypress(self, key: int) -> None:
+1 -1
View File
@@ -40,7 +40,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.uv]
dev-dependencies = ["pre-commit>=2.16.0"]
dev-dependencies = ["pre-commit>=2.16.0", "pytest>=8.0"]
[tool.black]
line-length = 79
+4
View File
@@ -0,0 +1,4 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
+1871
View File
File diff suppressed because it is too large. Load diff
+1857
View File
File diff suppressed because it is too large. Load diff
+1867
View File
File diff suppressed because it is too large. Load diff
+69
View File
@@ -0,0 +1,69 @@
import datetime
import typing as T
from pqcli import random
from pqcli.config import StatType
from pqcli.game_data import load_game_data
from pqcli.mechanic import Player, Simulation, Stats, monster_task
STATS = {
StatType.strength: 12,
StatType.condition: 11,
StatType.dexterity: 10,
StatType.intelligence: 13,
StatType.wisdom: 14,
StatType.charisma: 9,
StatType.hp_max: 6,
StatType.mp_max: 5,
}
def run_trace(seed: str, ticks: int = 3000) -> T.List[str]:
"""Play a deterministic game and return everything that happened."""
data = load_game_data()
player = Player(
name="Tester",
birthday=datetime.datetime(2000, 1, 1),
race=data.races[0],
class_=data.classes[0],
stats=Stats(dict(STATS)),
)
random.seed(seed)
simulation = Simulation(player, data)
lines: T.List[str] = []
player.connect(
"new_task",
lambda: lines.append(f"task: {player.task.description}"),
)
for _ in range(ticks):
simulation.tick(100000.0)
# plot cinematics and quests are rare in a short run, so force them
for _ in range(30):
simulation.interplot_cinematic()
simulation.complete_quest()
lines.extend(f"queued: {task.description}" for task in player.queue)
player.queue.clear()
# high level monsters are unreachable in a short run
for level in range(1, 100):
task = monster_task(data, level, player.quest_book.monster)
lines.append(f"monster: {task.description} {task.duration}")
lines.append(f"level: {player.level}")
lines.append(f"act: {player.quest_book.act}")
lines.append(f"gold: {player.inventory.gold}")
for stat, value in player.stats:
lines.append(f"stat: {stat.value} {value}")
for equipment_type, item in player.equipment:
lines.append(f"equipment: {equipment_type.value} {item}")
for inventory_item in player.inventory:
lines.append(
f"inventory: {inventory_item.name} {inventory_item.quantity}"
)
for spell in player.spell_book:
lines.append(f"spell: {spell.name} {spell.level}")
for quest in player.quest_book.quests:
lines.append(f"quest: {quest}")
return lines
+182
View File
@@ -0,0 +1,182 @@
import datetime
import json
import pickle
import typing as T
from pathlib import Path
import pytest
from pqcli import random
from pqcli.config import Class, Monster, Race, StatType
from pqcli.game_data import (
DATA_FILES,
DataError,
GameData,
dump_builtin_data,
load_game_data,
)
from pqcli.mechanic import Player, Stats, monster_task
def write(directory: Path, name: str, value: T.Any) -> Path:
path = directory / f"{name}.json"
path.write_text(json.dumps(value), encoding="utf-8")
return path
def test_builtin_data_loads() -> None:
data = load_game_data()
assert data.spells[0] == "Slime Finger"
assert data.monsters[0] == Monster("Anhkheg", 6, "chitin")
assert data.races[0] == Race("Half Orc", [StatType.hp_max])
assert data.classes[-1] == Class("Vermineer", [StatType.intelligence])
assert data.combat_verbs == ("Executing {target}",)
assert all(getattr(data, name) for name in DATA_FILES)
def test_builtin_data_keeps_quirks() -> None:
data = load_game_data()
names = [monster.name for monster in data.monsters]
assert names.count("Rat") == 2
assert Monster("Neo-Otyugh", 11, "organ ") in data.monsters
assert any(monster.item is None for monster in data.monsters)
def test_missing_override_dir_uses_builtin(tmp_path: Path) -> None:
assert load_game_data(tmp_path / "nope") == load_game_data()
def test_override_replaces_only_that_list(tmp_path: Path) -> None:
write(tmp_path, "spells", ["Zap", "Bonk"])
write(
tmp_path,
"races",
[{"name": "Robot", "attr": ["STR", "hp_max", "Wisdom"]}],
)
builtin = load_game_data()
data = load_game_data(tmp_path)
assert data.spells == ("Zap", "Bonk")
assert data.races == (
Race("Robot", [StatType.strength, StatType.hp_max, StatType.wisdom]),
)
assert data.monsters == builtin.monsters
assert data.classes == builtin.classes
def test_override_accepts_all_shapes(tmp_path: Path) -> None:
write(tmp_path, "monsters", [{"name": "Bob", "level": 0, "item": None}])
write(tmp_path, "weapons", [{"name": "Spoon", "quality": -1}])
write(tmp_path, "offense_bad", [{"name": "Soggy", "quality": -2}])
data = load_game_data(tmp_path)
assert data.monsters == (Monster("Bob", 0, None),)
assert data.weapons[0].name == "Spoon"
assert data.offense_bad[0].quality == -2
def test_unknown_file_warns(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
write(tmp_path, "monster", [])
load_game_data(tmp_path)
assert "monster.json" in capsys.readouterr().err
@pytest.mark.parametrize(
"name,value,message",
[
("spells", [], "must not be empty"),
("spells", {"a": 1}, "must be a list"),
("spells", ["ok", 3], "[1]"),
("monsters", [{"name": "Imp", "level": "2", "item": "x"}], "level"),
("monsters", [{"name": "Imp", "level": True, "item": "x"}], "level"),
("monsters", [{"name": "Imp", "level": 2}], "item"),
("monsters", [{"name": "Imp", "level": 2, "item": 1}], "item"),
("weapons", [{"name": "Spoon", "quality": 1, "x": 1}], "x"),
("races", [{"name": "Elf", "attr": ["LUCK"]}], "LUCK"),
("races", [{"name": "Elf", "attr": "STR"}], "attr"),
("combat_verbs", ["Hitting"], "{target}"),
],
)
def test_invalid_override_raises(
tmp_path: Path, name: str, value: T.Any, message: str
) -> None:
write(tmp_path, name, value)
with pytest.raises(DataError) as excinfo:
load_game_data(tmp_path)
assert f"{name}.json" in str(excinfo.value)
assert message in str(excinfo.value)
def test_malformed_json_raises(tmp_path: Path) -> None:
(tmp_path / "spells.json").write_text("[oops", encoding="utf-8")
with pytest.raises(DataError, match="spells.json"):
load_game_data(tmp_path)
def test_non_utf8_raises(tmp_path: Path) -> None:
(tmp_path / "spells.json").write_bytes(b'["\xff"]')
with pytest.raises(DataError, match="spells.json"):
load_game_data(tmp_path)
def test_override_dir_must_be_directory(tmp_path: Path) -> None:
path = write(tmp_path, "spells", ["Zap"])
with pytest.raises(DataError, match="not a directory"):
load_game_data(path)
def test_combat_verbs_are_used(tmp_path: Path) -> None:
verbs = [
"Fighting bravely against {target}",
"Kicking the butt of {target}",
]
write(tmp_path, "combat_verbs", verbs)
data = load_game_data(tmp_path)
random.seed("verbs")
seen = set()
for _ in range(200):
description = monster_task(data, 5, None).description
prefix = description.split(" ")[0]
assert prefix in {"Fighting", "Kicking"}
assert "{target}" not in description
seen.add(prefix)
assert seen == {"Fighting", "Kicking"}
def test_dump_builtin_data_round_trips(tmp_path: Path) -> None:
written = dump_builtin_data(tmp_path / "out")
assert sorted(path.stem for path in written) == sorted(DATA_FILES)
assert load_game_data(tmp_path / "out") == load_game_data()
def test_dump_builtin_data_refuses_to_overwrite(tmp_path: Path) -> None:
existing = write(tmp_path, "spells", ["Mine"])
with pytest.raises(DataError, match="spells.json"):
dump_builtin_data(tmp_path)
assert json.loads(existing.read_text()) == ["Mine"]
assert list(tmp_path.iterdir()) == [existing]
def test_saved_player_survives_removed_race(tmp_path: Path) -> None:
data = load_game_data()
player = Player(
name="Saved",
birthday=datetime.datetime(2000, 1, 1),
race=data.races[3],
class_=data.classes[2],
stats=Stats({stat: 10 for stat in StatType}),
)
blob = pickle.dumps(player)
write(tmp_path, "races", [{"name": "Robot", "attr": ["STR"]}])
load_game_data(tmp_path)
restored = pickle.loads(blob)
assert restored.race == Race("Double Hobbit", [StatType.strength])
assert type(restored.race).__module__ == "pqcli.config"
def test_game_data_is_immutable() -> None:
data = load_game_data()
assert isinstance(data, GameData)
with pytest.raises(AttributeError):
data.spells = () # type: ignore[misc]
+14
View File
@@ -0,0 +1,14 @@
from pathlib import Path
import pytest
from game_trace import run_trace
FIXTURES = Path(__file__).parent / "fixtures"
@pytest.mark.parametrize("seed", ["alpha", "beta", "gamma"])
def test_gameplay_matches_recorded_trace(seed: str) -> None:
# The fixtures were recorded before the word lists moved to data files;
# stock data must reproduce them exactly, RNG stream included.
expected = (FIXTURES / f"parity_{seed}.txt").read_text().splitlines()
assert run_trace(seed) == expected
Generated
+152 -1
View File
@@ -11,6 +11,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "distlib"
version = "0.4.0"
@@ -20,6 +29,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
]
[[package]]
name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]]
name = "filelock"
version = "3.20.0"
@@ -38,6 +59,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" },
]
[[package]]
name = "nodeenv"
version = "1.9.1"
@@ -47,6 +77,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
]
[[package]]
name = "packaging"
version = "26.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
]
[[package]]
name = "platformdirs"
version = "4.5.1"
@@ -56,6 +95,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
]
[[package]]
name = "pluggy"
version = "1.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" },
]
[[package]]
name = "pqcli"
version = "1.0.4"
@@ -75,6 +123,7 @@ windows = [
[package.dev-dependencies]
dev = [
{ name = "pre-commit" },
{ name = "pytest" },
]
[package.metadata]
@@ -88,7 +137,10 @@ requires-dist = [
provides-extras = ["windows"]
[package.metadata.requires-dev]
dev = [{ name = "pre-commit", specifier = ">=2.16.0" }]
dev = [
{ name = "pre-commit", specifier = ">=2.16.0" },
{ name = "pytest", specifier = ">=8.0" },
]
[[package]]
name = "pre-commit"
@@ -106,6 +158,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" },
]
[[package]]
name = "pygments"
version = "2.21.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
]
[[package]]
name = "pytest"
version = "9.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
[[package]]
name = "pyyaml"
version = "6.0.3"
@@ -170,6 +249,78 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
[[package]]
name = "tomli"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/78/9ad63712633ed3ab5cc1a648d863d7e7da371e9425e209555a0fe711b695/tomli-2.5.0.tar.gz", hash = "sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6", size = 17662, upload-time = "2026-10-07T12:23:37.892Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/a6/ab99b60ee52acd949684febabc3005d0045d0f66bebd9cdebd67372d26dd/tomli-2.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4dc1c1781f2f716de763d1e9a7b34c6a894e167e291c7c5d16c72f7a9538545", size = 163901, upload-time = "2026-10-07T12:22:15.601Z" },
{ url = "https://files.pythonhosted.org/packages/bc/00/ee01b7ed4579180fff07142d290257f25ba786f23f3ec6005f620933c2f5/tomli-2.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eff8babca5a7999bc137acbc7482a8b7e17ffca5075ab41f5d770ab408c7bfef", size = 163756, upload-time = "2026-10-07T12:22:16.957Z" },
{ url = "https://files.pythonhosted.org/packages/72/c2/4efebf65372f6583185f79799312109dddb61102d47e5c33dcfd1a297aca/tomli-2.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86665cee9c4835b7a7f1e8ec2c719b5258d4dc782887aded5a8ae7352a96843b", size = 268038, upload-time = "2026-10-07T12:22:18.135Z" },
{ url = "https://files.pythonhosted.org/packages/53/07/5850468e925d898abb36038666f9c333a94d2a223e802a8ba5b6d319d23f/tomli-2.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e369fd63331746182360977b1892bfc215476a30d61612d732425311639f56", size = 276422, upload-time = "2026-10-07T12:22:19.567Z" },
{ url = "https://files.pythonhosted.org/packages/b4/87/f293984cdcf83c054196d4fd3dad44fc68ae55b4b8c44bc76cef360c3150/tomli-2.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ad1ea345759240d6463efa0ed1c704402752e49aa21476620738d74d72d8aa1", size = 272616, upload-time = "2026-10-07T12:22:20.794Z" },
{ url = "https://files.pythonhosted.org/packages/ce/ce/db582886b3c1219d3fec93ebd669332482e5aee7a91e0f7838d84f2d1759/tomli-2.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96243987194634bd411066ce40c952e108f86af04db533ecd8ac3ff2a85b1885", size = 276593, upload-time = "2026-10-07T12:22:22.12Z" },
{ url = "https://files.pythonhosted.org/packages/bf/72/7619b87dea4261fc27dd7b54c4461c129c1f7d9bb7ba3aec89c797a431b8/tomli-2.5.0-cp311-cp311-win32.whl", hash = "sha256:610b27d99f28ec5f191c7064a48f3ddb179a1fe6ca73d571483ae859f57b605e", size = 101830, upload-time = "2026-10-07T12:22:23.651Z" },
{ url = "https://files.pythonhosted.org/packages/1e/74/220106da34502304b6751a2a9b8a9fbca6c3fd47e737a2e2e3da7c61c9db/tomli-2.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:c804ae44fe7b4bab5da295e4f980a1ff04670bca9d23fe0a4e887e08ebd741a8", size = 112742, upload-time = "2026-10-07T12:22:24.972Z" },
{ url = "https://files.pythonhosted.org/packages/27/99/7d9c8b41837a7773613e169504147375c157a290167aa59ad74a085f521f/tomli-2.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:cfac177ebd6236003846ea339981f71457cb6eb748f23381eb257e45092e3980", size = 109332, upload-time = "2026-10-07T12:22:26.117Z" },
{ url = "https://files.pythonhosted.org/packages/52/ed/7baa86f87493646a594de388c7c1c40a39dd0461f7e9c0359cbeefc91fe8/tomli-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df", size = 164854, upload-time = "2026-10-07T12:22:27.444Z" },
{ url = "https://files.pythonhosted.org/packages/a5/b1/44c0341f2224397855723c7a8a39f718ea6fcbcc3dacc66e5aeca0f334e3/tomli-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b", size = 164074, upload-time = "2026-10-07T12:22:28.679Z" },
{ url = "https://files.pythonhosted.org/packages/23/04/e2d5b7d3fba47adedb23de616c16d428ea076c79a3d8e1d95d649ffe197e/tomli-2.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0", size = 274274, upload-time = "2026-10-07T12:22:29.804Z" },
{ url = "https://files.pythonhosted.org/packages/43/90/6090e706ff27a6f89f4a40578e3324b95c3cd8c4150868aabf33a8f414c3/tomli-2.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6", size = 286435, upload-time = "2026-10-07T12:22:31.297Z" },
{ url = "https://files.pythonhosted.org/packages/0a/9e/a2c40768df16c408f22430afb0a73e9d7e5f79c950884954649d1146b74d/tomli-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc", size = 278119, upload-time = "2026-10-07T12:22:32.601Z" },
{ url = "https://files.pythonhosted.org/packages/12/25/3c0cb485b98e9cfac495629b1c93c87ccf0b72fbe9d2689fd8fe62c6d5a3/tomli-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7", size = 286177, upload-time = "2026-10-07T12:22:33.745Z" },
{ url = "https://files.pythonhosted.org/packages/77/8b/0144c65f0e37e51c18d04ae15c21b19431c165002d0131fe9aa8b0b8b1e8/tomli-2.5.0-cp312-cp312-win32.whl", hash = "sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2", size = 102760, upload-time = "2026-10-07T12:22:34.887Z" },
{ url = "https://files.pythonhosted.org/packages/de/32/5d6d8f42fc9a05fce69354e00ff256484192f5f2fc9a2165718fa0de61ec/tomli-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7", size = 112722, upload-time = "2026-10-07T12:22:36.162Z" },
{ url = "https://files.pythonhosted.org/packages/30/65/df18032218db0fb9b769fb23c8039a051f15c811993995ea04c350273a32/tomli-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea", size = 109534, upload-time = "2026-10-07T12:22:37.296Z" },
{ url = "https://files.pythonhosted.org/packages/42/e5/51736d70da209350969e15aca5c5ab6e2ce1ea87a0a892a6c13aec172a86/tomli-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea", size = 163328, upload-time = "2026-10-07T12:22:38.373Z" },
{ url = "https://files.pythonhosted.org/packages/ec/55/086f80dab4ab497602644274e6dea7ec5dd0b4e262e443a8ad3bb7edee2d/tomli-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043", size = 162246, upload-time = "2026-10-07T12:22:39.673Z" },
{ url = "https://files.pythonhosted.org/packages/aa/eb/3ecc94459f3635c92321f4e7bde571323fdb2267c50e19e3188a281eae3b/tomli-2.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0", size = 272655, upload-time = "2026-10-07T12:22:41.08Z" },
{ url = "https://files.pythonhosted.org/packages/c0/d7/494fd1f0c37a621f1ad9975c2efadb523e8101f144ed6edb2e7fe64738f2/tomli-2.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b", size = 283595, upload-time = "2026-10-07T12:22:42.222Z" },
{ url = "https://files.pythonhosted.org/packages/70/51/bb8d62b1317e6640866f6949b2d5855e5300f2c99d46de1cd245570bba65/tomli-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066", size = 276253, upload-time = "2026-10-07T12:22:43.625Z" },
{ url = "https://files.pythonhosted.org/packages/66/f4/f46bd7f0763cd47de2db697dca9257c6a4adfd1a93b018cc75c8190ed5a8/tomli-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b", size = 283582, upload-time = "2026-10-07T12:22:44.983Z" },
{ url = "https://files.pythonhosted.org/packages/ac/03/70f2bcb2923a6db37818d917e124270a7f4cfd38ea576f5aa753a91c0ef5/tomli-2.5.0-cp313-cp313-win32.whl", hash = "sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68", size = 102628, upload-time = "2026-10-07T12:22:46.508Z" },
{ url = "https://files.pythonhosted.org/packages/dc/98/d52024bb5b0ff68b4f0d276d867f634c84a67319a7e9f6b7708a37742333/tomli-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc", size = 113301, upload-time = "2026-10-07T12:22:47.647Z" },
{ url = "https://files.pythonhosted.org/packages/6f/f2/540db3a70572a8c23a28aba3e9c358ce0ffffbafc990905c1343aa265b31/tomli-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84", size = 109744, upload-time = "2026-10-07T12:22:48.925Z" },
{ url = "https://files.pythonhosted.org/packages/e4/49/caf6b307766eb9567664a8707e9d6be5fcc0e8903f18781c6677a60d80c7/tomli-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105", size = 162899, upload-time = "2026-10-07T12:22:50.088Z" },
{ url = "https://files.pythonhosted.org/packages/d3/c8/68cfce773a2733a49c74f99d627fb461bd990756860099eac25617889585/tomli-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646", size = 162080, upload-time = "2026-10-07T12:22:51.558Z" },
{ url = "https://files.pythonhosted.org/packages/7e/b2/e5bb8651fdad593f670501a7d718b1a7f73f064d44dea15e04c04dfef45d/tomli-2.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b", size = 273380, upload-time = "2026-10-07T12:22:52.918Z" },
{ url = "https://files.pythonhosted.org/packages/8d/d2/9e2d7f8b1dfe0e2b34c245986ebd55c4c553ea4ce6c47c443b332673253f/tomli-2.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75", size = 283228, upload-time = "2026-10-07T12:22:54.173Z" },
{ url = "https://files.pythonhosted.org/packages/ba/df/ec7b876b7b1a2718bd74a3743c076fff565b04029ba33e8f61fac262739f/tomli-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb", size = 277189, upload-time = "2026-10-07T12:22:55.342Z" },
{ url = "https://files.pythonhosted.org/packages/7d/7b/e192d9eed0b9cb80da799f4d77052297fb9a2c3cc9b19f571f56ea88add6/tomli-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3", size = 283632, upload-time = "2026-10-07T12:22:56.735Z" },
{ url = "https://files.pythonhosted.org/packages/84/50/ff94454e75461d75623e47401ed323d65c10aab8fe9033242c20cd2fdf32/tomli-2.5.0-cp314-cp314-win32.whl", hash = "sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b", size = 103535, upload-time = "2026-10-07T12:22:58.084Z" },
{ url = "https://files.pythonhosted.org/packages/54/0b/bdacf05f963bd6026ebf6eeb0beda847d1d60e03e440725c64a4e08a0afd/tomli-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a", size = 114621, upload-time = "2026-10-07T12:22:59.2Z" },
{ url = "https://files.pythonhosted.org/packages/61/99/53f438fa6ae4f9d4ed0ddde3e7242b3bdc34b48c8f9948b72b9e9b127676/tomli-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3", size = 111572, upload-time = "2026-10-07T12:23:00.479Z" },
{ url = "https://files.pythonhosted.org/packages/b9/20/1f88f19427d380a40e90a770e087489eaafe4aeee070ae88ed2bbec00acd/tomli-2.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4", size = 171814, upload-time = "2026-10-07T12:23:01.914Z" },
{ url = "https://files.pythonhosted.org/packages/d0/56/cbe5079c9f9a54b9b3e27fc82f08f3cb36edee75561679f53d2380c801d6/tomli-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d", size = 171324, upload-time = "2026-10-07T12:23:03.18Z" },
{ url = "https://files.pythonhosted.org/packages/2b/30/1d53fd3b0f1cb3ba542e345ec32c26aefdddc4e829e4f3429af8a4f27782/tomli-2.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9", size = 297441, upload-time = "2026-10-07T12:23:04.345Z" },
{ url = "https://files.pythonhosted.org/packages/66/d9/0800acb6a111686f764c1b91ef15cc42a20a66a46013bb42220f1d2c61c1/tomli-2.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f", size = 307476, upload-time = "2026-10-07T12:23:05.671Z" },
{ url = "https://files.pythonhosted.org/packages/e8/63/30a8f3cd51b5bec37f04744bad0b0dc6160df84aad4f27b0e9283d66f221/tomli-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374", size = 296113, upload-time = "2026-10-07T12:23:07.202Z" },
{ url = "https://files.pythonhosted.org/packages/ab/18/0b9ffc597e69c5a1e20a7823cb60d54b39a9f54e91edcb8574f022186758/tomli-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442", size = 307725, upload-time = "2026-10-07T12:23:08.508Z" },
{ url = "https://files.pythonhosted.org/packages/ab/c7/18f8baae0b5607a60e8e19b4a7fedee43a8ff6458e3896dcbbadeeac9c22/tomli-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03", size = 108546, upload-time = "2026-10-07T12:23:09.956Z" },
{ url = "https://files.pythonhosted.org/packages/72/34/4cca9739254130627bde87500b3f2b512154fe2f278efa7e2a5e10ad4bcb/tomli-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1", size = 117814, upload-time = "2026-10-07T12:23:11.486Z" },
{ url = "https://files.pythonhosted.org/packages/7d/fb/afa530d47dd80a78fce43beac6bc6e00f84558eafcffbc6f37b21e80d056/tomli-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0", size = 115188, upload-time = "2026-10-07T12:23:12.728Z" },
{ url = "https://files.pythonhosted.org/packages/66/98/316fdc00f8c0939e6fe50461dd343c162d3ad51d1286eb25b7db54361d50/tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc", size = 162775, upload-time = "2026-10-07T12:23:13.941Z" },
{ url = "https://files.pythonhosted.org/packages/c5/22/7b10fa5bb01c9539f53f69b619361b19350acc73657772ea7ac70ba309a8/tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276", size = 161406, upload-time = "2026-10-07T12:23:15.215Z" },
{ url = "https://files.pythonhosted.org/packages/9c/e7/1a069d86dfd20f1f84f71c63faed9f83c1d890bc06c27d82dc7d888fb573/tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52", size = 273855, upload-time = "2026-10-07T12:23:16.471Z" },
{ url = "https://files.pythonhosted.org/packages/ae/83/d1ef43d1687d092ab9c235455c76e6e709483b346b056f086095c7c263a5/tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7", size = 284910, upload-time = "2026-10-07T12:23:18.166Z" },
{ url = "https://files.pythonhosted.org/packages/cc/05/f4d9cf7de61822ece0c3873f30d291e324911c71a378b8bfe5ced13fd9f5/tomli-2.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391", size = 277723, upload-time = "2026-10-07T12:23:19.355Z" },
{ url = "https://files.pythonhosted.org/packages/42/28/78262493141fa543151cf005760c3cb01d09fc28a11f993c05109902cb8c/tomli-2.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859", size = 285115, upload-time = "2026-10-07T12:23:20.698Z" },
{ url = "https://files.pythonhosted.org/packages/1a/b9/e1dab9a30bcb677b5cc5cee810609cfd64f24306a3055767dd3fda00b1e0/tomli-2.5.0-cp315-cp315-win32.whl", hash = "sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb", size = 103475, upload-time = "2026-10-07T12:23:21.941Z" },
{ url = "https://files.pythonhosted.org/packages/4c/bd/31a3790c11d6ea95fcf5e6022ac0f8d0543c9b61120b730fc481bd43d3b4/tomli-2.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5", size = 114589, upload-time = "2026-10-07T12:23:23.098Z" },
{ url = "https://files.pythonhosted.org/packages/47/a2/4f6310fa699364f0e3af7ee3af88dddd9af066d33e716a0265bbe2b3ea84/tomli-2.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd", size = 111493, upload-time = "2026-10-07T12:23:24.233Z" },
{ url = "https://files.pythonhosted.org/packages/68/14/00853f0b396d8971107ae1921bb5b322fdee1650d2f16bf06c20adb532e5/tomli-2.5.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57", size = 171380, upload-time = "2026-10-07T12:23:25.512Z" },
{ url = "https://files.pythonhosted.org/packages/89/ad/fa6949321dadee46b27363974fb197b94c911c3b0f7a5fd26d7dc18fc2a0/tomli-2.5.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd", size = 170553, upload-time = "2026-10-07T12:23:26.855Z" },
{ url = "https://files.pythonhosted.org/packages/53/aa/3056c919eb3e084df3752b2cf5f865dcc04af0b27dba2f66d7b28af4633a/tomli-2.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01", size = 294428, upload-time = "2026-10-07T12:23:28.132Z" },
{ url = "https://files.pythonhosted.org/packages/96/b2/faeeb5d8769ea3832021d73e892c8391eae7b4b4f8b55a789127bd8b18a9/tomli-2.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f", size = 304909, upload-time = "2026-10-07T12:23:29.381Z" },
{ url = "https://files.pythonhosted.org/packages/f6/52/f094c09e73fb654b621716d019acb5d29bdfd1be01df80c281d552bda48d/tomli-2.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a", size = 293220, upload-time = "2026-10-07T12:23:30.608Z" },
{ url = "https://files.pythonhosted.org/packages/86/f5/0c30541078ca4b505ce3bd76ed931facbfec524dd018535d691d1af0a6d2/tomli-2.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142", size = 305705, upload-time = "2026-10-07T12:23:32.181Z" },
{ url = "https://files.pythonhosted.org/packages/05/74/590e7d19d6a118fc5cc5704ff358e21d95b8573f6b9443b1519f29ca8825/tomli-2.5.0-cp315-cp315t-win32.whl", hash = "sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5", size = 108432, upload-time = "2026-10-07T12:23:33.496Z" },
{ url = "https://files.pythonhosted.org/packages/1c/b8/63a75cfb27a17c38550e44025d3a6e7be64516fd8608a3b75703bf37d81b/tomli-2.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571", size = 117281, upload-time = "2026-10-07T12:23:34.648Z" },
{ url = "https://files.pythonhosted.org/packages/72/01/e8c1debb2173973372934c68fc8e46170ab60ef23ed4592dff4dec6e8993/tomli-2.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7", size = 115069, upload-time = "2026-10-07T12:23:35.77Z" },
{ url = "https://files.pythonhosted.org/packages/60/3f/3e3f8fd0919249b0200c80fbc4f9a1e70be19f9883da71dfb7f8b9ab8aca/tomli-2.5.0-py3-none-any.whl", hash = "sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b", size = 14765, upload-time = "2026-10-07T12:23:36.875Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"