Skip to main content

dndwright

A domain-neutral D&D 5e (2024) rules & character-sheet computation engine — formulas as data, not code.

PyPI Python versions CI License: MIT Typed

dndwright is a complete pure-Python D&D 5e toolkit: a rules engine (character sheet as a computation DAG), a dice engine, pure combat rules (HP, death saves, initiative, conditions), and bundled SRD content

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 at 0.x. Usable today — pin a version if you depend on it.

Install

pip install git+https://github.com/sligara7/dndwright.git
# or, for local development:
pip install -e ".[dev]"

Quickstart

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)

Command line

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)

Rolling dice

dndwright dice notation: 1d20+5, 4d6kh3, 2d6+1d8+3, advantage, reroll, exploding dice, and crit doubling — rolled into a typed frozen ExpressionResult

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())

Combat rules

dndwright combat as pure state transitions: a CombatantState moves between Healthy, Dying (0 HP, making death saves), Stable, and Dead, via apply_damage, roll_death_save, apply_healing and stabilize

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_percentage

Your app owns persistence: load a row → call these → write the new state back. The rules never see a database.

Why a computation graph?

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).

The dndwright computation graph: ability scores, level, class and equipment flow through ability modifiers and proficiency bonus to saves, skills, spell DC/attack, spell slots, HP, AC and initiative

Composable — snap mini-graphs onto the ruleset

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.

Components are lego-style mini-graphs that snap onto the dndwright ruleset: a Belt of Giant Strength sets the Strength score, a Ring of Protection adds to Armor Class, and Dwarven Resilience unions in poison resistance — compose() merges them and one snap-in recomputes the whole downstream subtree (strength modifier to athletics, saving throws and melee attack)

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 +1

Re-skin for any setting — theme scaling

The 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.

dndwright theme scaling: one computation graph re-skinned per setting. A ThemeScalingLayer applies input_overrides (re-baseline node defaults), lookup_overrides (merge tables like armor AC and weapon ranges) and flavor_renames (display labels only). The same plate armor node emerges as 'plate AC 18' in traditional D&D, 'tactical body armor AC 18' in modern warfare, 'power armor AC 19' in sci-fi, and 'clockwork full-plate AC 19' in steampunk

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)

What's inside

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.

API stability

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.

Credits & license

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.

Metadata

Release files for dndwright 0.28.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dndwright 0.28.0
File Size Uploaded
dndwright-0.28.0.tar.gz 417.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dndwright 0.28.0
File Interpreter ABI Platform
dndwright-0.28.0-py3-none-any.whl Python 3 none any Details

Total release size: 776.2 kB

Release files / dndwright-0.28.0.tar.gz

Download URL dndwright-0.28.0.tar.gz
Size 417.5 kB
Tags Source
SHA-256 checksum
How to use checksums
abe68631e3894c84e571ed89efec9f942123be9255cb91c3a6c0ebdfac9564b3
BLAKE2b-256 checksum
How to use checksums
347766606edc55fb7f6b4c528bd4ae99195c6286f4b9327dc5b311ebfc1eedaf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 16, 2026.

Transparency log

Release files / dndwright-0.28.0-py3-none-any.whl

Download URL dndwright-0.28.0-py3-none-any.whl
Size 358.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
62e32007c008b69f3b989b10b1d7b90a0a856c87a80dc9b98b85131a22349aa8
BLAKE2b-256 checksum
How to use checksums
9181db871e8fc431c00d84f9419514be7c50cd22672e5d7d41fc712de1d9886c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 16, 2026.

Transparency log
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page