A domain-neutral D&D 5e (2024) rules & character-sheet computation engine — formulas as data, not code.
No further versions of
dndwrightwill be published to PyPI. This library is being absorbed into the StoryFlow application that consumes it; it will stop being an independently released package.What this does and does not mean. The versions already on PyPI (through v0.29.0) remain published and remain MIT-licensed. MIT is irrevocable for what has already shipped — if you depend on this package it keeps working, and you are free to fork it. Nothing is being withdrawn or yanked. What ends is new releases.
The PyPI publish workflow has been removed from this repository accordingly.
A character sheet is modelled as a directed acyclic computation graph — nodes are values,
edges are dependencies, and formulas are data (a JSON-serialisable DSL), not code. Pure
Python (pydantic + stdlib), no application or framework coupling: map your own character
data in, read computed stats out.
⚠️ Early development (alpha). The API is still moving and may change between minor versions while at0.x. Usable today — pin a version if you depend on it.
pip install git+https://github.com/sligara7/dndwright.git
# or, for local development:
pip install -e ".[dev]"from dndwright import evaluate_character
sheet = evaluate_character({
"ability_scores": {"strength": 8, "dexterity": 14, "constitution": 14,
"intelligence": 18, "wisdom": 12, "charisma": 10},
"class_data": {"class_name": "wizard"},
"species_data": {"name": "Human", "speed": 30},
"level": 5,
})
sheet["proficiency_bonus"] # 3
sheet["ability_modifiers"] # {"intelligence": 4, "dexterity": 2, ...}
sheet["spellcasting_type"] # "full_caster"
# ...plus armor_class, hit_points, hit_dice, initiative, saves, features, ...Lower level — assemble typed inputs and evaluate against the ruleset:
from dndwright import DND_5E_2024_RULESET, assemble_character_inputs, evaluate, apply_modifiers
from dndwright.rules.components import ClassMechanics
inputs = assemble_character_inputs(class_mechanics=..., ability_scores={...}, level=5)
computed = apply_modifiers(evaluate(DND_5E_2024_RULESET, inputs), inputs)Installing the package also installs a dndwright command (no Python required):
dndwright eval character.json # character JSON → computed sheet (or '-' for stdin)
dndwright graph --format mermaid # export the computation DAG (mermaid|dot)
dndwright content magic_items # dump bundled content (omit category to list)
dndwright validate ruleset.json # check a ruleset (built-in if omitted)A self-contained, typed dice engine (dndwright.dice) — deterministic by default:
from dndwright.dice import DiceEngine
eng = DiceEngine(seed=42) # reproducible (stdlib RNG)
eng.roll("4d6kh3").total # keep highest 3 of 4
eng.roll("1d20", advantage=True) # -> ExpressionResult
eng.roll_attack(modifier=5, target_ac=15).is_hit
eng.roll_damage("2d8", is_critical=True) # crit doubles the dice
# unpredictable production rolls (no NumPy dependency):
import secrets
DiceEngine(rng=secrets.SystemRandom())Pure, persistence-free 5e combat (dndwright.combat) — state is a frozen value object,
every op is (state, input) → (new_state, explanation):
from dndwright.combat import CombatantState, apply_damage, roll_death_save
from dndwright.dice import DiceEngine
s = CombatantState(current_hp=8, max_hp=20, temp_hp=3)
s, applied = apply_damage(s, 10) # temp HP absorbs first, overkill tracked
s, save = roll_death_save(s, DiceEngine(seed=1)) # nat 20 → 1 HP; 3 fails → dead
s.is_stable, s.is_dead, s.hp_percentageYour app owns persistence: load a row → call these → write the new state back. The rules never see a database.
Derived character values form a dependency DAG: ability scores → modifiers → proficiency →
save DCs / spell slots / AC / HP. dndwright represents that DAG explicitly and stores the
formulas as data (FormulaSpec: an op + args), so the rules are inspectable, testable,
and serialisable — not buried in imperative code. DND_5E_2024_RULESET is a 138-node graph (incl. damage-defence channels).
Items, feats and species traits are themselves tiny graphs. compose() merges a
Component's nodes and contributions onto a base ruleset and returns a new, larger
Ruleset — the base is never mutated. Because each contribution keeps its target node's
id, every existing edge downstream re-derives for free: a set/add/union on one
node ripples out to every modifier, save, skill and attack that depends on it.
from dndwright import DND_5E_2024_RULESET, compose, modifier
ring = modifier("ring_of_protection", target="armor_class", amount=1)
rs = compose(DND_5E_2024_RULESET, ring) # base untouched; AC now aggregates the +1The same engine runs sci-fi, modern-warfare, steampunk or cosmic-horror. A ThemeScalingLayer
folds three kinds of override onto a ruleset via apply_theme_scaling() (pure, like compose):
input_overrides re-baseline a node's default value, lookup_overrides deep-merge into the
lookup tables (so plate armour can read AC 19 instead of 18), and flavor_renames relabel
terms for display without ever changing a computed value. The graph's shape never changes —
only its numbers and names.
from dndwright import DND_5E_2024_RULESET, apply_theme_scaling, get_theme_scaling
rs = apply_theme_scaling(DND_5E_2024_RULESET, get_theme_scaling("sci_fi"))
rs.lookup_tables["armor_base_ac"]["plate"] # 19 (base is still 18, untouched)| Component | What it does |
|---|---|
evaluate_character |
One call: character data dict → fully computed sheet. |
DND_5E_2024_RULESET |
The 138-node 5e-2024 computation DAG (formulas as data). |
evaluate / assemble_character_inputs / apply_modifiers |
The lower-level engine. |
Ruleset / ComputationNode / FormulaSpec / NodeType |
The DAG schema. |
validate_ruleset / assert_valid_ruleset |
Static integrity check for a ruleset (unknown ops, cycles, dangling refs) — catch authoring errors before evaluation. |
validate_class_homebrew / validate_species_homebrew / validate_subclass_homebrew / validate_background_homebrew / validate_homebrew |
Validate homebrew class/species/subclass/background data against SRD 5.2.1 structural rules (hit die, save profs, archetype, feature levels, skill counts, speed limits). Returns list[str] of violations — empty = legal. |
compose / modifier / Component |
Snap mini-graphs (items/feats/traits) onto a ruleset; downstream values cascade. |
component_from_content |
Build a Component from a bundled item/feat's component field — magic items & feats as data that snap onto a character (constant, dynamic, player-chosen, or conditional effects). |
apply_theme_scaling / ThemeScalingLayer / get_theme_scaling |
Re-skin the ruleset for any setting (sci-fi, modern, steampunk, …): override node defaults & lookup tables and re-flavor names, same graph shape. PREDEFINED_THEME_SCALING ships ready-made themes. |
to_mermaid / to_dot |
Render the computation DAG as Mermaid or Graphviz DOT — see the dependency graph. |
dndwright.dice |
Typed dice engine: parse/roll 5e expressions, attacks, saves, damage, stat arrays. |
dndwright.combat |
Pure combat rules over a frozen CombatantState: damage, temp HP, healing, death saves. |
dndwright.combat.initiative |
Pure initiative: roll, order (DEX tie-break), advance/rewind turns. |
dndwright.combat.conditions |
Pure conditions over the bundled SRD catalog: effects, ticking, saves. |
dndwright.rules.components |
Typed inputs (ClassMechanics, SpeciesMechanics, …). |
dndwright.rules.lookup_tables |
SRD-derived rules tables (hit dice, spell slots, AC, saves). |
load_content("feats") / load_content("magic_items") |
Bundled SRD feats & magic items as data — many carry a composable component. |
The public API is exactly dndwright.__all__, pinned by tests/test_api_contract.py.
Versioning follows SemVer; at 0.x minor versions may break, with
every change recorded in CHANGELOG.md. Maintainers: the release process is documented in
RELEASING.md.
MIT licensed (see LICENSE). The bundled content and rules tables encode game mechanics
derived from the D&D System Reference Document 5.2.1 (English, published May 1, 2025;
© Wizards of the Coast, CC-BY-4.0) — source PDF:
SRD_CC_v5.2.1.pdf.
See NOTICE. Not affiliated with or endorsed by Wizards of the Coast. Contains no PHB/DMG/MM content.