Compare commits
5 Commits
notebook
...
cypher-rol
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cde1b5a4d | |||
| 4d92935fda | |||
| bf69d7463e | |||
| 6d68faf73c | |||
| 3e8bdfbc59 |
697
data/cypher-list.yaml
Normal file
697
data/cypher-list.yaml
Normal file
@@ -0,0 +1,697 @@
|
||||
categories:
|
||||
generic_fantasy: Fantasy/Fairy Tale
|
||||
fantasy: Fantasy
|
||||
modern: Modern/Romance
|
||||
scifi: Science Fiction/Post-Apocalyptic
|
||||
horror: Horror
|
||||
superhero: Superhero
|
||||
|
||||
forms:
|
||||
generic_fantasy:
|
||||
list:
|
||||
- Potion
|
||||
- Scroll
|
||||
- Runeplate
|
||||
- Tattoo
|
||||
- Charm
|
||||
- Powder
|
||||
- Crystal
|
||||
- Book
|
||||
fantasy:
|
||||
include: generic_fantasy
|
||||
list:
|
||||
- min_roll: 1
|
||||
max-roll: 2
|
||||
name: Bone runeplate
|
||||
- min_roll: 3
|
||||
max-roll: 4
|
||||
name: Book page
|
||||
- min_roll: 5
|
||||
max-roll: 7
|
||||
name: Bottle of powder
|
||||
- min_roll: 8
|
||||
max-roll: 9
|
||||
name: Brand
|
||||
- min_roll: 10
|
||||
max-roll: 12
|
||||
name: Brick
|
||||
- min_roll: 13
|
||||
max-roll: 15
|
||||
name: Carved bone
|
||||
- min_roll: 16
|
||||
max-roll: 18
|
||||
name: Carved stick
|
||||
- min_roll: 19
|
||||
max-roll: 20
|
||||
name: Carved tooth
|
||||
- min_roll: 21
|
||||
max-roll: 23
|
||||
name: Chalky potion
|
||||
- min_roll: 30
|
||||
max-roll: 33
|
||||
name: Clay runeplate
|
||||
- min_roll: 34
|
||||
max-roll: 37
|
||||
name: Crystal
|
||||
- min_roll: 38
|
||||
max-roll: 39
|
||||
name: Elaborate scar
|
||||
- min_roll: 40
|
||||
max-roll: 42
|
||||
name: Envelope of powder
|
||||
- min_roll: 43
|
||||
max-roll: 44
|
||||
name: Fuming potion
|
||||
- min_roll: 45
|
||||
max-roll: 47
|
||||
name: Glass
|
||||
- min_roll: 48
|
||||
max-roll: 50
|
||||
name: Leaf
|
||||
- min_roll: 51
|
||||
max-roll: 54
|
||||
name: Leather scroll
|
||||
- min_roll: 55
|
||||
max-roll: 57
|
||||
name: Metal runeplate
|
||||
- min_roll: 58
|
||||
max-roll: 60
|
||||
name: Oily potion
|
||||
- min_roll: 61
|
||||
max-roll: 62
|
||||
name: Paper scroll
|
||||
- min_roll: 63
|
||||
max-roll: 66
|
||||
name: Papyrus scroll
|
||||
- min_roll: 67
|
||||
max-roll: 71
|
||||
name: Parchment scroll
|
||||
- min_roll: 72
|
||||
max-roll: 74
|
||||
name: Pouch of powder
|
||||
- min_roll: 75
|
||||
max-roll: 76
|
||||
name: Skin drawing
|
||||
- min_roll: 77
|
||||
max-roll: 80
|
||||
name: Stone
|
||||
- min_roll: 81
|
||||
max-roll: 82
|
||||
name: Tattoo
|
||||
- min_roll: 83
|
||||
max-roll: 85
|
||||
name: Thick potion
|
||||
- min_roll: 86
|
||||
max-roll: 88
|
||||
name: Tube of power
|
||||
- min_roll: 89
|
||||
max-roll: 92
|
||||
name: Vellum scroll
|
||||
- min_roll: 93
|
||||
max-roll: 96
|
||||
name: Watery potion
|
||||
- min_roll: 97
|
||||
max-roll: 00
|
||||
name: Wood runeplate
|
||||
modern:
|
||||
list:
|
||||
- Drug
|
||||
- Virus
|
||||
- Smartphone app
|
||||
scifi:
|
||||
list:
|
||||
- Drug
|
||||
- Computer program
|
||||
- Crystal
|
||||
- Gadget
|
||||
- Virus
|
||||
- Biological implant
|
||||
- Mechanical implant
|
||||
- Nanotechnological injection
|
||||
horror:
|
||||
list:
|
||||
- Burrowing worm or insect
|
||||
- Page from a forbidden book
|
||||
- Horrific image
|
||||
superhero:
|
||||
include: all
|
||||
|
||||
cyphers:
|
||||
manifest:
|
||||
- min_roll: 1
|
||||
max-roll: 3
|
||||
name: Adhesion
|
||||
level: d6
|
||||
- min_roll: 4
|
||||
max-roll: 5
|
||||
name: Antivenom
|
||||
- min_roll: 6
|
||||
max-roll: 9
|
||||
name: Armor reinforcer
|
||||
- min_roll: 10
|
||||
max-roll: 11
|
||||
name: Attractor
|
||||
- min_roll: 12
|
||||
max-roll: 13
|
||||
name: Blackout
|
||||
- min_roll: 14
|
||||
max-roll: 15
|
||||
name: Catholicon
|
||||
- min_roll: 16
|
||||
max-roll: 17
|
||||
name: Curse bringer
|
||||
- min_roll: 18
|
||||
max-roll: 19
|
||||
name: Death bringer
|
||||
- min_roll: 20
|
||||
max-roll: 22
|
||||
name: Density
|
||||
- min_roll: 23
|
||||
max-roll: 26
|
||||
name: Detonation
|
||||
- min_roll: 27
|
||||
max-roll: 29
|
||||
name: Detonation (flash)
|
||||
- min_roll: 30
|
||||
max-roll: 31
|
||||
name: Detonation (massive)
|
||||
- min_roll: 32
|
||||
max-roll: 34
|
||||
name: Detonation (pressure)
|
||||
- min_roll: 35
|
||||
max-roll: 36
|
||||
name: Detonation (sonic)
|
||||
- min_roll: 37
|
||||
max-roll: 38
|
||||
name: Detonation (spawn)
|
||||
- min_roll: 39
|
||||
max-roll: 41
|
||||
name: Detonation (web)
|
||||
- min_roll: 42
|
||||
max-roll: 44
|
||||
name: Equipment cache
|
||||
- min_roll: 45
|
||||
max-roll: 46
|
||||
name: Fireproofing
|
||||
- min_roll: 47
|
||||
max-roll: 49
|
||||
name: Friction reducer
|
||||
- min_roll: 50
|
||||
max-roll: 52
|
||||
name: Gas bomb
|
||||
- min_roll: 53
|
||||
max-roll: 55
|
||||
name: Hunter/seeker
|
||||
- min_roll: 56
|
||||
max-roll: 57
|
||||
name: Infiltrator
|
||||
- min_roll: 58
|
||||
max-roll: 60
|
||||
name: Information sensor
|
||||
- min_roll: 61
|
||||
max-roll: 63
|
||||
name: Metal death
|
||||
- min_roll: 64
|
||||
max-roll: 65
|
||||
name: Nullification ray
|
||||
- min_roll: 66
|
||||
max-roll: 68
|
||||
name: Poison (emotion)
|
||||
- min_roll: 69
|
||||
max-roll: 70
|
||||
name: Poison (mind disrupting)
|
||||
- min_roll: 71
|
||||
max-roll: 73
|
||||
name: Radiation spike
|
||||
- min_roll: 74
|
||||
max-roll: 76
|
||||
name: Remote viewer
|
||||
- min_roll: 77
|
||||
max-roll: 79
|
||||
name: Shocker
|
||||
- min_roll: 80
|
||||
max-roll: 82
|
||||
name: Sleep inducer
|
||||
- min_roll: 83
|
||||
max-roll: 85
|
||||
name: Sniper module
|
||||
- min_roll: 86
|
||||
max-roll: 88
|
||||
name: Solvent
|
||||
- min_roll: 89
|
||||
max-roll: 90
|
||||
name: Spy
|
||||
- min_roll: 91
|
||||
max-roll: 92
|
||||
name: Tracer
|
||||
- min_roll: 93
|
||||
max-roll: 94
|
||||
name: Uninterruptible power source
|
||||
- min_roll: 95
|
||||
max-roll: 96
|
||||
name: Warmth
|
||||
- min_roll: 97
|
||||
max-roll: 98
|
||||
name: Water adapter
|
||||
- min_roll: 99
|
||||
max-roll: 100
|
||||
name: X-ray viewer
|
||||
fantastic:
|
||||
- min_roll: 01
|
||||
max-roll: 01
|
||||
name: Age taker
|
||||
- min_roll: 02
|
||||
max-roll: 02
|
||||
name: Banishing
|
||||
- min_roll: 03
|
||||
max-roll: 04
|
||||
name: Blinking
|
||||
- min_roll: 05
|
||||
max-roll: 05
|
||||
name: Chemical factory
|
||||
- min_roll: 06
|
||||
max-roll: 06
|
||||
name: Comprehension
|
||||
- min_roll: 07
|
||||
max-roll: 08
|
||||
name: Condition remover
|
||||
- min_roll: 09
|
||||
max-roll: 09
|
||||
name: Controlled blinking
|
||||
- min_roll: 10
|
||||
max-roll: 10
|
||||
name: Detonation (creature)
|
||||
- min_roll: 11
|
||||
max-roll: 11
|
||||
name: Detonation (desiccating)
|
||||
- min_roll: 12
|
||||
max-roll: 12
|
||||
name: Detonation (gravity)
|
||||
- min_roll: 13
|
||||
max-roll: 13
|
||||
name: Detonation (gravity inversion)
|
||||
- min_roll: 14
|
||||
max-roll: 14
|
||||
name: Detonation (matter disruption)
|
||||
- min_roll: 15
|
||||
max-roll: 15
|
||||
name: Detonation (singularity)
|
||||
- min_roll: 16
|
||||
max-roll: 16
|
||||
name: Disguise module
|
||||
- min_roll: 17
|
||||
max-roll: 17
|
||||
name: Disrupting
|
||||
- min_roll: 18
|
||||
max-roll: 18
|
||||
name: Farsight
|
||||
- min_roll: 19
|
||||
max-roll: 19
|
||||
name: Flame-retardant wall
|
||||
- min_roll: 20
|
||||
max-roll: 20
|
||||
name: Force cube
|
||||
- min_roll: 21
|
||||
max-roll: 22
|
||||
name: Force field
|
||||
- min_roll: 23
|
||||
max-roll: 23
|
||||
name: Force screen projector
|
||||
- min_roll: 24
|
||||
max-roll: 24
|
||||
name: Force shield projector
|
||||
- min_roll: 25
|
||||
max-roll: 25
|
||||
name: Frigid wall
|
||||
- min_roll: 26
|
||||
max-roll: 27
|
||||
name: Gravity nullifier
|
||||
- min_roll: 28
|
||||
max-roll: 28
|
||||
name: Gravity-nullifying application
|
||||
- min_roll: 29
|
||||
max-roll: 30
|
||||
name: Heat attack
|
||||
- min_roll: 31
|
||||
max-roll: 31
|
||||
name: Image projector
|
||||
- min_roll: 32
|
||||
max-roll: 32
|
||||
name: Inferno wall
|
||||
- min_roll: 33
|
||||
max-roll: 34
|
||||
name: Instant servant
|
||||
- min_roll: 35
|
||||
max-roll: 35
|
||||
name: Instant shelter
|
||||
- min_roll: 36
|
||||
max-roll: 36
|
||||
name: Lightning wall
|
||||
- min_roll: 37
|
||||
max-roll: 38
|
||||
name: Machine control
|
||||
- min_roll: 39
|
||||
max-roll: 39
|
||||
name: Magnetic attack drill
|
||||
- min_roll: 40
|
||||
max-roll: 40
|
||||
name: Magnetic master
|
||||
- min_roll: 41
|
||||
max-roll: 41
|
||||
name: Magnetic shield
|
||||
- min_roll: 42
|
||||
max-roll: 42
|
||||
name: Manipulation beam
|
||||
- min_roll: 43
|
||||
max-roll: 43
|
||||
name: Matter transference ray
|
||||
- min_roll: 44
|
||||
max-roll: 44
|
||||
name: Memory switch
|
||||
- min_roll: 45
|
||||
max-roll: 45
|
||||
name: Mental scrambler
|
||||
- min_roll: 46
|
||||
max-roll: 46
|
||||
name: Mind meld
|
||||
- min_roll: 47
|
||||
max-roll: 47
|
||||
name: Mind-restricting wall
|
||||
- min_roll: 48
|
||||
max-roll: 49
|
||||
name: Monoblade
|
||||
- min_roll: 50
|
||||
max-roll: 50
|
||||
name: Monohorn
|
||||
- min_roll: 51
|
||||
max-roll: 51
|
||||
name: Null field
|
||||
- min_roll: 52
|
||||
max-roll: 53
|
||||
name: Personal environment field
|
||||
- min_roll: 54
|
||||
max-roll: 55
|
||||
name: Phase changer
|
||||
- min_roll: 56
|
||||
max-roll: 56
|
||||
name: Phase disruptor
|
||||
- min_roll: 57
|
||||
max-roll: 57
|
||||
name: Poison (explosive)
|
||||
- min_roll: 58
|
||||
max-roll: 58
|
||||
name: Poison (mind controlling)
|
||||
- min_roll: 59
|
||||
max-roll: 59
|
||||
name: Psychic communique
|
||||
- min_roll: 60
|
||||
max-roll: 60
|
||||
name: Ray emitter
|
||||
- min_roll: 61
|
||||
max-roll: 61
|
||||
name: Ray emitter (command)
|
||||
- min_roll: 62
|
||||
max-roll: 62
|
||||
name: Ray emitter (fear)
|
||||
- min_roll: 63
|
||||
max-roll: 63
|
||||
name: Ray emitter (friend slaying)
|
||||
- min_roll: 64
|
||||
max-roll: 64
|
||||
name: Ray emitter (mind disrupting)
|
||||
- min_roll: 65
|
||||
max-roll: 65
|
||||
name: Ray emitter (numbing)
|
||||
- min_roll: 66
|
||||
max-roll: 66
|
||||
name: Ray emitter (paralysis)
|
||||
- min_roll: 67
|
||||
max-roll: 67
|
||||
name: Reality spike
|
||||
- min_roll: 68
|
||||
max-roll: 68
|
||||
name: Repair unit
|
||||
- min_roll: 69
|
||||
max-roll: 69
|
||||
name: Repeater
|
||||
- min_roll: 70
|
||||
max-roll: 71
|
||||
name: Retaliation
|
||||
- min_roll: 72
|
||||
max-roll: 72
|
||||
name: Sheen
|
||||
- min_roll: 73
|
||||
max-roll: 74
|
||||
name: Shock attack
|
||||
- min_roll: 75
|
||||
max-roll: 75
|
||||
name: Slave maker
|
||||
- min_roll: 76
|
||||
max-roll: 76
|
||||
name: Sonic hole
|
||||
- min_roll: 77
|
||||
max-roll: 78
|
||||
name: Sound dampener
|
||||
- min_roll: 79
|
||||
max-roll: 79
|
||||
name: Spatial warp
|
||||
- min_roll: 80
|
||||
max-roll: 80
|
||||
name: Stasis keeper
|
||||
- min_roll: 81
|
||||
max-roll: 81
|
||||
name: Subdual field
|
||||
- min_roll: 82
|
||||
max-roll: 83
|
||||
name: Telepathy
|
||||
- min_roll: 84
|
||||
max-roll: 84
|
||||
name: Teleporter (bounder)
|
||||
- min_roll: 85
|
||||
max-roll: 85
|
||||
name: Teleporter (interstellar)
|
||||
- min_roll: 86
|
||||
max-roll: 86
|
||||
name: Teleporter (planetary)
|
||||
- min_roll: 87
|
||||
max-roll: 87
|
||||
name: Teleporter (traveler)
|
||||
- min_roll: 88
|
||||
max-roll: 88
|
||||
name: Temporal viewer
|
||||
- min_roll: 89
|
||||
max-roll: 89
|
||||
name: Time dilation (defensive)
|
||||
- min_roll: 90
|
||||
max-roll: 90
|
||||
name: Time dilation (offensive)
|
||||
- min_roll: 91
|
||||
max-roll: 91
|
||||
name: Trick embedder
|
||||
- min_roll: 92
|
||||
max-roll: 92
|
||||
name: Vanisher
|
||||
- min_roll: 93
|
||||
max-roll: 94
|
||||
name: Visage changer
|
||||
- min_roll: 95
|
||||
max-roll: 95
|
||||
name: Visual displacement device
|
||||
- min_roll: 96
|
||||
max-roll: 96
|
||||
name: Vocal translator
|
||||
- min_roll: 97
|
||||
max-roll: 98
|
||||
name: Weapon enhancement
|
||||
- min_roll: 99
|
||||
max-roll: 99
|
||||
name: Wings
|
||||
- min_roll: 00
|
||||
max-roll: 00
|
||||
name: Zero point field
|
||||
subtle:
|
||||
- min_roll: 1
|
||||
max-roll: 4
|
||||
name: Analeptic
|
||||
- min_roll: 5
|
||||
max-roll: 7
|
||||
name: Best tool
|
||||
- min_roll: 8
|
||||
max-roll: 10
|
||||
name: Burst of speed
|
||||
- min_roll: 11
|
||||
max-roll: 13
|
||||
name: Contingent activator
|
||||
- min_roll: 14
|
||||
max-roll: 17
|
||||
name: Curative
|
||||
- min_roll: 18
|
||||
max-roll: 20
|
||||
name: Darksight
|
||||
- min_roll: 21
|
||||
max-roll: 23
|
||||
name: Disarm
|
||||
- min_roll: 24
|
||||
max-roll: 26
|
||||
name: Eagleseye
|
||||
- min_roll: 27
|
||||
max-roll: 29
|
||||
name: Effect resistance
|
||||
- min_roll: 30
|
||||
max-roll: 32
|
||||
name: Effort enhancer (combat)
|
||||
- min_roll: 33
|
||||
max-roll: 35
|
||||
name: Effort enhancer (noncombat)
|
||||
- min_roll: 36
|
||||
max-roll: 39
|
||||
name: Enduring shield
|
||||
- min_roll: 40
|
||||
max-roll: 42
|
||||
name: Intellect booster
|
||||
- min_roll: 43
|
||||
max-roll: 45
|
||||
name: Intelligence enhancement
|
||||
- min_roll: 46
|
||||
max-roll: 48
|
||||
name: Knowledge enhancement
|
||||
- min_roll: 49
|
||||
max-roll: 51
|
||||
name: Meditation aid
|
||||
- min_roll: 52
|
||||
max-roll: 54
|
||||
name: Mind stabilizer
|
||||
- min_roll: 55
|
||||
max-roll: 57
|
||||
name: Motion sensor
|
||||
- min_roll: 58
|
||||
max-roll: 60
|
||||
name: Nutrition and hydration
|
||||
- min_roll: 61
|
||||
max-roll: 63
|
||||
name: Perfect memory
|
||||
- min_roll: 64
|
||||
max-roll: 66
|
||||
name: Perfection
|
||||
- min_roll: 67
|
||||
max-roll: 69
|
||||
name: Reflex enhancer
|
||||
- min_roll: 70
|
||||
max-roll: 73
|
||||
name: Rejuvenator
|
||||
- min_roll: 74
|
||||
max-roll: 76
|
||||
name: Remembering
|
||||
- min_roll: 77
|
||||
max-roll: 79
|
||||
name: Repel
|
||||
- min_roll: 80
|
||||
max-roll: 82
|
||||
name: Secret
|
||||
- min_roll: 83
|
||||
max-roll: 85
|
||||
name: Skill boost
|
||||
- min_roll: 86
|
||||
max-roll: 88
|
||||
name: Speed boost
|
||||
- min_roll: 89
|
||||
max-roll: 91
|
||||
name: Stim
|
||||
- min_roll: 92
|
||||
max-roll: 94
|
||||
name: Strength boost
|
||||
- min_roll: 95
|
||||
max-roll: 97
|
||||
name: Strength enhancer
|
||||
- min_roll: 98
|
||||
max-roll: 100
|
||||
name: Tissue regeneration
|
||||
fantasy:
|
||||
- min_roll: 1
|
||||
max-roll: 5
|
||||
name: Acid resistance
|
||||
- min_roll: 6
|
||||
max-roll: 11
|
||||
name: Animal control
|
||||
- min_roll: 12
|
||||
max-roll: 18
|
||||
name: Beast shape
|
||||
- min_roll: 19
|
||||
max-roll: 27
|
||||
name: Cold resistance
|
||||
- min_roll: 28
|
||||
max-roll: 34
|
||||
name: Demon ward
|
||||
- min_roll: 35
|
||||
max-roll: 39
|
||||
name: Dragon ward
|
||||
- min_roll: 40
|
||||
max-roll: 44
|
||||
name: Electricity resistance
|
||||
- min_roll: 45
|
||||
max-roll: 48
|
||||
name: Elemental conjuration
|
||||
- min_roll: 49
|
||||
max-roll: 57
|
||||
name: Fire resistance
|
||||
- min_roll: 58
|
||||
max-roll: 61
|
||||
name: Giant size
|
||||
- min_roll: 62
|
||||
max-roll: 65
|
||||
name: Instant boat
|
||||
- min_roll: 66
|
||||
max-roll: 68
|
||||
name: Instant tower
|
||||
- min_roll: 69
|
||||
max-roll: 72
|
||||
name: Lycanthrope ward
|
||||
- min_roll: 73
|
||||
max-roll: 76
|
||||
name: Penultimate key
|
||||
- min_roll: 77
|
||||
max-roll: 82
|
||||
name: Poison resistance
|
||||
- min_roll: 83
|
||||
max-roll: 86
|
||||
name: Restorative aura
|
||||
- min_roll: 87
|
||||
max-roll: 89
|
||||
name: Thought listening
|
||||
- min_roll: 90
|
||||
max-roll: 93
|
||||
name: Tiny size
|
||||
- min_roll: 94
|
||||
max-roll: 98
|
||||
name: Undead ward
|
||||
- min_roll: 99
|
||||
max-roll: 00
|
||||
name: Walking corpse
|
||||
superhero:
|
||||
- min_roll: 01
|
||||
max-roll: 10
|
||||
name: Area boost
|
||||
- min_roll: 11
|
||||
max-roll: 20
|
||||
name: Burst boost
|
||||
- min_roll: 21
|
||||
max-roll: 30
|
||||
name: Damage boost
|
||||
- min_roll: 31
|
||||
max-roll: 40
|
||||
name: Efficacy boost
|
||||
- min_roll: 41
|
||||
max-roll: 50
|
||||
name: Energy boost
|
||||
- min_roll: 51
|
||||
max-roll: 60
|
||||
name: Range boost
|
||||
- min_roll: 61
|
||||
max-roll: 80
|
||||
name: Shift boost
|
||||
- min_roll: 81
|
||||
max-roll: 90
|
||||
name: Stunt boost
|
||||
- min_roll: 91
|
||||
max-roll: 00
|
||||
name: Target boost
|
||||
2435
data/og-csrd-artifacts.yaml
Normal file
2435
data/og-csrd-artifacts.yaml
Normal file
File diff suppressed because it is too large
Load Diff
5899
data/og-csrd-cyphers.yaml
Normal file
5899
data/og-csrd-cyphers.yaml
Normal file
File diff suppressed because it is too large
Load Diff
192
gm_assistant/fate_chart.py
Normal file
192
gm_assistant/fate_chart.py
Normal file
@@ -0,0 +1,192 @@
|
||||
# SPDX-FileCopyrightText: 2025 2025
|
||||
# SPDX-FileContributor: Gergely Polonkai
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Calculate the odds using Mythic RPG’s Fate Chart"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from .dice import Die
|
||||
|
||||
|
||||
class FateOutcome(Enum):
|
||||
"""Possible outcomes"""
|
||||
|
||||
EXC_YES = "eyes"
|
||||
YES = "yes"
|
||||
NO = "no"
|
||||
EXC_NO = "eno"
|
||||
|
||||
|
||||
class FateOdds(Enum):
|
||||
"""Odds of a “yes” answer"""
|
||||
|
||||
IMPOSSIBLE = 0
|
||||
NO_WAY = 1
|
||||
VERY_UNLIKELY = 2
|
||||
UNLIKELY = 3
|
||||
FIFTY_FIFTY = 4
|
||||
SOMEWHAT_LIKELY = 5
|
||||
LIKELY = 6
|
||||
VERY_LIKELY = 7
|
||||
NEAR_SURE_THING = 8
|
||||
SURE_THING = 9
|
||||
HAS_TO_BE = 10
|
||||
HAS_TO_BE2 = 11
|
||||
|
||||
|
||||
TABLE: dict[FateOdds, list[tuple[int, int, int]]] = {
|
||||
FateOdds.IMPOSSIBLE: [
|
||||
(0, -20, 77),
|
||||
(0, 0, 81),
|
||||
(0, 0, 81),
|
||||
(1, 5, 82),
|
||||
(1, 5, 82),
|
||||
(2, 10, 83),
|
||||
(3, 15, 84),
|
||||
(5, 25, 86),
|
||||
(10, 50, 91),
|
||||
],
|
||||
FateOdds.NO_WAY: [
|
||||
(0, 0, 81),
|
||||
(1, 5, 82),
|
||||
(1, 5, 82),
|
||||
(2, 10, 83),
|
||||
(3, 15, 84),
|
||||
(5, 25, 86),
|
||||
(7, 35, 88),
|
||||
(10, 50, 91),
|
||||
(15, 75, 96),
|
||||
],
|
||||
FateOdds.VERY_UNLIKELY: [
|
||||
(1, 5, 82),
|
||||
(1, 5, 82),
|
||||
(2, 10, 83),
|
||||
(3, 15, 84),
|
||||
(5, 25, 86),
|
||||
(9, 45, 90),
|
||||
(10, 50, 91),
|
||||
(13, 65, 94),
|
||||
(16, 85, 97),
|
||||
],
|
||||
FateOdds.UNLIKELY: [
|
||||
(1, 5, 82),
|
||||
(2, 10, 83),
|
||||
(3, 15, 84),
|
||||
(4, 20, 85),
|
||||
(7, 35, 88),
|
||||
(10, 50, 91),
|
||||
(11, 55, 92),
|
||||
(15, 75, 96),
|
||||
(18, 90, 99),
|
||||
],
|
||||
FateOdds.FIFTY_FIFTY: [
|
||||
(2, 10, 83),
|
||||
(3, 15, 84),
|
||||
(5, 25, 86),
|
||||
(7, 35, 88),
|
||||
(10, 50, 91),
|
||||
(13, 65, 94),
|
||||
(15, 75, 96),
|
||||
(16, 85, 97),
|
||||
(19, 95, 100),
|
||||
],
|
||||
FateOdds.SOMEWHAT_LIKELY: [
|
||||
(4, 20, 85),
|
||||
(5, 25, 86),
|
||||
(9, 45, 90),
|
||||
(10, 50, 91),
|
||||
(13, 65, 94),
|
||||
(16, 80, 97),
|
||||
(16, 85, 97),
|
||||
(18, 90, 99),
|
||||
(19, 95, 100),
|
||||
],
|
||||
FateOdds.LIKELY: [
|
||||
(5, 25, 86),
|
||||
(7, 35, 88),
|
||||
(10, 50, 91),
|
||||
(11, 55, 92),
|
||||
(15, 75, 96),
|
||||
(16, 85, 97),
|
||||
(18, 90, 99),
|
||||
(19, 95, 100),
|
||||
(20, 100, 0),
|
||||
],
|
||||
FateOdds.VERY_LIKELY: [
|
||||
(9, 45, 90),
|
||||
(10, 50, 91),
|
||||
(13, 65, 94),
|
||||
(15, 75, 96),
|
||||
(16, 85, 97),
|
||||
(18, 90, 99),
|
||||
(19, 95, 100),
|
||||
(19, 95, 100),
|
||||
(21, 105, 0),
|
||||
],
|
||||
FateOdds.NEAR_SURE_THING: [
|
||||
(10, 50, 91),
|
||||
(11, 55, 92),
|
||||
(15, 75, 96),
|
||||
(16, 80, 97),
|
||||
(18, 90, 99),
|
||||
(19, 95, 100),
|
||||
(19, 95, 100),
|
||||
(20, 100, 0),
|
||||
(23, 115, 0),
|
||||
],
|
||||
FateOdds.SURE_THING: [
|
||||
(11, 55, 92),
|
||||
(13, 65, 94),
|
||||
(16, 80, 97),
|
||||
(16, 85, 97),
|
||||
(18, 90, 99),
|
||||
(19, 95, 100),
|
||||
(19, 95, 100),
|
||||
(22, 110, 0),
|
||||
(25, 125, 0),
|
||||
],
|
||||
FateOdds.HAS_TO_BE: [
|
||||
(16, 80, 97),
|
||||
(16, 85, 97),
|
||||
(18, 90, 99),
|
||||
(19, 95, 100),
|
||||
(19, 95, 100),
|
||||
(20, 100, 0),
|
||||
(20, 100, 0),
|
||||
(26, 130, 0),
|
||||
(26, 145, 0),
|
||||
],
|
||||
FateOdds.HAS_TO_BE2: [
|
||||
(18, 90, 99),
|
||||
(19, 95, 100),
|
||||
(19, 95, 100),
|
||||
(20, 100, 0),
|
||||
(22, 110, 0),
|
||||
(24, 120, 0),
|
||||
(24, 120, 0),
|
||||
(30, 150, 0),
|
||||
(29, 165, 0),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def decide(odds: FateOdds, chaos_level: int = 5) -> FateOutcome:
|
||||
"""Decide the outcome of a situation using the Fate Chart"""
|
||||
|
||||
# Chaos levels range from 1 and 9 (both included), but list indices start at 0
|
||||
chaos_level -= 1
|
||||
|
||||
eyes, yes, eno = TABLE[odds][chaos_level]
|
||||
value = Die("d100").roll()
|
||||
|
||||
if value <= eyes:
|
||||
return FateOutcome.EXC_YES
|
||||
|
||||
if value <= yes:
|
||||
return FateOutcome.YES
|
||||
|
||||
if value >= eno:
|
||||
return FateOutcome.EXC_NO
|
||||
|
||||
return FateOutcome.NO
|
||||
@@ -2,3 +2,52 @@
|
||||
# SPDX-FileContributor: Gergely Polonkai
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Oracle classes and related functions"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Type
|
||||
|
||||
import yaml
|
||||
|
||||
from .base import Oracle
|
||||
from .object_generator import ObjectGeneratorOracle
|
||||
from .random_choice import RandomChoiceOracle
|
||||
|
||||
|
||||
def generate_type_classes(class_list: dict[str, Any]) -> dict[str, Type[Oracle]]:
|
||||
"""Generate a dictionary of oracle type handlers"""
|
||||
|
||||
ret: dict[str, Type[Oracle]] = {}
|
||||
|
||||
for klass in class_list.values():
|
||||
if not isinstance(klass, type) or klass == Oracle or not issubclass(klass, Oracle):
|
||||
continue
|
||||
|
||||
if klass.TYPE_MARKER in ret:
|
||||
raise KeyError(
|
||||
f"{ret[klass.TYPE_MARKER].__name__} is already registered as a handler for {klass.TYPE_MARKER}"
|
||||
)
|
||||
|
||||
ret[klass.TYPE_MARKER] = klass
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
TYPE_CLASSES: dict[str, Type[Oracle]] = generate_type_classes(globals())
|
||||
|
||||
|
||||
def load_oracle_from_yaml(file_path: str | Path) -> Oracle:
|
||||
"""Create an Oracle from a YAML file"""
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as fhand:
|
||||
data = yaml.safe_load(fhand)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise TypeError("Oracle data must be a YAML object")
|
||||
|
||||
if (generator_type := data.get("type")) not in TYPE_CLASSES:
|
||||
raise KeyError(f"No information on how to handle {generator_type} data")
|
||||
|
||||
handler_class = TYPE_CLASSES[generator_type]
|
||||
|
||||
return handler_class(data)
|
||||
|
||||
16
poetry.lock
generated
16
poetry.lock
generated
@@ -873,7 +873,7 @@ version = "6.0.2"
|
||||
description = "YAML parser and emitter for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
|
||||
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
|
||||
@@ -994,6 +994,18 @@ files = [
|
||||
{file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-pyyaml"
|
||||
version = "6.0.12.20250516"
|
||||
description = "Typing stubs for PyYAML"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530"},
|
||||
{file = "types_pyyaml-6.0.12.20250516.tar.gz", hash = "sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.13.2"
|
||||
@@ -1042,4 +1054,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "~3.13"
|
||||
content-hash = "c778f03ed4db825597abdfa53d9dc90fed168d978a929383922ab8acabe71f54"
|
||||
content-hash = "4450e2a03c5ea31eee3b6ddbb097737a778911e8ea1d4d389229a39596ffa9d9"
|
||||
|
||||
@@ -14,6 +14,7 @@ license = {text = "GPL-3.0-or-later"}
|
||||
readme = "README.md"
|
||||
requires-python = "~3.13"
|
||||
dependencies = [
|
||||
"pyyaml (>=6.0.2,<7.0.0)"
|
||||
]
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
@@ -27,6 +28,7 @@ pytest = "^8.3.5"
|
||||
pytest-cov = "^6.1.1"
|
||||
pytest-mock = "^3.14.0"
|
||||
reuse = "^5.0.2"
|
||||
types-pyyaml = "^6.0.12.20250516"
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
||||
51
tests/test_fate_chart.py
Normal file
51
tests/test_fate_chart.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# SPDX-FileCopyrightText: 2025 2025
|
||||
# SPDX-FileContributor: Gergely Polonkai
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Tests for the Fate Chart module"""
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from gm_assistant.fate_chart import FateOdds, FateOutcome, decide
|
||||
|
||||
|
||||
@pytest.mark.parametrize("odds", FateOdds)
|
||||
@pytest.mark.parametrize("chaos_level", range(9))
|
||||
def test_all_valid_values(odds: FateOdds, chaos_level: int) -> None:
|
||||
"""Test if every valid value yields some result"""
|
||||
|
||||
assert isinstance(decide(odds, chaos_level=chaos_level), FateOutcome)
|
||||
|
||||
|
||||
# WARNING
|
||||
#
|
||||
# Tests below this line rely on exact values in the fate_chart module
|
||||
|
||||
|
||||
def test_exceptional_yes(mocker: MockerFixture) -> None:
|
||||
"""Test if exceptional yes result is returned"""
|
||||
|
||||
mocker.patch("gm_assistant.fate_chart.Die.roll", return_value=10)
|
||||
assert decide(FateOdds.FIFTY_FIFTY, chaos_level=5) == FateOutcome.EXC_YES
|
||||
|
||||
|
||||
def test_yes(mocker: MockerFixture) -> None:
|
||||
"""Test if yes result is returned"""
|
||||
|
||||
mocker.patch("gm_assistant.fate_chart.Die.roll", return_value=50)
|
||||
assert decide(FateOdds.FIFTY_FIFTY, chaos_level=5) == FateOutcome.YES
|
||||
|
||||
|
||||
def test_exceptional_no(mocker: MockerFixture) -> None:
|
||||
"""Test if exceptional no result is returned"""
|
||||
|
||||
mocker.patch("gm_assistant.fate_chart.Die.roll", return_value=91)
|
||||
assert decide(FateOdds.FIFTY_FIFTY, chaos_level=5) == FateOutcome.EXC_NO
|
||||
|
||||
|
||||
def test_no(mocker: MockerFixture) -> None:
|
||||
"""Test if exceptional no result is returned"""
|
||||
|
||||
mocker.patch("gm_assistant.fate_chart.Die.roll", return_value=51)
|
||||
assert decide(FateOdds.FIFTY_FIFTY, chaos_level=5) == FateOutcome.NO
|
||||
55
tests/test_load_oracle_from_yaml.py
Normal file
55
tests/test_load_oracle_from_yaml.py
Normal file
@@ -0,0 +1,55 @@
|
||||
# SPDX-FileCopyrightText: 2025 2025
|
||||
# SPDX-FileContributor: Gergely Polonkai
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Tests for the ``load_oracle_from_yaml`` function"""
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from gm_assistant.oracle import load_oracle_from_yaml
|
||||
from gm_assistant.oracle.random_choice import RandomChoiceOracle
|
||||
|
||||
|
||||
def test_non_object(mocker: MockerFixture) -> None:
|
||||
"""Test loading something that is not a YAML object"""
|
||||
|
||||
mocker.patch("gm_assistant.oracle.open")
|
||||
mocker.patch("gm_assistant.oracle.yaml.safe_load", return_value=[])
|
||||
|
||||
with pytest.raises(TypeError) as ctx:
|
||||
load_oracle_from_yaml("test_file")
|
||||
|
||||
assert str(ctx.value) == "Oracle data must be a YAML object"
|
||||
|
||||
|
||||
# WARNING
|
||||
#
|
||||
# Tests below this line rely on specific classes getting loaded into gm_assistant.oracle.TYPE_CLASSES
|
||||
|
||||
|
||||
def test_unknown_type(mocker: MockerFixture) -> None:
|
||||
"""Test loading an oracle with an unknown type"""
|
||||
|
||||
mocker.patch("gm_assistant.oracle.open")
|
||||
mocker.patch("gm_assistant.oracle.yaml.safe_load", return_value={"type": "something-non-existing"})
|
||||
|
||||
with pytest.raises(KeyError) as ctx:
|
||||
load_oracle_from_yaml("test_file")
|
||||
|
||||
assert str(ctx.value) == "'No information on how to handle something-non-existing data'"
|
||||
|
||||
|
||||
def test_load_oracle(mocker: MockerFixture) -> None:
|
||||
"""Test loading a specific oracle"""
|
||||
|
||||
mocker.patch("gm_assistant.oracle.open")
|
||||
mocker.patch(
|
||||
"gm_assistant.oracle.yaml.safe_load",
|
||||
return_value={"type": "random-choice", "name": "Test Oracle", "source": "Test Source", "choices": ["A", "B"]},
|
||||
)
|
||||
|
||||
oracle = load_oracle_from_yaml("test_file")
|
||||
|
||||
assert isinstance(oracle, RandomChoiceOracle)
|
||||
assert oracle.choices == ["A", "B"]
|
||||
53
tests/test_oracle_type_class_lister.py
Normal file
53
tests/test_oracle_type_class_lister.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# SPDX-FileCopyrightText: 2025 2025
|
||||
# SPDX-FileContributor: Gergely Polonkai
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Tests for the type class lister"""
|
||||
|
||||
import pytest
|
||||
|
||||
from gm_assistant.oracle import generate_type_classes
|
||||
from gm_assistant.oracle.base import Oracle
|
||||
from gm_assistant.oracle.object_generator import ObjectGeneratorOracle
|
||||
|
||||
|
||||
class _TestOracle(Oracle):
|
||||
"""Test oracle class that has the same marker as ObjectGeneratorOracle"""
|
||||
|
||||
TYPE_MARKER = ObjectGeneratorOracle.TYPE_MARKER
|
||||
|
||||
def generate(self) -> str: # pragma: no cover
|
||||
return ""
|
||||
|
||||
|
||||
def test_generate_empty() -> None:
|
||||
"""Test generating the type class list from an empty dictionary"""
|
||||
|
||||
assert generate_type_classes({}) == {} # pylint: disable=use-implicit-booleaness-not-comparison
|
||||
|
||||
|
||||
def test_nontype_not_present() -> None:
|
||||
"""Test that non-types don’t get included in the results"""
|
||||
|
||||
assert generate_type_classes({"test": True}) == {} # pylint: disable=use-implicit-booleaness-not-comparison
|
||||
|
||||
|
||||
def test_non_oracle_not_present() -> None:
|
||||
"""Test that non-oracle types don’t get included in the results"""
|
||||
|
||||
assert generate_type_classes({"test": dict}) == {} # pylint: disable=use-implicit-booleaness-not-comparison
|
||||
|
||||
|
||||
def test_oracle_not_present() -> None:
|
||||
"""Test that the ``Oracle`` class doesn’t get included in the results"""
|
||||
|
||||
assert generate_type_classes({"oracle": Oracle}) == {} # pylint: disable=use-implicit-booleaness-not-comparison
|
||||
|
||||
|
||||
def test_duplace_type_marker() -> None:
|
||||
"""Test if ``generate_type_classes`` raises an error if a type marker appears twice"""
|
||||
|
||||
with pytest.raises(KeyError) as ctx:
|
||||
generate_type_classes({"ObjectGeneratorOracle": ObjectGeneratorOracle, "TestOracle": _TestOracle})
|
||||
|
||||
assert str(ctx.value) == "'ObjectGeneratorOracle is already registered as a handler for object-generator'"
|
||||
Reference in New Issue
Block a user