dndwright 0.25.2__tar.gz → 0.26.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.25.2 → dndwright-0.26.0}/PKG-INFO +1 -1
- {dndwright-0.25.2 → dndwright-0.26.0}/pyproject.toml +1 -1
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/__init__.py +1 -1
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/adapters.py +0 -1
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/character_evaluator.py +11 -2
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/homebrew_validator.py +1 -1
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_api_contract.py +2 -1
- dndwright-0.26.0/tests/test_evaluate_character_components.py +69 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_homebrew_validator.py +0 -1
- {dndwright-0.25.2 → dndwright-0.26.0}/.github/workflows/ci.yml +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/.github/workflows/publish.yml +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/.gitignore +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/CHANGELOG.md +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/LICENSE +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/NOTICE +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/README.md +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/RELEASING.md +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/assets/combat-flow.svg +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/assets/compose.svg +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/assets/computation-graph.svg +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/assets/dice.svg +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/assets/overview.svg +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/assets/theme-scaling.svg +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/examples/README.md +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/examples/combat.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/examples/compose.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/examples/conditions.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/examples/content_components.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/examples/custom_operation.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/examples/dice.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/examples/export_graph.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/examples/feat_components.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/examples/gated_contributions.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/examples/initiative.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/examples/multiclass.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/examples/quickstart.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/examples/stat_diff.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/examples/weapon_attack.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/cli.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/combat/__init__.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/combat/combat.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/combat/conditions.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/combat/initiative.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/combat/weapons.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/__init__.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/armor.json +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/backgrounds.json +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/classes.json +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/conditions.json +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/creatures.json +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/feats.json +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/generate.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/magic_items.json +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/models.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/species.json +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/spells.json +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/weapons.json +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/dice/__init__.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/dice/engine.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/ontology/__init__.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/ontology/dnd.yaml +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/ontology/loader.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/py.typed +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/__init__.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/assembler.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/components.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/compose.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/dnd_5e_2024.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/evaluator.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/export.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/lookup_tables.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/operations.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/schema.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/theme_scaling.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/validation.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_adapters_bugs.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_background_components.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_backgrounds.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_character_theme_scaling.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_classes.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_cli.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_combat.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_combat_api_contract.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_compose.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_conditions.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_content.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_content_components.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_content_models.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_creatures.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_defenses_and_serialization.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_dice.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_dice_api_contract.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_engine.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_evaluator_cache.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_examples.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_export.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_feat_components.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_gated_contributions.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_initiative.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_input_validation.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_ontology.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_operations_registry.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_properties.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_public_graph_api.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_species_components.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_spell_components.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_spells.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_theme_scaling.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_validation.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_weapon_attack.py +0 -0
- {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_weapons_armor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dndwright
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.26.0
|
|
4
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
5
|
Project-URL: Homepage, https://github.com/sligara7/dndwright
|
|
6
6
|
Project-URL: Repository, https://github.com/sligara7/dndwright
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dndwright"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.26.0"
|
|
8
8
|
description = "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)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -17,6 +17,7 @@ from typing import Any
|
|
|
17
17
|
|
|
18
18
|
from .adapters import character_data_to_inputs, computed_values_to_sheet
|
|
19
19
|
from .assembler import apply_modifiers
|
|
20
|
+
from .compose import Component, compose, component_from_dict
|
|
20
21
|
from .dnd_5e_2024 import DND_5E_2024_RULESET
|
|
21
22
|
from .evaluator import evaluate
|
|
22
23
|
from .theme_scaling import ThemeScalingLayer, apply_theme_scaling
|
|
@@ -178,6 +179,7 @@ def evaluate_character(
|
|
|
178
179
|
*,
|
|
179
180
|
strict: bool = False,
|
|
180
181
|
scaling: ThemeScalingLayer | None = None,
|
|
182
|
+
components: list | None = None,
|
|
181
183
|
) -> dict:
|
|
182
184
|
"""Evaluate a character from session data → full computed character sheet.
|
|
183
185
|
|
|
@@ -218,8 +220,15 @@ def evaluate_character(
|
|
|
218
220
|
equipment=fields["equipment"],
|
|
219
221
|
)
|
|
220
222
|
|
|
221
|
-
#
|
|
222
|
-
|
|
223
|
+
# Compose any adopted item/equipment components onto the ruleset before evaluating —
|
|
224
|
+
# e.g. an allocated "+1 armor_class" or "resistance to fire" item snaps its modifier onto
|
|
225
|
+
# the computed sheet (each `component` is a Component, or a component_from_dict-shaped spec
|
|
226
|
+
# dict as persisted on a GraphComponent). Then evaluate the (theme-scaled) computation graph.
|
|
227
|
+
ruleset = _ruleset_for(scaling)
|
|
228
|
+
if components:
|
|
229
|
+
comps = [c if isinstance(c, Component) else component_from_dict(c) for c in components]
|
|
230
|
+
ruleset = compose(ruleset, *comps)
|
|
231
|
+
computed = evaluate(ruleset, inputs)
|
|
223
232
|
|
|
224
233
|
# Apply NodeModifiers from feats, class features, etc.
|
|
225
234
|
computed = apply_modifiers(computed, inputs)
|
|
@@ -231,7 +231,7 @@ def validate_species_homebrew(species_data: dict[str, Any]) -> list[str]:
|
|
|
231
231
|
continue
|
|
232
232
|
name = trait.get("name", "?")
|
|
233
233
|
if not isinstance(name, str) or not name.strip():
|
|
234
|
-
problems.append(
|
|
234
|
+
problems.append("Species trait missing name")
|
|
235
235
|
|
|
236
236
|
return problems
|
|
237
237
|
|
|
@@ -179,7 +179,8 @@ EXPECTED_SIGNATURES = {
|
|
|
179
179
|
"evaluate": "(ruleset: 'Ruleset', input_values: 'dict[str, Any]') -> 'dict[str, Any]'",
|
|
180
180
|
"evaluate_character": (
|
|
181
181
|
"(session_data: 'dict', *, strict: 'bool' = False, "
|
|
182
|
-
"scaling: 'ThemeScalingLayer | None' = None
|
|
182
|
+
"scaling: 'ThemeScalingLayer | None' = None, "
|
|
183
|
+
"components: 'list | None' = None) -> 'dict'"
|
|
183
184
|
),
|
|
184
185
|
"generate_library": (
|
|
185
186
|
"(llm: 'JsonLLM', classes: 'int' = 6, species: 'int' = 6, creatures: 'int' = 12) "
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""evaluate_character(..., components=[...]) — compose allocated item modifiers onto the sheet.
|
|
2
|
+
|
|
3
|
+
Covers the v0.26 `components` kwarg: an allocated item's dndwright Component (or a
|
|
4
|
+
component_from_dict-shaped spec dict, as persisted on a storyflow GraphComponent) is composed
|
|
5
|
+
onto the ruleset before evaluation, so its modifier lands on the computed sheet. `None`/[]
|
|
6
|
+
is a pure no-op (default behaviour unchanged).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dndwright.rules.character_evaluator import evaluate_character
|
|
10
|
+
from dndwright.rules.compose import (
|
|
11
|
+
Component,
|
|
12
|
+
component_from_content,
|
|
13
|
+
component_to_dict,
|
|
14
|
+
modifier,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Wizard, DEX 14 (+2) → unarmored AC 12; deterministic base.
|
|
18
|
+
CHAR = {
|
|
19
|
+
"ability_scores": {"strength": 8, "dexterity": 14, "constitution": 14,
|
|
20
|
+
"intelligence": 18, "wisdom": 12, "charisma": 10},
|
|
21
|
+
"class_data": {"class_name": "wizard"},
|
|
22
|
+
"species_data": {"name": "Human", "speed": 30},
|
|
23
|
+
"level": 5,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _plus1_ac() -> Component:
|
|
28
|
+
return component_from_content({"component": [{"target": "armor_class", "mode": "add", "amount": 1}]})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_no_components_is_a_noop():
|
|
32
|
+
assert evaluate_character(CHAR) == evaluate_character(CHAR, components=None)
|
|
33
|
+
assert evaluate_character(CHAR) == evaluate_character(CHAR, components=[])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_plus1_ac_component_object_composes():
|
|
37
|
+
base = evaluate_character(CHAR)["armor_class"]
|
|
38
|
+
composed = evaluate_character(CHAR, components=[_plus1_ac()])["armor_class"]
|
|
39
|
+
assert composed == base + 1, f"expected +1 AC ({base + 1}), got {composed}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_plus1_ac_serialized_dict_composes():
|
|
43
|
+
"""A component_to_dict-round-tripped spec (as stored on a GraphComponent) composes too."""
|
|
44
|
+
base = evaluate_character(CHAR)["armor_class"]
|
|
45
|
+
spec = component_to_dict(_plus1_ac())
|
|
46
|
+
composed = evaluate_character(CHAR, components=[spec])["armor_class"]
|
|
47
|
+
assert composed == base + 1, f"expected +1 AC ({base + 1}) from serialized spec, got {composed}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_two_unique_ac_items_stack():
|
|
51
|
+
"""Two DISTINCT-id AC items stack (+1 +1 = +2). Item components MUST carry unique ids —
|
|
52
|
+
dndwright namespaces each component's nodes by ``component.id``, so two components sharing
|
|
53
|
+
an id (e.g. the ``component_from_content`` default ``"item"``) collide and cross-contaminate.
|
|
54
|
+
storyflow uses each owned GraphComponent's unique node id, so real allocations are safe."""
|
|
55
|
+
base = evaluate_character(CHAR)["armor_class"]
|
|
56
|
+
comps = [
|
|
57
|
+
modifier("shield_of_vantor", target="armor_class", amount=1),
|
|
58
|
+
modifier("ring_of_protection", target="armor_class", amount=1),
|
|
59
|
+
]
|
|
60
|
+
composed = evaluate_character(CHAR, components=comps)["armor_class"]
|
|
61
|
+
assert composed == base + 2, f"two +1-AC items should stack to +2 ({base + 2}), got {composed}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# SCOPE NOTE (inc-4 v1): item components compose onto the computed graph, but only targets the
|
|
65
|
+
# character-sheet reshape reads from computed NODES surface on the sheet — confirmed: `armor_class`
|
|
66
|
+
# and `initiative` do; `hp_max`/`speed`/ability-scores/`resistances` compose the node but the
|
|
67
|
+
# reshape reads those from input/other keys, so they don't yet surface. inc-4 targets AC (the
|
|
68
|
+
# "+1 shield → +1 AC" headline). Ability-score-setting + resistance items (sheet-reshape to read
|
|
69
|
+
# composed values) are a tracked dndwright follow-up in the items workstream.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|