dndwright 0.2.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {dndwright-0.2.0 → dndwright-0.4.0}/.github/workflows/ci.yml +1 -1
  2. {dndwright-0.2.0 → dndwright-0.4.0}/.github/workflows/publish.yml +2 -0
  3. {dndwright-0.2.0 → dndwright-0.4.0}/.gitignore +4 -0
  4. dndwright-0.4.0/CHANGELOG.md +131 -0
  5. dndwright-0.4.0/PKG-INFO +158 -0
  6. dndwright-0.4.0/README.md +125 -0
  7. dndwright-0.4.0/assets/computation-graph.svg +83 -0
  8. dndwright-0.4.0/examples/README.md +19 -0
  9. dndwright-0.4.0/examples/custom_operation.py +34 -0
  10. dndwright-0.4.0/examples/dice.py +34 -0
  11. dndwright-0.4.0/examples/export_graph.py +21 -0
  12. dndwright-0.4.0/examples/multiclass.py +36 -0
  13. dndwright-0.4.0/examples/quickstart.py +28 -0
  14. dndwright-0.4.0/examples/stat_diff.py +25 -0
  15. {dndwright-0.2.0 → dndwright-0.4.0}/pyproject.toml +14 -2
  16. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/__init__.py +33 -2
  17. dndwright-0.4.0/src/dndwright/cli.py +153 -0
  18. dndwright-0.4.0/src/dndwright/dice/__init__.py +52 -0
  19. dndwright-0.4.0/src/dndwright/dice/engine.py +551 -0
  20. dndwright-0.4.0/src/dndwright/py.typed +0 -0
  21. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/__init__.py +18 -0
  22. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/character_evaluator.py +75 -1
  23. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/evaluator.py +25 -3
  24. dndwright-0.4.0/src/dndwright/rules/export.py +147 -0
  25. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/operations.py +40 -1
  26. dndwright-0.4.0/src/dndwright/rules/validation.py +138 -0
  27. {dndwright-0.2.0 → dndwright-0.4.0}/tests/test_api_contract.py +25 -0
  28. dndwright-0.4.0/tests/test_cli.py +104 -0
  29. dndwright-0.4.0/tests/test_dice.py +291 -0
  30. dndwright-0.4.0/tests/test_dice_api_contract.py +30 -0
  31. dndwright-0.4.0/tests/test_evaluator_cache.py +49 -0
  32. dndwright-0.4.0/tests/test_examples.py +18 -0
  33. dndwright-0.4.0/tests/test_export.py +113 -0
  34. dndwright-0.4.0/tests/test_input_validation.py +94 -0
  35. dndwright-0.4.0/tests/test_operations_registry.py +68 -0
  36. dndwright-0.4.0/tests/test_properties.py +51 -0
  37. dndwright-0.4.0/tests/test_validation.py +104 -0
  38. dndwright-0.2.0/CHANGELOG.md +0 -53
  39. dndwright-0.2.0/PKG-INFO +0 -101
  40. dndwright-0.2.0/README.md +0 -78
  41. {dndwright-0.2.0 → dndwright-0.4.0}/LICENSE +0 -0
  42. {dndwright-0.2.0 → dndwright-0.4.0}/NOTICE +0 -0
  43. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/content/__init__.py +0 -0
  44. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/content/classes.json +0 -0
  45. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/content/creatures.json +0 -0
  46. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/content/generate.py +0 -0
  47. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/content/magic_items.json +0 -0
  48. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/content/species.json +0 -0
  49. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/ontology/__init__.py +0 -0
  50. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/ontology/dnd.yaml +0 -0
  51. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/ontology/loader.py +0 -0
  52. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/adapters.py +0 -0
  53. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/assembler.py +0 -0
  54. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/components.py +0 -0
  55. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/dnd_5e_2024.py +0 -0
  56. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/lookup_tables.py +0 -0
  57. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/schema.py +0 -0
  58. {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/theme_scaling.py +0 -0
  59. {dndwright-0.2.0 → dndwright-0.4.0}/tests/test_content.py +0 -0
  60. {dndwright-0.2.0 → dndwright-0.4.0}/tests/test_engine.py +0 -0
  61. {dndwright-0.2.0 → dndwright-0.4.0}/tests/test_ontology.py +0 -0
@@ -22,6 +22,6 @@ jobs:
22
22
  python -m pip install --upgrade pip
23
23
  pip install -e ".[dev]"
24
24
  - name: Lint (ruff)
25
- run: ruff check src tests
25
+ run: ruff check src tests examples
26
26
  - name: Test (pytest)
27
27
  run: pytest -q
@@ -8,6 +8,7 @@ name: Publish to PyPI
8
8
  on:
9
9
  release:
10
10
  types: [published]
11
+ workflow_dispatch: # allow manual publish of the current main (version in pyproject)
11
12
 
12
13
  jobs:
13
14
  build:
@@ -32,6 +33,7 @@ jobs:
32
33
  environment: pypi # must match the "Environment name" in PyPI's publisher config
33
34
  permissions:
34
35
  id-token: write # REQUIRED for Trusted Publishing (OIDC)
36
+ contents: read
35
37
  steps:
36
38
  - uses: actions/download-artifact@v4
37
39
  with:
@@ -21,3 +21,7 @@ htmlcov/
21
21
  .vscode/
22
22
  .idea/
23
23
  .DS_Store
24
+
25
+ # Local planning / scratch notes (never committed)
26
+ *.local.md
27
+ *.local
@@ -0,0 +1,131 @@
1
+ # Changelog
2
+
3
+ All notable changes to dndwright are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/), and the project aims to follow
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ **Public API** = the names exported in `dndwright.__all__` (pinned by
8
+ `tests/test_api_contract.py`). While the version is `0.x`, minor versions may make
9
+ breaking changes; these will always be noted here.
10
+
11
+ ## [Unreleased]
12
+
13
+ ## [0.4.0] — 2026-06-01
14
+
15
+ ### Added
16
+ - **Dice engine** (`dndwright.dice`) — `DiceEngine`: parse and roll D&D 5e dice
17
+ expressions (`1d20+5`, `4d6kh3`, `2d6r1`, `1d6!`, advantage/disadvantage) plus
18
+ `roll_attack`/`roll_save`/`roll_check`/`roll_damage`/`roll_initiative`/`roll_stat_array`/
19
+ `roll_hit_dice`/`roll_death_save`. Returns a **typed, frozen result surface**
20
+ (`ExpressionResult`, `RollResult`, `AttackRoll`, `SaveRoll`, `DamageRoll`, `DeathSave`,
21
+ `StatArray`, `HitDiceResult`, …). Deterministic by default (`DiceEngine(seed=…)`); for
22
+ unpredictable production rolls inject any `random.Random` (e.g.
23
+ `DiceEngine(rng=secrets.SystemRandom())`) — no NumPy dependency. `DiceEngine` is also
24
+ re-exported at the top level.
25
+
26
+ ### Fixed
27
+ - **Dice engine hardening** — pathological groups can no longer hang the engine: rerolls
28
+ whose set covers every die face are skipped, exploding requires `sides > 1`, and both
29
+ loops are capped (`1d1!`, `1d2r1,2` now terminate). `reroll_once` is detected per dice
30
+ group instead of from the whole expression, so a later `ro` group can't flip an earlier
31
+ `r` group. Result value types are now genuinely immutable — sequence fields are tuples,
32
+ making every result hashable and usable as a set member / dict key.
33
+ - **CLI robustness** — `dndwright eval` now reports a clean error for non-object JSON
34
+ (was an uncaught `AttributeError`), and `dndwright validate` reports a clean error for
35
+ valid-JSON-but-invalid rulesets (was an uncaught pydantic `ValidationError`).
36
+ - **Graph export escaping** — `to_mermaid` now escapes label special characters
37
+ (`[](){}<>"#`) and emits subgraphs as `id["title"]`, so labels and group names with
38
+ spaces/punctuation no longer break the diagram. `to_dot` now escapes backslashes,
39
+ quotes, and newlines in labels and node ids.
40
+ - **Strict input validation** — `validate_character_data` now rejects non-integer float
41
+ ability scores (e.g. `15.7`; integral floats like `15.0` are fine), reports an omitted
42
+ `level` (previously masked by the default-to-1 normalization), and handles non-dict
43
+ input without crashing.
44
+
45
+ ### Added
46
+ - **Property-based tests** (hypothesis, dev-only) — invariants checked over wide input
47
+ ranges: ability-modifier and proficiency-bonus formulas, PB in its published 2..6 range,
48
+ and HP monotonic in level.
49
+ - **Examples** — runnable scripts under `examples/` (quickstart, multiclass, stat-diff,
50
+ custom operation, graph export), exercised in CI so they can't rot.
51
+ - **Input validation** — `validate_character_data(data) -> list[str]` reports problems
52
+ (missing/out-of-range ability scores, bad level, missing class) that would otherwise be
53
+ silently coerced into a plausible-but-wrong sheet. `evaluate_character(data, strict=True)`
54
+ raises `CharacterInputError` on those; default lenient behaviour is unchanged. The CLI
55
+ gains `dndwright eval --strict`.
56
+ - **Custom operations** — `register_operation(name, fn)` extends the formula DSL without
57
+ forking. Custom ops are recognised everywhere the registry is consulted (`evaluate`,
58
+ `validate_ruleset`, `known_operations`). Built-in op names cannot be overwritten; the
59
+ `Operation` callable type is exported for typing custom ops.
60
+ - **CLI** — a `dndwright` console command (stdlib only): `eval` (character JSON → sheet,
61
+ file or stdin), `graph` (export the DAG as Mermaid/DOT), `content` (list/dump bundled
62
+ content), `validate` (check a ruleset). Usable without writing Python.
63
+ - **Graph export** — `to_mermaid(ruleset)` and `to_dot(ruleset)` render the computation
64
+ DAG as Mermaid or Graphviz DOT (node shapes by type, optional clustering by group),
65
+ making the "formulas as data / inspectable DAG" design visible for docs and debugging.
66
+ - **Ruleset validation** — `validate_ruleset(ruleset) -> list[ValidationIssue]` and
67
+ `assert_valid_ruleset(ruleset)` statically check a ruleset before evaluation, catching
68
+ authoring mistakes (unknown op, key/`id` mismatch, missing formula, cycles) with clear
69
+ messages instead of a deep runtime `EvaluationError`, plus warnings (INPUT-with-formula,
70
+ unknown explicit input ref, absent lookup table). Also `known_operations()` lists every
71
+ op a formula may use, and `ValidationIssue` / `RulesetValidationError` are exported.
72
+
73
+ ### Changed
74
+ - **Faster evaluation** — the evaluator now caches each ruleset's topological order
75
+ instead of recomputing it on every `evaluate()` call (~2.2× faster per evaluation;
76
+ ~0.41 → ~0.18 ms for a level-5 character). Cache is keyed per ruleset instance and
77
+ evicted on garbage collection, so custom/transient rulesets neither leak nor go stale.
78
+
79
+ ### Docs
80
+ - Revamped the README landing page: centered header, status badges (PyPI version, Python
81
+ versions, CI, license, typed), and a hero SVG of the computation graph (`assets/`).
82
+ - Added a "Command line" section and `validate_ruleset` / `to_mermaid` / `to_dot` rows.
83
+
84
+ ### Packaging
85
+ - Ship a `py.typed` marker (PEP 561) so downstream type-checkers see dndwright's type
86
+ hints. Added Python 3.10–3.13, `Typing :: Typed`, `Intended Audience :: Developers`,
87
+ and `Topic :: Software Development :: Libraries` classifiers, plus `Changelog`/`Issues`
88
+ project URLs. Internal: `OPERATIONS` is now typed `dict[str, Operation]`.
89
+
90
+ ## [0.3.0] — 2026-06-01
91
+
92
+ ### Added
93
+ - **Bundled starter content** — `load_content(category)` + `categories()`: original
94
+ homebrew classes/species/creatures, plus 236 SRD 5.2 (CC-BY) magic items.
95
+ - **LLM-agnostic content generator** — `generate_library(llm, ...)` (and
96
+ `generate_classes`/`species`/`creatures`): you pass a `complete_json(prompt, system)
97
+ -> dict` callable wrapping your own LLM; prompts produce *original homebrew* (no
98
+ official content), matching the bundled schema and component ontology.
99
+
100
+ ## [0.2.0] — 2026-06-01
101
+
102
+ ### Added
103
+ - **Component ontology** — `load_ontology()` → `Ontology`: a graph schema for D&D
104
+ building blocks (Class, Species, Spell, Equipment, MagicItem, Background, Feat,
105
+ Subclass, Creature) and how a Character connects to them (`HAS_*`, `INSTANCE_OF`,
106
+ `HAS_STAT_BLOCK`). Typed models (`NodeTypeDef`, `EdgeTypeDef`, `PropertyDef`) with
107
+ `edges_from`/`edges_to` helpers, parsed from the bundled `dnd.yaml`.
108
+ - Dependency: `pyyaml` (for the ontology loader).
109
+
110
+ ## [0.1.0] — 2026-06-01
111
+
112
+ Initial release. The D&D 5e (2024) rules & character-computation engine, extracted
113
+ from a working application.
114
+
115
+ ### Added
116
+ - `evaluate_character(data) -> dict` — one-call evaluation: character data in,
117
+ computed sheet out (ability modifiers, proficiency, saves, spell DC/attack, HP,
118
+ AC, initiative, …).
119
+ - The computation engine: `DND_5E_2024_RULESET` (a 135-node DAG), `evaluate`,
120
+ `assemble_character_inputs`, `apply_modifiers`, and the `Ruleset` / `ComputationNode`
121
+ / `FormulaSpec` / `NodeType` schema — formulas as data, not code.
122
+ - Neutral adapters: `character_data_to_inputs`, `computed_values_to_sheet`.
123
+ - Typed component models under `dndwright.rules.components`
124
+ (`ClassMechanics`, `SpeciesMechanics`, …) and SRD-derived rules tables.
125
+
126
+ Pure (pydantic + stdlib); no application/framework coupling. Rules content derives
127
+ from the SRD 5.2 (CC-BY-4.0); see NOTICE.
128
+
129
+ [Unreleased]: https://github.com/sligara7/dndwright/compare/v0.4.0...HEAD
130
+ [0.4.0]: https://github.com/sligara7/dndwright/compare/v0.3.0...v0.4.0
131
+ [0.1.0]: https://github.com/sligara7/dndwright/releases/tag/v0.1.0
@@ -0,0 +1,158 @@
1
+ Metadata-Version: 2.4
2
+ Name: dndwright
3
+ Version: 0.4.0
4
+ Summary: Domain-neutral D&D 5e (2024) rules & character-sheet computation engine: a data-driven DAG of formulas (ability mods, proficiency, spell DC/slots, HP, AC).
5
+ Project-URL: Homepage, https://github.com/sligara7/dndwright
6
+ Project-URL: Repository, https://github.com/sligara7/dndwright
7
+ Project-URL: Changelog, https://github.com/sligara7/dndwright/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/sligara7/dndwright/issues
9
+ Author: Anthony Sligar
10
+ License: MIT
11
+ License-File: LICENSE
12
+ License-File: NOTICE
13
+ Keywords: character-sheet,computation-graph,dnd,dnd5e,rules-engine,srd,ttrpg
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Games/Entertainment :: Role-Playing
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: pydantic>=2.5
27
+ Requires-Dist: pyyaml>=6.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: hypothesis>=6.0; extra == 'dev'
30
+ Requires-Dist: pytest>=7.4; extra == 'dev'
31
+ Requires-Dist: ruff>=0.1; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ <h1 align="center">dndwright</h1>
35
+
36
+ <p align="center">
37
+ <em>A domain-neutral D&amp;D 5e (2024) rules &amp; character-sheet computation engine —
38
+ formulas as data, not code.</em>
39
+ </p>
40
+
41
+ <p align="center">
42
+ <a href="https://pypi.org/project/dndwright/"><img alt="PyPI" src="https://img.shields.io/pypi/v/dndwright.svg"></a>
43
+ <a href="https://pypi.org/project/dndwright/"><img alt="Python versions" src="https://img.shields.io/pypi/pyversions/dndwright.svg"></a>
44
+ <a href="https://github.com/sligara7/dndwright/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/sligara7/dndwright/actions/workflows/ci.yml/badge.svg"></a>
45
+ <a href="https://github.com/sligara7/dndwright/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg"></a>
46
+ <img alt="Typed" src="https://img.shields.io/badge/typing-PEP%20561-blue.svg">
47
+ </p>
48
+
49
+ <p align="center">
50
+ <img alt="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" width="760" src="https://raw.githubusercontent.com/sligara7/dndwright/main/assets/computation-graph.svg">
51
+ </p>
52
+
53
+ A character sheet is modelled as a **directed acyclic computation graph** — nodes are values,
54
+ edges are dependencies, and formulas are *data* (a JSON-serialisable DSL), not code. Pure
55
+ Python (`pydantic` + stdlib), no application or framework coupling: map your own character
56
+ data in, read computed stats out.
57
+
58
+ > ⚠️ **Early development (alpha).** The API is still moving and may change between minor
59
+ > versions while at `0.x`. Usable today — pin a version if you depend on it.
60
+
61
+ ## Install
62
+
63
+ ```bash
64
+ pip install git+https://github.com/sligara7/dndwright.git
65
+ # or, for local development:
66
+ pip install -e ".[dev]"
67
+ ```
68
+
69
+ ## Quickstart
70
+
71
+ ```python
72
+ from dndwright import evaluate_character
73
+
74
+ sheet = evaluate_character({
75
+ "ability_scores": {"strength": 8, "dexterity": 14, "constitution": 14,
76
+ "intelligence": 18, "wisdom": 12, "charisma": 10},
77
+ "class_data": {"class_name": "wizard"},
78
+ "species_data": {"name": "Human", "speed": 30},
79
+ "level": 5,
80
+ })
81
+
82
+ sheet["proficiency_bonus"] # 3
83
+ sheet["ability_modifiers"] # {"intelligence": 4, "dexterity": 2, ...}
84
+ sheet["spellcasting_type"] # "full_caster"
85
+ # ...plus armor_class, hit_points, hit_dice, initiative, saves, features, ...
86
+ ```
87
+
88
+ Lower level — assemble typed inputs and evaluate against the ruleset:
89
+
90
+ ```python
91
+ from dndwright import DND_5E_2024_RULESET, assemble_character_inputs, evaluate, apply_modifiers
92
+ from dndwright.rules.components import ClassMechanics
93
+
94
+ inputs = assemble_character_inputs(class_mechanics=..., ability_scores={...}, level=5)
95
+ computed = apply_modifiers(evaluate(DND_5E_2024_RULESET, inputs), inputs)
96
+ ```
97
+
98
+ ## Command line
99
+
100
+ Installing the package also installs a `dndwright` command (no Python required):
101
+
102
+ ```bash
103
+ dndwright eval character.json # character JSON → computed sheet (or '-' for stdin)
104
+ dndwright graph --format mermaid # export the computation DAG (mermaid|dot)
105
+ dndwright content magic_items # dump bundled content (omit category to list)
106
+ dndwright validate ruleset.json # check a ruleset (built-in if omitted)
107
+ ```
108
+
109
+ ## Rolling dice
110
+
111
+ A self-contained, typed dice engine (`dndwright.dice`) — deterministic by default:
112
+
113
+ ```python
114
+ from dndwright.dice import DiceEngine
115
+
116
+ eng = DiceEngine(seed=42) # reproducible (stdlib RNG)
117
+ eng.roll("4d6kh3").total # keep highest 3 of 4
118
+ eng.roll("1d20", advantage=True) # -> ExpressionResult
119
+ eng.roll_attack(modifier=5, target_ac=15).is_hit
120
+ eng.roll_damage("2d8", is_critical=True) # crit doubles the dice
121
+
122
+ # unpredictable production rolls (no NumPy dependency):
123
+ import secrets
124
+ DiceEngine(rng=secrets.SystemRandom())
125
+ ```
126
+
127
+ ## Why a computation graph?
128
+
129
+ Derived character values form a dependency DAG: ability scores → modifiers → proficiency →
130
+ save DCs / spell slots / AC / HP. dndwright represents that DAG explicitly and stores the
131
+ formulas as **data** (`FormulaSpec`: an op + args), so the rules are inspectable, testable,
132
+ and serialisable — not buried in imperative code. `DND_5E_2024_RULESET` is a 135-node graph.
133
+
134
+ ## What's inside
135
+
136
+ | Component | What it does |
137
+ |-----------|--------------|
138
+ | `evaluate_character` | One call: character data dict → fully computed sheet. |
139
+ | `DND_5E_2024_RULESET` | The 135-node 5e-2024 computation DAG (formulas as data). |
140
+ | `evaluate` / `assemble_character_inputs` / `apply_modifiers` | The lower-level engine. |
141
+ | `Ruleset` / `ComputationNode` / `FormulaSpec` / `NodeType` | The DAG schema. |
142
+ | `validate_ruleset` / `assert_valid_ruleset` | Static integrity check for a ruleset (unknown ops, cycles, dangling refs) — catch authoring errors before evaluation. |
143
+ | `to_mermaid` / `to_dot` | Render the computation DAG as Mermaid or Graphviz DOT — *see* the dependency graph. |
144
+ | `dndwright.dice` | Typed dice engine: parse/roll 5e expressions, attacks, saves, damage, stat arrays. |
145
+ | `dndwright.rules.components` | Typed inputs (`ClassMechanics`, `SpeciesMechanics`, …). |
146
+ | `dndwright.rules.lookup_tables` | SRD-derived rules tables (hit dice, spell slots, AC, saves). |
147
+
148
+ ## API stability
149
+
150
+ The public API is exactly `dndwright.__all__`, pinned by `tests/test_api_contract.py`.
151
+ Versioning follows [SemVer](https://semver.org/); at `0.x` minor versions may break, with
152
+ every change recorded in `CHANGELOG.md`.
153
+
154
+ ## Credits & license
155
+
156
+ MIT licensed (see `LICENSE`). The rules tables encode game *mechanics* derived from the
157
+ **D&D System Reference Document 5.2** (© Wizards of the Coast, **CC-BY-4.0**); see `NOTICE`.
158
+ Not affiliated with or endorsed by Wizards of the Coast. Contains no PHB/DMG/MM content.
@@ -0,0 +1,125 @@
1
+ <h1 align="center">dndwright</h1>
2
+
3
+ <p align="center">
4
+ <em>A domain-neutral D&amp;D 5e (2024) rules &amp; character-sheet computation engine —
5
+ formulas as data, not code.</em>
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://pypi.org/project/dndwright/"><img alt="PyPI" src="https://img.shields.io/pypi/v/dndwright.svg"></a>
10
+ <a href="https://pypi.org/project/dndwright/"><img alt="Python versions" src="https://img.shields.io/pypi/pyversions/dndwright.svg"></a>
11
+ <a href="https://github.com/sligara7/dndwright/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/sligara7/dndwright/actions/workflows/ci.yml/badge.svg"></a>
12
+ <a href="https://github.com/sligara7/dndwright/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg"></a>
13
+ <img alt="Typed" src="https://img.shields.io/badge/typing-PEP%20561-blue.svg">
14
+ </p>
15
+
16
+ <p align="center">
17
+ <img alt="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" width="760" src="https://raw.githubusercontent.com/sligara7/dndwright/main/assets/computation-graph.svg">
18
+ </p>
19
+
20
+ A character sheet is modelled as a **directed acyclic computation graph** — nodes are values,
21
+ edges are dependencies, and formulas are *data* (a JSON-serialisable DSL), not code. Pure
22
+ Python (`pydantic` + stdlib), no application or framework coupling: map your own character
23
+ data in, read computed stats out.
24
+
25
+ > ⚠️ **Early development (alpha).** The API is still moving and may change between minor
26
+ > versions while at `0.x`. Usable today — pin a version if you depend on it.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ pip install git+https://github.com/sligara7/dndwright.git
32
+ # or, for local development:
33
+ pip install -e ".[dev]"
34
+ ```
35
+
36
+ ## Quickstart
37
+
38
+ ```python
39
+ from dndwright import evaluate_character
40
+
41
+ sheet = evaluate_character({
42
+ "ability_scores": {"strength": 8, "dexterity": 14, "constitution": 14,
43
+ "intelligence": 18, "wisdom": 12, "charisma": 10},
44
+ "class_data": {"class_name": "wizard"},
45
+ "species_data": {"name": "Human", "speed": 30},
46
+ "level": 5,
47
+ })
48
+
49
+ sheet["proficiency_bonus"] # 3
50
+ sheet["ability_modifiers"] # {"intelligence": 4, "dexterity": 2, ...}
51
+ sheet["spellcasting_type"] # "full_caster"
52
+ # ...plus armor_class, hit_points, hit_dice, initiative, saves, features, ...
53
+ ```
54
+
55
+ Lower level — assemble typed inputs and evaluate against the ruleset:
56
+
57
+ ```python
58
+ from dndwright import DND_5E_2024_RULESET, assemble_character_inputs, evaluate, apply_modifiers
59
+ from dndwright.rules.components import ClassMechanics
60
+
61
+ inputs = assemble_character_inputs(class_mechanics=..., ability_scores={...}, level=5)
62
+ computed = apply_modifiers(evaluate(DND_5E_2024_RULESET, inputs), inputs)
63
+ ```
64
+
65
+ ## Command line
66
+
67
+ Installing the package also installs a `dndwright` command (no Python required):
68
+
69
+ ```bash
70
+ dndwright eval character.json # character JSON → computed sheet (or '-' for stdin)
71
+ dndwright graph --format mermaid # export the computation DAG (mermaid|dot)
72
+ dndwright content magic_items # dump bundled content (omit category to list)
73
+ dndwright validate ruleset.json # check a ruleset (built-in if omitted)
74
+ ```
75
+
76
+ ## Rolling dice
77
+
78
+ A self-contained, typed dice engine (`dndwright.dice`) — deterministic by default:
79
+
80
+ ```python
81
+ from dndwright.dice import DiceEngine
82
+
83
+ eng = DiceEngine(seed=42) # reproducible (stdlib RNG)
84
+ eng.roll("4d6kh3").total # keep highest 3 of 4
85
+ eng.roll("1d20", advantage=True) # -> ExpressionResult
86
+ eng.roll_attack(modifier=5, target_ac=15).is_hit
87
+ eng.roll_damage("2d8", is_critical=True) # crit doubles the dice
88
+
89
+ # unpredictable production rolls (no NumPy dependency):
90
+ import secrets
91
+ DiceEngine(rng=secrets.SystemRandom())
92
+ ```
93
+
94
+ ## Why a computation graph?
95
+
96
+ Derived character values form a dependency DAG: ability scores → modifiers → proficiency →
97
+ save DCs / spell slots / AC / HP. dndwright represents that DAG explicitly and stores the
98
+ formulas as **data** (`FormulaSpec`: an op + args), so the rules are inspectable, testable,
99
+ and serialisable — not buried in imperative code. `DND_5E_2024_RULESET` is a 135-node graph.
100
+
101
+ ## What's inside
102
+
103
+ | Component | What it does |
104
+ |-----------|--------------|
105
+ | `evaluate_character` | One call: character data dict → fully computed sheet. |
106
+ | `DND_5E_2024_RULESET` | The 135-node 5e-2024 computation DAG (formulas as data). |
107
+ | `evaluate` / `assemble_character_inputs` / `apply_modifiers` | The lower-level engine. |
108
+ | `Ruleset` / `ComputationNode` / `FormulaSpec` / `NodeType` | The DAG schema. |
109
+ | `validate_ruleset` / `assert_valid_ruleset` | Static integrity check for a ruleset (unknown ops, cycles, dangling refs) — catch authoring errors before evaluation. |
110
+ | `to_mermaid` / `to_dot` | Render the computation DAG as Mermaid or Graphviz DOT — *see* the dependency graph. |
111
+ | `dndwright.dice` | Typed dice engine: parse/roll 5e expressions, attacks, saves, damage, stat arrays. |
112
+ | `dndwright.rules.components` | Typed inputs (`ClassMechanics`, `SpeciesMechanics`, …). |
113
+ | `dndwright.rules.lookup_tables` | SRD-derived rules tables (hit dice, spell slots, AC, saves). |
114
+
115
+ ## API stability
116
+
117
+ The public API is exactly `dndwright.__all__`, pinned by `tests/test_api_contract.py`.
118
+ Versioning follows [SemVer](https://semver.org/); at `0.x` minor versions may break, with
119
+ every change recorded in `CHANGELOG.md`.
120
+
121
+ ## Credits & license
122
+
123
+ MIT licensed (see `LICENSE`). The rules tables encode game *mechanics* derived from the
124
+ **D&D System Reference Document 5.2** (© Wizards of the Coast, **CC-BY-4.0**); see `NOTICE`.
125
+ Not affiliated with or endorsed by Wizards of the Coast. Contains no PHB/DMG/MM content.
@@ -0,0 +1,83 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 470" font-family="ui-sans-serif, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
4
+ <stop offset="0" stop-color="#0f172a"/>
5
+ <stop offset="1" stop-color="#111827"/>
6
+ </linearGradient>
7
+ <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
8
+ <path d="M0 0 L10 5 L0 10 z" fill="#64748b"/>
9
+ </marker>
10
+ <style>
11
+ .lbl { fill:#e2e8f0; font-size:13px; }
12
+ .node { stroke-width:1.5; rx:9; }
13
+ .input { fill:#1e293b; stroke:#f59e0b; }
14
+ .derive { fill:#1e293b; stroke:#2dd4bf; }
15
+ .output { fill:#1e293b; stroke:#a78bfa; }
16
+ .layer { fill:#94a3b8; font-size:12px; letter-spacing:.14em; text-transform:uppercase; }
17
+ .edge { stroke:#475569; stroke-width:1.5; fill:none; }
18
+ </style>
19
+ </defs>
20
+
21
+ <rect x="0" y="0" width="900" height="470" rx="16" fill="url(#bg)"/>
22
+
23
+ <text x="36" y="46" fill="#f8fafc" font-size="26" font-weight="700">dndwright</text>
24
+ <text x="36" y="72" fill="#94a3b8" font-size="15">A D&amp;D 5e character sheet as a directed acyclic computation graph — formulas as data.</text>
25
+
26
+ <!-- layer captions -->
27
+ <text class="layer" x="120" y="118" text-anchor="middle">Inputs</text>
28
+ <text class="layer" x="450" y="118" text-anchor="middle">Derived</text>
29
+ <text class="layer" x="780" y="118" text-anchor="middle">Computed outputs</text>
30
+
31
+ <!-- edges (drawn first, behind nodes) -->
32
+ <!-- inputs -> derived -->
33
+ <path class="edge" marker-end="url(#arrow)" d="M210 165 C300 165 300 190 360 190"/>
34
+ <path class="edge" marker-end="url(#arrow)" d="M210 225 C300 225 300 260 360 260"/>
35
+ <path class="edge" marker-end="url(#arrow)" d="M210 285 C300 285 300 268 360 264"/>
36
+ <!-- derived -> outputs -->
37
+ <path class="edge" marker-end="url(#arrow)" d="M540 190 C620 190 620 165 690 165"/>
38
+ <path class="edge" marker-end="url(#arrow)" d="M540 190 C620 190 620 215 690 215"/>
39
+ <path class="edge" marker-end="url(#arrow)" d="M540 190 C620 190 620 265 690 265"/>
40
+ <path class="edge" marker-end="url(#arrow)" d="M540 260 C620 260 620 315 690 315"/>
41
+ <path class="edge" marker-end="url(#arrow)" d="M540 260 C620 260 620 165 690 165"/>
42
+ <!-- level/class/equipment -> outputs directly -->
43
+ <path class="edge" marker-end="url(#arrow)" d="M210 285 C440 360 470 365 690 365"/>
44
+ <path class="edge" marker-end="url(#arrow)" d="M210 345 C440 415 470 415 690 415"/>
45
+
46
+ <!-- input nodes -->
47
+ <g>
48
+ <rect rx="9" class="node input" x="40" y="148" width="170" height="34"/>
49
+ <text class="lbl" x="56" y="170">Ability scores</text>
50
+ <rect rx="9" class="node input" x="40" y="208" width="170" height="34"/>
51
+ <text class="lbl" x="56" y="230">Level</text>
52
+ <rect rx="9" class="node input" x="40" y="268" width="170" height="34"/>
53
+ <text class="lbl" x="56" y="290">Class</text>
54
+ <rect rx="9" class="node input" x="40" y="328" width="170" height="34"/>
55
+ <text class="lbl" x="56" y="350">Equipment</text>
56
+ </g>
57
+
58
+ <!-- derived nodes -->
59
+ <g>
60
+ <rect rx="9" class="node derive" x="360" y="173" width="180" height="34"/>
61
+ <text class="lbl" x="376" y="195">Ability modifiers</text>
62
+ <rect rx="9" class="node derive" x="360" y="243" width="180" height="34"/>
63
+ <text class="lbl" x="376" y="265">Proficiency bonus</text>
64
+ </g>
65
+
66
+ <!-- output nodes -->
67
+ <g>
68
+ <rect rx="9" class="node output" x="690" y="148" width="180" height="34"/>
69
+ <text class="lbl" x="706" y="170">Saves &amp; skills</text>
70
+ <rect rx="9" class="node output" x="690" y="198" width="180" height="34"/>
71
+ <text class="lbl" x="706" y="220">Spell DC / attack</text>
72
+ <rect rx="9" class="node output" x="690" y="248" width="180" height="34"/>
73
+ <text class="lbl" x="706" y="270">Spell slots</text>
74
+ <rect rx="9" class="node output" x="690" y="298" width="180" height="34"/>
75
+ <text class="lbl" x="706" y="320">Hit points</text>
76
+ <rect rx="9" class="node output" x="690" y="348" width="180" height="34"/>
77
+ <text class="lbl" x="706" y="370">Armor class</text>
78
+ <rect rx="9" class="node output" x="690" y="398" width="180" height="34"/>
79
+ <text class="lbl" x="706" y="420">Initiative</text>
80
+ </g>
81
+
82
+ <text x="36" y="454" fill="#64748b" font-size="12">135-node ruleset · pure Python (pydantic + stdlib) · inspectable, testable, serialisable</text>
83
+ </svg>
@@ -0,0 +1,19 @@
1
+ # Examples
2
+
3
+ Runnable scripts for the main dndwright workflows. From the repo root:
4
+
5
+ ```bash
6
+ pip install -e .
7
+ python examples/quickstart.py
8
+ ```
9
+
10
+ | Script | Shows |
11
+ |--------|-------|
12
+ | [`quickstart.py`](quickstart.py) | One call: character dict → computed sheet (plus `strict=True`). |
13
+ | [`multiclass.py`](multiclass.py) | Fighter 5 / Wizard 3 via the typed lower-level engine. |
14
+ | [`stat_diff.py`](stat_diff.py) | Which key stats change on level-up (`compute_stat_diff`). |
15
+ | [`custom_operation.py`](custom_operation.py) | Extend the DSL with `register_operation` + a custom `Ruleset`. |
16
+ | [`export_graph.py`](export_graph.py) | Render the computation DAG as Mermaid / Graphviz DOT. |
17
+ | [`dice.py`](dice.py) | Roll 5e dice (expressions, advantage, attacks/saves, crit damage, stat arrays). |
18
+
19
+ See also the `dndwright` CLI (`dndwright eval`, `graph`, `content`, `validate`).
@@ -0,0 +1,34 @@
1
+ """Extend the formula DSL with your own operation, then build a tiny custom ruleset.
2
+
3
+ Shows ``register_operation`` + authoring a ``Ruleset`` from nodes + ``validate_ruleset``.
4
+ Everything that consults the registry (evaluate, validation, known_operations) picks up
5
+ the new op automatically.
6
+
7
+ python examples/custom_operation.py
8
+ """
9
+
10
+ from dndwright import (
11
+ ComputationNode,
12
+ FormulaSpec,
13
+ NodeType,
14
+ Ruleset,
15
+ assert_valid_ruleset,
16
+ evaluate,
17
+ register_operation,
18
+ )
19
+
20
+ # A pure (args, tables) -> value function — same shape as the built-ins.
21
+ register_operation("average", lambda args, _tables: sum(args) / len(args))
22
+
23
+ ruleset = Ruleset(
24
+ id="demo", name="Average demo",
25
+ nodes={
26
+ "a": ComputationNode(id="a", node_type=NodeType.INPUT, label="A"),
27
+ "b": ComputationNode(id="b", node_type=NodeType.INPUT, label="B"),
28
+ "mean": ComputationNode(id="mean", node_type=NodeType.FORMULA, label="Mean",
29
+ formula=FormulaSpec(op="average", args=["a", "b"])),
30
+ },
31
+ )
32
+
33
+ assert_valid_ruleset(ruleset) # raises if the graph is broken
34
+ print("mean of 4 and 8 =", evaluate(ruleset, {"a": 4, "b": 8})["mean"]) # 6.0
@@ -0,0 +1,34 @@
1
+ """Roll D&D 5e dice with a typed, reproducible engine.
2
+
3
+ python examples/dice.py
4
+
5
+ For unpredictable production rolls, inject OS entropy instead of seeding:
6
+ import secrets; DiceEngine(rng=secrets.SystemRandom())
7
+ """
8
+
9
+ from dndwright.dice import DiceEngine
10
+
11
+ eng = DiceEngine(seed=42) # reproducible — same seed, same rolls
12
+
13
+ # Basic expressions → ExpressionResult
14
+ print("2d6+3 =", eng.roll("2d6+3").total)
15
+ print("4d6kh3 =", eng.roll("4d6kh3").total, "(keep highest 3 of 4)")
16
+
17
+ # Advantage on a single d20
18
+ adv = eng.roll("1d20", advantage=True)
19
+ print(f"1d20 adv = {adv.total} (rolled {adv.advantage_data.roll1} & "
20
+ f"{adv.advantage_data.roll2}, kept {adv.advantage_data.chosen})")
21
+
22
+ # Attack vs AC, save vs DC
23
+ atk = eng.roll_attack(modifier=5, target_ac=15)
24
+ print(f"attack +5 vs AC 15 = {atk.roll.total} → {'HIT' if atk.is_hit else 'miss'}")
25
+ save = eng.roll_save(modifier=3, dc=13)
26
+ print(f"save +3 vs DC 13 = {save.roll.total} → {'pass' if save.is_success else 'fail'}")
27
+
28
+ # Critical damage doubles the dice
29
+ crit = eng.roll_damage("2d8", is_critical=True)
30
+ print(f"crit 2d8 = {crit.roll.total} (rolled as {crit.roll.dice_results[0].dice_group})")
31
+
32
+ # A full ability-score array
33
+ array = eng.roll_stat_array("4d6kh3")
34
+ print("stat array =", list(array.scores))
@@ -0,0 +1,21 @@
1
+ """Render the 135-node computation DAG as Mermaid or Graphviz DOT.
2
+
3
+ python examples/export_graph.py # Mermaid to stdout
4
+ python examples/export_graph.py | dot -Tsvg # (with --dot) → SVG via Graphviz
5
+
6
+ Or use the CLI: ``dndwright graph --format dot``.
7
+ """
8
+
9
+ import sys
10
+
11
+ from dndwright import DND_5E_2024_RULESET, to_dot, to_mermaid
12
+
13
+ if "--dot" in sys.argv:
14
+ print(to_dot(DND_5E_2024_RULESET))
15
+ else:
16
+ # Just the first lines — the full graph is large (135 nodes).
17
+ text = to_mermaid(DND_5E_2024_RULESET)
18
+ print("\n".join(text.splitlines()[:12]))
19
+ print("...")
20
+ print(f"\n({len(DND_5E_2024_RULESET.nodes)} nodes total — "
21
+ "pipe `dndwright graph --format dot | dot -Tsvg` for the full picture)")