From 1c0bd7357dd425ebf39994e823eff875d3a04cf4 Mon Sep 17 00:00:00 2001 From: evilchili Date: Tue, 16 Jan 2024 22:05:06 -0800 Subject: [PATCH] Fix random value generator, new CLI This commit fixes a defect in the random value generator introduced when the random_sets module was refactored. It also introduces three useful CLI options, for generating roll tables of random trinkets, wild magic surges, and psychadelic effects. --- README.md | 2 +- rolltable/cli.py | 34 +++- rolltable/sources/psychadelic_effects.yaml | 40 ++++ rolltable/sources/trinkets.yaml | 213 +++++++++++++++++++ rolltable/sources/wild_magic.yaml | 137 +++++++++++++ rolltable/types.py | 27 ++- tests/test_tables.py | 225 ++------------------- 7 files changed, 456 insertions(+), 222 deletions(-) create mode 100644 rolltable/sources/psychadelic_effects.yaml create mode 100644 rolltable/sources/trinkets.yaml create mode 100644 rolltable/sources/wild_magic.yaml diff --git a/README.md b/README.md index 9841602..e358236 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Clone the repo and install the virtual env: Invoke the CLI: ``` -% poetry run roll-table example.yaml +% poetry run roll-table custom example.yaml ┏━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓ ┃ Roll ┃ Rarity ┃ Color ┃ Notes ┃ ┡━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩ diff --git a/rolltable/cli.py b/rolltable/cli.py index 36e50b7..58d501b 100644 --- a/rolltable/cli.py +++ b/rolltable/cli.py @@ -2,7 +2,6 @@ from rolltable.types import RollTable import typer from enum import Enum from rich import print -from rich.table import Table from pathlib import Path from typing import List @@ -16,8 +15,35 @@ class OUTPUT_FORMATS(Enum): markdown = 'markdown' -@app.command("roll-table") -def create( +@app.command("psychadelic-effects") +def psycheffects(): + """ + Generate a roll table of psychadelic effects. + """ + rt = RollTable([(Path(__file__).parent / 'sources' / 'psychadelic_effects.yaml').read_text()]) + print(rt.as_table()) + + +@app.command("trinkets") +def trinkets(): + """ + Generate a roll table of random junk. + """ + rt = RollTable([(Path(__file__).parent / 'sources' / 'trinkets.yaml').read_text()]) + print(rt.as_table()) + + +@app.command("wild-magic") +def wildmagic(): + """ + Generate a wild magic surge table. + """ + rt = RollTable([(Path(__file__).parent / 'sources' / 'wild_magic.yaml').read_text()]) + print(rt.as_table()) + + +@app.command("custom") +def custom( sources: List[Path] = typer.Argument( ..., help="Path to one or more yaml-formatted source file."), @@ -43,7 +69,7 @@ def create( ) ): """ - CLI for creating roll tables. + Create roll tables from custom sources. """ rt = RollTable([Path(s).read_text() for s in sources], frequency=frequency, die=die, hide_rolls=hide_rolls) diff --git a/rolltable/sources/psychadelic_effects.yaml b/rolltable/sources/psychadelic_effects.yaml new file mode 100644 index 0000000..1f3d0ec --- /dev/null +++ b/rolltable/sources/psychadelic_effects.yaml @@ -0,0 +1,40 @@ +metadata: + headers: + - Effect + - Name + - Description + - Mechanic + frequencies: + default: + Effect: 1 +Effect: + - Elation: + - You are consumed with love for all things + - Charmed by all creatures, +2 to AC + - Paranoia: + - Everyone is out to get you. Everyone. What are they hiding? + - Automatically succeed all insight checks vs deception; no one is your ally + - Baked: + - Everything is hilarious, and you are always hungry for snacks. + - Disadvantage on all attack rolls, Advantage on saves against psychic effects. + - Wasted: + - Your speech is slurred, you have difficulty thinking clearly, and have mild vertigo + - You cannot use reactions; disadvantage on all checks and saves using DEX. +2 to AC. + - Sleepy: + - Why do we have to keep doing things? Doing things is hard. + - Your movement speed is reduced by 10ft and you cannot use reactions. Advantage on saves against psychic effects. + - Horror: + - You have stared into the abyss of your soul and found only blackness. + - Frightened by all creatures. Disadvantage on all saves against psychic effects. + - Mania: + - You are possessed by feverish mania; ideas and words tumble out of as quickly as possible. You cannot stop them. + - Automatic critical success on all perception checks; advantage on DEX saves; disadvantage on all attack rolls. + - Rage: + - You are seething with anger at the injustice of your situation. Everything is a personal attack, a challengge. + - You are raged. + - Cosmic Awareness: + - Your mind expands to contain the cosmos, which fills you with a wonder you cannot convey. Whenever you speak it is in metaphor. + - Once per day, you can decide something about the world is true and it will become so. + - Ego-Death: + - Your sense of self, your identity, is revealed as an empty construct. You are all things and all things are you. + - Whenever an ally takes damage, you take 1 point of psychic damage. Whenever an ally heals, you regain 1 HP. diff --git a/rolltable/sources/trinkets.yaml b/rolltable/sources/trinkets.yaml new file mode 100644 index 0000000..05634ad --- /dev/null +++ b/rolltable/sources/trinkets.yaml @@ -0,0 +1,213 @@ +metadata: + headers: + - Trinket + frequencies: + default: + Trinket: 1 +Trinket: + - A carved ivory mimic. + - A carved marble elephant. + - A small round cactus with two eyes. + - A pocket book of dwarven poetry. + - A bronze box containing a tiny wooden owl. + - A solid blue metal sphere, one inch in diameter, with three parallel grooves around the circumference. + - A pouch containing ten dried peas. + - A ceramic puzzle cube, with each face divided into four independently rotating squares enameled with astronomical signs. + - A sheet of vellum on which is crudely painted a herbal plant that you have yet to identify. + - A petrified frog. + - A twenty-sided die. + - A cut yellow chrysanthemum that never dies. + - A palm-sized iron cage; the door doesn't shut properly, as the tiny lock was broken from the inside. + - A blob of grey goo, slippy but safe to touch, kept in a ceramic pot. + - A glowing blue-green line, six inches long, but with no discernible radius. + - A pretty conch shell. + - A scrap of paper on which is written, in Goblin, "My dearest Bess,". + - A keychain holding the head of a broken key. + - An echo pearl from the depths of the Vibration Lake. + - A toy crossbow. + - A fossil of an extinct many-limbed critter. + - A brass prosthetic nose. + - A corkscrew. + - A dried poison gland of a jagged fish. + - A bronze gear on which is etched the word "Moon". + - A map of a labyrinth, on which is penciled a line that starts at the centre but fails to connect to the entrance. + - A cube of ice that never melts. + - A square of ironsilk sewn by the geargrubs of ancient Siclari. + - An ivory knitting needle. + - A peacock feather. + - A travel set of paints; someone has used up all the black. + - A wig of short platinum-blonde hair. + - A child's charm bracelet. + - A small bar of orichalcum, a metal only mentioned in ancient literature. + - A deed to a tower. + - An invitation to a formal ball to be held 1129 years ago. + - A smoking pipe carved from granite. + - A vial of scented oil. + - A preserved basilisk eye. + - An intricate knot that nobody seems to know how to tie or untie – sand sailors believe it to be bad luck. + - Pages ripped out of an accounting journal of a local merchant. + - A ring with a poison reservoir for slipping into drinks and a tiny razor edge for cutting purse-strings. + - A glass globe of swirling green goop. + - A portfolio of pressed flowers, none of which still exist. + - A small handbook of foreign coins, for travelers to identify denominations. + - A slightly out of date guidebook to foreign inns, taverns, and transportation. + - Two false fingernails painted with mysterious symbols. + - A set of cosmetic tools for cleaning the ears. + - A harmless stage dagger with retracting blade and blood-compartment. + - A floating glass orb that follows you around and makes whirring sounds. + - A key that can lock any door, but unlock none. + - A translucent coin. + - A bronze ring engraved with dark symbols. + - A thimble on which is an enamel painting of a turtle. + - A puzzle box that cannot be solved. + - A pair of badly worn hairdressing scissors. + - A wax hand shaped to hold a large cup. + - A measuring tape, marked in ink at 23 inches. + - A seashell that is silent when held up to your ear. + - One piece of unknown paper currency with no obvious denomination. + - A bootlace entwined with gold thread. + - A dented sheriff's badge. + - A belt buckle. + - A letter of complaint to a toy shop owner written in high elvish. + - A decorative leather stud. + - A penny whistle that plays the same note no matter which holes are covered. + - A ticket admitting an adult and child onto a thing called a "semiotic tram". + - A small glass vial holding three eyelashes. + - A tub of petrified putty. + - A leather shoe made for a dog. + - A doll head with no hair and poorly applied makeup. + - A pewter spork. + - Illustrated instructions on how to make a paper hat. + - A clear glass dish with four round notches around the outside edge. + - A wire circlet that bestows upon its wearer perfect posture. + - A small hand-sized box covered with numbered buttons. + - An empty whiskey tumbler that causes any liquid poured into it to become bourbon. + - A hunk of metal which appears to be several gears jammed together at unnatural and impossible angles; attempting to turn it causes it to emit a horrible shrieking sound. + - A crystal prism that refracts shadow instead of light. + - A smokeless and odorless candle. + - A flat disc of layered metal and prismatic glass with a hole in the centre. + - An ornate pewter tankard made without a bottom. + - A wooden device designed to be gripped in two hands; two levers protrude from the top, and two triggers from the underside. + - Two perfectly identical pine cones. + - A sponge that can absorb 60 gallons of ale (and only ale). + - A pepper grinder containing an unlimited supply of pepper unless opened, at which point it becomes half empty. + - A poorly cultivated bonsai juniper in a glazed ceramic pot. + - An oval-shaped soapstone tablet inscribed with a short list of religious prohibitions. + - A wooden doll with a door that opens to reveal a slightly smaller, identical, doll; this one is empty, perhaps there are still smaller dolls that are missing? + - A stone figure of a snake that changes positions after every full moon. + - A silver key of unknown origins on a leather cord as a pendant that emits strange magical energy. + - The right half of a broken bronze circlet with a light leafy pattern that when placed on your head, stays in place as though the other half was still there. + - A small silver rod which when rolled between your hands emits sounds as though a lute were being played by a master softly nearby. + - A music box that can only be heard by someone who has wound it at least once. + - A small stone cube that, when tapped with a rod of metal, looks as though it were made of that metal for a few seconds. + - A wooden sphere with a white marking that always faces the sun, and a black marking that always faces the moon. + - An opaque jar that cannot be opened or broken, no matter how hard you try. + - A journal that details the great adventures of a hero you have never heard of, complete with vivid descriptions of nonsensical creatures and terms, all written in messy handwriting, but with impressive diction. + - A tiny sack that, when opened, is full of sand, but feels as heavy as a large stone when lifted. + - A puppet in the likeness of someone you distantly know, that echoes your movements when you (and only you) place it on the ground. + - An iron rod that bends in unusual ways when you look directly at it, but rights itself when you look away. + - A book that perfectly records the holder's dreams when held while sleeping. + - A tin pot that is just the right size for you to wear as a helmet. + - A marble sculpture of a tiny elf holding a lute seated on a chair that plays music every so often. + - A piece of pure white cloth that never gets dirty. + - A hat that never gets wet. + - A mask that copies its wearer's facial expressions. + - A humanoid skull with ethereal green orbs in its eye sockets. + - A wineskin that only holds wine. Any other liquid pours out after a few seconds. + - A leather belt that, when worn, glows with faint blue light. + - A flask that freezes any liquids stored in it at midnight, and unfreezes them at noon. + - A book with words that change every time it is read. + - A cube with tiny animals wandering on each face that change when they cross onto a different face. + - A mummified hand that wiggles when pointed at the ground. + - A terribly written novel whose plot seems to match events in the reader's life. + - A crown made out of ice that never melts. + - A piece of string that always emits smoke, like the wick of a snuffed candle. + - A rod of indeterminate metal that changes its length at random every other day. + - A stone carved to look like a head that occasionally speaks, asking questions that change every time. + - A petrified wooden stick that glows brightly when held by an undead creature. + - A flute that, when played, makes the sound of a random instrument, though never a flute. + - A copper ring that weighs literally nothing but remains precisely where it is placed. + - A towel embroidered with high elivsh writing which reads "In case of mind flayer, wear towel." + - A set of brass wind chimes that chime when still and are silent when moving. + - A grotesque statuette of a humanoid with rat-like features. + - A handbook of etiquette for nobles. Its contents can only be remembered for 6 seconds. + - A rolled parchment depicting a translation key for a ficticious language. + - A wooden plank that refuses to burn. + - A detailed guide on the anatomy of rocs. + - A compass that points towards the nearest bottle of rum. + - A stone that weighs 5 pounds but floats in liquid. + - A diagram of a forest on an island with no named artist. + - A brass locket with a strange rune carved inside. + - A green coat with numerous pockets, each with a separate piece of a moustache trimming kit inside. + - A vial of tree sap. + - A rod of metal that produces tiny sparks from a red marking when a black button on the other end is pressed. + - A wooden cane that, when placed on the ground, stands perfectly straight, and cannot be tipped over. + - A feather with a piece of red string tied on the end of the shaft. + - A book that you faintly remember from your childhood that you thought was lost for many years. + - A wooden training sword sized for a child. + - A perfectly normal brown leather glove that fits either hand. + - A golden statue of a beautiful maiden. It glows faintly in the presence of money. + - A gear that refuses to mesh with any other. + - A suit of armour made from oak wood, sized for a doll, painted in vivid red and black. + - A jug that turns liquids stored in it into goat's milk after a few minutes. + - A waxy white flower in a tiny pot that constantly moves as though in a light breeze. + - A robe with a tag on the inside that reads "to my dearest pupil" in draconic. + - A beige jar of red ointment without a label. + - A calligrapher's brush. + - A small cylinder of stone that smells faintly of blood. + - A tunic that smells of the sea. + - A curious looking pair of goggles with the words "Property of Ice! DON'T TOUCH!" scrawled into the side. + - A small blue cube that gives off a faint orange glow. + - A small orb with water and a small living jellyfish inside. + - A cat figurine. + - A hat that has a secret pocket on the inside. + - A bar of scented soap. + - A scrap of parchment with the phrase "I aint'nt dead" written on it in Common. + - An unusually sharp spoon. + - An unusually small humanoid skull. + - A bit of rock from a headstone. + - A tiny bag of purple powder. + - A box of black licorice. + - A strangely shaped bone. + - A sealed, corked bottle with a single globule of blood that never spreads or dries. + - A detailed guide to making pickled foods. + - A palm-sized coin with the carved image of a dragon's head on each side. + - A miniature shield. + - A sea shell with a strange rune carved into it. + - An arm band in the shape of a snake, with emeralds for eyes. + - An old coin, showing a hare on one side and the moon on the reverse. + - A bird carved out of lapis lazuli. + - A small pouch filled with various animal teeth. + - A tiny painting showing a vulture carrying a bone in its beak. + - An ingot of copper with an unusual hue. + - A small notebook full of drawings of food. + - A drawing of a flower with open petals that only appear open when viewed from an angle. + - A perfectly flat wheel made from terracotta. + - The journal of a philosopher, full of wise sayings and anecdotes. + - A malevolent-looking raven skull that has been charred black. + - A small glass case containing several glossy butterfly wings. + - A small glass box containing an undead frog. + - A guide to changing bowstrings written in draconic but full of grammatical errors. + - A ceramic troll statuette with no arms, just legs. + - A sliver ring that feels very slippery. + - An ornately carved figurine of a storm giant made from bone. + - A fork. + - A shoe made from crystal. + - A small wooden box of vibrantly-coloured powders, each colour in its own tiny drawer. + - A silver hand mirror with a dragon etched onto the back. + - A miniature porcelain teapot. + - A bejeweled statuette of a dragonborne knight. + - A stone carving of a piece of bacon. + - A horn that has been cut cleanly in half. + - A small clay pig. + - A small metal can containing a non-Newtonian black fluid that sparks when touched. + - A necklace strung with tiny beetles. + - A fist-sized green seed covered in brown spines. + - A fist-sized metal frog. + - A glass figurine in the shape of a giant ant. + - A petrified wooden spoon. + - A jeweled goblet that never spills. + - A bowl covered in ornate designs depicting hill giants in combat with dwarves. + - A set of ceramic castles that have a space for a candle in the middle. When the candle is lit the castle looks like it has lights on in the windows. + - A purple amulet in the shape of a frog's head. + - A vine covered in thorns that writhes occasionally. diff --git a/rolltable/sources/wild_magic.yaml b/rolltable/sources/wild_magic.yaml new file mode 100644 index 0000000..f34c00a --- /dev/null +++ b/rolltable/sources/wild_magic.yaml @@ -0,0 +1,137 @@ +# Wild Magic Table +metadata: + headers: + - Type + - Effect + - Notes + frequencies: + default: + Boon: 0.20 + Bane: 0.20 + Weird: 0.55 + Trouble: 0.05 + boon: + Boon: 0.65 + Bane: 0.15 + Weird: 0.15 + Trouble: 0.05 + bane: + Boon: 0.15 + Bane: 0.65 + Weird: 0.15 + Trouble: 0.05 + weird: + Boon: 0.05 + Bane: 0.05 + Weird: 0.85 + Trouble: 0.05 + trouble: + Boon: 0.1 + Bane: 0.2 + Weird: 0.3 + Trouble: 0.4 +Boon: + - A +1 martial weapon appears in an unoccupied space within 15ft of you: The weapon disappears if it is carried out of the Sahwat desert. + - Barkskin is cast on you: 1 minute duration. + - Stoneskin is cast on you: 1 minute duration. + - Invisibility is cast on you: 1 minute duration. + - You gain 1d4 temporary hit points: + - The Enlarge spell is cast on you: 1 minute duration. + - Echos of a celestial choir fills the air. Allies within 30ft of you gains 2d4+4 HP: Good-aligned creatures only. + - You are hasted.: 1 minute duration. + - You have truesight: 1 minute duration. + - A third eye opens in your forehead: Curse. Advantage on perception checks. + - Levitate is cast on you: 1 minute duration. + - Your next spell attack does max damage: + - You have resistance to nonmagical damage: 1 minute duration. + - You have resistance to magical damage: 1 minute duration. + - You have advantage on ability checks and saves against spells and magical effects: 1 minute duration + - d100 gold pieces appear in an unoccupied space within 30ft of you: + - You regain one 1st level spell slot: No effect if you do not have 1st level slots. + - Mirror Image is cast on you: 1 minute duration. + - No effect: If the caster dies in the next minute, they are resurrected with 1 HP + - You smell like apple pie: Every creature within 15ft of you must succeed on a DC 13 CHA save or be charmed by you for 1d4 rounds. + - Whenever you hit with a spell attack, it does an additional 1d6 radiant damage: 1d4 rounds +Bane: + - Magic Missile is cast at first level, targeting random creatures 60ft of you: One dart always hits the caster. + - Color spray is cast on a random creature within 30ft of you: 1 minute duration. + - The Reduce spell is cast on you: 1 minute duration. + - The call lightning spell appears centered on a random point 60ft away from you: Lasts for 1d4 rounds. + - A Fireball spell detonates centered on a random point 30ft away from you: + - 1d6 random creatures within 30ft of you must succeed on a Wisdom save or fall unconscious: Lasts until they take damage or someone spends an action to wake them up. + - A 50ft radius sphere of purple gas appears centered on a random point 25ft from you: The sphere lasts 1d4 rounds. Any creature which ends their turn in the sphere takes 1d10 poison damage. + - Infernal whispers fill the air. All creatures within 100ft of you must succeed on a DC14 Wisdom saving throw or take 1d6 psychic damage: + - You are slowed: 1 minute duration. + - Spectral shackles appear on the legs of each creature within 60ft of you: Speed reduced to 5ft. Lasts 1 round. Can be dispelled. + - You are vulnerable to nonmagical damage: 1 minute duration. + - You are vulnerable to magical damage: 1 minute duration. + - You have disadvantage on ability checks and saves against spells and magical effects: 1 minute duration + - You expend one additional 1st level spell slot: No effect if you do not have 1st level slots. + - Mirror Image is cast on hostile creature you can see: 1 minute duration. + - Your skin glows like a small sun. Every creature that can see you is blinded: Until the start of next turn. + - You use all your remaining movement to move in a random direction: +Weird: + - Your skin turns bright blue: Curse. + - It begins snowing from a small cloud 10ft directly over you: Duration 1d4 days. Can be dispelled. + - If it is daytime, you believes it is night. If it is night, you believe it is daytime: Curse. Duration 1d4 days. + - A Zone of Truth spell aura extends from you to a range of 300ft: Duration one hour. Can be dispelled. + - You hear every sound magnified 10x. You are weak to thunder damage: Curse. Duration 1d4 hours. + - Whatever you are holding in your left hand turns into a sticky bun: Duration 1 minute. Can be dispelled. + - You are wearing a hat shaped like your favourite animal: + - If you were alive, you are now undead. If you were undead, you are now alive: Curse. + - A surprised 3rd level wizard appears in an unoccupied space within 15ft of you: random alignment; will disappear again in 1d4 rounds. + - A planar portal opens in the fabric of spacetime in an unoccupied space within 30ft of you. 1d4 modrons appear and begin repairing it.: Disappears after 3 rounds. + - Dispel magic is cast at 4th level on you and every creature within 60ft of you: + - A random trinket falls from the sky: Roll on the trinkets table + - Desert flowers appear in the hair of a random creature within 30ft of you: + - You no longer cast a shadow: Curse. At dawn, shadow attacks as a wraith. + - You swap places with a random creature within 60ft: + - A random creature within 30ft of you is polymorphed into a bowl of petunias: Duration 1 round. 1 HP, AC 10. + - You smell like ham: Curse. Duration 1d4 days. + - A pillar of stone 15ft in diameter and 30ft tall erupts from the ground centered on you: + - A pig appears in the nearest unoccupied space: Immune to all damage. Disappears after 1d4 rounds. + - 1d4 stone statues appear depicting you at key moments in your life: + - A CR 15+ creature apperas 120ft away from you: Major Illusion. + - All plant life within 300ft of you dies: + - All plant life within 300ft of you doubles in size: + - A humanoid man appears and attacks you with 20lb fish: +6 to hit, 1d10 bludgeoning damage. Disappears afer attack. + - 1d8 ghostly figures appear, observing: Duration 1d4 rounds. + - Your body is covered in fur: Curse. + - You are teleported to an unoccupied space 15ft away: + - You age increases by 5d10 years: Can be reversed by Greater Restoration. + - 50lbs of cabbages appear stacked in a pyramid in an unoccupied space: You hear a faint voice yelling, "My cabbages!" + - You are immune to the effects of alcohol: Curse. + - A floating eyeball 1ft in diameter follows you: Disappears after 1d6 days. + - You can only shout when speaking: 1 minute duration. + - Your arms are polymorphed into tentacles: Cannot cast somatic spells. 1 minute duration. + - You are frightened of your favourite color: 1 hour duration. + - You are surrounded by magical silence: 10 minute duration. + - You are encased in a 10ft diameter sphere of glass: AC 10, 15 HP. Vulnerable to bludgeoning. + - A plant within 30ft of you sprouts limbs and runs away: walking speed 80ft. + - Invisibility is cast on every creature you can see: 1 minute duration. + - Any item held by a creature flies from its grip and lands in an unoccupied space 10ft away: + - Illusory butterflies flutter in 5ft radius about you: 1 minute duration. + - You generate a Draining Aura in a 10ft radius: Any creature ending its turn in the aura takes 1d4 necrotic damage. You heal this amount. Duration 1 minute. + - Levitate is cast on 1d4 random creatures within 120ft of you: 1 minute duration. + - No effect: Next phrase spoken by caster becomes true. + - Disguise Self is cast on you: You look like whoever you're most afraid of in life. + - Each creature that can see you must succeed on a DC 13 WIS save or laugh uncontrollably: Until the end of their next turn. +Trouble: + - A random creature within 60ft of you (including you) is banished: Duration 1 minute. + - The Resilient Sphere spell is cast on a random creature (including you) within 30ft of you: + - 1d6 zombies merge from the ground in random unoccupied spaces within 100ft of you: + - A swarm of beetles appears in your space: + - A pit 20ft deep and 5 feet wide appears directly beneath you: 2d6 fall damage. + - You swap current HP with a random creature within 30ft of you: + - 2d8 imps are summoned from the Nine Hells: Will attack closest creature. Disappear after 1d4 rounds. + - Nothing happens: Next time you speak, you breathe fire in a straight line 30ft long and 5ft wide. DEX 13 save or take 2d6 fire damage. + - Gravity reverses in a 30ft radius: Until start of next turn. Can be dispelled. + - Your left eye is a ruby worth 300 GP: You can see alignment. Eye restored with Greater Restoration. + - A random metal weapon bursts in a 60ft cloud of metallic shards: DC 13 DEX or 3d6 piercing damage. + - You sweat blood from every pore: CON 14 or your max HP is reduced by 1d4 per round. + - A random creature within 30ft becomes a pillar of sand: Effect ends if a drop of water touches the pillar. + - A random PC suffers from a random lingering injury: DMG pp272 + - 1d6 random spaces within 100ft of you becomes lava 5ft deep: 10d10 Fire damage. Roll 2d10 for X/Y coords. + - Every creature and object within 120ft of you moves 10ft in a straight line towards you: Any object or creature that collides with something else takes 1d4 bludgeoning damage. + - You hear ethereal music: Lasts until dawn. You cannot long rest during this time. + - 10ft tall crystal spikes erupt from the ground in 1d10 spaces within 100ft of you: Any creature in a space must succeed on a DC 15 DEX save or take 3d6 piercing damage and be restrained. diff --git a/rolltable/types.py b/rolltable/types.py index ca0a8c5..c4ee38c 100644 --- a/rolltable/types.py +++ b/rolltable/types.py @@ -56,14 +56,27 @@ class RollTable: @property def _values(self) -> List: + """ + For each data source, select N random values, where N is the size of the die. + we then zip those random values so that each member of the generated list + contains one value from each data source. So if _data is: + + [ + ['axe', 'shortsword', 'dagger'], + ['fire', 'ice', 'poison'], + ] + + and the die is 2, the resulting generated values might be: + + [ + ['axe', 'fire'], + ['dagger', 'ice'], + ] + """ if not self._generated_values: - ds_values = [t.random_values(self.die) for t in self._data] - self._generated_values = [] - for face in range(self._die): - value = [] - for index, ds in enumerate(ds_values): - value += ds_values[index][face] - self._generated_values.append(value) + self._generated_values = list(zip(*[ + t.random_values(self.die) for t in self._data + ])) return self._generated_values @property diff --git a/tests/test_tables.py b/tests/test_tables.py index dfe8a5b..41ff011 100644 --- a/tests/test_tables.py +++ b/tests/test_tables.py @@ -1,217 +1,22 @@ import pytest -from rolltable import types - -fixture_metadata = """ -metadata: - headers: - - Header 1 - - Header 2 - - Header 3 - die: 10 - frequencies: - default: - Option 1: 0.3 - Option 2: 0.5 - Option 3: 0.2 - nondefault: - Option 1: 0.0 - Option 2: 0.1 - Option 3: 0.9 -""" - -fixture_source = """ -Option 1: - - choice 1: description 1 - - choice 2: description 2 - - choice 3: description 3 -Option 2: - - choice 1: description 4 - - choice 2: description 5 - - choice 3: description 6 -Option 3: - - choice 1: description 7 - - choice 2: description 8 - - choice 3: description 9 -""" - -fixture_one_choice = """ -option 1: - - choice 1: description 1 -""" - -fixture_repeated_choices = """ -option 1: - - choice 1: description 1 - - choice 1: description 1 - - choice 1: description 1 -""" - -fixture_no_descriptions = """ -metadata: - headers: - - option - - choice -option 1: - - choice 1 -""" - -fixture_combined_A = """ -A1: - - A choice 1 - - A choice 2 - - A choice 3 -A2: - - A choice 4 - - A choice 5 - - A choice 6 -A3: - - A choice 7 - - A choice 8 - - A choice 9 -""" - -fixture_combined_B = """ -metadata: - headers: - - HeaderB - - HeaderB_Choice -B1: - - B choice 1 -B2: - - B choice 2 -B3: - - B choice 3 -""" - -fixture_no_options = """ -metadata: - headers: - - headerA - - headerB -B1: -B2: -B3: -""" - -fixture_lists_and_dicts = [""" -# -# category one two three four -# Category foo bar baz quz -# -metadata: - headers: - - category - - one - - two - - three - - four -list: - - foo: - - bar - - baz - - quz -""", """ -# -# category one two three four -# Category foo bar baz quz -# -metadata: - headers: - - category - - one - - two - - three - - four -dict: - foo: - - bar - - baz - - quz - bar: - - a - - b - - c -"""] +from pathlib import Path +from rolltable.types import RollTable -@pytest.mark.parametrize('fixture', fixture_lists_and_dicts) -def test_lists_and_dicts(fixture): - t = types.RollTable([fixture], die=1) - assert(str(t)) +sources = Path(__file__).parent / '..' / 'rolltable' / 'sources' + +flat_list = (sources / 'trinkets.yaml').read_text() +dict_of_dicts = (sources / 'wild_magic.yaml').read_text() +dict_of_lists = (sources / 'psychadelic_effects.yaml').read_text() -def test_combined_tables(): - combined = types.RollTable([fixture_combined_A, fixture_combined_B], die=6) - assert str(combined) - - -def test_table_end_to_end(): - assert str(types.RollTable([fixture_source])) - - -def test_table_end_to_end_with_metadata(): - assert str(types.RollTable([fixture_metadata + fixture_source])) - - -def test_table_frequency(): - t = types.RollTable([fixture_metadata + fixture_source], frequency='nondefault') - assert t._data[0].frequencies['Option 1'] == 0.0 - assert t._data[0].frequencies['Option 2'] == 0.1 - assert t._data[0].frequencies['Option 3'] == 0.9 - - -def test_one_option(): - t = types.RollTable([fixture_one_choice], die=1) - assert t._values == [['option 1', 'choice 1', 'description 1']] - - -def test_collapsed(): - t = types.RollTable([fixture_repeated_choices], die=6) - assert len(list(t.rows)) == 2 # (+1 for headers) - - -def test_not_collapsed(): - t = types.RollTable([fixture_repeated_choices], die=6) - assert len(list(t.expanded_rows)) == 7 # (+1 for headers) - - -def test_no_descriptions(): - t = types.RollTable([fixture_no_descriptions], die=1) - assert 'd1' in str(t) - assert 'option 1' in str(t) - - -def test_no_options(): - t = types.RollTable([fixture_no_options]) - assert str(t) - - -@pytest.mark.parametrize('table', [ - types.RollTable([fixture_no_options]), - types.RollTable([fixture_one_choice]), - types.RollTable([fixture_metadata + fixture_source]), - types.RollTable([fixture_source]), +@pytest.mark.parametrize('data, expected', [ + ([dict_of_dicts], ['d1000 ', 'A third eye', 'Advantage on perception checks']), + ([flat_list], ['d1000 ', 'ivory mimic']), + ([dict_of_lists], ['d1000', 'Cosmic', 'mind expands', 'it will become so']), ]) -def test_yaml(table): - assert table.as_yaml() - - -def test_text(): - assert repr(types.RollTable([fixture_no_options])) - assert repr(types.RollTable([fixture_one_choice])) - assert repr(types.RollTable([fixture_metadata + fixture_source])) - assert repr(types.RollTable([fixture_source])) - - -@pytest.mark.parametrize('table', [ - types.RollTable([fixture_no_options]), - types.RollTable([fixture_one_choice]), - types.RollTable([fixture_metadata + fixture_source]), - types.RollTable([fixture_source]), - types.RollTable([fixture_no_options]), - types.RollTable([fixture_lists_and_dicts]), -]) -def test_as_dict(table): - for src in table.datasources: - assert src.as_dict() +def test_flat(data, expected): + rt = RollTable(data, die=1000) + for txt in expected: + assert txt in str(rt)