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.
Files changed (111) hide show
  1. {dndwright-0.25.2 → dndwright-0.26.0}/PKG-INFO +1 -1
  2. {dndwright-0.25.2 → dndwright-0.26.0}/pyproject.toml +1 -1
  3. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/__init__.py +1 -1
  4. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/adapters.py +0 -1
  5. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/character_evaluator.py +11 -2
  6. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/homebrew_validator.py +1 -1
  7. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_api_contract.py +2 -1
  8. dndwright-0.26.0/tests/test_evaluate_character_components.py +69 -0
  9. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_homebrew_validator.py +0 -1
  10. {dndwright-0.25.2 → dndwright-0.26.0}/.github/workflows/ci.yml +0 -0
  11. {dndwright-0.25.2 → dndwright-0.26.0}/.github/workflows/publish.yml +0 -0
  12. {dndwright-0.25.2 → dndwright-0.26.0}/.gitignore +0 -0
  13. {dndwright-0.25.2 → dndwright-0.26.0}/CHANGELOG.md +0 -0
  14. {dndwright-0.25.2 → dndwright-0.26.0}/LICENSE +0 -0
  15. {dndwright-0.25.2 → dndwright-0.26.0}/NOTICE +0 -0
  16. {dndwright-0.25.2 → dndwright-0.26.0}/README.md +0 -0
  17. {dndwright-0.25.2 → dndwright-0.26.0}/RELEASING.md +0 -0
  18. {dndwright-0.25.2 → dndwright-0.26.0}/assets/combat-flow.svg +0 -0
  19. {dndwright-0.25.2 → dndwright-0.26.0}/assets/compose.svg +0 -0
  20. {dndwright-0.25.2 → dndwright-0.26.0}/assets/computation-graph.svg +0 -0
  21. {dndwright-0.25.2 → dndwright-0.26.0}/assets/dice.svg +0 -0
  22. {dndwright-0.25.2 → dndwright-0.26.0}/assets/overview.svg +0 -0
  23. {dndwright-0.25.2 → dndwright-0.26.0}/assets/theme-scaling.svg +0 -0
  24. {dndwright-0.25.2 → dndwright-0.26.0}/examples/README.md +0 -0
  25. {dndwright-0.25.2 → dndwright-0.26.0}/examples/combat.py +0 -0
  26. {dndwright-0.25.2 → dndwright-0.26.0}/examples/compose.py +0 -0
  27. {dndwright-0.25.2 → dndwright-0.26.0}/examples/conditions.py +0 -0
  28. {dndwright-0.25.2 → dndwright-0.26.0}/examples/content_components.py +0 -0
  29. {dndwright-0.25.2 → dndwright-0.26.0}/examples/custom_operation.py +0 -0
  30. {dndwright-0.25.2 → dndwright-0.26.0}/examples/dice.py +0 -0
  31. {dndwright-0.25.2 → dndwright-0.26.0}/examples/export_graph.py +0 -0
  32. {dndwright-0.25.2 → dndwright-0.26.0}/examples/feat_components.py +0 -0
  33. {dndwright-0.25.2 → dndwright-0.26.0}/examples/gated_contributions.py +0 -0
  34. {dndwright-0.25.2 → dndwright-0.26.0}/examples/initiative.py +0 -0
  35. {dndwright-0.25.2 → dndwright-0.26.0}/examples/multiclass.py +0 -0
  36. {dndwright-0.25.2 → dndwright-0.26.0}/examples/quickstart.py +0 -0
  37. {dndwright-0.25.2 → dndwright-0.26.0}/examples/stat_diff.py +0 -0
  38. {dndwright-0.25.2 → dndwright-0.26.0}/examples/weapon_attack.py +0 -0
  39. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/cli.py +0 -0
  40. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/combat/__init__.py +0 -0
  41. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/combat/combat.py +0 -0
  42. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/combat/conditions.py +0 -0
  43. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/combat/initiative.py +0 -0
  44. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/combat/weapons.py +0 -0
  45. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/__init__.py +0 -0
  46. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/armor.json +0 -0
  47. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/backgrounds.json +0 -0
  48. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/classes.json +0 -0
  49. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/conditions.json +0 -0
  50. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/creatures.json +0 -0
  51. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/feats.json +0 -0
  52. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/generate.py +0 -0
  53. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/magic_items.json +0 -0
  54. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/models.py +0 -0
  55. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/species.json +0 -0
  56. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/spells.json +0 -0
  57. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/content/weapons.json +0 -0
  58. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/dice/__init__.py +0 -0
  59. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/dice/engine.py +0 -0
  60. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/ontology/__init__.py +0 -0
  61. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/ontology/dnd.yaml +0 -0
  62. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/ontology/loader.py +0 -0
  63. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/py.typed +0 -0
  64. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/__init__.py +0 -0
  65. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/assembler.py +0 -0
  66. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/components.py +0 -0
  67. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/compose.py +0 -0
  68. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/dnd_5e_2024.py +0 -0
  69. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/evaluator.py +0 -0
  70. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/export.py +0 -0
  71. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/lookup_tables.py +0 -0
  72. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/operations.py +0 -0
  73. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/schema.py +0 -0
  74. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/theme_scaling.py +0 -0
  75. {dndwright-0.25.2 → dndwright-0.26.0}/src/dndwright/rules/validation.py +0 -0
  76. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_adapters_bugs.py +0 -0
  77. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_background_components.py +0 -0
  78. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_backgrounds.py +0 -0
  79. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_character_theme_scaling.py +0 -0
  80. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_classes.py +0 -0
  81. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_cli.py +0 -0
  82. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_combat.py +0 -0
  83. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_combat_api_contract.py +0 -0
  84. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_compose.py +0 -0
  85. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_conditions.py +0 -0
  86. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_content.py +0 -0
  87. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_content_components.py +0 -0
  88. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_content_models.py +0 -0
  89. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_creatures.py +0 -0
  90. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_defenses_and_serialization.py +0 -0
  91. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_dice.py +0 -0
  92. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_dice_api_contract.py +0 -0
  93. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_engine.py +0 -0
  94. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_evaluator_cache.py +0 -0
  95. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_examples.py +0 -0
  96. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_export.py +0 -0
  97. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_feat_components.py +0 -0
  98. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_gated_contributions.py +0 -0
  99. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_initiative.py +0 -0
  100. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_input_validation.py +0 -0
  101. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_ontology.py +0 -0
  102. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_operations_registry.py +0 -0
  103. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_properties.py +0 -0
  104. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_public_graph_api.py +0 -0
  105. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_species_components.py +0 -0
  106. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_spell_components.py +0 -0
  107. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_spells.py +0 -0
  108. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_theme_scaling.py +0 -0
  109. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_validation.py +0 -0
  110. {dndwright-0.25.2 → dndwright-0.26.0}/tests/test_weapon_attack.py +0 -0
  111. {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.25.2
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.25.2"
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"
@@ -100,7 +100,7 @@ from .rules.validation import (
100
100
  validate_ruleset,
101
101
  )
102
102
 
103
- __version__ = "0.25.0"
103
+ __version__ = "0.26.0"
104
104
 
105
105
  # homebrew validation (structural rules checks on LLM-generated components)
106
106
  from .rules.homebrew_validator import (
@@ -21,7 +21,6 @@ from .lookup_tables import (
21
21
  import re
22
22
 
23
23
 
24
- import re
25
24
 
26
25
 
27
26
  def _resolve_hit_die(class_data: dict) -> int:
@@ -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
- # Evaluate the computation graph (theme-scaled when a scaling layer is supplied)
222
- computed = evaluate(_ruleset_for(scaling), inputs)
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(f"Species trait missing name")
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) -> 'dict'"
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.
@@ -1,5 +1,4 @@
1
1
  """Tests for homebrew validators."""
2
- import pytest
3
2
  from dndwright.rules.homebrew_validator import (
4
3
  validate_class_homebrew,
5
4
  validate_species_homebrew,
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