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.
- {dndwright-0.2.0 → dndwright-0.4.0}/.github/workflows/ci.yml +1 -1
- {dndwright-0.2.0 → dndwright-0.4.0}/.github/workflows/publish.yml +2 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/.gitignore +4 -0
- dndwright-0.4.0/CHANGELOG.md +131 -0
- dndwright-0.4.0/PKG-INFO +158 -0
- dndwright-0.4.0/README.md +125 -0
- dndwright-0.4.0/assets/computation-graph.svg +83 -0
- dndwright-0.4.0/examples/README.md +19 -0
- dndwright-0.4.0/examples/custom_operation.py +34 -0
- dndwright-0.4.0/examples/dice.py +34 -0
- dndwright-0.4.0/examples/export_graph.py +21 -0
- dndwright-0.4.0/examples/multiclass.py +36 -0
- dndwright-0.4.0/examples/quickstart.py +28 -0
- dndwright-0.4.0/examples/stat_diff.py +25 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/pyproject.toml +14 -2
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/__init__.py +33 -2
- dndwright-0.4.0/src/dndwright/cli.py +153 -0
- dndwright-0.4.0/src/dndwright/dice/__init__.py +52 -0
- dndwright-0.4.0/src/dndwright/dice/engine.py +551 -0
- dndwright-0.4.0/src/dndwright/py.typed +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/__init__.py +18 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/character_evaluator.py +75 -1
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/evaluator.py +25 -3
- dndwright-0.4.0/src/dndwright/rules/export.py +147 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/operations.py +40 -1
- dndwright-0.4.0/src/dndwright/rules/validation.py +138 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/tests/test_api_contract.py +25 -0
- dndwright-0.4.0/tests/test_cli.py +104 -0
- dndwright-0.4.0/tests/test_dice.py +291 -0
- dndwright-0.4.0/tests/test_dice_api_contract.py +30 -0
- dndwright-0.4.0/tests/test_evaluator_cache.py +49 -0
- dndwright-0.4.0/tests/test_examples.py +18 -0
- dndwright-0.4.0/tests/test_export.py +113 -0
- dndwright-0.4.0/tests/test_input_validation.py +94 -0
- dndwright-0.4.0/tests/test_operations_registry.py +68 -0
- dndwright-0.4.0/tests/test_properties.py +51 -0
- dndwright-0.4.0/tests/test_validation.py +104 -0
- dndwright-0.2.0/CHANGELOG.md +0 -53
- dndwright-0.2.0/PKG-INFO +0 -101
- dndwright-0.2.0/README.md +0 -78
- {dndwright-0.2.0 → dndwright-0.4.0}/LICENSE +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/NOTICE +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/content/__init__.py +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/content/classes.json +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/content/creatures.json +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/content/generate.py +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/content/magic_items.json +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/content/species.json +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/ontology/__init__.py +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/ontology/dnd.yaml +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/ontology/loader.py +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/adapters.py +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/assembler.py +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/components.py +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/dnd_5e_2024.py +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/lookup_tables.py +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/schema.py +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/src/dndwright/rules/theme_scaling.py +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/tests/test_content.py +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/tests/test_engine.py +0 -0
- {dndwright-0.2.0 → dndwright-0.4.0}/tests/test_ontology.py +0 -0
|
@@ -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:
|
|
@@ -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
|
dndwright-0.4.0/PKG-INFO
ADDED
|
@@ -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&D 5e (2024) rules & 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&D 5e (2024) rules & 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&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 & 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)")
|